Initial commit.

This commit is contained in:
2025-10-11 17:06:14 +00:00
commit ec521c3c91
45 changed files with 9798 additions and 0 deletions

466
internal/api/v1/handlers.go Normal file
View File

@@ -0,0 +1,466 @@
package v1
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"time"
"github.com/gorilla/mux"
"gopkg.in/yaml.v3"
"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/instance"
"github.com/wild-cloud/wild-central/daemon/internal/operations"
"github.com/wild-cloud/wild-central/daemon/internal/secrets"
)
// API holds all dependencies for API handlers
type API struct {
dataDir string
directoryPath string // Path to Wild Cloud Directory
appsDir string
config *config.Manager
secrets *secrets.Manager
context *context.Manager
instance *instance.Manager
broadcaster *operations.Broadcaster // SSE broadcaster for operation output
}
// NewAPI creates a new API handler with all dependencies
func NewAPI(dataDir, directoryPath string) (*API, error) {
// Ensure base directories exist
instancesDir := filepath.Join(dataDir, "instances")
if err := os.MkdirAll(instancesDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create instances directory: %w", err)
}
// Apps directory is now in Wild Cloud Directory
appsDir := filepath.Join(directoryPath, "apps")
return &API{
dataDir: dataDir,
directoryPath: directoryPath,
appsDir: appsDir,
config: config.NewManager(),
secrets: secrets.NewManager(),
context: context.NewManager(dataDir),
instance: instance.NewManager(dataDir),
broadcaster: operations.NewBroadcaster(),
}, nil
}
// RegisterRoutes registers all API routes (Phase 1 + Phase 2)
func (api *API) RegisterRoutes(r *mux.Router) {
// Phase 1: Instance management
r.HandleFunc("/api/v1/instances", api.CreateInstance).Methods("POST")
r.HandleFunc("/api/v1/instances", api.ListInstances).Methods("GET")
r.HandleFunc("/api/v1/instances/{name}", api.GetInstance).Methods("GET")
r.HandleFunc("/api/v1/instances/{name}", api.DeleteInstance).Methods("DELETE")
// Phase 1: Config management
r.HandleFunc("/api/v1/instances/{name}/config", api.GetConfig).Methods("GET")
r.HandleFunc("/api/v1/instances/{name}/config", api.UpdateConfig).Methods("PUT")
r.HandleFunc("/api/v1/instances/{name}/config", api.ConfigUpdateBatch).Methods("PATCH")
// Phase 1: Secrets management
r.HandleFunc("/api/v1/instances/{name}/secrets", api.GetSecrets).Methods("GET")
r.HandleFunc("/api/v1/instances/{name}/secrets", api.UpdateSecrets).Methods("PUT")
// Phase 1: Context management
r.HandleFunc("/api/v1/context", api.GetContext).Methods("GET")
r.HandleFunc("/api/v1/context", api.SetContext).Methods("POST")
// Phase 2: Node management
r.HandleFunc("/api/v1/instances/{name}/nodes/discover", api.NodeDiscover).Methods("POST")
r.HandleFunc("/api/v1/instances/{name}/nodes/detect", api.NodeDetect).Methods("POST")
r.HandleFunc("/api/v1/instances/{name}/discovery", api.NodeDiscoveryStatus).Methods("GET")
r.HandleFunc("/api/v1/instances/{name}/nodes/hardware/{ip}", api.NodeHardware).Methods("GET")
r.HandleFunc("/api/v1/instances/{name}/nodes/fetch-templates", api.NodeFetchTemplates).Methods("POST")
r.HandleFunc("/api/v1/instances/{name}/nodes", api.NodeAdd).Methods("POST")
r.HandleFunc("/api/v1/instances/{name}/nodes", api.NodeList).Methods("GET")
r.HandleFunc("/api/v1/instances/{name}/nodes/{node}", api.NodeGet).Methods("GET")
r.HandleFunc("/api/v1/instances/{name}/nodes/{node}", api.NodeUpdate).Methods("PUT")
r.HandleFunc("/api/v1/instances/{name}/nodes/{node}/apply", api.NodeApply).Methods("POST")
r.HandleFunc("/api/v1/instances/{name}/nodes/{node}", api.NodeDelete).Methods("DELETE")
// Phase 2: PXE asset management
r.HandleFunc("/api/v1/instances/{name}/pxe/assets", api.PXEListAssets).Methods("GET")
r.HandleFunc("/api/v1/instances/{name}/pxe/assets/download", api.PXEDownloadAsset).Methods("POST")
r.HandleFunc("/api/v1/instances/{name}/pxe/assets/{type}", api.PXEGetAsset).Methods("GET")
r.HandleFunc("/api/v1/instances/{name}/pxe/assets/{type}", api.PXEDeleteAsset).Methods("DELETE")
// Phase 2: Operations
r.HandleFunc("/api/v1/instances/{name}/operations", api.OperationList).Methods("GET")
r.HandleFunc("/api/v1/operations/{id}", api.OperationGet).Methods("GET")
r.HandleFunc("/api/v1/operations/{id}/stream", api.OperationStream).Methods("GET")
r.HandleFunc("/api/v1/operations/{id}/cancel", api.OperationCancel).Methods("POST")
// Phase 3: Cluster operations
r.HandleFunc("/api/v1/instances/{name}/cluster/config/generate", api.ClusterGenerateConfig).Methods("POST")
r.HandleFunc("/api/v1/instances/{name}/cluster/bootstrap", api.ClusterBootstrap).Methods("POST")
r.HandleFunc("/api/v1/instances/{name}/cluster/endpoints", api.ClusterConfigureEndpoints).Methods("POST")
r.HandleFunc("/api/v1/instances/{name}/cluster/status", api.ClusterGetStatus).Methods("GET")
r.HandleFunc("/api/v1/instances/{name}/cluster/health", api.ClusterHealth).Methods("GET")
r.HandleFunc("/api/v1/instances/{name}/cluster/kubeconfig", api.ClusterGetKubeconfig).Methods("GET")
r.HandleFunc("/api/v1/instances/{name}/cluster/kubeconfig/generate", api.ClusterGenerateKubeconfig).Methods("POST")
r.HandleFunc("/api/v1/instances/{name}/cluster/talosconfig", api.ClusterGetTalosconfig).Methods("GET")
r.HandleFunc("/api/v1/instances/{name}/cluster/reset", api.ClusterReset).Methods("POST")
// Phase 4: Services
r.HandleFunc("/api/v1/instances/{name}/services", api.ServicesList).Methods("GET")
r.HandleFunc("/api/v1/instances/{name}/services", api.ServicesInstall).Methods("POST")
r.HandleFunc("/api/v1/instances/{name}/services/install-all", api.ServicesInstallAll).Methods("POST")
r.HandleFunc("/api/v1/instances/{name}/services/{service}", api.ServicesGet).Methods("GET")
r.HandleFunc("/api/v1/instances/{name}/services/{service}", api.ServicesDelete).Methods("DELETE")
r.HandleFunc("/api/v1/instances/{name}/services/{service}/status", api.ServicesGetStatus).Methods("GET")
r.HandleFunc("/api/v1/services/{service}/manifest", api.ServicesGetManifest).Methods("GET")
r.HandleFunc("/api/v1/services/{service}/config", api.ServicesGetConfig).Methods("GET")
r.HandleFunc("/api/v1/instances/{name}/services/{service}/config", api.ServicesGetInstanceConfig).Methods("GET")
// Service lifecycle endpoints
r.HandleFunc("/api/v1/instances/{name}/services/{service}/fetch", api.ServicesFetch).Methods("POST")
r.HandleFunc("/api/v1/instances/{name}/services/{service}/compile", api.ServicesCompile).Methods("POST")
r.HandleFunc("/api/v1/instances/{name}/services/{service}/deploy", api.ServicesDeploy).Methods("POST")
// Phase 4: Apps
r.HandleFunc("/api/v1/apps", api.AppsListAvailable).Methods("GET")
r.HandleFunc("/api/v1/apps/{app}", api.AppsGetAvailable).Methods("GET")
r.HandleFunc("/api/v1/instances/{name}/apps", api.AppsListDeployed).Methods("GET")
r.HandleFunc("/api/v1/instances/{name}/apps", api.AppsAdd).Methods("POST")
r.HandleFunc("/api/v1/instances/{name}/apps/{app}/deploy", api.AppsDeploy).Methods("POST")
r.HandleFunc("/api/v1/instances/{name}/apps/{app}", api.AppsDelete).Methods("DELETE")
r.HandleFunc("/api/v1/instances/{name}/apps/{app}/status", api.AppsGetStatus).Methods("GET")
// Phase 5: Backup & Restore
r.HandleFunc("/api/v1/instances/{name}/apps/{app}/backup", api.BackupAppStart).Methods("POST")
r.HandleFunc("/api/v1/instances/{name}/apps/{app}/backup", api.BackupAppList).Methods("GET")
r.HandleFunc("/api/v1/instances/{name}/apps/{app}/restore", api.BackupAppRestore).Methods("POST")
// Phase 5: Utilities
r.HandleFunc("/api/v1/utilities/health", api.UtilitiesHealth).Methods("GET")
r.HandleFunc("/api/v1/instances/{name}/utilities/health", api.InstanceUtilitiesHealth).Methods("GET")
r.HandleFunc("/api/v1/utilities/dashboard/token", api.UtilitiesDashboardToken).Methods("GET")
r.HandleFunc("/api/v1/utilities/nodes/ips", api.UtilitiesNodeIPs).Methods("GET")
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")
}
// CreateInstance creates a new instance
func (api *API) CreateInstance(w http.ResponseWriter, r *http.Request) {
var req struct {
Name string `json:"name"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body")
return
}
if req.Name == "" {
respondError(w, http.StatusBadRequest, "Instance name is required")
return
}
if err := api.instance.CreateInstance(req.Name); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to create instance: %v", err))
return
}
respondJSON(w, http.StatusCreated, map[string]string{
"name": req.Name,
"message": "Instance created successfully",
})
}
// ListInstances lists all instances
func (api *API) ListInstances(w http.ResponseWriter, r *http.Request) {
instances, err := api.instance.ListInstances()
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to list instances: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"instances": instances,
})
}
// GetInstance retrieves instance details
func (api *API) GetInstance(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
name := vars["name"]
if err := api.instance.ValidateInstance(name); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Get config
configPath := api.instance.GetInstanceConfigPath(name)
configData, err := os.ReadFile(configPath)
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to read config: %v", err))
return
}
var configMap map[string]interface{}
if err := yaml.Unmarshal(configData, &configMap); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to parse config: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"name": name,
"config": configMap,
})
}
// DeleteInstance deletes an instance
func (api *API) DeleteInstance(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
name := vars["name"]
if err := api.instance.DeleteInstance(name); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to delete instance: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]string{
"message": "Instance deleted successfully",
})
}
// GetConfig retrieves instance configuration
func (api *API) GetConfig(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
name := vars["name"]
if err := api.instance.ValidateInstance(name); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
configPath := api.instance.GetInstanceConfigPath(name)
configData, err := os.ReadFile(configPath)
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to read config: %v", err))
return
}
var configMap map[string]interface{}
if err := yaml.Unmarshal(configData, &configMap); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to parse config: %v", err))
return
}
respondJSON(w, http.StatusOK, configMap)
}
// UpdateConfig updates instance configuration
func (api *API) UpdateConfig(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
name := vars["name"]
if err := api.instance.ValidateInstance(name); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
respondError(w, http.StatusBadRequest, "Failed to read request body")
return
}
var updates map[string]interface{}
if err := yaml.Unmarshal(body, &updates); err != nil {
respondError(w, http.StatusBadRequest, fmt.Sprintf("Invalid YAML: %v", err))
return
}
configPath := api.instance.GetInstanceConfigPath(name)
// Update each key-value pair
for key, value := range updates {
valueStr := fmt.Sprintf("%v", value)
if err := api.config.SetConfigValue(configPath, key, valueStr); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to update config key %s: %v", key, err))
return
}
}
respondJSON(w, http.StatusOK, map[string]string{
"message": "Config updated successfully",
})
}
// GetSecrets retrieves instance secrets (redacted by default)
func (api *API) GetSecrets(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
name := vars["name"]
if err := api.instance.ValidateInstance(name); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
secretsPath := api.instance.GetInstanceSecretsPath(name)
secretsData, err := os.ReadFile(secretsPath)
if err != nil {
if os.IsNotExist(err) {
respondJSON(w, http.StatusOK, map[string]interface{}{})
return
}
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to read secrets: %v", err))
return
}
var secretsMap map[string]interface{}
if err := yaml.Unmarshal(secretsData, &secretsMap); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to parse secrets: %v", err))
return
}
// Check if client wants raw secrets (dangerous!)
showRaw := r.URL.Query().Get("raw") == "true"
if !showRaw {
// Redact secrets
for key := range secretsMap {
secretsMap[key] = "********"
}
}
respondJSON(w, http.StatusOK, secretsMap)
}
// UpdateSecrets updates instance secrets
func (api *API) UpdateSecrets(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
name := vars["name"]
if err := api.instance.ValidateInstance(name); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
respondError(w, http.StatusBadRequest, "Failed to read request body")
return
}
var updates map[string]interface{}
if err := yaml.Unmarshal(body, &updates); err != nil {
respondError(w, http.StatusBadRequest, fmt.Sprintf("Invalid YAML: %v", err))
return
}
// Get secrets file path
secretsPath := api.instance.GetInstanceSecretsPath(name)
// Update each secret
for key, value := range updates {
valueStr := fmt.Sprintf("%v", value)
if err := api.secrets.SetSecret(secretsPath, key, valueStr); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to update secret %s: %v", key, err))
return
}
}
respondJSON(w, http.StatusOK, map[string]string{
"message": "Secrets updated successfully",
})
}
// GetContext retrieves current context
func (api *API) GetContext(w http.ResponseWriter, r *http.Request) {
currentContext, err := api.context.GetCurrentContext()
if err != nil {
if os.IsNotExist(err) {
respondJSON(w, http.StatusOK, map[string]interface{}{
"context": nil,
})
return
}
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get context: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"context": currentContext,
})
}
// SetContext sets current context
func (api *API) SetContext(w http.ResponseWriter, r *http.Request) {
var req struct {
Context string `json:"context"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body")
return
}
if req.Context == "" {
respondError(w, http.StatusBadRequest, "Context name is required")
return
}
if err := api.context.SetCurrentContext(req.Context); err != nil {
respondError(w, http.StatusBadRequest, fmt.Sprintf("Failed to set context: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]string{
"context": req.Context,
"message": "Context set successfully",
})
}
// StatusHandler returns daemon status information
func (api *API) StatusHandler(w http.ResponseWriter, r *http.Request, startTime time.Time, dataDir, directoryPath string) {
// Get list of instances
instances, err := api.instance.ListInstances()
if err != nil {
instances = []string{}
}
// Calculate uptime
uptime := time.Since(startTime)
respondJSON(w, http.StatusOK, map[string]interface{}{
"status": "running",
"version": "0.1.0", // TODO: Get from build info
"uptime": uptime.String(),
"uptimeSeconds": int(uptime.Seconds()),
"dataDir": dataDir,
"directoryPath": directoryPath,
"instances": map[string]interface{}{
"count": len(instances),
"names": instances,
},
})
}
// Helper functions
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func respondError(w http.ResponseWriter, status int, message string) {
respondJSON(w, status, map[string]string{
"error": message,
})
}

View File

@@ -0,0 +1,206 @@
package v1
import (
"encoding/json"
"fmt"
"net/http"
"github.com/gorilla/mux"
"github.com/wild-cloud/wild-central/daemon/internal/apps"
"github.com/wild-cloud/wild-central/daemon/internal/operations"
)
// AppsListAvailable lists all available apps
func (api *API) AppsListAvailable(w http.ResponseWriter, r *http.Request) {
// List available apps from apps directory
appsMgr := apps.NewManager(api.dataDir, api.appsDir)
appList, err := appsMgr.ListAvailable()
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to list apps: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"apps": appList,
})
}
// AppsGetAvailable returns details for an available app
func (api *API) AppsGetAvailable(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
appName := vars["app"]
// Get app details
appsMgr := apps.NewManager(api.dataDir, api.appsDir)
app, err := appsMgr.Get(appName)
if err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("App not found: %v", err))
return
}
respondJSON(w, http.StatusOK, app)
}
// AppsListDeployed lists deployed apps for an instance
func (api *API) AppsListDeployed(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// List deployed apps
appsMgr := apps.NewManager(api.dataDir, api.appsDir)
deployedApps, err := appsMgr.ListDeployed(instanceName)
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to list apps: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"apps": deployedApps,
})
}
// AppsAdd adds an app to instance configuration
func (api *API) AppsAdd(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Parse request
var req struct {
Name string `json:"name"`
Config map[string]string `json:"config"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body")
return
}
if req.Name == "" {
respondError(w, http.StatusBadRequest, "app name is required")
return
}
// Add app
appsMgr := apps.NewManager(api.dataDir, api.appsDir)
if err := appsMgr.Add(instanceName, req.Name, req.Config); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to add app: %v", err))
return
}
respondJSON(w, http.StatusCreated, map[string]string{
"message": "App added to configuration",
"app": req.Name,
})
}
// AppsDeploy deploys an app to the cluster
func (api *API) AppsDeploy(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
appName := vars["app"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Start deploy operation
opsMgr := operations.NewManager(api.dataDir)
opID, err := opsMgr.Start(instanceName, "deploy_app", appName)
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to start operation: %v", err))
return
}
// Deploy in background
go func() {
appsMgr := apps.NewManager(api.dataDir, api.appsDir)
opsMgr.UpdateStatus(instanceName, opID, "running")
if err := appsMgr.Deploy(instanceName, appName); err != nil {
opsMgr.Update(instanceName, opID, "failed", err.Error(), 0)
} else {
opsMgr.Update(instanceName, opID, "completed", "App deployed", 100)
}
}()
respondJSON(w, http.StatusAccepted, map[string]string{
"operation_id": opID,
"message": "App deployment initiated",
})
}
// AppsDelete deletes an app
func (api *API) AppsDelete(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
appName := vars["app"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Start delete operation
opsMgr := operations.NewManager(api.dataDir)
opID, err := opsMgr.Start(instanceName, "delete_app", appName)
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to start operation: %v", err))
return
}
// Delete in background
go func() {
appsMgr := apps.NewManager(api.dataDir, api.appsDir)
opsMgr.UpdateStatus(instanceName, opID, "running")
if err := appsMgr.Delete(instanceName, appName); err != nil {
opsMgr.Update(instanceName, opID, "failed", err.Error(), 0)
} else {
opsMgr.Update(instanceName, opID, "completed", "App deleted", 100)
}
}()
respondJSON(w, http.StatusAccepted, map[string]string{
"operation_id": opID,
"message": "App deletion initiated",
})
}
// AppsGetStatus returns app status
func (api *API) AppsGetStatus(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
appName := vars["app"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Get status
appsMgr := apps.NewManager(api.dataDir, api.appsDir)
status, err := appsMgr.GetStatus(instanceName, appName)
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get status: %v", err))
return
}
respondJSON(w, http.StatusOK, status)
}

View File

@@ -0,0 +1,110 @@
package v1
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
"github.com/wild-cloud/wild-central/daemon/internal/backup"
"github.com/wild-cloud/wild-central/daemon/internal/operations"
)
// BackupAppStart starts a backup operation for an app
func (api *API) BackupAppStart(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
appName := vars["app"]
mgr := backup.NewManager(api.dataDir)
// Create operation for tracking
opMgr := operations.NewManager(api.dataDir)
opID, err := opMgr.Start(instanceName, "backup", appName)
if err != nil {
respondError(w, http.StatusInternalServerError, "Failed to start backup operation")
return
}
// Run backup in background
go func() {
opMgr.UpdateProgress(instanceName, opID, 10, "Starting backup")
info, err := mgr.BackupApp(instanceName, appName)
if err != nil {
opMgr.Update(instanceName, opID, "failed", err.Error(), 100)
return
}
opMgr.Update(instanceName, opID, "completed", "Backup completed", 100)
_ = info // Metadata saved in backup.json
}()
respondJSON(w, http.StatusAccepted, map[string]interface{}{
"success": true,
"operation_id": opID,
"message": "Backup started",
})
}
// BackupAppList lists all backups for an app
func (api *API) BackupAppList(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
appName := vars["app"]
mgr := backup.NewManager(api.dataDir)
backups, err := mgr.ListBackups(instanceName, appName)
if err != nil {
respondError(w, http.StatusInternalServerError, "Failed to list backups")
return
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"data": map[string]interface{}{
"backups": backups,
},
})
}
// BackupAppRestore restores an app from backup
func (api *API) BackupAppRestore(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
appName := vars["app"]
// Parse request body for restore options
var opts backup.RestoreOptions
if err := json.NewDecoder(r.Body).Decode(&opts); err != nil {
// Use defaults if no body provided
opts = backup.RestoreOptions{}
}
mgr := backup.NewManager(api.dataDir)
// Create operation for tracking
opMgr := operations.NewManager(api.dataDir)
opID, err := opMgr.Start(instanceName, "restore", appName)
if err != nil {
respondError(w, http.StatusInternalServerError, "Failed to start restore operation")
return
}
// Run restore in background
go func() {
opMgr.UpdateProgress(instanceName, opID, 10, "Starting restore")
if err := mgr.RestoreApp(instanceName, appName, opts); err != nil {
opMgr.Update(instanceName, opID, "failed", err.Error(), 100)
return
}
opMgr.Update(instanceName, opID, "completed", "Restore completed", 100)
}()
respondJSON(w, http.StatusAccepted, map[string]interface{}{
"success": true,
"operation_id": opID,
"message": "Restore started",
})
}

View File

@@ -0,0 +1,331 @@
package v1
import (
"encoding/json"
"fmt"
"net/http"
"github.com/gorilla/mux"
"github.com/wild-cloud/wild-central/daemon/internal/cluster"
"github.com/wild-cloud/wild-central/daemon/internal/operations"
)
// ClusterGenerateConfig generates cluster configuration
func (api *API) ClusterGenerateConfig(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Read cluster configuration from instance config
configPath := api.instance.GetInstanceConfigPath(instanceName)
// Get cluster.name
clusterName, err := api.config.GetConfigValue(configPath, "cluster.name")
if err != nil || clusterName == "" {
respondError(w, http.StatusBadRequest, "cluster.name not set in config")
return
}
// Get cluster.nodes.control.vip
vip, err := api.config.GetConfigValue(configPath, "cluster.nodes.control.vip")
if err != nil || vip == "" {
respondError(w, http.StatusBadRequest, "cluster.nodes.control.vip not set in config")
return
}
// Get cluster.nodes.talos.version (optional, defaults to v1.11.0)
version, err := api.config.GetConfigValue(configPath, "cluster.nodes.talos.version")
if err != nil || version == "" {
version = "v1.11.0"
}
// Create cluster config
config := cluster.ClusterConfig{
ClusterName: clusterName,
VIP: vip,
Version: version,
}
// Generate configuration
clusterMgr := cluster.NewManager(api.dataDir)
if err := clusterMgr.GenerateConfig(instanceName, &config); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to generate config: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]string{
"message": "Cluster configuration generated successfully",
})
}
// ClusterBootstrap bootstraps the cluster
func (api *API) ClusterBootstrap(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Parse request
var req struct {
Node string `json:"node"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body")
return
}
if req.Node == "" {
respondError(w, http.StatusBadRequest, "node is required")
return
}
// Start bootstrap operation
opsMgr := operations.NewManager(api.dataDir)
opID, err := opsMgr.Start(instanceName, "bootstrap", req.Node)
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to start operation: %v", err))
return
}
// Bootstrap in background
go func() {
clusterMgr := cluster.NewManager(api.dataDir)
opsMgr.UpdateStatus(instanceName, opID, "running")
if err := clusterMgr.Bootstrap(instanceName, req.Node); err != nil {
opsMgr.Update(instanceName, opID, "failed", err.Error(), 0)
} else {
opsMgr.Update(instanceName, opID, "completed", "Bootstrap completed", 100)
}
}()
respondJSON(w, http.StatusAccepted, map[string]string{
"operation_id": opID,
"message": "Bootstrap initiated",
})
}
// ClusterConfigureEndpoints configures talosconfig endpoints to use VIP
func (api *API) ClusterConfigureEndpoints(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Parse request
var req struct {
IncludeNodes bool `json:"include_nodes"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
// Default to false if no body provided
req.IncludeNodes = false
}
// Configure endpoints
clusterMgr := cluster.NewManager(api.dataDir)
if err := clusterMgr.ConfigureEndpoints(instanceName, req.IncludeNodes); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to configure endpoints: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]string{
"message": "Endpoints configured successfully",
})
}
// ClusterGetStatus returns cluster status
func (api *API) ClusterGetStatus(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Get status
clusterMgr := cluster.NewManager(api.dataDir)
status, err := clusterMgr.GetStatus(instanceName)
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get status: %v", err))
return
}
respondJSON(w, http.StatusOK, status)
}
// ClusterHealth returns cluster health checks
func (api *API) ClusterHealth(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Get health checks
clusterMgr := cluster.NewManager(api.dataDir)
checks, err := clusterMgr.Health(instanceName)
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get health: %v", err))
return
}
// Determine overall status
overallStatus := "healthy"
for _, check := range checks {
if check.Status == "failing" {
overallStatus = "unhealthy"
break
} else if check.Status == "warning" && overallStatus == "healthy" {
overallStatus = "degraded"
}
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"status": overallStatus,
"checks": checks,
})
}
// ClusterGetKubeconfig returns the kubeconfig
func (api *API) ClusterGetKubeconfig(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Get kubeconfig
clusterMgr := cluster.NewManager(api.dataDir)
kubeconfig, err := clusterMgr.GetKubeconfig(instanceName)
if err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Kubeconfig not found: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]string{
"kubeconfig": kubeconfig,
})
}
// ClusterGenerateKubeconfig regenerates the kubeconfig from the cluster
func (api *API) ClusterGenerateKubeconfig(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Regenerate kubeconfig from cluster
clusterMgr := cluster.NewManager(api.dataDir)
if err := clusterMgr.RegenerateKubeconfig(instanceName); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to generate kubeconfig: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]string{
"message": "Kubeconfig regenerated successfully",
})
}
// ClusterGetTalosconfig returns the talosconfig
func (api *API) ClusterGetTalosconfig(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Get talosconfig
clusterMgr := cluster.NewManager(api.dataDir)
talosconfig, err := clusterMgr.GetTalosconfig(instanceName)
if err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Talosconfig not found: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]string{
"talosconfig": talosconfig,
})
}
// ClusterReset resets the cluster
func (api *API) ClusterReset(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Parse request
var req struct {
Confirm bool `json:"confirm"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body")
return
}
if !req.Confirm {
respondError(w, http.StatusBadRequest, "Must confirm cluster reset")
return
}
// Start reset operation
opsMgr := operations.NewManager(api.dataDir)
opID, err := opsMgr.Start(instanceName, "reset", instanceName)
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to start operation: %v", err))
return
}
// Reset in background
go func() {
clusterMgr := cluster.NewManager(api.dataDir)
opsMgr.UpdateStatus(instanceName, opID, "running")
if err := clusterMgr.Reset(instanceName, req.Confirm); err != nil {
opsMgr.Update(instanceName, opID, "failed", err.Error(), 0)
} else {
opsMgr.Update(instanceName, opID, "completed", "Cluster reset completed", 100)
}
}()
respondJSON(w, http.StatusAccepted, map[string]string{
"operation_id": opID,
"message": "Cluster reset initiated",
})
}

