Instance-namespace additional utility endpoints.

This commit is contained in:
2025-10-14 21:04:18 +00:00
parent 67ca1b85be
commit 5b7d2835e7
7 changed files with 118 additions and 60 deletions

View File

@@ -89,9 +89,19 @@ func (api *API) UtilitiesDashboardToken(w http.ResponseWriter, r *http.Request)
} }
``` ```
#### Using Kubeconfig with kubectl/talosctl #### Key Principles
When making kubectl or talosctl calls for a specific instance, use the `tools.WithKubeconfig()` helper to set the KUBECONFIG environment variable: 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. **Use path helpers**: Use `tools.GetKubeconfigPath()`, `tools.GetInstanceConfigPath()`, etc. instead of inline `filepath.Join()` constructions
5. **Stateless handlers**: Handlers should not depend on session state or current context
### kubectl and talosctl Commands
When making kubectl or talosctl calls for a specific instance, always use the `tools` package helpers to set the correct context.
#### Using kubectl with Instance Kubeconfig
```go ```go
// In utilities.go or similar // In utilities.go or similar
@@ -108,11 +118,25 @@ func GetDashboardToken(kubeconfigPath string) (*DashboardToken, error) {
} }
``` ```
#### Using talosctl with Instance Talosconfig
```go
// In cluster operations
func GetClusterHealth(talosconfigPath string, nodeIP string) error {
cmd := exec.Command("talosctl", "health", "--nodes", nodeIP)
tools.WithTalosconfig(cmd, talosconfigPath)
output, err := cmd.Output()
if err != nil {
return fmt.Errorf("failed to check health: %w", err)
}
// Process output...
return nil
}
```
#### Key Principles #### Key Principles
1. **Instance name in URL**: Always include instance name as a path parameter (`{name}`) 1. **Use tools helpers**: Always use `tools.WithKubeconfig()` or `tools.WithTalosconfig()` instead of manually setting environment variables
2. **Extract from mux.Vars()**: Get instance name from `mux.Vars(r)["name"]`, not from context 2. **Get paths from tools package**: Use `tools.GetKubeconfigPath()` or `tools.GetTalosconfigPath()` to construct config paths
3. **Validate instance**: Always validate the instance exists before operations 3. **One config per command**: Each exec.Command should have its config set via the appropriate helper
4. **Use path helpers**: Use `tools.GetKubeconfigPath()`, `tools.GetInstanceConfigPath()`, etc. instead of inline `filepath.Join()` constructions 4. **Error handling**: Always check for command execution errors and provide context
5. **Stateless handlers**: Handlers should not depend on session state or current context
6. **Use tools helpers**: Use `tools.WithKubeconfig()` for kubectl/talosctl commands

View File

