diff --git a/internal/api/v1/handlers.go b/internal/api/v1/handlers.go index ca989e2..c2d524b 100644 --- a/internal/api/v1/handlers.go +++ b/internal/api/v1/handlers.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "io" + "log" "net/http" "os" "path/filepath" @@ -14,6 +15,7 @@ import ( "github.com/wild-cloud/wild-central/daemon/internal/config" "github.com/wild-cloud/wild-central/daemon/internal/context" + "github.com/wild-cloud/wild-central/daemon/internal/dnsmasq" "github.com/wild-cloud/wild-central/daemon/internal/instance" "github.com/wild-cloud/wild-central/daemon/internal/operations" "github.com/wild-cloud/wild-central/daemon/internal/secrets" @@ -27,6 +29,7 @@ type API struct { secrets *secrets.Manager context *context.Manager instance *instance.Manager + dnsmasq *dnsmasq.ConfigGenerator broadcaster *operations.Broadcaster // SSE broadcaster for operation output } @@ -39,6 +42,13 @@ func NewAPI(dataDir, appsDir string) (*API, error) { return nil, fmt.Errorf("failed to create instances directory: %w", err) } + // Determine dnsmasq config path + dnsmasqConfigPath := "/etc/dnsmasq.d/wild-cloud.conf" + if os.Getenv("WILD_API_DNSMASQ_CONFIG_PATH") != "" { + dnsmasqConfigPath = os.Getenv("WILD_API_DNSMASQ_CONFIG_PATH") + log.Printf("Using custom dnsmasq config path: %s", dnsmasqConfigPath) + } + return &API{ dataDir: dataDir, appsDir: appsDir, @@ -46,6 +56,7 @@ func NewAPI(dataDir, appsDir string) (*API, error) { secrets: secrets.NewManager(), context: context.NewManager(dataDir), instance: instance.NewManager(dataDir), + dnsmasq: dnsmasq.NewConfigGenerator(dnsmasqConfigPath), broadcaster: operations.NewBroadcaster(), }, nil } @@ -145,6 +156,12 @@ func (api *API) RegisterRoutes(r *mux.Router) { r.HandleFunc("/api/v1/utilities/controlplane/ip", api.UtilitiesControlPlaneIP).Methods("GET") r.HandleFunc("/api/v1/utilities/secrets/{secret}/copy", api.UtilitiesSecretCopy).Methods("POST") r.HandleFunc("/api/v1/utilities/version", api.UtilitiesVersion).Methods("GET") + + // dnsmasq management + r.HandleFunc("/api/v1/dnsmasq/status", api.DnsmasqStatus).Methods("GET") + r.HandleFunc("/api/v1/dnsmasq/config", api.DnsmasqGetConfig).Methods("GET") + r.HandleFunc("/api/v1/dnsmasq/restart", api.DnsmasqRestart).Methods("POST") + r.HandleFunc("/api/v1/dnsmasq/update", api.DnsmasqUpdate).Methods("POST") } // CreateInstance creates a new instance @@ -168,10 +185,19 @@ func (api *API) CreateInstance(w http.ResponseWriter, r *http.Request) { return } - respondJSON(w, http.StatusCreated, map[string]string{ + // Attempt to update dnsmasq configuration with all instances + // This is non-critical - include warning in response if it fails + response := map[string]interface{}{ "name": req.Name, "message": "Instance created successfully", - }) + } + + if err := api.updateDnsmasqForAllInstances(); err != nil { + log.Printf("Warning: Could not update dnsmasq configuration: %v", err) + response["warning"] = fmt.Sprintf("dnsmasq update failed: %v. Use POST /api/v1/dnsmasq/update to retry.", err) + } + + respondJSON(w, http.StatusCreated, response) } // ListInstances lists all instances diff --git a/internal/api/v1/handlers_dnsmasq.go b/internal/api/v1/handlers_dnsmasq.go new file mode 100644 index 0000000..3fa7d5f --- /dev/null +++ b/internal/api/v1/handlers_dnsmasq.go @@ -0,0 +1,99 @@ +package v1 + +import ( + "fmt" + "log" + "net/http" + + "github.com/wild-cloud/wild-central/daemon/internal/config" +) + +// DnsmasqStatus returns the status of the dnsmasq service +func (api *API) DnsmasqStatus(w http.ResponseWriter, r *http.Request) { + status, err := api.dnsmasq.GetStatus() + if err != nil { + respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get dnsmasq status: %v", err)) + return + } + + if status.Status != "active" { + w.WriteHeader(http.StatusServiceUnavailable) + } + + respondJSON(w, http.StatusOK, status) +} + +// DnsmasqGetConfig returns the current dnsmasq configuration +func (api *API) DnsmasqGetConfig(w http.ResponseWriter, r *http.Request) { + configContent, err := api.dnsmasq.ReadConfig() + if err != nil { + respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to read dnsmasq config: %v", err)) + return + } + + respondJSON(w, http.StatusOK, map[string]interface{}{ + "config_file": api.dnsmasq.GetConfigPath(), + "content": configContent, + }) +} + +// DnsmasqRestart restarts the dnsmasq service +func (api *API) DnsmasqRestart(w http.ResponseWriter, r *http.Request) { + if err := api.dnsmasq.RestartService(); err != nil { + respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to restart dnsmasq: %v", err)) + return + } + + respondJSON(w, http.StatusOK, map[string]string{ + "message": "dnsmasq service restarted successfully", + }) +} + +// DnsmasqUpdate regenerates and updates the dnsmasq configuration with all instances +func (api *API) DnsmasqUpdate(w http.ResponseWriter, r *http.Request) { + if err := api.updateDnsmasqForAllInstances(); err != nil { + respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to update dnsmasq: %v", err)) + return + } + + respondJSON(w, http.StatusOK, map[string]string{ + "message": "dnsmasq configuration updated successfully", + }) +} + +// updateDnsmasqForAllInstances helper regenerates dnsmasq config from all instances +func (api *API) updateDnsmasqForAllInstances() error { + // Get all instances + instanceNames, err := api.instance.ListInstances() + if err != nil { + return fmt.Errorf("listing instances: %w", err) + } + + // Load global config + globalConfigPath := api.getGlobalConfigPath() + globalCfg, err := config.LoadGlobalConfig(globalConfigPath) + if err != nil { + return fmt.Errorf("loading global config: %w", err) + } + + // Load all instance configs + var instanceConfigs []config.InstanceConfig + for _, name := range instanceNames { + instanceConfigPath := api.instance.GetInstanceConfigPath(name) + instanceCfg, err := config.LoadCloudConfig(instanceConfigPath) + if err != nil { + log.Printf("Warning: Could not load instance config for %s: %v", name, err) + continue + } + instanceConfigs = append(instanceConfigs, *instanceCfg) + } + + // Regenerate and write dnsmasq config + return api.dnsmasq.UpdateConfig(globalCfg, instanceConfigs) +} + +// getGlobalConfigPath returns the path to the global config file +func (api *API) getGlobalConfigPath() string { + // This should match the structure from data.Paths + return api.dataDir + "/config.yaml" +} diff --git a/internal/dnsmasq/config.go b/internal/dnsmasq/config.go index 3979ffb..214014d 100644 --- a/internal/dnsmasq/config.go +++ b/internal/dnsmasq/config.go @@ -5,16 +5,31 @@ import ( "log" "os" "os/exec" + "strconv" + "strings" + "time" "github.com/wild-cloud/wild-central/daemon/internal/config" ) // ConfigGenerator handles dnsmasq configuration generation -type ConfigGenerator struct{} +type ConfigGenerator struct { + configPath string +} // NewConfigGenerator creates a new dnsmasq config generator -func NewConfigGenerator() *ConfigGenerator { - return &ConfigGenerator{} +func NewConfigGenerator(configPath string) *ConfigGenerator { + if configPath == "" { + configPath = "/etc/dnsmasq.d/wild-cloud.conf" + } + return &ConfigGenerator{ + configPath: configPath, + } +} + +// GetConfigPath returns the dnsmasq config file path +func (g *ConfigGenerator) GetConfigPath() string { + return g.configPath } // Generate creates a dnsmasq configuration from the app config @@ -71,3 +86,91 @@ func (g *ConfigGenerator) RestartService() error { } return nil } + +// ServiceStatus represents the status of the dnsmasq service +type ServiceStatus struct { + Status string `json:"status"` + PID int `json:"pid"` + ConfigFile string `json:"config_file"` + InstancesConfigured int `json:"instances_configured"` + LastRestart time.Time `json:"last_restart"` +} + +// GetStatus checks the status of the dnsmasq service +func (g *ConfigGenerator) GetStatus() (*ServiceStatus, error) { + status := &ServiceStatus{ + ConfigFile: g.configPath, + } + + // Check if service is active + cmd := exec.Command("systemctl", "is-active", "dnsmasq.service") + output, err := cmd.Output() + if err != nil { + status.Status = "inactive" + return status, nil + } + + statusStr := strings.TrimSpace(string(output)) + status.Status = statusStr + + // Get PID if running + if statusStr == "active" { + cmd = exec.Command("systemctl", "show", "dnsmasq.service", "--property=MainPID") + output, err := cmd.Output() + if err == nil { + parts := strings.Split(strings.TrimSpace(string(output)), "=") + if len(parts) == 2 { + if pid, err := strconv.Atoi(parts[1]); err == nil { + status.PID = pid + } + } + } + + // Get last restart time + cmd = exec.Command("systemctl", "show", "dnsmasq.service", "--property=ActiveEnterTimestamp") + output, err = cmd.Output() + if err == nil { + parts := strings.Split(strings.TrimSpace(string(output)), "=") + if len(parts) == 2 { + // Parse systemd timestamp format + if t, err := time.Parse("Mon 2006-01-02 15:04:05 MST", parts[1]); err == nil { + status.LastRestart = t + } + } + } + } + + // Count instances in config + if data, err := os.ReadFile(g.configPath); err == nil { + // Count "local=/" occurrences (each instance has multiple) + count := strings.Count(string(data), "local=/") + // Each instance creates 2 "local=/" entries (domain and internal domain) + status.InstancesConfigured = count / 2 + } + + return status, nil +} + +// ReadConfig reads the current dnsmasq configuration +func (g *ConfigGenerator) ReadConfig() (string, error) { + data, err := os.ReadFile(g.configPath) + if err != nil { + return "", fmt.Errorf("reading dnsmasq config: %w", err) + } + return string(data), nil +} + +// UpdateConfig regenerates and writes the dnsmasq configuration for all instances +func (g *ConfigGenerator) UpdateConfig(cfg *config.GlobalConfig, instances []config.InstanceConfig) error { + // Generate fresh config from scratch + configContent := g.Generate(cfg, instances) + + // Write config + log.Printf("Writing dnsmasq config to: %s", g.configPath) + if err := os.WriteFile(g.configPath, []byte(configContent), 0644); err != nil { + return fmt.Errorf("writing dnsmasq config: %w", err) + } + + // Restart service to apply changes + return g.RestartService() +}