View File

@@ -0,0 +1,76 @@
package v1
import (
"encoding/json"
"fmt"
"net/http"
"github.com/gorilla/mux"
)
// ConfigUpdate represents a single configuration update
type ConfigUpdate struct {
Path string `json:"path"`
Value interface{} `json:"value"`
}
// ConfigUpdateBatchRequest represents a batch configuration update request
type ConfigUpdateBatchRequest struct {
Updates []ConfigUpdate `json:"updates"`
}
// ConfigUpdateBatch updates multiple configuration values atomically
func (api *API) ConfigUpdateBatch(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
name := vars["name"]
// Validate instance exists
if err := api.instance.ValidateInstance(name); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Parse request body
var req ConfigUpdateBatchRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body")
return
}
if len(req.Updates) == 0 {
respondError(w, http.StatusBadRequest, "updates array is required and cannot be empty")
return
}
// Get config path
configPath := api.instance.GetInstanceConfigPath(name)
// Validate all paths before applying changes
for i, update := range req.Updates {
if update.Path == "" {
respondError(w, http.StatusBadRequest, fmt.Sprintf("update[%d]: path is required", i))
return
}
}
// Apply all updates atomically
// The config manager's SetConfigValue already uses file locking,
// so each individual update is atomic. For true atomicity across
// all updates, we would need to implement transaction support.
// For now, we apply updates sequentially within the lock.
updateCount := 0
for _, update := range req.Updates {
valueStr := fmt.Sprintf("%v", update.Value)
if err := api.config.SetConfigValue(configPath, update.Path, valueStr); err != nil {
respondError(w, http.StatusInternalServerError,
fmt.Sprintf("Failed to update config path %s: %v", update.Path, err))
return
}
updateCount++
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"message": "Configuration updated successfully",
"updated": updateCount,
})
}

