mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-06-09 23:59:45 +00:00
a297ba7073
When user starts kportal for the first time, and there is no config file, kportal should create an empty config file with default values and empty forwarding rules, so that user can easily edit the config file and add their own rules.
520 lines
13 KiB
Go
520 lines
13 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestLoadConfig_ValidYAML(t *testing.T) {
|
|
// Create a temporary config file
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, ".kportal.yaml")
|
|
|
|
validYAML := `contexts:
|
|
- name: dev-cluster
|
|
namespaces:
|
|
- name: default
|
|
forwards:
|
|
- resource: pod/my-app
|
|
protocol: tcp
|
|
port: 8080
|
|
localPort: 8080
|
|
- name: staging
|
|
forwards:
|
|
- resource: service/postgres
|
|
protocol: tcp
|
|
port: 5432
|
|
localPort: 5433
|
|
- name: prod-cluster
|
|
namespaces:
|
|
- name: production
|
|
forwards:
|
|
- resource: pod
|
|
selector: app=nginx,env=prod
|
|
protocol: tcp
|
|
port: 80
|
|
localPort: 8081
|
|
`
|
|
|
|
err := os.WriteFile(configPath, []byte(validYAML), 0644)
|
|
assert.NoError(t, err, "should write temp config file")
|
|
|
|
// Load the config
|
|
cfg, err := LoadConfig(configPath)
|
|
assert.NoError(t, err, "LoadConfig should succeed")
|
|
assert.NotNil(t, cfg, "config should not be nil")
|
|
|
|
// Verify structure
|
|
assert.Len(t, cfg.Contexts, 2, "should have 2 contexts")
|
|
|
|
// Verify first context
|
|
assert.Equal(t, "dev-cluster", cfg.Contexts[0].Name)
|
|
assert.Len(t, cfg.Contexts[0].Namespaces, 2, "dev-cluster should have 2 namespaces")
|
|
|
|
// Verify first namespace in first context
|
|
assert.Equal(t, "default", cfg.Contexts[0].Namespaces[0].Name)
|
|
assert.Len(t, cfg.Contexts[0].Namespaces[0].Forwards, 1)
|
|
|
|
// Verify forward details
|
|
fwd := cfg.Contexts[0].Namespaces[0].Forwards[0]
|
|
assert.Equal(t, "pod/my-app", fwd.Resource)
|
|
assert.Equal(t, "tcp", fwd.Protocol)
|
|
assert.Equal(t, 8080, fwd.Port)
|
|
assert.Equal(t, 8080, fwd.LocalPort)
|
|
assert.Equal(t, "", fwd.Selector)
|
|
|
|
// Verify runtime fields are populated
|
|
assert.Equal(t, "dev-cluster", fwd.GetContext())
|
|
assert.Equal(t, "default", fwd.GetNamespace())
|
|
}
|
|
|
|
func TestLoadConfig_InvalidYAML(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, ".kportal.yaml")
|
|
|
|
invalidYAML := `contexts:
|
|
- name: dev-cluster
|
|
namespaces:
|
|
- name: default
|
|
forwards: [this is invalid yaml syntax
|
|
`
|
|
|
|
err := os.WriteFile(configPath, []byte(invalidYAML), 0644)
|
|
assert.NoError(t, err, "should write temp config file")
|
|
|
|
// Load the config
|
|
cfg, err := LoadConfig(configPath)
|
|
assert.Error(t, err, "LoadConfig should fail with invalid YAML")
|
|
assert.Nil(t, cfg, "config should be nil on error")
|
|
assert.Contains(t, err.Error(), "failed to parse YAML", "error should mention YAML parsing")
|
|
}
|
|
|
|
func TestLoadConfig_FileNotFound(t *testing.T) {
|
|
// Try to load a non-existent file
|
|
cfg, err := LoadConfig("/non/existent/path/.kportal.yaml")
|
|
assert.Error(t, err, "LoadConfig should fail with non-existent file")
|
|
assert.Nil(t, cfg, "config should be nil on error")
|
|
assert.Equal(t, ErrConfigNotFound, err, "should return ErrConfigNotFound")
|
|
}
|
|
|
|
func TestForward_ID(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
forward Forward
|
|
expectedID string
|
|
}{
|
|
{
|
|
name: "pod with explicit name",
|
|
forward: Forward{
|
|
Resource: "pod/my-app",
|
|
Port: 8080,
|
|
LocalPort: 8080,
|
|
contextName: "dev-cluster",
|
|
namespaceName: "default",
|
|
},
|
|
expectedID: "dev-cluster/default/pod/my-app:8080",
|
|
},
|
|
{
|
|
name: "service resource",
|
|
forward: Forward{
|
|
Resource: "service/postgres",
|
|
Port: 5432,
|
|
LocalPort: 5433,
|
|
contextName: "prod-cluster",
|
|
namespaceName: "database",
|
|
},
|
|
expectedID: "prod-cluster/database/service/postgres:5433",
|
|
},
|
|
{
|
|
name: "pod with selector",
|
|
forward: Forward{
|
|
Resource: "pod",
|
|
Selector: "app=nginx",
|
|
Port: 80,
|
|
LocalPort: 8081,
|
|
contextName: "staging",
|
|
namespaceName: "web",
|
|
},
|
|
expectedID: "staging/web/pod:8081",
|
|
},
|
|
{
|
|
name: "forward with alias",
|
|
forward: Forward{
|
|
Resource: "service/postgres",
|
|
Port: 5432,
|
|
LocalPort: 5432,
|
|
Alias: "shared-postgres",
|
|
contextName: "home",
|
|
namespaceName: "shared-resources",
|
|
},
|
|
expectedID: "shared-postgres:5432",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
id := tt.forward.ID()
|
|
assert.Equal(t, tt.expectedID, id, "ID() should return correct format")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestForward_String(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
forward Forward
|
|
expectedString string
|
|
}{
|
|
{
|
|
name: "pod without selector",
|
|
forward: Forward{
|
|
Resource: "pod/my-app",
|
|
Port: 8080,
|
|
LocalPort: 8080,
|
|
contextName: "dev-cluster",
|
|
namespaceName: "default",
|
|
},
|
|
expectedString: "dev-cluster/default/pod/my-app:8080→8080",
|
|
},
|
|
{
|
|
name: "service resource",
|
|
forward: Forward{
|
|
Resource: "service/postgres",
|
|
Port: 5432,
|
|
LocalPort: 5433,
|
|
contextName: "prod-cluster",
|
|
namespaceName: "database",
|
|
},
|
|
expectedString: "prod-cluster/database/service/postgres:5432→5433",
|
|
},
|
|
{
|
|
name: "pod with selector",
|
|
forward: Forward{
|
|
Resource: "pod",
|
|
Selector: "app=nginx,env=prod",
|
|
Port: 80,
|
|
LocalPort: 8081,
|
|
contextName: "staging",
|
|
namespaceName: "web",
|
|
},
|
|
expectedString: "staging/web/pod[app=nginx,env=prod]:80→8081",
|
|
},
|
|
{
|
|
name: "forward with alias",
|
|
forward: Forward{
|
|
Resource: "service/redis",
|
|
Port: 6379,
|
|
LocalPort: 6379,
|
|
Alias: "redis-at-home",
|
|
contextName: "home",
|
|
namespaceName: "shared-resources",
|
|
},
|
|
expectedString: "redis-at-home:6379→6379",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
str := tt.forward.String()
|
|
assert.Equal(t, tt.expectedString, str, "String() should return correct format")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseConfig_ValidYAML(t *testing.T) {
|
|
yamlData := []byte(`contexts:
|
|
- name: test-cluster
|
|
namespaces:
|
|
- name: default
|
|
forwards:
|
|
- resource: pod/app
|
|
protocol: tcp
|
|
port: 8080
|
|
localPort: 8080
|
|
`)
|
|
|
|
cfg, err := ParseConfig(yamlData)
|
|
assert.NoError(t, err, "ParseConfig should succeed")
|
|
assert.NotNil(t, cfg, "config should not be nil")
|
|
assert.Len(t, cfg.Contexts, 1)
|
|
assert.Equal(t, "test-cluster", cfg.Contexts[0].Name)
|
|
}
|
|
|
|
func TestParseConfig_PopulatesRuntimeFields(t *testing.T) {
|
|
yamlData := []byte(`contexts:
|
|
- name: my-cluster
|
|
namespaces:
|
|
- name: my-namespace
|
|
forwards:
|
|
- resource: pod/my-pod
|
|
port: 8080
|
|
localPort: 8080
|
|
`)
|
|
|
|
cfg, err := ParseConfig(yamlData)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, cfg)
|
|
|
|
// Check that runtime fields are populated
|
|
fwd := cfg.Contexts[0].Namespaces[0].Forwards[0]
|
|
assert.Equal(t, "my-cluster", fwd.GetContext())
|
|
assert.Equal(t, "my-namespace", fwd.GetNamespace())
|
|
assert.Equal(t, "my-cluster/my-namespace/pod/my-pod:8080", fwd.ID())
|
|
}
|
|
|
|
func TestConfig_GetAllForwards(t *testing.T) {
|
|
yamlData := []byte(`contexts:
|
|
- name: cluster1
|
|
namespaces:
|
|
- name: ns1
|
|
forwards:
|
|
- resource: pod/app1
|
|
port: 8080
|
|
localPort: 8080
|
|
- resource: pod/app2
|
|
port: 8081
|
|
localPort: 8081
|
|
- name: ns2
|
|
forwards:
|
|
- resource: service/db
|
|
port: 5432
|
|
localPort: 5432
|
|
- name: cluster2
|
|
namespaces:
|
|
- name: ns3
|
|
forwards:
|
|
- resource: pod/app3
|
|
port: 9090
|
|
localPort: 9090
|
|
`)
|
|
|
|
cfg, err := ParseConfig(yamlData)
|
|
assert.NoError(t, err)
|
|
|
|
forwards := cfg.GetAllForwards()
|
|
assert.Len(t, forwards, 4, "should return all forwards from all contexts and namespaces")
|
|
}
|
|
|
|
func TestForward_SetContext(t *testing.T) {
|
|
fwd := Forward{
|
|
Resource: "pod/my-app",
|
|
Port: 8080,
|
|
LocalPort: 8080,
|
|
}
|
|
|
|
assert.Equal(t, "", fwd.GetContext(), "initial context should be empty")
|
|
assert.Equal(t, "", fwd.GetNamespace(), "initial namespace should be empty")
|
|
|
|
fwd.SetContext("my-cluster", "my-namespace")
|
|
|
|
assert.Equal(t, "my-cluster", fwd.GetContext())
|
|
assert.Equal(t, "my-namespace", fwd.GetNamespace())
|
|
}
|
|
|
|
func TestHTTPLogSpec_UnmarshalYAML(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
yaml string
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "httpLog as boolean true",
|
|
yaml: `contexts:
|
|
- name: test
|
|
namespaces:
|
|
- name: default
|
|
forwards:
|
|
- resource: service/api
|
|
port: 8080
|
|
localPort: 8080
|
|
httpLog: true
|
|
`,
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "httpLog as boolean false",
|
|
yaml: `contexts:
|
|
- name: test
|
|
namespaces:
|
|
- name: default
|
|
forwards:
|
|
- resource: service/api
|
|
port: 8080
|
|
localPort: 8080
|
|
httpLog: false
|
|
`,
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "httpLog as struct",
|
|
yaml: `contexts:
|
|
- name: test
|
|
namespaces:
|
|
- name: default
|
|
forwards:
|
|
- resource: service/api
|
|
port: 8080
|
|
localPort: 8080
|
|
httpLog:
|
|
enabled: true
|
|
includeHeaders: true
|
|
`,
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "httpLog not specified",
|
|
yaml: `contexts:
|
|
- name: test
|
|
namespaces:
|
|
- name: default
|
|
forwards:
|
|
- resource: service/api
|
|
port: 8080
|
|
localPort: 8080
|
|
`,
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
cfg, err := ParseConfig([]byte(tt.yaml))
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, cfg)
|
|
|
|
fwd := cfg.Contexts[0].Namespaces[0].Forwards[0]
|
|
if tt.expected {
|
|
assert.NotNil(t, fwd.HTTPLog, "HTTPLog should not be nil")
|
|
assert.True(t, fwd.HTTPLog.Enabled, "HTTPLog.Enabled should be true")
|
|
} else {
|
|
if fwd.HTTPLog != nil {
|
|
assert.False(t, fwd.HTTPLog.Enabled, "HTTPLog.Enabled should be false")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewEmptyConfig(t *testing.T) {
|
|
cfg := NewEmptyConfig()
|
|
assert.NotNil(t, cfg, "NewEmptyConfig should return non-nil config")
|
|
assert.Empty(t, cfg.Contexts, "NewEmptyConfig should have empty contexts")
|
|
assert.True(t, cfg.IsEmpty(), "NewEmptyConfig should be considered empty")
|
|
}
|
|
|
|
func TestConfig_IsEmpty(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config *Config
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "nil contexts",
|
|
config: &Config{},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "empty contexts slice",
|
|
config: &Config{Contexts: []Context{}},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "context with empty namespaces",
|
|
config: &Config{
|
|
Contexts: []Context{
|
|
{Name: "test", Namespaces: []Namespace{}},
|
|
},
|
|
},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "context with namespace but no forwards",
|
|
config: &Config{
|
|
Contexts: []Context{
|
|
{
|
|
Name: "test",
|
|
Namespaces: []Namespace{
|
|
{Name: "default", Forwards: []Forward{}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "config with forward",
|
|
config: &Config{
|
|
Contexts: []Context{
|
|
{
|
|
Name: "test",
|
|
Namespaces: []Namespace{
|
|
{
|
|
Name: "default",
|
|
Forwards: []Forward{
|
|
{Resource: "pod/app", Port: 8080, LocalPort: 8080},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert.Equal(t, tt.expected, tt.config.IsEmpty())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCreateEmptyConfigFile(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, ".kportal.yaml")
|
|
|
|
// Create empty config file
|
|
err := CreateEmptyConfigFile(configPath)
|
|
assert.NoError(t, err, "CreateEmptyConfigFile should succeed")
|
|
|
|
// Verify file exists
|
|
_, err = os.Stat(configPath)
|
|
assert.NoError(t, err, "config file should exist")
|
|
|
|
// Verify file is readable and parseable
|
|
cfg, err := LoadConfig(configPath)
|
|
assert.NoError(t, err, "should be able to load created config")
|
|
assert.NotNil(t, cfg, "config should not be nil")
|
|
assert.True(t, cfg.IsEmpty(), "created config should be empty")
|
|
|
|
// Verify file permissions (0600)
|
|
info, _ := os.Stat(configPath)
|
|
assert.Equal(t, os.FileMode(0600), info.Mode().Perm(), "file should have 0600 permissions")
|
|
|
|
// Verify file contains helpful header
|
|
content, _ := os.ReadFile(configPath)
|
|
assert.Contains(t, string(content), "# kportal configuration file", "should contain header comment")
|
|
assert.Contains(t, string(content), "Example forward", "should contain example")
|
|
}
|
|
|
|
func TestCreateEmptyConfigFile_AlreadyExists(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, ".kportal.yaml")
|
|
|
|
// Create existing file
|
|
err := os.WriteFile(configPath, []byte("existing content"), 0644)
|
|
assert.NoError(t, err)
|
|
|
|
// Try to create config file - should fail
|
|
err = CreateEmptyConfigFile(configPath)
|
|
assert.Error(t, err, "CreateEmptyConfigFile should fail when file exists")
|
|
assert.Contains(t, err.Error(), "already exists")
|
|
|
|
// Verify original content is preserved
|
|
content, _ := os.ReadFile(configPath)
|
|
assert.Equal(t, "existing content", string(content))
|
|
}
|