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

@@ -108,9 +108,9 @@ func (api *API) RegisterRoutes(r *mux.Router) {
// 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")
r.HandleFunc("/api/v1/instances/{name}/operations/{id}", api.OperationGet).Methods("GET")
r.HandleFunc("/api/v1/instances/{name}/operations/{id}/stream", api.OperationStream).Methods("GET")
r.HandleFunc("/api/v1/instances/{name}/operations/{id}/cancel", api.OperationCancel).Methods("POST")
// Cluster operations
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")
// 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/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")
r.HandleFunc("/api/v1/instances/{name}/utilities/nodes/ips", api.UtilitiesNodeIPs).Methods("GET")
r.HandleFunc("/api/v1/instances/{name}/utilities/controlplane/ip", api.UtilitiesControlPlaneIP).Methods("GET")
r.HandleFunc("/api/v1/instances/{name}/utilities/secrets/{secret}/copy", api.UtilitiesSecretCopy).Methods("POST")
r.HandleFunc("/api/v1/instances/{name}/utilities/version", api.UtilitiesVersion).Methods("GET")
// dnsmasq management
r.HandleFunc("/api/v1/dnsmasq/status", api.DnsmasqStatus).Methods("GET")

View File

@@ -17,12 +17,12 @@ import (
// OperationGet returns operation status
func (api *API) OperationGet(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
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")
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
@@ -64,12 +64,12 @@ func (api *API) OperationList(w http.ResponseWriter, r *http.Request) {
// OperationCancel cancels an operation
func (api *API) OperationCancel(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
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")
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}
@@ -89,12 +89,12 @@ func (api *API) OperationCancel(w http.ResponseWriter, r *http.Request) {
// OperationStream streams operation output via Server-Sent Events (SSE)
func (api *API) OperationStream(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
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")
// Validate instance exists
if err := api.instance.ValidateInstance(instanceName); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
return
}

View File

@@ -10,20 +10,6 @@ import (
"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)
@@ -82,7 +68,19 @@ func (api *API) UtilitiesDashboardToken(w http.ResponseWriter, r *http.Request)
// UtilitiesNodeIPs returns IP addresses for all cluster nodes
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 {
respondError(w, http.StatusInternalServerError, "Failed to get node IPs")
return
@@ -98,7 +96,19 @@ func (api *API) UtilitiesNodeIPs(w http.ResponseWriter, r *http.Request) {
// UtilitiesControlPlaneIP returns the control plane IP
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 {
respondError(w, http.StatusInternalServerError, "Failed to get control plane IP")
return
@@ -115,8 +125,15 @@ func (api *API) UtilitiesControlPlaneIP(w http.ResponseWriter, r *http.Request)
// UtilitiesSecretCopy copies a secret between namespaces
func (api *API) UtilitiesSecretCopy(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceName := vars["name"]
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 {
SourceNamespace string `json:"source_namespace"`
DestinationNamespace string `json:"destination_namespace"`
@@ -132,7 +149,10 @@ func (api *API) UtilitiesSecretCopy(w http.ResponseWriter, r *http.Request) {
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")
return
}
@@ -145,7 +165,19 @@ func (api *API) UtilitiesSecretCopy(w http.ResponseWriter, r *http.Request) {
// UtilitiesVersion returns cluster and Talos versions
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 {
respondError(w, http.StatusInternalServerError, "Failed to get cluster version")
return

View File

@@ -273,7 +273,8 @@ func (m *Manager) GetStatus(instanceName string) (*ClusterStatus, error) {
}
// 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()
if err != nil {
status.Status = "unreachable"
@@ -356,9 +357,9 @@ func (m *Manager) GetStatus(instanceName string) (*ClusterStatus, error) {
}
for _, svc := range services {
cmd := exec.Command("kubectl", "--kubeconfig", kubeconfigPath,
"get", "pods", "-n", svc.namespace, "-l", svc.selector,
cmd := exec.Command("kubectl", "get", "pods", "-n", svc.namespace, "-l", svc.selector,
"-o", "jsonpath={.items[*].status.phase}")
tools.WithKubeconfig(cmd, kubeconfigPath)
output, err := cmd.Output()
if err != nil || len(output) == 0 {
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
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 {
return "deployed"
}

View File

@@ -84,12 +84,8 @@ func GetClusterHealth(kubeconfigPath string) (*HealthStatus, error) {
// checkComponent checks if a component is running
func checkComponent(kubeconfigPath, namespace, selector string) error {
args := []string{"get", "pods", "-n", namespace, "-l", selector, "-o", "json"}
if kubeconfigPath != "" {
args = append([]string{"--kubeconfig", kubeconfigPath}, args...)
}
cmd := exec.Command("kubectl", args...)
cmd := exec.Command("kubectl", "get", "pods", "-n", namespace, "-l", selector, "-o", "json")
tools.WithKubeconfig(cmd, kubeconfigPath)
output, err := cmd.Output()
if err != nil {
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
func GetNodeIPs() ([]*NodeIP, error) {
func GetNodeIPs(kubeconfigPath string) ([]*NodeIP, error) {
cmd := exec.Command("kubectl", "get", "nodes", "-o", "json")
tools.WithKubeconfig(cmd, kubeconfigPath)
output, err := cmd.Output()
if err != nil {
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
func GetControlPlaneIP() (string, error) {
func GetControlPlaneIP(kubeconfigPath string) (string, error) {
cmd := exec.Command("kubectl", "get", "nodes", "-l", "node-role.kubernetes.io/control-plane",
"-o", "jsonpath={.items[0].status.addresses[?(@.type==\"InternalIP\")].address}")
tools.WithKubeconfig(cmd, kubeconfigPath)
output, err := cmd.Output()
if err != nil {
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
func CopySecretBetweenNamespaces(secretName, srcNamespace, dstNamespace string) error {
func CopySecretBetweenNamespaces(kubeconfigPath, secretName, srcNamespace, dstNamespace string) error {
// Get secret from source namespace
cmd := exec.Command("kubectl", "get", "secret", "-n", srcNamespace, secretName, "-o", "json")
tools.WithKubeconfig(cmd, kubeconfigPath)
output, err := cmd.Output()
if err != nil {
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
cmd = exec.Command("kubectl", "apply", "-f", "-")
tools.WithKubeconfig(cmd, kubeconfigPath)
cmd.Stdin = strings.NewReader(string(secretJSON))
if output, err := cmd.CombinedOutput(); err != nil {
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
func GetClusterVersion() (string, error) {
func GetClusterVersion(kubeconfigPath string) (string, error) {
cmd := exec.Command("kubectl", "version", "-o", "json")
tools.WithKubeconfig(cmd, kubeconfigPath)
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to get cluster version: %w", err)