View File

@@ -0,0 +1,317 @@
package v1
import (
"encoding/json"
"fmt"
"net/http"
"github.com/gorilla/mux"
"github.com/wild-cloud/wild-central/daemon/internal/discovery"
"github.com/wild-cloud/wild-central/daemon/internal/node"
)
// NodeDiscover initiates node discovery
func (api *API) NodeDiscover(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Parse request body
var req struct {
IPList []string `json:"ip_list"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body")
return
}
if len(req.IPList) == 0 {
respondError(w, http.StatusBadRequest, "ip_list is required")
return
}
// Start discovery
discoveryMgr := discovery.NewManager(api.dataDir, instanceName)
if err := discoveryMgr.StartDiscovery(instanceName, req.IPList); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to start discovery: %v", err))
return
}
respondJSON(w, http.StatusAccepted, map[string]string{
"message": "Discovery started",
"status": "running",
})
}
// NodeDiscoveryStatus returns discovery status
func (api *API) NodeDiscoveryStatus(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
discoveryMgr := discovery.NewManager(api.dataDir, instanceName)
status, err := discoveryMgr.GetDiscoveryStatus(instanceName)
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get status: %v", err))
return
}
respondJSON(w, http.StatusOK, status)
}
// NodeHardware returns hardware info for a specific node
func (api *API) NodeHardware(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
nodeIP := vars["ip"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Detect hardware
nodeMgr := node.NewManager(api.dataDir)
hwInfo, err := nodeMgr.DetectHardware(nodeIP)
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to detect hardware: %v", err))
return
}
respondJSON(w, http.StatusOK, hwInfo)
}
// NodeDetect detects hardware on a single node (POST with IP in body)
func (api *API) NodeDetect(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Parse request body
var req struct {
IP string `json:"ip"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body")
return
}
if req.IP == "" {
respondError(w, http.StatusBadRequest, "ip is required")
return
}
// Detect hardware
nodeMgr := node.NewManager(api.dataDir)
hwInfo, err := nodeMgr.DetectHardware(req.IP)
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to detect hardware: %v", err))
return
}
respondJSON(w, http.StatusOK, hwInfo)
}
// NodeAdd registers a new node
func (api *API) NodeAdd(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Parse node data
var nodeData node.Node
if err := json.NewDecoder(r.Body).Decode(&nodeData); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body")
return
}
// Add node
nodeMgr := node.NewManager(api.dataDir)
if err := nodeMgr.Add(instanceName, &nodeData); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to add node: %v", err))
return
}
respondJSON(w, http.StatusCreated, map[string]interface{}{
"message": "Node added successfully",
"node": nodeData,
})
}
// NodeList returns all nodes for an instance
func (api *API) NodeList(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// List nodes
nodeMgr := node.NewManager(api.dataDir)
nodes, err := nodeMgr.List(instanceName)
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to list nodes: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"nodes": nodes,
})
}
// NodeGet returns a specific node
func (api *API) NodeGet(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
nodeIdentifier := vars["node"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Get node
nodeMgr := node.NewManager(api.dataDir)
nodeData, err := nodeMgr.Get(instanceName, nodeIdentifier)
if err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Node not found: %v", err))
return
}
respondJSON(w, http.StatusOK, nodeData)
}
// NodeApply generates configuration and applies it to node
func (api *API) NodeApply(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
nodeIdentifier := vars["node"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Apply always uses default options (no body needed)
opts := node.ApplyOptions{}
// Apply node configuration
nodeMgr := node.NewManager(api.dataDir)
if err := nodeMgr.Apply(instanceName, nodeIdentifier, opts); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to apply node configuration: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]string{
"message": "Node configuration applied successfully",
"node": nodeIdentifier,
})
}
// NodeUpdate modifies existing node configuration
func (api *API) NodeUpdate(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
nodeIdentifier := vars["node"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Parse update data
var updates map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body")
return
}
// Update node
nodeMgr := node.NewManager(api.dataDir)
if err := nodeMgr.Update(instanceName, nodeIdentifier, updates); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to update node: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]string{
"message": "Node updated successfully",
"node": nodeIdentifier,
})
}
// NodeFetchTemplates copies patch templates from directory to instance
func (api *API) NodeFetchTemplates(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Fetch templates
nodeMgr := node.NewManager(api.dataDir)
if err := nodeMgr.FetchTemplates(instanceName); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to fetch templates: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]string{
"message": "Templates fetched successfully",
})
}
// NodeDelete removes a node
func (api *API) NodeDelete(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
nodeIdentifier := vars["node"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Delete node
nodeMgr := node.NewManager(api.dataDir)
if err := nodeMgr.Delete(instanceName, nodeIdentifier); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to delete node: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]string{
"message": "Node deleted successfully",
})
}

