feat: Extract Wild Central as standalone Go service

Extract the Central networking functionality from wild-cloud/api into
a standalone service. Wild Central manages DNS (dnsmasq), gateway
(HAProxy), firewall (nftables), VPN (WireGuard), TLS (certbot),
security (CrowdSec), and DDNS — all the network appliance concerns.

All Cloud-specific code (instances, clusters, nodes, apps, backups,
operations, kubectl/talosctl tooling) has been removed. The API struct
and route registration contain only Central endpoints. Tests updated
to match the new API signature.

Builds and all tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-08 23:31:16 +00:00
commit beb643f76f
52 changed files with 12814 additions and 0 deletions

View File

@@ -0,0 +1,699 @@
package config
import (
"os"
"path/filepath"
"strings"
"testing"
"gopkg.in/yaml.v3"
)
// Test: LoadGlobalConfig loads valid configuration
func TestLoadGlobalConfig(t *testing.T) {
tests := []struct {
name string
configYAML string
verify func(t *testing.T, config *GlobalConfig)
wantErr bool
}{
{
name: "loads complete configuration",
configYAML: `operator:
email: "admin@example.com"
cloud:
router:
ip: "192.168.1.254"
dynamicDns: "example.dyndns.org"
`,
verify: func(t *testing.T, config *GlobalConfig) {
if config.Operator.Email != "admin@example.com" {
t.Error("operator email not loaded correctly")
}
if config.Cloud.Router.IP != "192.168.1.254" {
t.Error("router IP not loaded correctly")
}
},
wantErr: false,
},
{
name: "loads minimal configuration",
configYAML: `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 configuration",
configYAML: `{}
`,
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()
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 := LoadGlobalConfig(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: LoadGlobalConfig error cases
func TestLoadGlobalConfig_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 := LoadGlobalConfig(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: SaveGlobalConfig saves configuration correctly
func TestSaveGlobalConfig(t *testing.T) {
tests := []struct {
name string
config *GlobalConfig
verify func(t *testing.T, configPath string)
}{
{
name: "saves complete configuration",
config: func() *GlobalConfig {
cfg := &GlobalConfig{}
cfg.Operator.Email = "admin@example.com"
cfg.Cloud.Router.IP = "192.168.1.254"
cfg.Cloud.Router.DynamicDns = "example.dyndns.org"
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, "admin@example.com") {
t.Error("saved config missing operator email")
}
if !strings.Contains(contentStr, "192.168.1.254") {
t.Error("saved config missing router IP")
}
},
},
{
name: "saves empty configuration",
config: &GlobalConfig{},
verify: func(t *testing.T, configPath string) {
if _, err := os.Stat(configPath); os.IsNotExist(err) {
t.Error("config file not created")
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "subdir", "config.yaml")
err := SaveGlobalConfig(tt.config, configPath)
if err != nil {
t.Errorf("SaveGlobalConfig failed: %v", err)
return
}
// Verify file exists
if _, err := os.Stat(configPath); err != nil {
t.Errorf("config file not created: %v", err)
return
}
// Verify file permissions
info, err := os.Stat(configPath)
if err != nil {
t.Fatalf("failed to stat config 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 := LoadGlobalConfig(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: SaveGlobalConfig creates directory
func TestSaveGlobalConfig_CreatesDirectory(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "nested", "dirs", "config.yaml")
config := &GlobalConfig{}
err := SaveGlobalConfig(config, configPath)
if err != nil {
t.Fatalf("SaveGlobalConfig failed: %v", err)
}
// Verify nested directories were created
if _, err := os.Stat(filepath.Dir(configPath)); err != nil {
t.Errorf("directory not created: %v", err)
}
// Verify file exists
if _, err := os.Stat(configPath); err != nil {
t.Errorf("config 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 router IP is not empty",
config: func() *GlobalConfig {
cfg := &GlobalConfig{}
cfg.Cloud.Router.IP = "192.168.1.254"
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.Router.IP = "192.168.1.254"
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()
configPath := filepath.Join(tempDir, "config.yaml")
// Create config with all fields
original := &GlobalConfig{}
original.Operator.Email = "admin@example.com"
original.Cloud.Router.IP = "192.168.1.254"
original.Cloud.Router.DynamicDns = "example.dyndns.org"
// Save config
if err := SaveGlobalConfig(original, configPath); err != nil {
t.Fatalf("SaveGlobalConfig failed: %v", err)
}
// Load config
loaded, err := LoadGlobalConfig(configPath)
if err != nil {
t.Fatalf("LoadGlobalConfig 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.Router.IP != original.Cloud.Router.IP {
t.Errorf("router IP mismatch: got %q, want %q", loaded.Cloud.Router.IP, original.Cloud.Router.IP)
}
if loaded.Cloud.Router.DynamicDns != original.Cloud.Router.DynamicDns {
t.Errorf("dynamic DNS mismatch: got %q, want %q", loaded.Cloud.Router.DynamicDns, original.Cloud.Router.DynamicDns)
}
}
// 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{
"router": map[string]any{"ip": "192.168.1.1"},
},
},
src: map[string]any{
"cloud": map[string]any{
"domain": "example.com",
},
},
expected: map[string]any{
"cloud": map[string]any{
"router": map[string]any{"ip": "192.168.1.1"},
"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)
globalConfig := `operator:
email: test@example.com
cloud:
router:
ip: 192.168.1.1
`
instanceConfig := `cluster:
name: test
nodes:
control:
vip: 192.168.1.100
cloud:
domain: cloud.example.com
`
os.WriteFile(filepath.Join(dataDir, "config.yaml"), []byte(globalConfig), 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")
}
router, ok := cloud["router"].(map[string]any)
if !ok {
t.Fatal("missing cloud.router key — global config not merged")
}
if router["ip"] != "192.168.1.1" {
t.Errorf("cloud.router.ip = %v, want 192.168.1.1", router["ip"])
}
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 config not merged")
}
if operator["email"] != "test@example.com" {
t.Errorf("operator.email = %v, want test@example.com", operator["email"])
}
}
func TestLoadMergedInstanceConfig_NoGlobalConfig(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"])
}
}