Replace InstanceConfig with DNSEntry for dnsmasq config generation
InstanceConfig was a ~50-field struct designed for Wild Cloud k8s instances,
but only 3 fields were ever read by dnsmasq (the sole consumer). The
reconciliation bridge constructed fake InstanceConfig objects from registered
domains just to satisfy this interface.
Replace with DNSEntry{Domain, IP} — a 2-field struct purpose-built for
dnsmasq. All domains get local=/ directives to prevent AAAA queries from
leaking to upstream DNS (Happy Eyeballs / RFC 8305 latency on LAN).
Removed dead code: InstanceConfig, NodeConfig, LoadCloudConfig,
SaveCloudConfig, DeepMerge, LoadMergedInstanceConfig, EnsureInstanceConfig,
instance path helpers, instanceLoadBalancerIP, GenerateInstanceConfig,
and all modular per-instance dnsmasq methods.
This commit is contained in:
@@ -3,7 +3,6 @@ package config
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -158,168 +157,3 @@ func (c *State) IsEmpty() bool {
|
||||
return c.Cloud.Central.Domain == "" && c.Operator.Email == ""
|
||||
}
|
||||
|
||||
type NodeConfig struct {
|
||||
Role string `yaml:"role" json:"role"`
|
||||
Interface string `yaml:"interface" json:"interface"`
|
||||
Disk string `yaml:"disk" json:"disk"`
|
||||
CurrentIp string `yaml:"currentIp" json:"currentIp"`
|
||||
}
|
||||
|
||||
type InstanceConfig struct {
|
||||
Operator struct {
|
||||
Email string `yaml:"email" json:"email"`
|
||||
} `yaml:"operator" json:"operator"`
|
||||
Cloud struct {
|
||||
BaseDomain string `yaml:"baseDomain" json:"baseDomain"`
|
||||
Domain string `yaml:"domain" json:"domain"`
|
||||
InternalDomain string `yaml:"internalDomain" json:"internalDomain"`
|
||||
DHCPRange string `yaml:"dhcpRange" json:"dhcpRange"`
|
||||
NFS struct {
|
||||
Host string `yaml:"host" json:"host"`
|
||||
MediaPath string `yaml:"mediaPath" json:"mediaPath"`
|
||||
StorageCapacity string `yaml:"storageCapacity" json:"storageCapacity"`
|
||||
} `yaml:"nfs" json:"nfs"`
|
||||
DockerRegistryHost string `yaml:"dockerRegistryHost" json:"dockerRegistryHost"`
|
||||
} `yaml:"cloud" json:"cloud"`
|
||||
Cluster struct {
|
||||
Name string `yaml:"name" json:"name"`
|
||||
LoadBalancerIp string `yaml:"loadBalancerIp" json:"loadBalancerIp"`
|
||||
IpAddressPool string `yaml:"ipAddressPool" json:"ipAddressPool"`
|
||||
HostnamePrefix string `yaml:"hostnamePrefix" json:"hostnamePrefix"`
|
||||
CertManager struct {
|
||||
Cloudflare struct {
|
||||
Domain string `yaml:"domain" json:"domain"`
|
||||
} `yaml:"cloudflare" json:"cloudflare"`
|
||||
} `yaml:"certManager" json:"certManager"`
|
||||
ExternalDns struct {
|
||||
OwnerId string `yaml:"ownerId" json:"ownerId"`
|
||||
} `yaml:"externalDns" json:"externalDns"`
|
||||
InternalDns struct {
|
||||
ExternalResolver string `yaml:"externalResolver" json:"externalResolver"`
|
||||
} `yaml:"internalDns" json:"internalDns"`
|
||||
DockerRegistry struct {
|
||||
Storage string `yaml:"storage" json:"storage"`
|
||||
} `yaml:"dockerRegistry" json:"dockerRegistry"`
|
||||
Nodes struct {
|
||||
Talos struct {
|
||||
Version string `yaml:"version" json:"version"`
|
||||
SchematicId string `yaml:"schematicId" json:"schematicId"`
|
||||
} `yaml:"talos" json:"talos"`
|
||||
Control struct {
|
||||
Vip string `yaml:"vip" json:"vip"`
|
||||
} `yaml:"control" json:"control"`
|
||||
Active map[string]NodeConfig `yaml:"active" json:"active"`
|
||||
} `yaml:"nodes" json:"nodes"`
|
||||
} `yaml:"cluster" json:"cluster"`
|
||||
Apps map[string]any `yaml:"apps" json:"apps"`
|
||||
}
|
||||
|
||||
func LoadCloudConfig(configPath string) (*InstanceConfig, error) {
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading config file %s: %w", configPath, err)
|
||||
}
|
||||
|
||||
config := &InstanceConfig{}
|
||||
if err := yaml.Unmarshal(data, config); err != nil {
|
||||
return nil, fmt.Errorf("parsing config file: %w", err)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func SaveCloudConfig(config *InstanceConfig, configPath string) error {
|
||||
// Ensure the directory exists
|
||||
if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
|
||||
return fmt.Errorf("creating config directory: %w", err)
|
||||
}
|
||||
|
||||
data, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshaling config: %w", err)
|
||||
}
|
||||
|
||||
return os.WriteFile(configPath, data, 0644)
|
||||
}
|
||||
|
||||
// DeepMerge recursively merges src into dst, with src values taking precedence.
|
||||
// Nested maps are merged recursively; all other types are overwritten by src.
|
||||
func DeepMerge(dst, src map[string]any) map[string]any {
|
||||
result := make(map[string]any)
|
||||
for k, v := range dst {
|
||||
result[k] = v
|
||||
}
|
||||
for k, v := range src {
|
||||
if srcMap, ok := v.(map[string]any); ok {
|
||||
if dstMap, ok := result[k].(map[string]any); ok {
|
||||
result[k] = DeepMerge(dstMap, srcMap)
|
||||
continue
|
||||
}
|
||||
}
|
||||
result[k] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// LoadMergedInstanceConfig loads the global config as a base and merges the
|
||||
// instance config on top. Returns the merged result as an untyped map suitable
|
||||
// for passing to gomplate as template context.
|
||||
// If the global config is missing, returns the instance config alone.
|
||||
// Assembles apps.* from config/apps/*/config.yaml files.
|
||||
func LoadMergedInstanceConfig(dataDir, instanceName string) (map[string]any, error) {
|
||||
instanceConfigPath := filepath.Join(dataDir, "instances", instanceName, "config", "instance.yaml")
|
||||
globalConfigPath := filepath.Join(dataDir, "state.yaml")
|
||||
|
||||
instanceData, err := os.ReadFile(instanceConfigPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading instance config %s: %w", instanceConfigPath, err)
|
||||
}
|
||||
|
||||
var instanceMap map[string]any
|
||||
if err := yaml.Unmarshal(instanceData, &instanceMap); err != nil {
|
||||
return nil, fmt.Errorf("parsing instance config: %w", err)
|
||||
}
|
||||
if instanceMap == nil {
|
||||
instanceMap = make(map[string]any)
|
||||
}
|
||||
|
||||
// Assemble apps.* map from per-app config files
|
||||
appsConfigDir := filepath.Join(dataDir, "instances", instanceName, "config", "apps")
|
||||
if entries, err := os.ReadDir(appsConfigDir); err == nil {
|
||||
appsMap := make(map[string]any)
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
appName := entry.Name()
|
||||
appConfigPath := filepath.Join(dataDir, "instances", instanceName, "config", "apps", appName, "config.yaml")
|
||||
appData, err := os.ReadFile(appConfigPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var appConfig map[string]any
|
||||
if err := yaml.Unmarshal(appData, &appConfig); err == nil && appConfig != nil {
|
||||
appsMap[appName] = appConfig
|
||||
}
|
||||
}
|
||||
if len(appsMap) > 0 {
|
||||
instanceMap["apps"] = appsMap
|
||||
}
|
||||
}
|
||||
|
||||
globalData, err := os.ReadFile(globalConfigPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
slog.Warn("no global config found, using instance config only", "path", globalConfigPath)
|
||||
return instanceMap, nil
|
||||
}
|
||||
return nil, fmt.Errorf("reading global config %s: %w", globalConfigPath, err)
|
||||
}
|
||||
|
||||
var globalMap map[string]any
|
||||
if err := yaml.Unmarshal(globalData, &globalMap); err != nil {
|
||||
return nil, fmt.Errorf("parsing global config: %w", err)
|
||||
}
|
||||
|
||||
return DeepMerge(globalMap, instanceMap), nil
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Test: LoadState loads valid state
|
||||
@@ -295,187 +293,6 @@ func TestState_IsEmpty(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 TestState_RoundTrip(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
@@ -506,187 +323,3 @@ func TestState_RoundTrip(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,31 +87,6 @@ func NewManager() *Manager {
|
||||
}
|
||||
}
|
||||
|
||||
// EnsureInstanceConfig ensures an instance config file exists with proper structure
|
||||
func (m *Manager) EnsureInstanceConfig(name string, dataDir string) error {
|
||||
configPath := filepath.Join(dataDir, "instances", name, "config", "instance.yaml")
|
||||
|
||||
// Check if config already exists
|
||||
if storage.FileExists(configPath) {
|
||||
// Validate existing config
|
||||
if err := m.yq.Validate(configPath); err != nil {
|
||||
return fmt.Errorf("invalid config file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure config directory exists
|
||||
if err := storage.EnsureDir(filepath.Join(dataDir, "instances", name, "config"), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
initialConfig := &InstanceConfig{}
|
||||
initialConfig.Cluster.Name = name
|
||||
initialConfig.Cluster.Nodes.Active = make(map[string]NodeConfig)
|
||||
initialConfig.Apps = make(map[string]any)
|
||||
return SaveCloudConfig(initialConfig, configPath)
|
||||
}
|
||||
|
||||
// GetConfigValue retrieves a value from a config file
|
||||
func (m *Manager) GetConfigValue(configPath, key string) (string, error) {
|
||||
if !storage.FileExists(configPath) {
|
||||
@@ -186,17 +161,3 @@ func (m *Manager) CopyConfig(srcPath, dstPath string) error {
|
||||
return storage.WriteFile(dstPath, content, 0644)
|
||||
}
|
||||
|
||||
// GetInstanceConfigPath returns the path to an instance's config file
|
||||
func GetInstanceConfigPath(dataDir, instanceName string) string {
|
||||
return filepath.Join(dataDir, "instances", instanceName, "config", "instance.yaml")
|
||||
}
|
||||
|
||||
// GetInstanceSecretsPath returns the path to an instance's secrets file
|
||||
func GetInstanceSecretsPath(dataDir, instanceName string) string {
|
||||
return filepath.Join(dataDir, "instances", instanceName, "config", "secrets.yaml")
|
||||
}
|
||||
|
||||
// GetInstancePath returns the path to an instance directory
|
||||
func GetInstancePath(dataDir, instanceName string) string {
|
||||
return filepath.Join(dataDir, "instances", instanceName)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -18,139 +17,6 @@ func TestNewManager(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test: EnsureInstanceConfig creates config file with proper structure
|
||||
func TestEnsureInstanceConfig(t *testing.T) {
|
||||
const instanceName = "test-instance"
|
||||
tests := []struct {
|
||||
name string
|
||||
setupFunc func(t *testing.T, dataDir string)
|
||||
wantErr bool
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "creates config when not exists",
|
||||
setupFunc: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "returns nil when config exists",
|
||||
setupFunc: func(t *testing.T, dataDir string) {
|
||||
configPath := filepath.Join(dataDir, "instances", instanceName, "config", "instance.yaml")
|
||||
if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
|
||||
t.Fatalf("setup mkdir failed: %v", err)
|
||||
}
|
||||
content := `operator:
|
||||
email: ""
|
||||
cloud:
|
||||
baseDomain: "test.local"
|
||||
domain: "test"
|
||||
internalDomain: "internal.test"
|
||||
dhcpRange: ""
|
||||
dns:
|
||||
ip: ""
|
||||
router:
|
||||
ip: ""
|
||||
dnsmasq:
|
||||
interface: ""
|
||||
nfs:
|
||||
host: ""
|
||||
mediaPath: ""
|
||||
storageCapacity: ""
|
||||
dockerRegistryHost: ""
|
||||
cluster:
|
||||
name: ""
|
||||
loadBalancerIp: ""
|
||||
ipAddressPool: ""
|
||||
hostnamePrefix: ""
|
||||
certManager:
|
||||
cloudflare:
|
||||
domain: ""
|
||||
externalDns:
|
||||
ownerId: ""
|
||||
dockerRegistry:
|
||||
storage: ""
|
||||
nodes:
|
||||
talos:
|
||||
version: ""
|
||||
schematicId: ""
|
||||
control:
|
||||
vip: ""
|
||||
active: {}
|
||||
apps: {}
|
||||
`
|
||||
if err := storage.WriteFile(configPath, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("setup failed: %v", err)
|
||||
}
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "returns error when config is invalid yaml",
|
||||
setupFunc: func(t *testing.T, dataDir string) {
|
||||
configPath := filepath.Join(dataDir, "instances", instanceName, "config", "instance.yaml")
|
||||
if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
|
||||
t.Fatalf("setup mkdir failed: %v", err)
|
||||
}
|
||||
if err := storage.WriteFile(configPath, []byte(`invalid: yaml: content: [[[`), 0644); err != nil {
|
||||
t.Fatalf("setup failed: %v", err)
|
||||
}
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "invalid config file",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
dataDir := t.TempDir()
|
||||
m := NewManager()
|
||||
|
||||
if tt.setupFunc != nil {
|
||||
tt.setupFunc(t, dataDir)
|
||||
}
|
||||
|
||||
err := m.EnsureInstanceConfig(instanceName, dataDir)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Error("expected error, got nil")
|
||||
} else if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
|
||||
t.Errorf("error %q does not contain %q", err.Error(), tt.errContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify config file exists
|
||||
configPath := filepath.Join(dataDir, "instances", instanceName, "config", "instance.yaml")
|
||||
if !storage.FileExists(configPath) {
|
||||
t.Error("config file not created")
|
||||
}
|
||||
|
||||
// Verify config is valid YAML
|
||||
if err := m.ValidateConfig(configPath); err != nil {
|
||||
t.Errorf("config validation failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify config has expected structure (canonical nested format)
|
||||
content, err := storage.ReadFile(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read config: %v", err)
|
||||
}
|
||||
contentStr := string(content)
|
||||
requiredFields := []string{"operator:", "cloud:", "cluster:", "apps:"}
|
||||
for _, field := range requiredFields {
|
||||
if !strings.Contains(contentStr, field) {
|
||||
t.Errorf("config missing required field: %s", field)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test: GetConfigValue retrieves values correctly
|
||||
func TestGetConfigValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
@@ -821,102 +687,3 @@ func TestCopyConfig_Errors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test: File permissions are preserved
|
||||
func TestEnsureInstanceConfig_FilePermissions(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
m := NewManager()
|
||||
|
||||
if err := m.EnsureInstanceConfig("my-wild-cloud", tempDir); err != nil {
|
||||
t.Fatalf("EnsureInstanceConfig failed: %v", err)
|
||||
}
|
||||
|
||||
configPath := filepath.Join(tempDir, "instances", "my-wild-cloud", "config", "instance.yaml")
|
||||
info, err := os.Stat(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to stat config file: %v", err)
|
||||
}
|
||||
|
||||
// Verify file has 0644 permissions
|
||||
if info.Mode().Perm() != 0644 {
|
||||
t.Errorf("expected permissions 0644, got %v", info.Mode().Perm())
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Idempotent config creation
|
||||
func TestEnsureInstanceConfig_Idempotent(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
m := NewManager()
|
||||
|
||||
// First call creates config
|
||||
if err := m.EnsureInstanceConfig("my-wild-cloud", tempDir); err != nil {
|
||||
t.Fatalf("first EnsureInstanceConfig failed: %v", err)
|
||||
}
|
||||
|
||||
configPath := filepath.Join(tempDir, "instances", "my-wild-cloud", "config", "instance.yaml")
|
||||
firstContent, err := storage.ReadFile(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read config: %v", err)
|
||||
}
|
||||
|
||||
// Second call should not modify config
|
||||
if err := m.EnsureInstanceConfig("my-wild-cloud", tempDir); err != nil {
|
||||
t.Fatalf("second EnsureInstanceConfig failed: %v", err)
|
||||
}
|
||||
|
||||
secondContent, err := storage.ReadFile(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read config: %v", err)
|
||||
}
|
||||
|
||||
if string(firstContent) != string(secondContent) {
|
||||
t.Error("config content changed on second call")
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Config structure contains all required fields
|
||||
func TestEnsureInstanceConfig_RequiredFields(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
m := NewManager()
|
||||
|
||||
if err := m.EnsureInstanceConfig("my-wild-cloud", tempDir); err != nil {
|
||||
t.Fatalf("EnsureInstanceConfig failed: %v", err)
|
||||
}
|
||||
|
||||
configPath := filepath.Join(tempDir, "instances", "my-wild-cloud", "config", "instance.yaml")
|
||||
content, err := storage.ReadFile(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read config: %v", err)
|
||||
}
|
||||
|
||||
contentStr := string(content)
|
||||
requiredFields := []string{
|
||||
"operator:",
|
||||
"cloud:",
|
||||
"baseDomain:",
|
||||
"domain:",
|
||||
"internalDomain:",
|
||||
"dhcpRange:",
|
||||
"nfs:",
|
||||
"cluster:",
|
||||
"loadBalancerIp:",
|
||||
"ipAddressPool:",
|
||||
"hostnamePrefix:",
|
||||
"certManager:",
|
||||
"externalDns:",
|
||||
"dockerRegistry:",
|
||||
"nodes:",
|
||||
"talos:",
|
||||
"version:",
|
||||
"schematicId:",
|
||||
"control:",
|
||||
"vip:",
|
||||
"active:",
|
||||
"apps:",
|
||||
}
|
||||
|
||||
for _, field := range requiredFields {
|
||||
if !strings.Contains(contentStr, field) {
|
||||
t.Errorf("config missing required field: %s", field)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user