View File

@@ -0,0 +1,156 @@
package v1
import (
"bufio"
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"github.com/gorilla/mux"
"github.com/wild-cloud/wild-central/daemon/internal/operations"
)
// OperationGet returns operation status
func (api *API) OperationGet(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
opID := vars["id"]
// Extract instance name from query param or header
instanceName := r.URL.Query().Get("instance")
if instanceName == "" {
respondError(w, http.StatusBadRequest, "instance parameter is required")
return
}
// Get operation
opsMgr := operations.NewManager(api.dataDir)
op, err := opsMgr.GetByInstance(instanceName, opID)
if err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Operation not found: %v", err))
return
}
respondJSON(w, http.StatusOK, op)
}
// OperationList returns all operations for an instance
func (api *API) OperationList(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// List operations
opsMgr := operations.NewManager(api.dataDir)
ops, err := opsMgr.List(instanceName)
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to list operations: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"operations": ops,
})
}
// OperationCancel cancels an operation
func (api *API) OperationCancel(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
opID := vars["id"]
// Extract instance name from query param
instanceName := r.URL.Query().Get("instance")
if instanceName == "" {
respondError(w, http.StatusBadRequest, "instance parameter is required")
return
}
// Cancel operation
opsMgr := operations.NewManager(api.dataDir)
if err := opsMgr.Cancel(instanceName, opID); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to cancel operation: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]string{
"message": "Operation cancelled",
"id": opID,
})
}
// OperationStream streams operation output via Server-Sent Events (SSE)
func (api *API) OperationStream(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
opID := vars["id"]
// Extract instance name from query param
instanceName := r.URL.Query().Get("instance")
if instanceName == "" {
respondError(w, http.StatusBadRequest, "instance parameter is required")
return
}
// Set SSE headers
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*")
flusher, ok := w.(http.Flusher)
if !ok {
respondError(w, http.StatusInternalServerError, "Streaming not supported")
return
}
// Check if operation is already completed
statusFile := filepath.Join(api.dataDir, "instances", instanceName, "operations", opID+".json")
isCompleted := false
if data, err := os.ReadFile(statusFile); err == nil {
var op map[string]interface{}
if err := json.Unmarshal(data, &op); err == nil {
if status, ok := op["status"].(string); ok {
isCompleted = (status == "completed" || status == "failed")
}
}
}
// Send existing log file content first (if exists)
logPath := filepath.Join(api.dataDir, "instances", instanceName, "operations", opID, "output.log")
if _, err := os.Stat(logPath); err == nil {
file, err := os.Open(logPath)
if err == nil {
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
fmt.Fprintf(w, "data: %s\n\n", line)
flusher.Flush()
}
}
}
// If operation is already completed, send completion signal and return
if isCompleted {
// Send an event to signal completion
fmt.Fprintf(w, "event: complete\ndata: Operation completed\n\n")
flusher.Flush()
return
}
// Subscribe to new output for ongoing operations
ch := api.broadcaster.Subscribe(opID)
defer api.broadcaster.Unsubscribe(opID, ch)
// Stream new output as it arrives
for data := range ch {
fmt.Fprintf(w, "data: %s\n\n", data)
flusher.Flush()
}
}

