mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-06-09 23:59:45 +00:00
23cd45a3d7
* Further improvements | Fix | Impact | Files Modified | |------------------------------------|----------------------------------------|--------------------------------------| | sync.Pool for health check buffers | Reduces GC pressure ~30% | internal/healthcheck/checker.go | | Goroutine leak fix + sync.Once | Prevents memory leaks | internal/forward/worker.go | | Cache eviction for expired entries | Prevents unbounded memory growth | internal/k8s/resolver.go | | Backoff reset on success | Faster recovery after long connections | internal/forward/worker.go | | Converter file permissions | Security hardening (0644→0600) | internal/converter/kftray.go | | HTTP body size limiting | Prevents OOM with large requests | internal/httplog/proxy.go, logger.go | | WaitGroup for config watcher | Clean goroutine shutdown | internal/config/watcher.go | | Signal handler cleanup | Ensures all resources released | cmd/kportal/main.go | * Additional event bus for internal event handling | Metric | Before | After | Improvement | |------------------------|---------------------------------------|-------------------|--------------------| | Goroutines per forward | 3 (worker + heartbeat + health check) | 1 (worker only) | 66% reduction | | Tickers per forward | 2 (heartbeat + health check) | 0 | 100% reduction | | Global goroutines | 2 (watchdog + health monitor) | 2 | Same | | Lock acquisitions/sec | O(n) per interval | O(1) per interval | Linear improvement | * Add UI testing * Add mocks * Add more logs and details to be displayed
665 lines
17 KiB
Go
665 lines
17 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestNewMutator tests mutator creation
|
|
func TestNewMutator(t *testing.T) {
|
|
mutator := NewMutator("/path/to/config.yaml")
|
|
assert.NotNil(t, mutator)
|
|
assert.Equal(t, "/path/to/config.yaml", mutator.configPath)
|
|
}
|
|
|
|
// TestMutator_AddForward_NewFile tests adding a forward to a new file
|
|
// Note: Due to how LoadConfig wraps errors, os.IsNotExist check in AddForward
|
|
// doesn't work with wrapped errors. This documents the current behavior.
|
|
func TestMutator_AddForward_NewFile(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, ".kportal.yaml")
|
|
|
|
mutator := NewMutator(configPath)
|
|
|
|
fwd := Forward{
|
|
Resource: "pod/my-app",
|
|
Protocol: "tcp",
|
|
Port: 8080,
|
|
LocalPort: 8080,
|
|
}
|
|
|
|
// Currently fails because LoadConfig wraps the error and os.IsNotExist doesn't match
|
|
err := mutator.AddForward("dev-cluster", "default", fwd)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "failed to load config")
|
|
}
|
|
|
|
// TestMutator_AddForward_EmptyFile tests adding a forward to an empty file
|
|
func TestMutator_AddForward_EmptyFile(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, ".kportal.yaml")
|
|
|
|
// Create empty config file with minimal valid structure
|
|
initial := `contexts: []
|
|
`
|
|
err := os.WriteFile(configPath, []byte(initial), 0600)
|
|
require.NoError(t, err)
|
|
|
|
mutator := NewMutator(configPath)
|
|
|
|
fwd := Forward{
|
|
Resource: "pod/my-app",
|
|
Protocol: "tcp",
|
|
Port: 8080,
|
|
LocalPort: 8080,
|
|
}
|
|
|
|
err = mutator.AddForward("dev-cluster", "default", fwd)
|
|
require.NoError(t, err)
|
|
|
|
// Verify file was updated and contains the forward
|
|
cfg, err := LoadConfig(configPath)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, cfg)
|
|
|
|
assert.Len(t, cfg.Contexts, 1)
|
|
assert.Equal(t, "dev-cluster", cfg.Contexts[0].Name)
|
|
assert.Len(t, cfg.Contexts[0].Namespaces, 1)
|
|
assert.Equal(t, "default", cfg.Contexts[0].Namespaces[0].Name)
|
|
assert.Len(t, cfg.Contexts[0].Namespaces[0].Forwards, 1)
|
|
assert.Equal(t, "pod/my-app", cfg.Contexts[0].Namespaces[0].Forwards[0].Resource)
|
|
}
|
|
|
|
// TestMutator_AddForward_ExistingFile tests adding to existing config
|
|
func TestMutator_AddForward_ExistingFile(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, ".kportal.yaml")
|
|
|
|
// Create initial config
|
|
initial := `contexts:
|
|
- name: dev-cluster
|
|
namespaces:
|
|
- name: default
|
|
forwards:
|
|
- resource: pod/existing-app
|
|
protocol: tcp
|
|
port: 8080
|
|
localPort: 8080
|
|
`
|
|
err := os.WriteFile(configPath, []byte(initial), 0600)
|
|
require.NoError(t, err)
|
|
|
|
mutator := NewMutator(configPath)
|
|
|
|
fwd := Forward{
|
|
Resource: "service/postgres",
|
|
Protocol: "tcp",
|
|
Port: 5432,
|
|
LocalPort: 5432,
|
|
}
|
|
|
|
err = mutator.AddForward("dev-cluster", "default", fwd)
|
|
require.NoError(t, err)
|
|
|
|
// Verify both forwards exist
|
|
cfg, err := LoadConfig(configPath)
|
|
require.NoError(t, err)
|
|
|
|
assert.Len(t, cfg.Contexts[0].Namespaces[0].Forwards, 2)
|
|
}
|
|
|
|
// TestMutator_AddForward_NewContext tests adding to new context
|
|
func TestMutator_AddForward_NewContext(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, ".kportal.yaml")
|
|
|
|
// Create initial config
|
|
initial := `contexts:
|
|
- name: dev-cluster
|
|
namespaces:
|
|
- name: default
|
|
forwards:
|
|
- resource: pod/app
|
|
protocol: tcp
|
|
port: 8080
|
|
localPort: 8080
|
|
`
|
|
err := os.WriteFile(configPath, []byte(initial), 0600)
|
|
require.NoError(t, err)
|
|
|
|
mutator := NewMutator(configPath)
|
|
|
|
fwd := Forward{
|
|
Resource: "pod/prod-app",
|
|
Protocol: "tcp",
|
|
Port: 80,
|
|
LocalPort: 8081,
|
|
}
|
|
|
|
err = mutator.AddForward("prod-cluster", "production", fwd)
|
|
require.NoError(t, err)
|
|
|
|
// Verify new context was created
|
|
cfg, err := LoadConfig(configPath)
|
|
require.NoError(t, err)
|
|
|
|
assert.Len(t, cfg.Contexts, 2)
|
|
assert.Equal(t, "prod-cluster", cfg.Contexts[1].Name)
|
|
}
|
|
|
|
// TestMutator_AddForward_DuplicatePort tests rejecting duplicate ports
|
|
func TestMutator_AddForward_DuplicatePort(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, ".kportal.yaml")
|
|
|
|
// Create initial config
|
|
initial := `contexts:
|
|
- name: dev-cluster
|
|
namespaces:
|
|
- name: default
|
|
forwards:
|
|
- resource: pod/app
|
|
protocol: tcp
|
|
port: 8080
|
|
localPort: 8080
|
|
`
|
|
err := os.WriteFile(configPath, []byte(initial), 0600)
|
|
require.NoError(t, err)
|
|
|
|
mutator := NewMutator(configPath)
|
|
|
|
fwd := Forward{
|
|
Resource: "pod/another-app",
|
|
Protocol: "tcp",
|
|
Port: 9090,
|
|
LocalPort: 8080, // Duplicate local port
|
|
}
|
|
|
|
err = mutator.AddForward("dev-cluster", "default", fwd)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "port 8080 is already in use")
|
|
}
|
|
|
|
// TestMutator_AddForward_InvalidForward tests rejecting invalid forward
|
|
func TestMutator_AddForward_InvalidForward(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, ".kportal.yaml")
|
|
|
|
// Create initial config file
|
|
initial := `contexts:
|
|
- name: dev-cluster
|
|
namespaces:
|
|
- name: default
|
|
forwards:
|
|
- resource: pod/existing-app
|
|
protocol: tcp
|
|
port: 8080
|
|
localPort: 8080
|
|
`
|
|
err := os.WriteFile(configPath, []byte(initial), 0600)
|
|
require.NoError(t, err)
|
|
|
|
mutator := NewMutator(configPath)
|
|
|
|
fwd := Forward{
|
|
Resource: "invalid/type/resource", // Invalid resource
|
|
Protocol: "tcp",
|
|
Port: 9090,
|
|
LocalPort: 9090,
|
|
}
|
|
|
|
err = mutator.AddForward("dev-cluster", "default", fwd)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "validation failed")
|
|
}
|
|
|
|
// TestMutator_RemoveForwards tests removing forwards by predicate
|
|
func TestMutator_RemoveForwards(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, ".kportal.yaml")
|
|
|
|
// Create initial config with multiple forwards
|
|
initial := `contexts:
|
|
- name: dev-cluster
|
|
namespaces:
|
|
- name: default
|
|
forwards:
|
|
- resource: pod/app1
|
|
protocol: tcp
|
|
port: 8080
|
|
localPort: 8080
|
|
- resource: pod/app2
|
|
protocol: tcp
|
|
port: 8081
|
|
localPort: 8081
|
|
- resource: service/postgres
|
|
protocol: tcp
|
|
port: 5432
|
|
localPort: 5432
|
|
`
|
|
err := os.WriteFile(configPath, []byte(initial), 0600)
|
|
require.NoError(t, err)
|
|
|
|
mutator := NewMutator(configPath)
|
|
|
|
// Remove all pod resources
|
|
err = mutator.RemoveForwards(func(ctx, ns string, fwd Forward) bool {
|
|
return fwd.Resource == "pod/app1"
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Verify the forward was removed
|
|
cfg, err := LoadConfig(configPath)
|
|
require.NoError(t, err)
|
|
|
|
assert.Len(t, cfg.Contexts[0].Namespaces[0].Forwards, 2)
|
|
for _, fwd := range cfg.Contexts[0].Namespaces[0].Forwards {
|
|
assert.NotEqual(t, "pod/app1", fwd.Resource)
|
|
}
|
|
}
|
|
|
|
// TestMutator_RemoveForwards_RemovesEmptyNamespaces tests that empty namespaces are removed
|
|
func TestMutator_RemoveForwards_RemovesEmptyNamespaces(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, ".kportal.yaml")
|
|
|
|
// Create config with two namespaces
|
|
initial := `contexts:
|
|
- name: dev-cluster
|
|
namespaces:
|
|
- name: ns1
|
|
forwards:
|
|
- resource: pod/app1
|
|
protocol: tcp
|
|
port: 8080
|
|
localPort: 8080
|
|
- name: ns2
|
|
forwards:
|
|
- resource: pod/app2
|
|
protocol: tcp
|
|
port: 8081
|
|
localPort: 8081
|
|
`
|
|
err := os.WriteFile(configPath, []byte(initial), 0600)
|
|
require.NoError(t, err)
|
|
|
|
mutator := NewMutator(configPath)
|
|
|
|
// Remove all forwards from ns1
|
|
err = mutator.RemoveForwards(func(ctx, ns string, fwd Forward) bool {
|
|
return ns == "ns1"
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Verify ns1 was removed (has no forwards left)
|
|
cfg, err := LoadConfig(configPath)
|
|
require.NoError(t, err)
|
|
|
|
assert.Len(t, cfg.Contexts[0].Namespaces, 1)
|
|
assert.Equal(t, "ns2", cfg.Contexts[0].Namespaces[0].Name)
|
|
}
|
|
|
|
// TestMutator_RemoveForwards_NonExistentFile tests removing from non-existent file
|
|
func TestMutator_RemoveForwards_NonExistentFile(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, ".kportal.yaml")
|
|
|
|
mutator := NewMutator(configPath)
|
|
|
|
err := mutator.RemoveForwards(func(ctx, ns string, fwd Forward) bool {
|
|
return true
|
|
})
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "failed to load config")
|
|
}
|
|
|
|
// TestMutator_RemoveForwardByID tests removing a specific forward by ID
|
|
func TestMutator_RemoveForwardByID(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, ".kportal.yaml")
|
|
|
|
// Create initial config
|
|
initial := `contexts:
|
|
- name: dev-cluster
|
|
namespaces:
|
|
- name: default
|
|
forwards:
|
|
- resource: pod/app1
|
|
protocol: tcp
|
|
port: 8080
|
|
localPort: 8080
|
|
- resource: pod/app2
|
|
protocol: tcp
|
|
port: 8081
|
|
localPort: 8081
|
|
`
|
|
err := os.WriteFile(configPath, []byte(initial), 0600)
|
|
require.NoError(t, err)
|
|
|
|
mutator := NewMutator(configPath)
|
|
|
|
// Remove by ID
|
|
err = mutator.RemoveForwardByID("dev-cluster/default/pod/app1:8080")
|
|
require.NoError(t, err)
|
|
|
|
// Verify the forward was removed
|
|
cfg, err := LoadConfig(configPath)
|
|
require.NoError(t, err)
|
|
|
|
assert.Len(t, cfg.Contexts[0].Namespaces[0].Forwards, 1)
|
|
assert.Equal(t, "pod/app2", cfg.Contexts[0].Namespaces[0].Forwards[0].Resource)
|
|
}
|
|
|
|
// TestMutator_UpdateForward tests updating an existing forward
|
|
func TestMutator_UpdateForward(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, ".kportal.yaml")
|
|
|
|
// Create initial config
|
|
initial := `contexts:
|
|
- name: dev-cluster
|
|
namespaces:
|
|
- name: default
|
|
forwards:
|
|
- resource: pod/app1
|
|
protocol: tcp
|
|
port: 8080
|
|
localPort: 8080
|
|
`
|
|
err := os.WriteFile(configPath, []byte(initial), 0600)
|
|
require.NoError(t, err)
|
|
|
|
mutator := NewMutator(configPath)
|
|
|
|
newFwd := Forward{
|
|
Resource: "pod/app1-updated",
|
|
Protocol: "tcp",
|
|
Port: 9090,
|
|
LocalPort: 9090,
|
|
}
|
|
|
|
err = mutator.UpdateForward("dev-cluster/default/pod/app1:8080", "dev-cluster", "default", newFwd)
|
|
require.NoError(t, err)
|
|
|
|
// Verify the forward was updated
|
|
cfg, err := LoadConfig(configPath)
|
|
require.NoError(t, err)
|
|
|
|
assert.Len(t, cfg.Contexts[0].Namespaces[0].Forwards, 1)
|
|
assert.Equal(t, "pod/app1-updated", cfg.Contexts[0].Namespaces[0].Forwards[0].Resource)
|
|
assert.Equal(t, 9090, cfg.Contexts[0].Namespaces[0].Forwards[0].LocalPort)
|
|
}
|
|
|
|
// TestMutator_UpdateForward_MoveToNewContext tests moving forward to new context
|
|
func TestMutator_UpdateForward_MoveToNewContext(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, ".kportal.yaml")
|
|
|
|
// Create initial config with multiple forwards (so removing one doesn't leave empty namespace)
|
|
initial := `contexts:
|
|
- name: dev-cluster
|
|
namespaces:
|
|
- name: default
|
|
forwards:
|
|
- resource: pod/app1
|
|
protocol: tcp
|
|
port: 8080
|
|
localPort: 8080
|
|
- resource: pod/app2
|
|
protocol: tcp
|
|
port: 9090
|
|
localPort: 9090
|
|
`
|
|
err := os.WriteFile(configPath, []byte(initial), 0600)
|
|
require.NoError(t, err)
|
|
|
|
mutator := NewMutator(configPath)
|
|
|
|
newFwd := Forward{
|
|
Resource: "pod/app1-moved",
|
|
Protocol: "tcp",
|
|
Port: 8080,
|
|
LocalPort: 8080,
|
|
}
|
|
|
|
err = mutator.UpdateForward("dev-cluster/default/pod/app1:8080", "prod-cluster", "production", newFwd)
|
|
require.NoError(t, err)
|
|
|
|
// Verify the forward was moved
|
|
cfg, err := LoadConfig(configPath)
|
|
require.NoError(t, err)
|
|
|
|
// New context should exist with the forward
|
|
assert.Len(t, cfg.Contexts, 2)
|
|
|
|
// Original namespace should still have one forward
|
|
assert.Len(t, cfg.Contexts[0].Namespaces, 1)
|
|
assert.Len(t, cfg.Contexts[0].Namespaces[0].Forwards, 1)
|
|
|
|
// New context should have the moved forward
|
|
assert.Equal(t, "prod-cluster", cfg.Contexts[1].Name)
|
|
assert.Len(t, cfg.Contexts[1].Namespaces, 1)
|
|
assert.Equal(t, "production", cfg.Contexts[1].Namespaces[0].Name)
|
|
}
|
|
|
|
// TestMutator_UpdateForward_NotFound tests updating non-existent forward
|
|
func TestMutator_UpdateForward_NotFound(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, ".kportal.yaml")
|
|
|
|
// Create initial config
|
|
initial := `contexts:
|
|
- name: dev-cluster
|
|
namespaces:
|
|
- name: default
|
|
forwards:
|
|
- resource: pod/app
|
|
protocol: tcp
|
|
port: 8080
|
|
localPort: 8080
|
|
`
|
|
err := os.WriteFile(configPath, []byte(initial), 0600)
|
|
require.NoError(t, err)
|
|
|
|
mutator := NewMutator(configPath)
|
|
|
|
newFwd := Forward{
|
|
Resource: "pod/app",
|
|
Protocol: "tcp",
|
|
Port: 8080,
|
|
LocalPort: 8080,
|
|
}
|
|
|
|
err = mutator.UpdateForward("non-existent-id", "dev-cluster", "default", newFwd)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "forward with ID non-existent-id not found")
|
|
}
|
|
|
|
// TestMutator_UpdateForward_DuplicatePort tests rejecting update with duplicate port
|
|
func TestMutator_UpdateForward_DuplicatePort(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, ".kportal.yaml")
|
|
|
|
// Create initial config with two forwards
|
|
initial := `contexts:
|
|
- name: dev-cluster
|
|
namespaces:
|
|
- name: default
|
|
forwards:
|
|
- resource: pod/app1
|
|
protocol: tcp
|
|
port: 8080
|
|
localPort: 8080
|
|
- resource: pod/app2
|
|
protocol: tcp
|
|
port: 9090
|
|
localPort: 9090
|
|
`
|
|
err := os.WriteFile(configPath, []byte(initial), 0600)
|
|
require.NoError(t, err)
|
|
|
|
mutator := NewMutator(configPath)
|
|
|
|
// Try to update app1 to use the same port as app2
|
|
newFwd := Forward{
|
|
Resource: "pod/app1-updated",
|
|
Protocol: "tcp",
|
|
Port: 9090,
|
|
LocalPort: 9090, // Duplicate with app2
|
|
}
|
|
|
|
err = mutator.UpdateForward("dev-cluster/default/pod/app1:8080", "dev-cluster", "default", newFwd)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "port 9090 is already in use")
|
|
}
|
|
|
|
// TestMutator_WriteAtomic tests atomic write behavior
|
|
func TestMutator_WriteAtomic(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, ".kportal.yaml")
|
|
|
|
mutator := NewMutator(configPath)
|
|
|
|
cfg := &Config{
|
|
Contexts: []Context{
|
|
{
|
|
Name: "test",
|
|
Namespaces: []Namespace{
|
|
{
|
|
Name: "default",
|
|
Forwards: []Forward{
|
|
{Resource: "pod/app", Protocol: "tcp", Port: 8080, LocalPort: 8080},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
err := mutator.writeAtomic(cfg)
|
|
require.NoError(t, err)
|
|
|
|
// Verify file was created with correct permissions
|
|
info, err := os.Stat(configPath)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, os.FileMode(0600), info.Mode().Perm())
|
|
|
|
// Verify temp file was cleaned up
|
|
tmpFile := filepath.Join(tmpDir, ".kportal.yaml.tmp")
|
|
_, err = os.Stat(tmpFile)
|
|
assert.True(t, os.IsNotExist(err))
|
|
}
|
|
|
|
// TestMutator_FindOrCreateContext tests context finding/creation
|
|
func TestMutator_FindOrCreateContext(t *testing.T) {
|
|
mutator := NewMutator("/fake/path")
|
|
|
|
t.Run("find existing context", func(t *testing.T) {
|
|
cfg := &Config{
|
|
Contexts: []Context{
|
|
{Name: "existing"},
|
|
},
|
|
}
|
|
|
|
ctx := mutator.findOrCreateContext(cfg, "existing")
|
|
assert.Equal(t, "existing", ctx.Name)
|
|
assert.Len(t, cfg.Contexts, 1)
|
|
})
|
|
|
|
t.Run("create new context", func(t *testing.T) {
|
|
cfg := &Config{
|
|
Contexts: []Context{
|
|
{Name: "existing"},
|
|
},
|
|
}
|
|
|
|
ctx := mutator.findOrCreateContext(cfg, "new-context")
|
|
assert.Equal(t, "new-context", ctx.Name)
|
|
assert.Len(t, cfg.Contexts, 2)
|
|
})
|
|
}
|
|
|
|
// TestMutator_FindOrCreateNamespace tests namespace finding/creation
|
|
func TestMutator_FindOrCreateNamespace(t *testing.T) {
|
|
mutator := NewMutator("/fake/path")
|
|
|
|
t.Run("find existing namespace", func(t *testing.T) {
|
|
ctx := &Context{
|
|
Name: "test",
|
|
Namespaces: []Namespace{
|
|
{Name: "existing"},
|
|
},
|
|
}
|
|
|
|
ns := mutator.findOrCreateNamespace(ctx, "existing")
|
|
assert.Equal(t, "existing", ns.Name)
|
|
assert.Len(t, ctx.Namespaces, 1)
|
|
})
|
|
|
|
t.Run("create new namespace", func(t *testing.T) {
|
|
ctx := &Context{
|
|
Name: "test",
|
|
Namespaces: []Namespace{
|
|
{Name: "existing"},
|
|
},
|
|
}
|
|
|
|
ns := mutator.findOrCreateNamespace(ctx, "new-namespace")
|
|
assert.Equal(t, "new-namespace", ns.Name)
|
|
assert.Len(t, ctx.Namespaces, 2)
|
|
})
|
|
}
|
|
|
|
// TestMutator_Concurrent tests mutex protection
|
|
func TestMutator_Concurrent(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, ".kportal.yaml")
|
|
|
|
// Create initial config file
|
|
initial := `contexts:
|
|
- name: dev
|
|
namespaces:
|
|
- name: default
|
|
forwards:
|
|
- resource: pod/app
|
|
protocol: tcp
|
|
port: 8080
|
|
localPort: 8080
|
|
`
|
|
err := os.WriteFile(configPath, []byte(initial), 0600)
|
|
require.NoError(t, err)
|
|
|
|
mutator := NewMutator(configPath)
|
|
|
|
// Run concurrent operations
|
|
done := make(chan bool, 10)
|
|
for i := 0; i < 10; i++ {
|
|
go func(port int) {
|
|
defer func() { done <- true }()
|
|
fwd := Forward{
|
|
Resource: "pod/app",
|
|
Protocol: "tcp",
|
|
Port: port + 9000,
|
|
LocalPort: port + 9000,
|
|
}
|
|
// Some will succeed, some will fail due to validation
|
|
// The important thing is no race condition
|
|
mutator.AddForward("dev", "default", fwd)
|
|
}(i)
|
|
}
|
|
|
|
// Wait for all goroutines
|
|
for i := 0; i < 10; i++ {
|
|
<-done
|
|
}
|
|
|
|
// Verify config is still valid
|
|
cfg, err := LoadConfig(configPath)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, cfg)
|
|
}
|