diff --git a/README.md b/README.md index 6b558cd..3d40c52 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,35 @@ The Wild Central API is a lightweight service that runs on a local machine (e.g. ## Development +Start the development server: + ```bash make dev ``` -## Usage +The API will be available at `http://localhost:5055`. -TBD +### Environment Variables + +- `WILD_API_DATA_DIR` - Directory for instance data (default: `/var/lib/wild-central`) +- `WILD_DIRECTORY` - Path to Wild Cloud apps directory (default: `/opt/wild-cloud/apps`) +- `WILD_API_DNSMASQ_CONFIG_PATH` - Path to dnsmasq config file (default: `/etc/dnsmasq.d/wild-cloud.conf`) +- `WILD_CORS_ORIGINS` - Comma-separated list of allowed CORS origins for production (default: localhost development origins) + +## API Endpoints + +The API provides the following endpoint categories: + +- **Instances** - Create, list, get, and delete Wild Cloud instances +- **Configuration** - Manage instance config.yaml +- **Secrets** - Manage instance secrets.yaml (redacted by default) +- **Nodes** - Discover, configure, and manage cluster nodes +- **Cluster** - Bootstrap and manage Talos/Kubernetes clusters +- **Services** - Install and manage base infrastructure services +- **Apps** - Deploy and manage Wild Cloud applications +- **PXE** - Manage PXE boot assets for network installation +- **Operations** - Track and stream long-running operations +- **Utilities** - Helper functions and status endpoints +- **dnsmasq** - Configure and manage dnsmasq for network services + +See the API handler files in `internal/api/v1/` for detailed endpoint documentation. diff --git a/go.mod b/go.mod index 2af3563..4ab0ca7 100644 --- a/go.mod +++ b/go.mod @@ -6,3 +6,5 @@ require ( github.com/gorilla/mux v1.8.1 gopkg.in/yaml.v3 v3.0.1 ) + +require github.com/rs/cors v1.11.1 // indirect diff --git a/go.sum b/go.sum index 3e12cf5..9a17f03 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/apps/apps.go b/internal/apps/apps.go index 4818f1a..681c8bd 100644 --- a/internal/apps/apps.go +++ b/internal/apps/apps.go @@ -139,44 +139,44 @@ func (m *Manager) ListDeployed(instanceName string) ([]DeployedApp, error) { appName := entry.Name() + // Initialize app with basic info + app := DeployedApp{ + Name: appName, + Namespace: appName, + Status: "added", // Default status: added but not deployed + } + + // Try to get version from manifest + manifestPath := filepath.Join(appsDir, appName, "manifest.yaml") + if storage.FileExists(manifestPath) { + manifestData, _ := os.ReadFile(manifestPath) + var manifest struct { + Version string `yaml:"version"` + } + if yaml.Unmarshal(manifestData, &manifest) == nil { + app.Version = manifest.Version + } + } + // Check if namespace exists in cluster checkCmd := exec.Command("kubectl", "get", "namespace", appName, "-o", "json") tools.WithKubeconfig(checkCmd, kubeconfigPath) output, err := checkCmd.CombinedOutput() - if err != nil { - // Namespace doesn't exist - app not deployed - continue - } - - // Parse namespace status - var ns struct { - Status struct { - Phase string `json:"phase"` - } `json:"status"` - } - if err := yaml.Unmarshal(output, &ns); err == nil && ns.Status.Phase == "Active" { - // App is deployed - get more details - app := DeployedApp{ - Name: appName, - Namespace: appName, - Status: "deployed", + if err == nil { + // Namespace exists - parse status + var ns struct { + Status struct { + Phase string `json:"phase"` + } `json:"status"` } - - // Try to get version from manifest - manifestPath := filepath.Join(appsDir, appName, "manifest.yaml") - if storage.FileExists(manifestPath) { - manifestData, _ := os.ReadFile(manifestPath) - var manifest struct { - Version string `yaml:"version"` - } - if yaml.Unmarshal(manifestData, &manifest) == nil { - app.Version = manifest.Version - } + if yaml.Unmarshal(output, &ns) == nil && ns.Status.Phase == "Active" { + // Namespace is active - app is deployed + app.Status = "deployed" } - - apps = append(apps, app) } + + apps = append(apps, app) } return apps, nil diff --git a/main.go b/main.go index bc5b687..96cbd90 100644 --- a/main.go +++ b/main.go @@ -5,15 +5,29 @@ import ( "log" "net/http" "os" + "strings" "time" "github.com/gorilla/mux" + "github.com/rs/cors" v1 "github.com/wild-cloud/wild-central/daemon/internal/api/v1" ) var startTime time.Time +// splitAndTrim splits a string by delimiter and trims whitespace from each part +func splitAndTrim(s string, sep string) []string { + parts := strings.Split(s, sep) + result := make([]string, 0, len(parts)) + for _, part := range parts { + if trimmed := strings.TrimSpace(part); trimmed != "" { + result = append(result, trimmed) + } + } + return result +} + func main() { // Record start time startTime = time.Now() @@ -61,6 +75,55 @@ func main() { api.StatusHandler(w, r, startTime, dataDir, appsDir) }).Methods("GET") + // Configure CORS + // Default to development origins + allowedOrigins := []string{ + "http://localhost:5173", // Vite dev server + "http://localhost:5174", // Alternative port + "http://localhost:3000", // Common React dev port + "http://127.0.0.1:5173", + "http://127.0.0.1:5174", + "http://127.0.0.1:3000", + } + + // Override with production origins if set + if corsOrigins := os.Getenv("WILD_CORS_ORIGINS"); corsOrigins != "" { + // Split comma-separated origins + allowedOrigins = []string{} + for _, origin := range splitAndTrim(corsOrigins, ",") { + allowedOrigins = append(allowedOrigins, origin) + } + log.Printf("CORS configured for production origins: %v", allowedOrigins) + } else { + log.Printf("CORS configured for development origins") + } + + corsHandler := cors.New(cors.Options{ + AllowedOrigins: allowedOrigins, + AllowedMethods: []string{ + http.MethodGet, + http.MethodPost, + http.MethodPut, + http.MethodPatch, + http.MethodDelete, + http.MethodOptions, + }, + AllowedHeaders: []string{ + "Accept", + "Authorization", + "Content-Type", + "X-CSRF-Token", + }, + ExposedHeaders: []string{ + "Link", + }, + AllowCredentials: true, + MaxAge: 300, // 5 minutes + }) + + // Wrap router with CORS middleware + handler := corsHandler.Handler(router) + // Default server settings host := "0.0.0.0" port := 5055 @@ -69,8 +132,9 @@ func main() { log.Printf("Starting wild-central daemon on %s", addr) log.Printf("Data directory: %s", dataDir) log.Printf("Apps directory: %s", appsDir) + log.Printf("CORS enabled for development origins") - if err := http.ListenAndServe(addr, router); err != nil { + if err := http.ListenAndServe(addr, handler); err != nil { log.Fatal("Server failed to start:", err) } }