@@ -108,9 +108,9 @@ func (api *API) RegisterRoutes(r *mux.Router) {
// Operations // Operations
r.HandleFunc("/api/v1/instances/{name}/operations", api.OperationList).Methods("GET") 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/instances/{name}/operations/{id}", api.OperationGet).Methods("GET")
r.HandleFunc("/api/v1/operations/{id}/stream", api.OperationStream).Methods("GET") r.HandleFunc("/api/v1/instances/{name}/operations/{id}/stream", api.OperationStream).Methods("GET")
r.HandleFunc("/api/v1/operations/{id}/cancel", api.OperationCancel).Methods("POST") r.HandleFunc("/api/v1/instances/{name}/operations/{id}/cancel", api.OperationCancel).Methods("POST")
// Cluster operations // Cluster operations
r.HandleFunc("/api/v1/instances/{name}/cluster/config/generate", api.ClusterGenerateConfig).Methods("POST") r.HandleFunc("/api/v1/instances/{name}/cluster/config/generate", api.ClusterGenerateConfig).Methods("POST")
@@ -156,13 +156,12 @@ func (api *API) RegisterRoutes(r *mux.Router) {
r.HandleFunc("/api/v1/instances/{name}/apps/{app}/restore", api.BackupAppRestore).Methods("POST") r.HandleFunc("/api/v1/instances/{name}/apps/{app}/restore", api.BackupAppRestore).Methods("POST")
// Utilities // 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/instances/{name}/utilities/health", api.InstanceUtilitiesHealth).Methods("GET")
r.HandleFunc("/api/v1/instances/{name}/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/instances/{name}/utilities/nodes/ips", api.UtilitiesNodeIPs).Methods("GET")
r.HandleFunc("/api/v1/utilities/controlplane/ip", api.UtilitiesControlPlaneIP).Methods("GET") r.HandleFunc("/api/v1/instances/{name}/utilities/controlplane/ip", api.UtilitiesControlPlaneIP).Methods("GET")
r.HandleFunc("/api/v1/utilities/secrets/{secret}/copy", api.UtilitiesSecretCopy).Methods("POST") r.HandleFunc("/api/v1/instances/{name}/utilities/secrets/{secret}/copy", api.UtilitiesSecretCopy).Methods("POST")
r.HandleFunc("/api/v1/utilities/version", api.UtilitiesVersion).Methods("GET") r.HandleFunc("/api/v1/instances/{name}/utilities/version", api.UtilitiesVersion).Methods("GET")
// dnsmasq management // dnsmasq management
r.HandleFunc("/api/v1/dnsmasq/status", api.DnsmasqStatus).Methods("GET") r.HandleFunc("/api/v1/dnsmasq/status", api.DnsmasqStatus).Methods("GET")

View File

@@ -17,12 +17,12 @@ import (
// OperationGet returns operation status // OperationGet returns operation status
func (api *API) OperationGet(w http.ResponseWriter, r *http.Request) { func (api *API) OperationGet(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
instanceName := vars["name"]
opID := vars["id"] opID := vars["id"]
// Extract instance name from query param or header // Validate instance exists
instanceName := r.URL.Query().Get("instance") if err := api.instance.ValidateInstance(instanceName); err != nil {
if instanceName == "" { respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
respondError(w, http.StatusBadRequest, "instance parameter is required")
return return
} }
@@ -64,12 +64,12 @@ func (api *API) OperationList(w http.ResponseWriter, r *http.Request) {
// OperationCancel cancels an operation // OperationCancel cancels an operation
func (api *API) OperationCancel(w http.ResponseWriter, r *http.Request) { func (api *API) OperationCancel(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
instanceName := vars["name"]
opID := vars["id"] opID := vars["id"]
// Extract instance name from query param // Validate instance exists
instanceName := r.URL.Query().Get("instance") if err := api.instance.ValidateInstance(instanceName); err != nil {
if instanceName == "" { respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
respondError(w, http.StatusBadRequest, "instance parameter is required")
return return
} }
@@ -89,12 +89,12 @@ func (api *API) OperationCancel(w http.ResponseWriter, r *http.Request) {
// OperationStream streams operation output via Server-Sent Events (SSE) // OperationStream streams operation output via Server-Sent Events (SSE)
func (api *API) OperationStream(w http.ResponseWriter, r *http.Request) { func (api *API) OperationStream(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
instanceName := vars["name"]
opID := vars["id"] opID := vars["id"]
// Extract instance name from query param // Validate instance exists
instanceName := r.URL.Query().Get("instance") if err := api.instance.ValidateInstance(instanceName); err != nil {
if instanceName == "" { respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
respondError(w, http.StatusBadRequest, "instance parameter is required")
return return
} }

View File

@@ -10,20 +10,6 @@ import (
"github.com/wild-cloud/wild-central/daemon/internal/utilities" "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 // InstanceUtilitiesHealth returns cluster health status for a specific instance
func (api *API) InstanceUtilitiesHealth(w http.ResponseWriter, r *http.Request) { func (api *API) InstanceUtilitiesHealth(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
@@ -82,7 +68,19 @@ func (api *API) UtilitiesDashboardToken(w http.ResponseWriter, r *http.Request)
// UtilitiesNodeIPs returns IP addresses for all cluster nodes // UtilitiesNodeIPs returns IP addresses for all cluster nodes
func (api *API) UtilitiesNodeIPs(w http.ResponseWriter, r *http.Request) { func (api *API) UtilitiesNodeIPs(w http.ResponseWriter, r *http.Request) {
nodes, err := utilities.GetNodeIPs() 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 := tools.GetKubeconfigPath(api.dataDir, instanceName)
nodes, err := utilities.GetNodeIPs(kubeconfigPath)
if err != nil { if err != nil {
respondError(w, http.StatusInternalServerError, "Failed to get node IPs") respondError(w, http.StatusInternalServerError, "Failed to get node IPs")
return return
@@ -98,7 +96,19 @@ func (api *API) UtilitiesNodeIPs(w http.ResponseWriter, r *http.Request) {
// UtilitiesControlPlaneIP returns the control plane IP // UtilitiesControlPlaneIP returns the control plane IP
func (api *API) UtilitiesControlPlaneIP(w http.ResponseWriter, r *http.Request) { func (api *API) UtilitiesControlPlaneIP(w http.ResponseWriter, r *http.Request) {
ip, err := utilities.GetControlPlaneIP() 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 := tools.GetKubeconfigPath(api.dataDir, instanceName)
ip, err := utilities.GetControlPlaneIP(kubeconfigPath)
if err != nil { if err != nil {
respondError(w, http.StatusInternalServerError, "Failed to get control plane IP") respondError(w, http.StatusInternalServerError, "Failed to get control plane IP")
return return
@@ -115,8 +125,15 @@ func (api *API) UtilitiesControlPlaneIP(w http.ResponseWriter, r *http.Request)
// UtilitiesSecretCopy copies a secret between namespaces // UtilitiesSecretCopy copies a secret between namespaces
func (api *API) UtilitiesSecretCopy(w http.ResponseWriter, r *http.Request) { func (api *API) UtilitiesSecretCopy(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
instanceName := vars["name"]
secretName := vars["secret"] secretName := vars["secret"]
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
var req struct { var req struct {
SourceNamespace string `json:"source_namespace"` SourceNamespace string `json:"source_namespace"`
DestinationNamespace string `json:"destination_namespace"` DestinationNamespace string `json:"destination_namespace"`
@@ -132,7 +149,10 @@ func (api *API) UtilitiesSecretCopy(w http.ResponseWriter, r *http.Request) {
return return
} }
if err := utilities.CopySecretBetweenNamespaces(secretName, req.SourceNamespace, req.DestinationNamespace); err != nil { // Get kubeconfig path for this instance
kubeconfigPath := tools.GetKubeconfigPath(api.dataDir, instanceName)
if err := utilities.CopySecretBetweenNamespaces(kubeconfigPath, secretName, req.SourceNamespace, req.DestinationNamespace); err != nil {
respondError(w, http.StatusInternalServerError, "Failed to copy secret") respondError(w, http.StatusInternalServerError, "Failed to copy secret")
return return
} }
@@ -145,7 +165,19 @@ func (api *API) UtilitiesSecretCopy(w http.ResponseWriter, r *http.Request) {
// UtilitiesVersion returns cluster and Talos versions // UtilitiesVersion returns cluster and Talos versions
func (api *API) UtilitiesVersion(w http.ResponseWriter, r *http.Request) { func (api *API) UtilitiesVersion(w http.ResponseWriter, r *http.Request) {
k8sVersion, err := utilities.GetClusterVersion() 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 := tools.GetKubeconfigPath(api.dataDir, instanceName)
k8sVersion, err := utilities.GetClusterVersion(kubeconfigPath)
if err != nil { if err != nil {
respondError(w, http.StatusInternalServerError, "Failed to get cluster version") respondError(w, http.StatusInternalServerError, "Failed to get cluster version")
return return

View File

@@ -273,7 +273,8 @@ func (m *Manager) GetStatus(instanceName string) (*ClusterStatus, error) {
} }
// Get node count and types using kubectl // Get node count and types using kubectl
cmd := exec.Command("kubectl", "--kubeconfig", kubeconfigPath, "get", "nodes", "-o", "json") cmd := exec.Command("kubectl", "get", "nodes", "-o", "json")
tools.WithKubeconfig(cmd, kubeconfigPath)
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil {
status.Status = "unreachable" status.Status = "unreachable"
@@ -356,9 +357,9 @@ func (m *Manager) GetStatus(instanceName string) (*ClusterStatus, error) {
} }
for _, svc := range services { for _, svc := range services {
cmd := exec.Command("kubectl", "--kubeconfig", kubeconfigPath, cmd := exec.Command("kubectl", "get", "pods", "-n", svc.namespace, "-l", svc.selector,
"get", "pods", "-n", svc.namespace, "-l", svc.selector,
"-o", "jsonpath={.items[*].status.phase}") "-o", "jsonpath={.items[*].status.phase}")
tools.WithKubeconfig(cmd, kubeconfigPath)
output, err := cmd.Output() output, err := cmd.Output()
if err != nil || len(output) == 0 { if err != nil || len(output) == 0 {
status.Services[svc.name] = "not_found" status.Services[svc.name] = "not_found"

View File

@@ -119,7 +119,8 @@ func (m *Manager) checkServiceStatus(instanceName, serviceName string) string {
// Special case: NFS doesn't have a deployment, check for StorageClass instead // Special case: NFS doesn't have a deployment, check for StorageClass instead
if serviceName == "nfs" { if serviceName == "nfs" {
cmd := exec.Command("kubectl", "--kubeconfig", kubeconfigPath, "get", "storageclass", "nfs", "-o", "name") cmd := exec.Command("kubectl", "get", "storageclass", "nfs", "-o", "name")
tools.WithKubeconfig(cmd, kubeconfigPath)
if err := cmd.Run(); err == nil { if err := cmd.Run(); err == nil {
return "deployed" return "deployed"
} }

View File

@@ -84,12 +84,8 @@ func GetClusterHealth(kubeconfigPath string) (*HealthStatus, error) {
// checkComponent checks if a component is running // checkComponent checks if a component is running
func checkComponent(kubeconfigPath, namespace, selector string) error { func checkComponent(kubeconfigPath, namespace, selector string) error {
args := []string{"get", "pods", "-n", namespace, "-l", selector, "-o", "json"} cmd := exec.Command("kubectl", "get", "pods", "-n", namespace, "-l", selector, "-o", "json")
if kubeconfigPath != "" { tools.WithKubeconfig(cmd, kubeconfigPath)
args = append([]string{"--kubeconfig", kubeconfigPath}, args...)
}
cmd := exec.Command("kubectl", args...)
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil {
return fmt.Errorf("failed to get pods: %w", err) return fmt.Errorf("failed to get pods: %w", err)
@@ -172,8 +168,9 @@ func GetDashboardTokenFromSecret(kubeconfigPath string) (*DashboardToken, error)
} }
// GetNodeIPs returns IP addresses for all cluster nodes // GetNodeIPs returns IP addresses for all cluster nodes
func GetNodeIPs() ([]*NodeIP, error) { func GetNodeIPs(kubeconfigPath string) ([]*NodeIP, error) {
cmd := exec.Command("kubectl", "get", "nodes", "-o", "json") cmd := exec.Command("kubectl", "get", "nodes", "-o", "json")
tools.WithKubeconfig(cmd, kubeconfigPath)
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get nodes: %w", err) return nil, fmt.Errorf("failed to get nodes: %w", err)
@@ -217,9 +214,10 @@ func GetNodeIPs() ([]*NodeIP, error) {
} }
// GetControlPlaneIP returns the IP of the first control plane node // GetControlPlaneIP returns the IP of the first control plane node
func GetControlPlaneIP() (string, error) { func GetControlPlaneIP(kubeconfigPath string) (string, error) {
cmd := exec.Command("kubectl", "get", "nodes", "-l", "node-role.kubernetes.io/control-plane", cmd := exec.Command("kubectl", "get", "nodes", "-l", "node-role.kubernetes.io/control-plane",
"-o", "jsonpath={.items[0].status.addresses[?(@.type==\"InternalIP\")].address}") "-o", "jsonpath={.items[0].status.addresses[?(@.type==\"InternalIP\")].address}")
tools.WithKubeconfig(cmd, kubeconfigPath)
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil {
return "", fmt.Errorf("failed to get control plane IP: %w", err) return "", fmt.Errorf("failed to get control plane IP: %w", err)
@@ -234,9 +232,10 @@ func GetControlPlaneIP() (string, error) {
} }
// CopySecretBetweenNamespaces copies a secret from one namespace to another // CopySecretBetweenNamespaces copies a secret from one namespace to another
func CopySecretBetweenNamespaces(secretName, srcNamespace, dstNamespace string) error { func CopySecretBetweenNamespaces(kubeconfigPath, secretName, srcNamespace, dstNamespace string) error {
// Get secret from source namespace // Get secret from source namespace
cmd := exec.Command("kubectl", "get", "secret", "-n", srcNamespace, secretName, "-o", "json") cmd := exec.Command("kubectl", "get", "secret", "-n", srcNamespace, secretName, "-o", "json")
tools.WithKubeconfig(cmd, kubeconfigPath)
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil {
return fmt.Errorf("failed to get secret from %s: %w", srcNamespace, err) return fmt.Errorf("failed to get secret from %s: %w", srcNamespace, err)
@@ -264,6 +263,7 @@ func CopySecretBetweenNamespaces(secretName, srcNamespace, dstNamespace string)
// Apply to destination namespace // Apply to destination namespace
cmd = exec.Command("kubectl", "apply", "-f", "-") cmd = exec.Command("kubectl", "apply", "-f", "-")
tools.WithKubeconfig(cmd, kubeconfigPath)
cmd.Stdin = strings.NewReader(string(secretJSON)) cmd.Stdin = strings.NewReader(string(secretJSON))
if output, err := cmd.CombinedOutput(); err != nil { if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to apply secret to %s: %w\nOutput: %s", dstNamespace, err, string(output)) return fmt.Errorf("failed to apply secret to %s: %w\nOutput: %s", dstNamespace, err, string(output))
@@ -273,8 +273,9 @@ func CopySecretBetweenNamespaces(secretName, srcNamespace, dstNamespace string)
} }
// GetClusterVersion returns the Kubernetes cluster version // GetClusterVersion returns the Kubernetes cluster version
func GetClusterVersion() (string, error) { func GetClusterVersion(kubeconfigPath string) (string, error) {
cmd := exec.Command("kubectl", "version", "-o", "json") cmd := exec.Command("kubectl", "version", "-o", "json")
tools.WithKubeconfig(cmd, kubeconfigPath)
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil {
return "", fmt.Errorf("failed to get cluster version: %w", err) return "", fmt.Errorf("failed to get cluster version: %w", err)