diff --git a/BUILDING_WILD_API.md b/BUILDING_WILD_API.md index d2aa9cd..9a877e9 100644 --- a/BUILDING_WILD_API.md +++ b/BUILDING_WILD_API.md @@ -42,3 +42,77 @@ ### Features - If WILD_CENTRAL_ENV environment variable is set to "development", the API should run in development mode. + +## Patterns + +### Instance-scoped Endpoints + +Instance-scoped endpoints follow a consistent pattern to ensure stateless, RESTful API design. The instance name is always included in the URL path, not retrieved from session state or context. + +#### Route Pattern + +```go +// In handlers.go +r.HandleFunc("/api/v1/instances/{name}/utilities/dashboard/token", api.UtilitiesDashboardToken).Methods("GET") +``` + +#### Handler Pattern + +```go +// In handlers_utilities.go +func (api *API) UtilitiesDashboardToken(w http.ResponseWriter, r *http.Request) { + // 1. Extract instance name from URL path parameters + vars := mux.Vars(r) + instanceName := vars["name"] + + // 2. Validate instance exists + if err := api.instance.ValidateInstance(instanceName); err != nil { + respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err)) + return + } + + // 3. Construct instance-specific paths + kubeconfigPath := filepath.Join(api.dataDir, "instances", instanceName, "kubeconfig") + + // 4. Perform instance-specific operations + token, err := utilities.GetDashboardToken(kubeconfigPath) + if err != nil { + respondError(w, http.StatusInternalServerError, "Failed to get dashboard token") + return + } + + // 5. Return response + respondJSON(w, http.StatusOK, map[string]interface{}{ + "success": true, + "data": token, + }) +} +``` + +#### Using Kubeconfig with kubectl/talosctl + +When making kubectl or talosctl calls for a specific instance, use the `tools.WithKubeconfig()` helper to set the KUBECONFIG environment variable: + +```go +// In utilities.go or similar +func GetDashboardToken(kubeconfigPath string) (*DashboardToken, error) { + cmd := exec.Command("kubectl", "-n", "kubernetes-dashboard", "create", "token", "dashboard-admin") + tools.WithKubeconfig(cmd, kubeconfigPath) + output, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("failed to create token: %w", err) + } + + token := strings.TrimSpace(string(output)) + return &DashboardToken{Token: token}, nil +} +``` + +#### Key Principles + +1. **Instance name in URL**: Always include instance name as a path parameter (`{name}`) +2. **Extract from mux.Vars()**: Get instance name from `mux.Vars(r)["name"]`, not from context +3. **Validate instance**: Always validate the instance exists before operations +4. **Construct paths explicitly**: Build instance-specific file paths from the instance name +5. **Stateless handlers**: Handlers should not depend on session state or current context +6. **Use tools helpers**: Use `tools.WithKubeconfig()` for kubectl/talosctl commands diff --git a/internal/api/v1/handlers.go b/internal/api/v1/handlers.go index 37be46b..9ccdae2 100644 --- a/internal/api/v1/handlers.go +++ b/internal/api/v1/handlers.go @@ -158,7 +158,7 @@ func (api *API) RegisterRoutes(r *mux.Router) { // 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/instances/{name}/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") diff --git a/internal/api/v1/handlers_utilities.go b/internal/api/v1/handlers_utilities.go index 879f9b1..f06cc2c 100644 --- a/internal/api/v1/handlers_utilities.go +++ b/internal/api/v1/handlers_utilities.go @@ -50,12 +50,24 @@ func (api *API) InstanceUtilitiesHealth(w http.ResponseWriter, r *http.Request) }) } -// UtilitiesDashboardToken returns a Kubernetes dashboard token +// InstanceUtilitiesDashboardToken returns a Kubernetes dashboard token for a specific instance func (api *API) UtilitiesDashboardToken(w http.ResponseWriter, r *http.Request) { - token, err := utilities.GetDashboardToken() + 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 the instance + kubeconfigPath := filepath.Join(api.dataDir, "instances", instanceName, "kubeconfig") + + token, err := utilities.GetDashboardToken(kubeconfigPath) if err != nil { // Try fallback method - token, err = utilities.GetDashboardTokenFromSecret() + token, err = utilities.GetDashboardTokenFromSecret(kubeconfigPath) if err != nil { respondError(w, http.StatusInternalServerError, "Failed to get dashboard token") return diff --git a/internal/utilities/utilities.go b/internal/utilities/utilities.go index 98f9b75..2dc0fe5 100644 --- a/internal/utilities/utilities.go +++ b/internal/utilities/utilities.go @@ -7,6 +7,8 @@ import ( "fmt" "os/exec" "strings" + + "github.com/wild-cloud/wild-central/daemon/internal/tools" ) // HealthStatus represents cluster health information @@ -127,15 +129,17 @@ func checkComponent(kubeconfigPath, namespace, selector string) error { } // GetDashboardToken retrieves or creates a Kubernetes dashboard token -func GetDashboardToken() (*DashboardToken, error) { +func GetDashboardToken(kubeconfigPath string) (*DashboardToken, error) { // Check if service account exists cmd := exec.Command("kubectl", "get", "serviceaccount", "-n", "kubernetes-dashboard", "dashboard-admin") + tools.WithKubeconfig(cmd, kubeconfigPath) if err := cmd.Run(); err != nil { return nil, fmt.Errorf("dashboard-admin service account not found") } // Create token cmd = exec.Command("kubectl", "-n", "kubernetes-dashboard", "create", "token", "dashboard-admin") + tools.WithKubeconfig(cmd, kubeconfigPath) output, err := cmd.Output() if err != nil { return nil, fmt.Errorf("failed to create token: %w", err) @@ -148,9 +152,10 @@ func GetDashboardToken() (*DashboardToken, error) { } // GetDashboardTokenFromSecret retrieves dashboard token from secret (fallback method) -func GetDashboardTokenFromSecret() (*DashboardToken, error) { +func GetDashboardTokenFromSecret(kubeconfigPath string) (*DashboardToken, error) { cmd := exec.Command("kubectl", "-n", "kubernetes-dashboard", "get", "secret", "dashboard-admin-token", "-o", "jsonpath={.data.token}") + tools.WithKubeconfig(cmd, kubeconfigPath) output, err := cmd.Output() if err != nil { return nil, fmt.Errorf("failed to get token secret: %w", err)