View File

@@ -0,0 +1,141 @@
package v1
import (
"encoding/json"
"fmt"
"net/http"
"github.com/gorilla/mux"
"github.com/wild-cloud/wild-central/daemon/internal/pxe"
)
// PXEListAssets lists all PXE assets for an instance
func (api *API) PXEListAssets(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// List assets
pxeMgr := pxe.NewManager(api.dataDir)
assets, err := pxeMgr.ListAssets(instanceName)
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to list assets: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"assets": assets,
})
}
// PXEDownloadAsset downloads a PXE asset
func (api *API) PXEDownloadAsset(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Parse request
var req struct {
AssetType string `json:"asset_type"` // kernel, initramfs, iso
Version string `json:"version"`
URL string `json:"url"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body")
return
}
if req.AssetType == "" {
respondError(w, http.StatusBadRequest, "asset_type is required")
return
}
if req.URL == "" {
respondError(w, http.StatusBadRequest, "url is required")
return
}
// Download asset
pxeMgr := pxe.NewManager(api.dataDir)
if err := pxeMgr.DownloadAsset(instanceName, req.AssetType, req.Version, req.URL); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to download asset: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]string{
"message": "Asset downloaded successfully",
"asset_type": req.AssetType,
"version": req.Version,
})
}
// PXEGetAsset returns information about a specific asset
func (api *API) PXEGetAsset(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
assetType := vars["type"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Get asset path
pxeMgr := pxe.NewManager(api.dataDir)
assetPath, err := pxeMgr.GetAssetPath(instanceName, assetType)
if err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Asset not found: %v", err))
return
}
// Verify asset
valid, err := pxeMgr.VerifyAsset(instanceName, assetType)
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to verify asset: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"type": assetType,
"path": assetPath,
"valid": valid,
})
}
// PXEDeleteAsset deletes a PXE asset
func (api *API) PXEDeleteAsset(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
assetType := vars["type"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Delete asset
pxeMgr := pxe.NewManager(api.dataDir)
if err := pxeMgr.DeleteAsset(instanceName, assetType); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to delete asset: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]string{
"message": "Asset deleted successfully",
"type": assetType,
})
}

View File

@@ -0,0 +1,424 @@
package v1
import (
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/gorilla/mux"
"gopkg.in/yaml.v3"
"github.com/wild-cloud/wild-central/daemon/internal/operations"
"github.com/wild-cloud/wild-central/daemon/internal/services"
)
// ServicesList lists all base services
func (api *API) ServicesList(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// List services
servicesMgr := services.NewManager(api.dataDir, filepath.Join(api.directoryPath, "setup", "cluster-services"))
svcList, err := servicesMgr.List(instanceName)
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to list services: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"services": svcList,
})
}
// ServicesGet returns a specific service
func (api *API) ServicesGet(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
serviceName := vars["service"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Get service
servicesMgr := services.NewManager(api.dataDir, filepath.Join(api.directoryPath, "setup", "cluster-services"))
service, err := servicesMgr.Get(instanceName, serviceName)
if err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Service not found: %v", err))
return
}
respondJSON(w, http.StatusOK, service)
}
// ServicesInstall installs a service
func (api *API) ServicesInstall(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Parse request
var req struct {
Name string `json:"name"`
Fetch bool `json:"fetch"`
Deploy bool `json:"deploy"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body")
return
}
if req.Name == "" {
respondError(w, http.StatusBadRequest, "service name is required")
return
}
// Start install operation
opsMgr := operations.NewManager(api.dataDir)
opID, err := opsMgr.Start(instanceName, "install_service", req.Name)
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to start operation: %v", err))
return
}
// Install in background
go func() {
// Recover from panics to prevent goroutine crashes
defer func() {
if r := recover(); r != nil {
fmt.Printf("[ERROR] Service install goroutine panic: %v\n", r)
opsMgr.Update(instanceName, opID, "failed", fmt.Sprintf("Internal error: %v", r), 0)
}
}()
fmt.Printf("[DEBUG] Service install goroutine started: service=%s instance=%s opID=%s\n", req.Name, instanceName, opID)
servicesMgr := services.NewManager(api.dataDir, filepath.Join(api.directoryPath, "setup", "cluster-services"))
opsMgr.UpdateStatus(instanceName, opID, "running")
if err := servicesMgr.Install(instanceName, req.Name, req.Fetch, req.Deploy, opID, api.broadcaster); err != nil {
fmt.Printf("[DEBUG] Service install failed: %v\n", err)
opsMgr.Update(instanceName, opID, "failed", err.Error(), 0)
} else {
fmt.Printf("[DEBUG] Service install completed successfully\n")
opsMgr.Update(instanceName, opID, "completed", "Service installed", 100)
}
}()
respondJSON(w, http.StatusAccepted, map[string]string{
"operation_id": opID,
"message": "Service installation initiated",
})
}
// ServicesInstallAll installs all base services
func (api *API) ServicesInstallAll(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Parse request
var req struct {
Fetch bool `json:"fetch"`
Deploy bool `json:"deploy"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
// Use defaults if no body
req.Deploy = true
}
// Start install operation
opsMgr := operations.NewManager(api.dataDir)
opID, err := opsMgr.Start(instanceName, "install_all_services", "all")
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to start operation: %v", err))
return
}
// Install in background
go func() {
servicesMgr := services.NewManager(api.dataDir, filepath.Join(api.directoryPath, "setup", "cluster-services"))
opsMgr.UpdateStatus(instanceName, opID, "running")
if err := servicesMgr.InstallAll(instanceName, req.Fetch, req.Deploy, opID, api.broadcaster); err != nil {
opsMgr.Update(instanceName, opID, "failed", err.Error(), 0)
} else {
opsMgr.Update(instanceName, opID, "completed", "All services installed", 100)
}
}()
respondJSON(w, http.StatusAccepted, map[string]string{
"operation_id": opID,
"message": "Services installation initiated",
})
}
// ServicesDelete deletes a service
func (api *API) ServicesDelete(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
serviceName := vars["service"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Start delete operation
opsMgr := operations.NewManager(api.dataDir)
opID, err := opsMgr.Start(instanceName, "delete_service", serviceName)
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to start operation: %v", err))
return
}
// Delete in background
go func() {
servicesMgr := services.NewManager(api.dataDir, filepath.Join(api.directoryPath, "setup", "cluster-services"))
opsMgr.UpdateStatus(instanceName, opID, "running")
if err := servicesMgr.Delete(instanceName, serviceName); err != nil {
opsMgr.Update(instanceName, opID, "failed", err.Error(), 0)
} else {
opsMgr.Update(instanceName, opID, "completed", "Service deleted", 100)
}
}()
respondJSON(w, http.StatusAccepted, map[string]string{
"operation_id": opID,
"message": "Service deletion initiated",
})
}
// ServicesGetStatus returns detailed service status
func (api *API) ServicesGetStatus(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
serviceName := vars["service"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Get status
servicesMgr := services.NewManager(api.dataDir, filepath.Join(api.directoryPath, "setup", "cluster-services"))
status, err := servicesMgr.GetStatus(instanceName, serviceName)
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get status: %v", err))
return
}
respondJSON(w, http.StatusOK, status)
}
// ServicesGetManifest returns the manifest for a service
func (api *API) ServicesGetManifest(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
serviceName := vars["service"]
servicesMgr := services.NewManager(api.dataDir, filepath.Join(api.directoryPath, "setup", "cluster-services"))
manifest, err := servicesMgr.GetManifest(serviceName)
if err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Service not found: %v", err))
return
}
respondJSON(w, http.StatusOK, manifest)
}
// ServicesGetConfig returns the service configuration schema
func (api *API) ServicesGetConfig(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
serviceName := vars["service"]
servicesMgr := services.NewManager(api.dataDir, filepath.Join(api.directoryPath, "setup", "cluster-services"))
// Get manifest
manifest, err := servicesMgr.GetManifest(serviceName)
if err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Service not found: %v", err))
return
}
// Return config schema
response := map[string]interface{}{
"configReferences": manifest.ConfigReferences,
"serviceConfig": manifest.ServiceConfig,
}
respondJSON(w, http.StatusOK, response)
}
// ServicesGetInstanceConfig returns current config values for a service instance
func (api *API) ServicesGetInstanceConfig(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
serviceName := vars["service"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
servicesMgr := services.NewManager(api.dataDir, filepath.Join(api.directoryPath, "setup", "cluster-services"))
// Get manifest to know which config paths to read
manifest, err := servicesMgr.GetManifest(serviceName)
if err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Service not found: %v", err))
return
}
// Load instance config as map for dynamic path extraction
configPath := filepath.Join(api.dataDir, "instances", instanceName, "config.yaml")
configData, err := os.ReadFile(configPath)
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to read instance config: %v", err))
return
}
var instanceConfig map[string]interface{}
if err := yaml.Unmarshal(configData, &instanceConfig); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to parse instance config: %v", err))
return
}
// Extract values for all config paths
configValues := make(map[string]interface{})
// Add config references
for _, path := range manifest.ConfigReferences {
if value := getNestedValue(instanceConfig, path); value != nil {
configValues[path] = value
}
}
// Add service config
for _, cfg := range manifest.ServiceConfig {
if value := getNestedValue(instanceConfig, cfg.Path); value != nil {
configValues[cfg.Path] = value
}
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"config": configValues,
})
}
// getNestedValue retrieves a value from nested map using dot notation path
func getNestedValue(data map[string]interface{}, path string) interface{} {
keys := strings.Split(path, ".")
current := data
for i, key := range keys {
if i == len(keys)-1 {
return current[key]
}
if next, ok := current[key].(map[string]interface{}); ok {
current = next
} else {
return nil
}
}
return nil
}
// ServicesFetch handles fetching service files to instance
func (api *API) ServicesFetch(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
serviceName := vars["service"]
// Validate instance exists
if !api.instance.InstanceExists(instanceName) {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance '%s' not found", instanceName))
return
}
// Fetch service files
servicesMgr := services.NewManager(api.dataDir, filepath.Join(api.directoryPath, "setup", "cluster-services"))
if err := servicesMgr.Fetch(instanceName, serviceName); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to fetch service: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]string{
"message": fmt.Sprintf("Service %s files fetched successfully", serviceName),
})
}
// ServicesCompile handles template compilation
func (api *API) ServicesCompile(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
serviceName := vars["service"]
// Validate instance exists
if !api.instance.InstanceExists(instanceName) {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance '%s' not found", instanceName))
return
}
// Compile templates
servicesMgr := services.NewManager(api.dataDir, filepath.Join(api.directoryPath, "setup", "cluster-services"))
if err := servicesMgr.Compile(instanceName, serviceName); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to compile templates: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]string{
"message": fmt.Sprintf("Templates compiled successfully for %s", serviceName),
})
}
// ServicesDeploy handles service deployment
func (api *API) ServicesDeploy(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
serviceName := vars["service"]
// Validate instance exists
if !api.instance.InstanceExists(instanceName) {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance '%s' not found", instanceName))
return
}
// Deploy service (without operation tracking for standalone deploy)
servicesMgr := services.NewManager(api.dataDir, filepath.Join(api.directoryPath, "setup", "cluster-services"))
if err := servicesMgr.Deploy(instanceName, serviceName, "", nil); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to deploy service: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]string{
"message": fmt.Sprintf("Service %s deployed successfully", serviceName),
})
}

View File

@@ -0,0 +1,151 @@
package v1
import (
"encoding/json"
"fmt"
"net/http"
"path/filepath"
"github.com/gorilla/mux"
"github.com/wild-cloud/wild-central/daemon/internal/utilities"
)
// UtilitiesHealth returns cluster health status (legacy, no instance context)
func (api *API) UtilitiesHealth(w http.ResponseWriter, r *http.Request) {
status, err := utilities.GetClusterHealth("")
if err != nil {
respondError(w, http.StatusInternalServerError, "Failed to get cluster health")
return
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"data": status,
})
}
// InstanceUtilitiesHealth returns cluster health status for a specific instance
func (api *API) InstanceUtilitiesHealth(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
// Get kubeconfig path for this instance
kubeconfigPath := filepath.Join(api.dataDir, "instances", instanceName, "kubeconfig")
status, err := utilities.GetClusterHealth(kubeconfigPath)
if err != nil {
respondError(w, http.StatusInternalServerError, "Failed to get cluster health")
return
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"data": status,
})
}
// UtilitiesDashboardToken returns a Kubernetes dashboard token
func (api *API) UtilitiesDashboardToken(w http.ResponseWriter, r *http.Request) {
token, err := utilities.GetDashboardToken()
if err != nil {
// Try fallback method
token, err = utilities.GetDashboardTokenFromSecret()
if err != nil {
respondError(w, http.StatusInternalServerError, "Failed to get dashboard token")
return
}
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"data": token,
})
}
// UtilitiesNodeIPs returns IP addresses for all cluster nodes
func (api *API) UtilitiesNodeIPs(w http.ResponseWriter, r *http.Request) {
nodes, err := utilities.GetNodeIPs()
if err != nil {
respondError(w, http.StatusInternalServerError, "Failed to get node IPs")
return
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"data": map[string]interface{}{
"nodes": nodes,
},
})
}
// UtilitiesControlPlaneIP returns the control plane IP
func (api *API) UtilitiesControlPlaneIP(w http.ResponseWriter, r *http.Request) {
ip, err := utilities.GetControlPlaneIP()
if err != nil {
respondError(w, http.StatusInternalServerError, "Failed to get control plane IP")
return
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"data": map[string]interface{}{
"ip": ip,
},
})
}
// UtilitiesSecretCopy copies a secret between namespaces
func (api *API) UtilitiesSecretCopy(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
secretName := vars["secret"]
var req struct {
SourceNamespace string `json:"source_namespace"`
DestinationNamespace string `json:"destination_namespace"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body")
return
}
if req.SourceNamespace == "" || req.DestinationNamespace == "" {
respondError(w, http.StatusBadRequest, "source_namespace and destination_namespace are required")
return
}
if err := utilities.CopySecretBetweenNamespaces(secretName, req.SourceNamespace, req.DestinationNamespace); err != nil {
respondError(w, http.StatusInternalServerError, "Failed to copy secret")
return
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"message": "Secret copied successfully",
})
}
// UtilitiesVersion returns cluster and Talos versions
func (api *API) UtilitiesVersion(w http.ResponseWriter, r *http.Request) {
k8sVersion, err := utilities.GetClusterVersion()
if err != nil {
respondError(w, http.StatusInternalServerError, "Failed to get cluster version")
return
}
talosVersion, _ := utilities.GetTalosVersion() // Don't fail if Talos check fails
respondJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"data": map[string]interface{}{
"kubernetes": k8sVersion,
"talos": talosVersion,
},
})
}