diff --git a/api/internal/api/v1/handlers.go b/api/internal/api/v1/handlers.go index 72fdad7..9f7caed 100644 --- a/api/internal/api/v1/handlers.go +++ b/api/internal/api/v1/handlers.go @@ -39,6 +39,14 @@ type API struct { // NewAPI creates a new API handler with all dependencies // Note: Setup files (cluster-services, cluster-nodes, etc.) are now embedded in the binary func NewAPI(dataDir, appsDir string) (*API, error) { + // Initialize config manager + configMgr := config.NewManager() + + // Ensure global config exists + if err := configMgr.EnsureGlobalConfig(dataDir); err != nil { + return nil, fmt.Errorf("failed to ensure global config: %w", err) + } + // Ensure base directories exist instancesDir := tools.GetInstancesPath(dataDir) if err := os.MkdirAll(instancesDir, 0755); err != nil { @@ -55,7 +63,7 @@ func NewAPI(dataDir, appsDir string) (*API, error) { return &API{ dataDir: dataDir, appsDir: appsDir, - config: config.NewManager(), + config: configMgr, secrets: secrets.NewManager(), context: context.NewManager(dataDir), instance: instance.NewManager(dataDir), diff --git a/api/internal/config/config.go b/api/internal/config/config.go index 27bdd60..21d3317 100644 --- a/api/internal/config/config.go +++ b/api/internal/config/config.go @@ -10,15 +10,6 @@ import ( // GlobalConfig represents the main configuration structure type GlobalConfig struct { - Wildcloud struct { - Repository string `yaml:"repository,omitempty" json:"repository,omitempty"` - CurrentPhase string `yaml:"currentPhase,omitempty" json:"currentPhase,omitempty"` - CompletedPhases []string `yaml:"completedPhases,omitempty" json:"completedPhases,omitempty"` - } `yaml:"wildcloud,omitempty" json:"wildcloud,omitempty"` - Server struct { - Port int `yaml:"port,omitempty" json:"port,omitempty"` - Host string `yaml:"host,omitempty" json:"host,omitempty"` - } `yaml:"server,omitempty" json:"server,omitempty"` Operator struct { Email string `yaml:"email,omitempty" json:"email,omitempty"` } `yaml:"operator,omitempty" json:"operator,omitempty"` @@ -35,14 +26,6 @@ type GlobalConfig struct { Interface string `yaml:"interface,omitempty" json:"interface,omitempty"` } `yaml:"dnsmasq,omitempty" json:"dnsmasq,omitempty"` } `yaml:"cloud,omitempty" json:"cloud,omitempty"` - Cluster struct { - EndpointIP string `yaml:"endpointIp,omitempty" json:"endpointIp,omitempty"` - Nodes struct { - Talos struct { - Version string `yaml:"version,omitempty" json:"version,omitempty"` - } `yaml:"talos,omitempty" json:"talos,omitempty"` - } `yaml:"nodes,omitempty" json:"nodes,omitempty"` - } `yaml:"cluster,omitempty" json:"cluster,omitempty"` } // LoadGlobalConfig loads configuration from the specified path @@ -57,14 +40,6 @@ func LoadGlobalConfig(configPath string) (*GlobalConfig, error) { return nil, fmt.Errorf("parsing config file: %w", err) } - // Set defaults - if config.Server.Port == 0 { - config.Server.Port = 5055 - } - if config.Server.Host == "" { - config.Server.Host = "0.0.0.0" - } - return config, nil } @@ -89,8 +64,8 @@ func (c *GlobalConfig) IsEmpty() bool { return true } - // Check if any essential fields are empty - return c.Cloud.DNS.IP == "" || c.Cluster.Nodes.Talos.Version == "" + // Check if essential fields are empty + return c.Cloud.DNS.IP == "" && c.Cloud.Router.IP == "" && c.Operator.Email == "" } type NodeConfig struct { @@ -109,18 +84,7 @@ type InstanceConfig struct { Domain string `yaml:"domain" json:"domain"` InternalDomain string `yaml:"internalDomain" json:"internalDomain"` DHCPRange string `yaml:"dhcpRange" json:"dhcpRange"` - DNS struct { - IP string `yaml:"ip" json:"ip"` - ExternalResolver string `yaml:"externalResolver" json:"externalResolver"` - } `yaml:"dns" json:"dns"` - Router struct { - IP string `yaml:"ip" json:"ip"` - DynamicDns string `yaml:"dynamicDns,omitempty" json:"dynamicDns,omitempty"` - } `yaml:"router" json:"router"` - Dnsmasq struct { - Interface string `yaml:"interface" json:"interface"` - } `yaml:"dnsmasq" json:"dnsmasq"` - NFS struct { + NFS struct { Host string `yaml:"host" json:"host"` MediaPath string `yaml:"mediaPath" json:"mediaPath"` StorageCapacity string `yaml:"storageCapacity" json:"storageCapacity"` diff --git a/api/internal/config/config_test.go b/api/internal/config/config_test.go index 6ea9931..125e267 100644 --- a/api/internal/config/config_test.go +++ b/api/internal/config/config_test.go @@ -17,16 +17,7 @@ func TestLoadGlobalConfig(t *testing.T) { }{ { name: "loads complete configuration", - configYAML: `wildcloud: - repository: "https://github.com/example/repo" - currentPhase: "setup" - completedPhases: - - "phase1" - - "phase2" -server: - port: 8080 - host: "localhost" -operator: + configYAML: `operator: email: "admin@example.com" cloud: dns: @@ -37,67 +28,43 @@ cloud: dynamicDns: "example.dyndns.org" dnsmasq: interface: "eth0" -cluster: - endpointIp: "192.168.1.100" - nodes: - talos: - version: "v1.8.0" `, verify: func(t *testing.T, config *GlobalConfig) { - if config.Wildcloud.Repository != "https://github.com/example/repo" { - t.Error("repository not loaded correctly") - } - if config.Server.Port != 8080 { - t.Error("port not loaded correctly") + if config.Operator.Email != "admin@example.com" { + t.Error("operator email not loaded correctly") } if config.Cloud.DNS.IP != "192.168.1.1" { t.Error("DNS IP not loaded correctly") } - if config.Cluster.EndpointIP != "192.168.1.100" { - t.Error("endpoint IP not loaded correctly") + if config.Cloud.Router.IP != "192.168.1.254" { + t.Error("router IP not loaded correctly") + } + if config.Cloud.Dnsmasq.Interface != "eth0" { + t.Error("dnsmasq interface not loaded correctly") } }, wantErr: false, }, { - name: "applies default values", + name: "loads minimal configuration", configYAML: `cloud: dns: ip: "192.168.1.1" -cluster: - nodes: - talos: - version: "v1.8.0" `, verify: func(t *testing.T, config *GlobalConfig) { - if config.Server.Port != 5055 { - t.Errorf("default port not applied, got %d, want 5055", config.Server.Port) - } - if config.Server.Host != "0.0.0.0" { - t.Errorf("default host not applied, got %q, want %q", config.Server.Host, "0.0.0.0") + if config.Cloud.DNS.IP != "192.168.1.1" { + t.Error("DNS IP not loaded correctly") } }, wantErr: false, }, { - name: "preserves custom port and host", - configYAML: `server: - port: 9000 - host: "127.0.0.1" -cloud: - dns: - ip: "192.168.1.1" -cluster: - nodes: - talos: - version: "v1.8.0" + name: "loads empty configuration", + configYAML: `{} `, verify: func(t *testing.T, config *GlobalConfig) { - if config.Server.Port != 9000 { - t.Errorf("custom port not preserved, got %d, want 9000", config.Server.Port) - } - if config.Server.Host != "127.0.0.1" { - t.Errorf("custom host not preserved, got %q, want %q", config.Server.Host, "127.0.0.1") + if config.Cloud.DNS.IP != "" { + t.Error("expected empty DNS IP") } }, wantErr: false, @@ -189,35 +156,25 @@ func TestSaveGlobalConfig(t *testing.T) { }{ { name: "saves complete configuration", - config: &GlobalConfig{ - Wildcloud: struct { - Repository string `yaml:"repository,omitempty" json:"repository,omitempty"` - CurrentPhase string `yaml:"currentPhase,omitempty" json:"currentPhase,omitempty"` - CompletedPhases []string `yaml:"completedPhases,omitempty" json:"completedPhases,omitempty"` - }{ - Repository: "https://github.com/example/repo", - CurrentPhase: "setup", - CompletedPhases: []string{"phase1", "phase2"}, - }, - Server: struct { - Port int `yaml:"port,omitempty" json:"port,omitempty"` - Host string `yaml:"host,omitempty" json:"host,omitempty"` - }{ - Port: 8080, - Host: "localhost", - }, - }, + config: func() *GlobalConfig { + cfg := &GlobalConfig{} + cfg.Operator.Email = "admin@example.com" + cfg.Cloud.DNS.IP = "192.168.1.1" + cfg.Cloud.Router.IP = "192.168.1.254" + cfg.Cloud.Dnsmasq.Interface = "eth0" + 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, "repository") { - t.Error("saved config missing repository field") + if !strings.Contains(contentStr, "admin@example.com") { + t.Error("saved config missing operator email") } - if !strings.Contains(contentStr, "8080") { - t.Error("saved config missing port value") + if !strings.Contains(contentStr, "192.168.1.1") { + t.Error("saved config missing DNS IP") } }, }, @@ -313,101 +270,41 @@ func TestGlobalConfig_IsEmpty(t *testing.T) { want: true, }, { - name: "config with only DNS IP is empty", - config: &GlobalConfig{ - Cloud: struct { - DNS struct { - IP string `yaml:"ip,omitempty" json:"ip,omitempty"` - ExternalResolver string `yaml:"externalResolver,omitempty" json:"externalResolver,omitempty"` - } `yaml:"dns,omitempty" json:"dns,omitempty"` - Router struct { - IP string `yaml:"ip,omitempty" json:"ip,omitempty"` - DynamicDns string `yaml:"dynamicDns,omitempty" json:"dynamicDns,omitempty"` - } `yaml:"router,omitempty" json:"router,omitempty"` - Dnsmasq struct { - Interface string `yaml:"interface,omitempty" json:"interface,omitempty"` - } `yaml:"dnsmasq,omitempty" json:"dnsmasq,omitempty"` - }{ - DNS: struct { - IP string `yaml:"ip,omitempty" json:"ip,omitempty"` - ExternalResolver string `yaml:"externalResolver,omitempty" json:"externalResolver,omitempty"` - }{ - IP: "192.168.1.1", - }, - }, - }, - want: true, + name: "config with only DNS IP is not empty", + config: func() *GlobalConfig { + cfg := &GlobalConfig{} + cfg.Cloud.DNS.IP = "192.168.1.1" + return cfg + }(), + want: false, }, { - name: "config with only Talos version is empty", - config: &GlobalConfig{ - Cluster: struct { - EndpointIP string `yaml:"endpointIp,omitempty" json:"endpointIp,omitempty"` - Nodes struct { - Talos struct { - Version string `yaml:"version,omitempty" json:"version,omitempty"` - } `yaml:"talos,omitempty" json:"talos,omitempty"` - } `yaml:"nodes,omitempty" json:"nodes,omitempty"` - }{ - Nodes: struct { - Talos struct { - Version string `yaml:"version,omitempty" json:"version,omitempty"` - } `yaml:"talos,omitempty" json:"talos,omitempty"` - }{ - Talos: struct { - Version string `yaml:"version,omitempty" json:"version,omitempty"` - }{ - Version: "v1.8.0", - }, - }, - }, - }, - 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 both DNS IP and Talos version is not empty", - config: &GlobalConfig{ - Cloud: struct { - DNS struct { - IP string `yaml:"ip,omitempty" json:"ip,omitempty"` - ExternalResolver string `yaml:"externalResolver,omitempty" json:"externalResolver,omitempty"` - } `yaml:"dns,omitempty" json:"dns,omitempty"` - Router struct { - IP string `yaml:"ip,omitempty" json:"ip,omitempty"` - DynamicDns string `yaml:"dynamicDns,omitempty" json:"dynamicDns,omitempty"` - } `yaml:"router,omitempty" json:"router,omitempty"` - Dnsmasq struct { - Interface string `yaml:"interface,omitempty" json:"interface,omitempty"` - } `yaml:"dnsmasq,omitempty" json:"dnsmasq,omitempty"` - }{ - DNS: struct { - IP string `yaml:"ip,omitempty" json:"ip,omitempty"` - ExternalResolver string `yaml:"externalResolver,omitempty" json:"externalResolver,omitempty"` - }{ - IP: "192.168.1.1", - }, - }, - Cluster: struct { - EndpointIP string `yaml:"endpointIp,omitempty" json:"endpointIp,omitempty"` - Nodes struct { - Talos struct { - Version string `yaml:"version,omitempty" json:"version,omitempty"` - } `yaml:"talos,omitempty" json:"talos,omitempty"` - } `yaml:"nodes,omitempty" json:"nodes,omitempty"` - }{ - Nodes: struct { - Talos struct { - Version string `yaml:"version,omitempty" json:"version,omitempty"` - } `yaml:"talos,omitempty" json:"talos,omitempty"` - }{ - Talos: struct { - Version string `yaml:"version,omitempty" json:"version,omitempty"` - }{ - Version: "v1.8.0", - }, - }, - }, - }, + 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.DNS.IP = "192.168.1.1" + cfg.Cloud.Router.IP = "192.168.1.254" + cfg.Operator.Email = "admin@example.com" + return cfg + }(), want: false, }, } @@ -433,11 +330,6 @@ func TestLoadCloudConfig(t *testing.T) { { name: "loads complete instance configuration", configYAML: `cloud: - router: - ip: "192.168.1.254" - dns: - ip: "192.168.1.1" - externalResolver: "8.8.8.8" dhcpRange: "192.168.1.100,192.168.1.200" baseDomain: "example.com" domain: "home" @@ -458,6 +350,9 @@ cluster: 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") } @@ -611,29 +506,13 @@ func TestGlobalConfig_RoundTrip(t *testing.T) { configPath := filepath.Join(tempDir, "config.yaml") // Create config with all fields - original := &GlobalConfig{ - Wildcloud: struct { - Repository string `yaml:"repository,omitempty" json:"repository,omitempty"` - CurrentPhase string `yaml:"currentPhase,omitempty" json:"currentPhase,omitempty"` - CompletedPhases []string `yaml:"completedPhases,omitempty" json:"completedPhases,omitempty"` - }{ - Repository: "https://github.com/example/repo", - CurrentPhase: "setup", - CompletedPhases: []string{"phase1", "phase2"}, - }, - Server: struct { - Port int `yaml:"port,omitempty" json:"port,omitempty"` - Host string `yaml:"host,omitempty" json:"host,omitempty"` - }{ - Port: 8080, - Host: "localhost", - }, - Operator: struct { - Email string `yaml:"email,omitempty" json:"email,omitempty"` - }{ - Email: "admin@example.com", - }, - } + original := &GlobalConfig{} + original.Operator.Email = "admin@example.com" + original.Cloud.DNS.IP = "192.168.1.1" + original.Cloud.DNS.ExternalResolver = "8.8.8.8" + original.Cloud.Router.IP = "192.168.1.254" + original.Cloud.Router.DynamicDns = "example.dyndns.org" + original.Cloud.Dnsmasq.Interface = "eth0" // Save config if err := SaveGlobalConfig(original, configPath); err != nil { @@ -647,15 +526,18 @@ func TestGlobalConfig_RoundTrip(t *testing.T) { } // Verify all fields match - if loaded.Wildcloud.Repository != original.Wildcloud.Repository { - t.Errorf("repository mismatch: got %q, want %q", loaded.Wildcloud.Repository, original.Wildcloud.Repository) - } - if loaded.Server.Port != original.Server.Port { - t.Errorf("port mismatch: got %d, want %d", loaded.Server.Port, original.Server.Port) - } if loaded.Operator.Email != original.Operator.Email { t.Errorf("email mismatch: got %q, want %q", loaded.Operator.Email, original.Operator.Email) } + if loaded.Cloud.DNS.IP != original.Cloud.DNS.IP { + t.Errorf("DNS IP mismatch: got %q, want %q", loaded.Cloud.DNS.IP, original.Cloud.DNS.IP) + } + 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.Dnsmasq.Interface != original.Cloud.Dnsmasq.Interface { + t.Errorf("dnsmasq interface mismatch: got %q, want %q", loaded.Cloud.Dnsmasq.Interface, original.Cloud.Dnsmasq.Interface) + } } // Test: Round-trip save and load for instance config diff --git a/api/internal/config/manager.go b/api/internal/config/manager.go index 86e2719..5e47176 100644 --- a/api/internal/config/manager.go +++ b/api/internal/config/manager.go @@ -2,8 +2,10 @@ package config import ( "fmt" + "log" "path/filepath" + "github.com/wild-cloud/wild-central/daemon/internal/network" "github.com/wild-cloud/wild-central/daemon/internal/storage" "github.com/wild-cloud/wild-central/daemon/internal/tools" ) @@ -20,6 +22,45 @@ func NewManager() *Manager { } } +// EnsureGlobalConfig ensures a global config file exists with proper structure +func (m *Manager) EnsureGlobalConfig(dataDir string) error { + configPath := filepath.Join(dataDir, "config.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 + } + + // Create config structure with detected defaults + initialConfig := &GlobalConfig{} + + // Detect network configuration + netInfo, err := network.DetectNetworkInfo() + if err != nil { + log.Printf("Warning: Could not detect network info, using empty defaults: %v", err) + } else { + // Set detected values + initialConfig.Cloud.DNS.IP = netInfo.PrimaryIP + initialConfig.Cloud.DNS.ExternalResolver = "1.1.1.1" // Default external resolver + initialConfig.Cloud.Router.IP = netInfo.Gateway + initialConfig.Cloud.Dnsmasq.Interface = netInfo.PrimaryInterface + log.Printf("Detected network: IP=%s, Gateway=%s, Interface=%s", + netInfo.PrimaryIP, netInfo.Gateway, netInfo.PrimaryInterface) + } + + // Ensure data directory exists + if err := storage.EnsureDir(dataDir, 0755); err != nil { + return err + } + + // Save config using the model's save function + return SaveGlobalConfig(initialConfig, configPath) +} + // EnsureInstanceConfig ensures an instance config file exists with proper structure func (m *Manager) EnsureInstanceConfig(instancePath string) error { configPath := filepath.Join(instancePath, "config.yaml") diff --git a/api/internal/config/manager_test.go b/api/internal/config/manager_test.go index 1453cd0..4ba27aa 100644 --- a/api/internal/config/manager_test.go +++ b/api/internal/config/manager_test.go @@ -901,9 +901,6 @@ func TestEnsureInstanceConfig_RequiredFields(t *testing.T) { "domain:", "internalDomain:", "dhcpRange:", - "dns:", - "router:", - "dnsmasq:", "nfs:", "smtp:", "cluster:", diff --git a/api/internal/network/detect.go b/api/internal/network/detect.go index a525d91..310efb6 100644 --- a/api/internal/network/detect.go +++ b/api/internal/network/detect.go @@ -11,6 +11,7 @@ import ( type NetworkInfo struct { PrimaryIP string `json:"primary_ip"` PrimaryInterface string `json:"primary_interface"` + Gateway string `json:"gateway"` } // DetectNetworkInfo detects the machine's primary network configuration @@ -28,6 +29,16 @@ func DetectNetworkInfo() (*NetworkInfo, error) { line := string(output) // Example output: "8.8.8.8 via 192.168.8.1 dev wlan0 src 192.168.8.152 uid 1000" + // Extract gateway (after "via") + gateway := "" + if idx := strings.Index(line, "via "); idx != -1 { + rest := line[idx+4:] + parts := strings.Fields(rest) + if len(parts) > 0 { + gateway = parts[0] + } + } + // Extract interface (after "dev") iface := "" if idx := strings.Index(line, "dev "); idx != -1 { @@ -56,6 +67,7 @@ func DetectNetworkInfo() (*NetworkInfo, error) { return &NetworkInfo{ PrimaryIP: ip, PrimaryInterface: iface, + Gateway: gateway, }, nil } diff --git a/api/internal/setup/detector.go b/api/internal/setup/detector.go index 19b30d6..ca508c0 100644 --- a/api/internal/setup/detector.go +++ b/api/internal/setup/detector.go @@ -236,8 +236,8 @@ func fileExists(path string) bool { } func checkDNSConfigured(instanceConfig *config.InstanceConfig, globalConfig *config.GlobalConfig) bool { - // DNS is configured if we have DNS IP and DHCP range - return instanceConfig.Cloud.DNS.IP != "" && + // DNS is configured if we have DNS IP (from global config) and DHCP range (from instance config) + return globalConfig.Cloud.DNS.IP != "" && instanceConfig.Cloud.DHCPRange != "" }