693 lines
17 KiB
Go
693 lines
17 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// Test: LoadState loads valid state
|
|
func TestLoadState(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
stateYAML string
|
|
verify func(t *testing.T, config *GlobalConfig)
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "loads complete state",
|
|
stateYAML: `operator:
|
|
email: "admin@example.com"
|
|
cloud:
|
|
central:
|
|
domain: "central.example.com"
|
|
`,
|
|
verify: func(t *testing.T, config *GlobalConfig) {
|
|
if config.Operator.Email != "admin@example.com" {
|
|
t.Error("operator email not loaded correctly")
|
|
}
|
|
if config.Cloud.Central.Domain != "central.example.com" {
|
|
t.Error("central domain not loaded correctly")
|
|
}
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "loads minimal state",
|
|
stateYAML: `operator:
|
|
email: "admin@example.com"
|
|
`,
|
|
verify: func(t *testing.T, config *GlobalConfig) {
|
|
if config.Operator.Email != "admin@example.com" {
|
|
t.Error("operator email not loaded correctly")
|
|
}
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "loads empty state",
|
|
stateYAML: "{}\n",
|
|
verify: func(t *testing.T, config *GlobalConfig) {
|
|
if config.Operator.Email != "" {
|
|
t.Error("expected empty operator email")
|
|
}
|
|
},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
statePath := filepath.Join(tempDir, "state.yaml")
|
|
|
|
if err := os.WriteFile(statePath, []byte(tt.stateYAML), 0644); err != nil {
|
|
t.Fatalf("setup failed: %v", err)
|
|
}
|
|
|
|
config, err := LoadState(statePath)
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Error("expected error, got nil")
|
|
}
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
return
|
|
}
|
|
|
|
if config == nil {
|
|
t.Fatal("config is nil")
|
|
}
|
|
|
|
if tt.verify != nil {
|
|
tt.verify(t, config)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test: LoadState error cases
|
|
func TestLoadState_Errors(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupFunc func(t *testing.T) string
|
|
errContains string
|
|
}{
|
|
{
|
|
name: "non-existent file",
|
|
setupFunc: func(t *testing.T) string {
|
|
return filepath.Join(t.TempDir(), "nonexistent.yaml")
|
|
},
|
|
errContains: "reading config file",
|
|
},
|
|
{
|
|
name: "invalid yaml",
|
|
setupFunc: func(t *testing.T) string {
|
|
tempDir := t.TempDir()
|
|
statePath := filepath.Join(tempDir, "state.yaml")
|
|
content := `invalid: yaml: [[[`
|
|
if err := os.WriteFile(statePath, []byte(content), 0644); err != nil {
|
|
t.Fatalf("setup failed: %v", err)
|
|
}
|
|
return statePath
|
|
},
|
|
errContains: "parsing config file",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
statePath := tt.setupFunc(t)
|
|
_, err := LoadState(statePath)
|
|
|
|
if err == nil {
|
|
t.Error("expected error, got nil")
|
|
} else if !strings.Contains(err.Error(), tt.errContains) {
|
|
t.Errorf("error %q does not contain %q", err.Error(), tt.errContains)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test: SaveState saves state correctly
|
|
func TestSaveState(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config *GlobalConfig
|
|
verify func(t *testing.T, statePath string)
|
|
}{
|
|
{
|
|
name: "saves complete state",
|
|
config: func() *GlobalConfig {
|
|
cfg := &GlobalConfig{}
|
|
cfg.Operator.Email = "admin@example.com"
|
|
cfg.Cloud.Central.Domain = "central.example.com"
|
|
return cfg
|
|
}(),
|
|
verify: func(t *testing.T, statePath string) {
|
|
content, err := os.ReadFile(statePath)
|
|
if err != nil {
|
|
t.Fatalf("failed to read saved state: %v", err)
|
|
}
|
|
contentStr := string(content)
|
|
if !strings.Contains(contentStr, "admin@example.com") {
|
|
t.Error("saved state missing operator email")
|
|
}
|
|
if !strings.Contains(contentStr, "central.example.com") {
|
|
t.Error("saved state missing central domain")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "saves empty state",
|
|
config: &GlobalConfig{},
|
|
verify: func(t *testing.T, statePath string) {
|
|
if _, err := os.Stat(statePath); os.IsNotExist(err) {
|
|
t.Error("state file not created")
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
statePath := filepath.Join(tempDir, "subdir", "state.yaml")
|
|
|
|
err := SaveState(tt.config, statePath)
|
|
if err != nil {
|
|
t.Errorf("SaveState failed: %v", err)
|
|
return
|
|
}
|
|
|
|
// Verify file exists
|
|
if _, err := os.Stat(statePath); err != nil {
|
|
t.Errorf("state file not created: %v", err)
|
|
return
|
|
}
|
|
|
|
// Verify file permissions
|
|
info, err := os.Stat(statePath)
|
|
if err != nil {
|
|
t.Fatalf("failed to stat state file: %v", err)
|
|
}
|
|
if info.Mode().Perm() != 0644 {
|
|
t.Errorf("expected permissions 0644, got %v", info.Mode().Perm())
|
|
}
|
|
|
|
// Verify content can be loaded back
|
|
loadedConfig, err := LoadState(statePath)
|
|
if err != nil {
|
|
t.Errorf("failed to reload saved state: %v", err)
|
|
} else if loadedConfig == nil {
|
|
t.Error("loaded config is nil")
|
|
}
|
|
|
|
if tt.verify != nil {
|
|
tt.verify(t, statePath)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test: SaveState creates directory
|
|
func TestSaveState_CreatesDirectory(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
statePath := filepath.Join(tempDir, "nested", "dirs", "state.yaml")
|
|
|
|
config := &GlobalConfig{}
|
|
err := SaveState(config, statePath)
|
|
if err != nil {
|
|
t.Fatalf("SaveState failed: %v", err)
|
|
}
|
|
|
|
// Verify nested directories were created
|
|
if _, err := os.Stat(filepath.Dir(statePath)); err != nil {
|
|
t.Errorf("directory not created: %v", err)
|
|
}
|
|
|
|
// Verify file exists
|
|
if _, err := os.Stat(statePath); err != nil {
|
|
t.Errorf("state file not created: %v", err)
|
|
}
|
|
}
|
|
|
|
// Test: GlobalConfig.IsEmpty checks if config is empty
|
|
func TestGlobalConfig_IsEmpty(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config *GlobalConfig
|
|
want bool
|
|
}{
|
|
{
|
|
name: "nil config is empty",
|
|
config: nil,
|
|
want: true,
|
|
},
|
|
{
|
|
name: "default config is empty",
|
|
config: &GlobalConfig{},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "config with only central domain is not empty",
|
|
config: func() *GlobalConfig {
|
|
cfg := &GlobalConfig{}
|
|
cfg.Cloud.Central.Domain = "central.example.com"
|
|
return cfg
|
|
}(),
|
|
want: false,
|
|
},
|
|
{
|
|
name: "config with only operator email is not empty",
|
|
config: func() *GlobalConfig {
|
|
cfg := &GlobalConfig{}
|
|
cfg.Operator.Email = "admin@example.com"
|
|
return cfg
|
|
}(),
|
|
want: false,
|
|
},
|
|
{
|
|
name: "config with all fields is not empty",
|
|
config: func() *GlobalConfig {
|
|
cfg := &GlobalConfig{}
|
|
cfg.Cloud.Central.Domain = "central.example.com"
|
|
cfg.Operator.Email = "admin@example.com"
|
|
return cfg
|
|
}(),
|
|
want: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := tt.config.IsEmpty()
|
|
if got != tt.want {
|
|
t.Errorf("IsEmpty() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test: LoadCloudConfig loads instance configuration
|
|
func TestLoadCloudConfig(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
configYAML string
|
|
verify func(t *testing.T, config *InstanceConfig)
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "loads complete instance configuration",
|
|
configYAML: `cloud:
|
|
dhcpRange: "192.168.1.100,192.168.1.200"
|
|
baseDomain: "example.com"
|
|
domain: "home"
|
|
internalDomain: "internal.example.com"
|
|
cluster:
|
|
name: "my-cluster"
|
|
loadBalancerIp: "192.168.1.10"
|
|
nodes:
|
|
talos:
|
|
version: "v1.8.0"
|
|
activeNodes:
|
|
- node1:
|
|
role: "control"
|
|
interface: "eth0"
|
|
disk: "/dev/sda"
|
|
`,
|
|
verify: func(t *testing.T, config *InstanceConfig) {
|
|
if config.Cloud.BaseDomain != "example.com" {
|
|
t.Error("base domain not loaded correctly")
|
|
}
|
|
if config.Cloud.DHCPRange != "192.168.1.100,192.168.1.200" {
|
|
t.Error("DHCP range not loaded correctly")
|
|
}
|
|
if config.Cluster.Name != "my-cluster" {
|
|
t.Error("cluster name not loaded correctly")
|
|
}
|
|
if config.Cluster.Nodes.Talos.Version != "v1.8.0" {
|
|
t.Error("talos version not loaded correctly")
|
|
}
|
|
},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
configPath := filepath.Join(tempDir, "config.yaml")
|
|
|
|
if err := os.WriteFile(configPath, []byte(tt.configYAML), 0644); err != nil {
|
|
t.Fatalf("setup failed: %v", err)
|
|
}
|
|
|
|
config, err := LoadCloudConfig(configPath)
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Error("expected error, got nil")
|
|
}
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
return
|
|
}
|
|
|
|
if config == nil {
|
|
t.Fatal("config is nil")
|
|
}
|
|
|
|
if tt.verify != nil {
|
|
tt.verify(t, config)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test: LoadCloudConfig error cases
|
|
func TestLoadCloudConfig_Errors(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupFunc func(t *testing.T) string
|
|
errContains string
|
|
}{
|
|
{
|
|
name: "non-existent file",
|
|
setupFunc: func(t *testing.T) string {
|
|
return filepath.Join(t.TempDir(), "nonexistent.yaml")
|
|
},
|
|
errContains: "reading config file",
|
|
},
|
|
{
|
|
name: "invalid yaml",
|
|
setupFunc: func(t *testing.T) string {
|
|
tempDir := t.TempDir()
|
|
configPath := filepath.Join(tempDir, "config.yaml")
|
|
content := `invalid: yaml: [[[`
|
|
if err := os.WriteFile(configPath, []byte(content), 0644); err != nil {
|
|
t.Fatalf("setup failed: %v", err)
|
|
}
|
|
return configPath
|
|
},
|
|
errContains: "parsing config file",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
configPath := tt.setupFunc(t)
|
|
_, err := LoadCloudConfig(configPath)
|
|
|
|
if err == nil {
|
|
t.Error("expected error, got nil")
|
|
} else if !strings.Contains(err.Error(), tt.errContains) {
|
|
t.Errorf("error %q does not contain %q", err.Error(), tt.errContains)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test: SaveCloudConfig saves instance configuration
|
|
func TestSaveCloudConfig(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config *InstanceConfig
|
|
verify func(t *testing.T, configPath string)
|
|
}{
|
|
{
|
|
name: "saves instance configuration",
|
|
config: func() *InstanceConfig {
|
|
cfg := &InstanceConfig{}
|
|
cfg.Cloud.BaseDomain = "example.com"
|
|
cfg.Cloud.Domain = "home"
|
|
return cfg
|
|
}(),
|
|
verify: func(t *testing.T, configPath string) {
|
|
content, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to read saved config: %v", err)
|
|
}
|
|
contentStr := string(content)
|
|
if !strings.Contains(contentStr, "example.com") {
|
|
t.Error("saved config missing base domain")
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
configPath := filepath.Join(tempDir, "subdir", "config.yaml")
|
|
|
|
err := SaveCloudConfig(tt.config, configPath)
|
|
if err != nil {
|
|
t.Errorf("SaveCloudConfig failed: %v", err)
|
|
return
|
|
}
|
|
|
|
// Verify file exists
|
|
if _, err := os.Stat(configPath); err != nil {
|
|
t.Errorf("config file not created: %v", err)
|
|
return
|
|
}
|
|
|
|
// Verify content can be loaded back
|
|
loadedConfig, err := LoadCloudConfig(configPath)
|
|
if err != nil {
|
|
t.Errorf("failed to reload saved config: %v", err)
|
|
} else if loadedConfig == nil {
|
|
t.Error("loaded config is nil")
|
|
}
|
|
|
|
if tt.verify != nil {
|
|
tt.verify(t, configPath)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test: Round-trip save and load preserves data
|
|
func TestGlobalConfig_RoundTrip(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
statePath := filepath.Join(tempDir, "state.yaml")
|
|
|
|
// Create config with all fields
|
|
original := &GlobalConfig{}
|
|
original.Operator.Email = "admin@example.com"
|
|
original.Cloud.Central.Domain = "central.example.com"
|
|
|
|
// Save
|
|
if err := SaveState(original, statePath); err != nil {
|
|
t.Fatalf("SaveState failed: %v", err)
|
|
}
|
|
|
|
// Load
|
|
loaded, err := LoadState(statePath)
|
|
if err != nil {
|
|
t.Fatalf("LoadState failed: %v", err)
|
|
}
|
|
|
|
// Verify all fields match
|
|
if loaded.Operator.Email != original.Operator.Email {
|
|
t.Errorf("email mismatch: got %q, want %q", loaded.Operator.Email, original.Operator.Email)
|
|
}
|
|
if loaded.Cloud.Central.Domain != original.Cloud.Central.Domain {
|
|
t.Errorf("central domain mismatch: got %q, want %q", loaded.Cloud.Central.Domain, original.Cloud.Central.Domain)
|
|
}
|
|
}
|
|
|
|
// Test: Round-trip save and load for instance config
|
|
func TestInstanceConfig_RoundTrip(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
configPath := filepath.Join(tempDir, "config.yaml")
|
|
|
|
// Create instance config
|
|
original := &InstanceConfig{}
|
|
original.Cloud.BaseDomain = "example.com"
|
|
original.Cloud.Domain = "home"
|
|
original.Cluster.Name = "my-cluster"
|
|
|
|
// Save config
|
|
if err := SaveCloudConfig(original, configPath); err != nil {
|
|
t.Fatalf("SaveCloudConfig failed: %v", err)
|
|
}
|
|
|
|
// Load config
|
|
loaded, err := LoadCloudConfig(configPath)
|
|
if err != nil {
|
|
t.Fatalf("LoadCloudConfig failed: %v", err)
|
|
}
|
|
|
|
// Verify fields match
|
|
if loaded.Cloud.BaseDomain != original.Cloud.BaseDomain {
|
|
t.Errorf("base domain mismatch: got %q, want %q", loaded.Cloud.BaseDomain, original.Cloud.BaseDomain)
|
|
}
|
|
if loaded.Cluster.Name != original.Cluster.Name {
|
|
t.Errorf("cluster name mismatch: got %q, want %q", loaded.Cluster.Name, original.Cluster.Name)
|
|
}
|
|
}
|
|
|
|
func TestDeepMerge(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
dst map[string]any
|
|
src map[string]any
|
|
expected map[string]any
|
|
}{
|
|
{
|
|
name: "src overrides dst flat keys",
|
|
dst: map[string]any{"a": "1", "b": "2"},
|
|
src: map[string]any{"b": "3", "c": "4"},
|
|
expected: map[string]any{"a": "1", "b": "3", "c": "4"},
|
|
},
|
|
{
|
|
name: "nested maps merge recursively",
|
|
dst: map[string]any{
|
|
"cloud": map[string]any{
|
|
"central": map[string]any{"domain": "central.example.com"},
|
|
},
|
|
},
|
|
src: map[string]any{
|
|
"cloud": map[string]any{
|
|
"domain": "example.com",
|
|
},
|
|
},
|
|
expected: map[string]any{
|
|
"cloud": map[string]any{
|
|
"central": map[string]any{"domain": "central.example.com"},
|
|
"domain": "example.com",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "src nested key overrides dst nested key",
|
|
dst: map[string]any{
|
|
"cloud": map[string]any{"domain": "old.com"},
|
|
},
|
|
src: map[string]any{
|
|
"cloud": map[string]any{"domain": "new.com"},
|
|
},
|
|
expected: map[string]any{
|
|
"cloud": map[string]any{"domain": "new.com"},
|
|
},
|
|
},
|
|
{
|
|
name: "empty src returns dst",
|
|
dst: map[string]any{"a": "1"},
|
|
src: map[string]any{},
|
|
expected: map[string]any{"a": "1"},
|
|
},
|
|
{
|
|
name: "empty dst returns src",
|
|
dst: map[string]any{},
|
|
src: map[string]any{"a": "1"},
|
|
expected: map[string]any{"a": "1"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := DeepMerge(tt.dst, tt.src)
|
|
resultYAML, _ := yaml.Marshal(result)
|
|
expectedYAML, _ := yaml.Marshal(tt.expected)
|
|
if string(resultYAML) != string(expectedYAML) {
|
|
t.Errorf("DeepMerge() =\n%s\nwant:\n%s", resultYAML, expectedYAML)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLoadMergedInstanceConfig(t *testing.T) {
|
|
dataDir := t.TempDir()
|
|
instanceName := "test"
|
|
instanceConfigDir := filepath.Join(dataDir, "instances", instanceName, "config")
|
|
os.MkdirAll(instanceConfigDir, 0755)
|
|
|
|
globalState := `operator:
|
|
email: test@example.com
|
|
cloud:
|
|
central:
|
|
domain: central.example.com
|
|
`
|
|
instanceConfig := `cluster:
|
|
name: test
|
|
nodes:
|
|
control:
|
|
vip: 192.168.1.100
|
|
cloud:
|
|
domain: cloud.example.com
|
|
`
|
|
|
|
os.WriteFile(filepath.Join(dataDir, "state.yaml"), []byte(globalState), 0644)
|
|
os.WriteFile(filepath.Join(instanceConfigDir, "instance.yaml"), []byte(instanceConfig), 0644)
|
|
|
|
merged, err := LoadMergedInstanceConfig(dataDir, instanceName)
|
|
if err != nil {
|
|
t.Fatalf("LoadMergedInstanceConfig() error: %v", err)
|
|
}
|
|
|
|
cloud, ok := merged["cloud"].(map[string]any)
|
|
if !ok {
|
|
t.Fatal("missing cloud key in merged config")
|
|
}
|
|
|
|
central, ok := cloud["central"].(map[string]any)
|
|
if !ok {
|
|
t.Fatal("missing cloud.central key — global state not merged")
|
|
}
|
|
if central["domain"] != "central.example.com" {
|
|
t.Errorf("cloud.central.domain = %v, want central.example.com", central["domain"])
|
|
}
|
|
|
|
if cloud["domain"] != "cloud.example.com" {
|
|
t.Errorf("cloud.domain = %v, want cloud.example.com", cloud["domain"])
|
|
}
|
|
|
|
cluster, ok := merged["cluster"].(map[string]any)
|
|
if !ok {
|
|
t.Fatal("missing cluster key — instance config not merged")
|
|
}
|
|
if cluster["name"] != "test" {
|
|
t.Errorf("cluster.name = %v, want test", cluster["name"])
|
|
}
|
|
|
|
operator, ok := merged["operator"].(map[string]any)
|
|
if !ok {
|
|
t.Fatal("missing operator key — global state not merged")
|
|
}
|
|
if operator["email"] != "test@example.com" {
|
|
t.Errorf("operator.email = %v, want test@example.com", operator["email"])
|
|
}
|
|
}
|
|
|
|
func TestLoadMergedInstanceConfig_NoGlobalState(t *testing.T) {
|
|
dataDir := t.TempDir()
|
|
instanceConfigDir := filepath.Join(dataDir, "instances", "test", "config")
|
|
os.MkdirAll(instanceConfigDir, 0755)
|
|
|
|
os.WriteFile(filepath.Join(instanceConfigDir, "instance.yaml"), []byte("cluster:\n name: test\n"), 0644)
|
|
|
|
merged, err := LoadMergedInstanceConfig(dataDir, "test")
|
|
if err != nil {
|
|
t.Fatalf("LoadMergedInstanceConfig() error: %v", err)
|
|
}
|
|
|
|
cluster, ok := merged["cluster"].(map[string]any)
|
|
if !ok {
|
|
t.Fatal("missing cluster key")
|
|
}
|
|
if cluster["name"] != "test" {
|
|
t.Errorf("cluster.name = %v, want test", cluster["name"])
|
|
}
|
|
}
|