Compare commits
4 Commits
d2c8ff716e
...
005dc30aa5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
005dc30aa5 | ||
|
|
5b7d2835e7 | ||
|
|
67ca1b85be | ||
|
|
679ea18446 |
@@ -42,3 +42,101 @@
|
|||||||
### Features
|
### Features
|
||||||
|
|
||||||
- If WILD_CENTRAL_ENV environment variable is set to "development", the API should run in development mode.
|
- 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 using tools helpers
|
||||||
|
kubeconfigPath := tools.GetKubeconfigPath(api.dataDir, instanceName)
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 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. **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
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 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
|
||||||
|
|
||||||
|
1. **Use tools helpers**: Always use `tools.WithKubeconfig()` or `tools.WithTalosconfig()` instead of manually setting environment variables
|
||||||
|
2. **Get paths from tools package**: Use `tools.GetKubeconfigPath()` or `tools.GetTalosconfigPath()` to construct config paths
|
||||||
|
3. **One config per command**: Each exec.Command should have its config set via the appropriate helper
|
||||||
|
4. **Error handling**: Always check for command execution errors and provide context
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
@@ -19,6 +18,7 @@ import (
|
|||||||
"github.com/wild-cloud/wild-central/daemon/internal/instance"
|
"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/operations"
|
||||||
"github.com/wild-cloud/wild-central/daemon/internal/secrets"
|
"github.com/wild-cloud/wild-central/daemon/internal/secrets"
|
||||||
|
"github.com/wild-cloud/wild-central/daemon/internal/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
// API holds all dependencies for API handlers
|
// API holds all dependencies for API handlers
|
||||||
@@ -37,7 +37,7 @@ type API struct {
|
|||||||
// Note: Setup files (cluster-services, cluster-nodes, etc.) are now embedded in the binary
|
// Note: Setup files (cluster-services, cluster-nodes, etc.) are now embedded in the binary
|
||||||
func NewAPI(dataDir, appsDir string) (*API, error) {
|
func NewAPI(dataDir, appsDir string) (*API, error) {
|
||||||
// Ensure base directories exist
|
// Ensure base directories exist
|
||||||
instancesDir := filepath.Join(dataDir, "instances")
|
instancesDir := tools.GetInstancesPath(dataDir)
|
||||||
if err := os.MkdirAll(instancesDir, 0755); err != nil {
|
if err := os.MkdirAll(instancesDir, 0755); err != nil {
|
||||||
return nil, fmt.Errorf("failed to create instances directory: %w", err)
|
return nil, fmt.Errorf("failed to create instances directory: %w", err)
|
||||||
}
|
}
|
||||||
@@ -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")
|
||||||
@@ -150,19 +150,25 @@ func (api *API) RegisterRoutes(r *mux.Router) {
|
|||||||
r.HandleFunc("/api/v1/instances/{name}/apps/{app}", api.AppsDelete).Methods("DELETE")
|
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")
|
r.HandleFunc("/api/v1/instances/{name}/apps/{app}/status", api.AppsGetStatus).Methods("GET")
|
||||||
|
|
||||||
|
// Enhanced app endpoints
|
||||||
|
r.HandleFunc("/api/v1/instances/{name}/apps/{app}/enhanced", api.AppsGetEnhanced).Methods("GET")
|
||||||
|
r.HandleFunc("/api/v1/instances/{name}/apps/{app}/runtime", api.AppsGetEnhancedStatus).Methods("GET")
|
||||||
|
r.HandleFunc("/api/v1/instances/{name}/apps/{app}/logs", api.AppsGetLogs).Methods("GET")
|
||||||
|
r.HandleFunc("/api/v1/instances/{name}/apps/{app}/events", api.AppsGetEvents).Methods("GET")
|
||||||
|
r.HandleFunc("/api/v1/instances/{name}/apps/{app}/readme", api.AppsGetReadme).Methods("GET")
|
||||||
|
|
||||||
// Backup & Restore
|
// 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.BackupAppStart).Methods("POST")
|
||||||
r.HandleFunc("/api/v1/instances/{name}/apps/{app}/backup", api.BackupAppList).Methods("GET")
|
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")
|
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/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")
|
||||||
|
|||||||
@@ -4,11 +4,16 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
"github.com/wild-cloud/wild-central/daemon/internal/apps"
|
"github.com/wild-cloud/wild-central/daemon/internal/apps"
|
||||||
"github.com/wild-cloud/wild-central/daemon/internal/operations"
|
"github.com/wild-cloud/wild-central/daemon/internal/operations"
|
||||||
|
"github.com/wild-cloud/wild-central/daemon/internal/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AppsListAvailable lists all available apps
|
// AppsListAvailable lists all available apps
|
||||||
@@ -186,3 +191,190 @@ func (api *API) AppsGetStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
respondJSON(w, http.StatusOK, status)
|
respondJSON(w, http.StatusOK, status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AppsGetEnhanced returns enhanced app details with runtime status
|
||||||
|
func (api *API) AppsGetEnhanced(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 enhanced app details
|
||||||
|
appsMgr := apps.NewManager(api.dataDir, api.appsDir)
|
||||||
|
enhanced, err := appsMgr.GetEnhanced(instanceName, appName)
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get app details: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respondJSON(w, http.StatusOK, enhanced)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppsGetEnhancedStatus returns just runtime status for an app
|
||||||
|
func (api *API) AppsGetEnhancedStatus(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 runtime status
|
||||||
|
appsMgr := apps.NewManager(api.dataDir, api.appsDir)
|
||||||
|
status, err := appsMgr.GetEnhancedStatus(instanceName, appName)
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusNotFound, fmt.Sprintf("Failed to get runtime status: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respondJSON(w, http.StatusOK, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppsGetLogs returns logs for an app (from first pod)
|
||||||
|
func (api *API) AppsGetLogs(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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse query parameters
|
||||||
|
tailStr := r.URL.Query().Get("tail")
|
||||||
|
sinceSecondsStr := r.URL.Query().Get("sinceSeconds")
|
||||||
|
podName := r.URL.Query().Get("pod")
|
||||||
|
|
||||||
|
tail := 100 // default
|
||||||
|
if tailStr != "" {
|
||||||
|
if t, err := strconv.Atoi(tailStr); err == nil && t > 0 {
|
||||||
|
tail = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sinceSeconds := 0
|
||||||
|
if sinceSecondsStr != "" {
|
||||||
|
if s, err := strconv.Atoi(sinceSecondsStr); err == nil && s > 0 {
|
||||||
|
sinceSeconds = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get logs
|
||||||
|
kubeconfigPath := api.dataDir + "/instances/" + instanceName + "/kubeconfig"
|
||||||
|
kubectl := tools.NewKubectl(kubeconfigPath)
|
||||||
|
|
||||||
|
// If no pod specified, get the first pod
|
||||||
|
if podName == "" {
|
||||||
|
pods, err := kubectl.GetPods(appName, true)
|
||||||
|
if err != nil || len(pods) == 0 {
|
||||||
|
respondError(w, http.StatusNotFound, "No pods found for app")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
podName = pods[0].Name
|
||||||
|
}
|
||||||
|
|
||||||
|
logOpts := tools.LogOptions{
|
||||||
|
Tail: tail,
|
||||||
|
SinceSeconds: sinceSeconds,
|
||||||
|
}
|
||||||
|
logs, err := kubectl.GetLogs(appName, podName, logOpts)
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get logs: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respondJSON(w, http.StatusOK, map[string]interface{}{
|
||||||
|
"pod": podName,
|
||||||
|
"logs": logs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppsGetEvents returns kubernetes events for an app
|
||||||
|
func (api *API) AppsGetEvents(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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse query parameters
|
||||||
|
limitStr := r.URL.Query().Get("limit")
|
||||||
|
limit := 20 // default
|
||||||
|
if limitStr != "" {
|
||||||
|
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
|
||||||
|
limit = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get events
|
||||||
|
kubeconfigPath := api.dataDir + "/instances/" + instanceName + "/kubeconfig"
|
||||||
|
kubectl := tools.NewKubectl(kubeconfigPath)
|
||||||
|
|
||||||
|
events, err := kubectl.GetRecentEvents(appName, limit)
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get events: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respondJSON(w, http.StatusOK, map[string]interface{}{
|
||||||
|
"events": events,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppsGetReadme returns the README.md content for an app
|
||||||
|
func (api *API) AppsGetReadme(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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate app name to prevent path traversal
|
||||||
|
if appName == "" || appName == "." || appName == ".." ||
|
||||||
|
strings.Contains(appName, "/") || strings.Contains(appName, "\\") {
|
||||||
|
respondError(w, http.StatusBadRequest, "Invalid app name")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try instance-specific README first
|
||||||
|
instancePath := filepath.Join(api.dataDir, "instances", instanceName, "apps", appName, "README.md")
|
||||||
|
content, err := os.ReadFile(instancePath)
|
||||||
|
if err == nil {
|
||||||
|
w.Header().Set("Content-Type", "text/markdown; charset=utf-8")
|
||||||
|
w.Write(content)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to global directory
|
||||||
|
globalPath := filepath.Join(api.appsDir, appName, "README.md")
|
||||||
|
content, err = os.ReadFile(globalPath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
respondError(w, http.StatusNotFound, fmt.Sprintf("README not found for app '%s' in instance '%s'", appName, instanceName))
|
||||||
|
} else {
|
||||||
|
respondError(w, http.StatusInternalServerError, "Failed to read README file")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/markdown; charset=utf-8")
|
||||||
|
w.Write(content)
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,17 +11,18 @@ import (
|
|||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
"github.com/wild-cloud/wild-central/daemon/internal/operations"
|
"github.com/wild-cloud/wild-central/daemon/internal/operations"
|
||||||
|
"github.com/wild-cloud/wild-central/daemon/internal/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +111,7 @@ func (api *API) OperationStream(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if operation is already completed
|
// Check if operation is already completed
|
||||||
statusFile := filepath.Join(api.dataDir, "instances", instanceName, "operations", opID+".json")
|
statusFile := filepath.Join(tools.GetInstanceOperationsPath(api.dataDir, instanceName), opID+".json")
|
||||||
isCompleted := false
|
isCompleted := false
|
||||||
if data, err := os.ReadFile(statusFile); err == nil {
|
if data, err := os.ReadFile(statusFile); err == nil {
|
||||||
var op map[string]interface{}
|
var op map[string]interface{}
|
||||||
@@ -122,7 +123,7 @@ func (api *API) OperationStream(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send existing log file content first (if exists)
|
// Send existing log file content first (if exists)
|
||||||
logPath := filepath.Join(api.dataDir, "instances", instanceName, "operations", opID, "output.log")
|
logPath := filepath.Join(tools.GetInstanceOperationsPath(api.dataDir, instanceName), opID, "output.log")
|
||||||
if _, err := os.Stat(logPath); err == nil {
|
if _, err := os.Stat(logPath); err == nil {
|
||||||
file, err := os.Open(logPath)
|
file, err := os.Open(logPath)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
@@ -14,6 +13,7 @@ import (
|
|||||||
"github.com/wild-cloud/wild-central/daemon/internal/contracts"
|
"github.com/wild-cloud/wild-central/daemon/internal/contracts"
|
||||||
"github.com/wild-cloud/wild-central/daemon/internal/operations"
|
"github.com/wild-cloud/wild-central/daemon/internal/operations"
|
||||||
"github.com/wild-cloud/wild-central/daemon/internal/services"
|
"github.com/wild-cloud/wild-central/daemon/internal/services"
|
||||||
|
"github.com/wild-cloud/wild-central/daemon/internal/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ServicesList lists all base services
|
// ServicesList lists all base services
|
||||||
@@ -297,7 +297,7 @@ func (api *API) ServicesGetInstanceConfig(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load instance config as map for dynamic path extraction
|
// Load instance config as map for dynamic path extraction
|
||||||
configPath := filepath.Join(api.dataDir, "instances", instanceName, "config.yaml")
|
configPath := tools.GetInstanceConfigPath(api.dataDir, instanceName)
|
||||||
configData, err := os.ReadFile(configPath)
|
configData, err := os.ReadFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to read instance config: %v", err))
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to read instance config: %v", err))
|
||||||
|
|||||||
@@ -4,26 +4,12 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/wild-cloud/wild-central/daemon/internal/tools"
|
||||||
"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)
|
||||||
@@ -36,7 +22,7 @@ func (api *API) InstanceUtilitiesHealth(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get kubeconfig path for this instance
|
// Get kubeconfig path for this instance
|
||||||
kubeconfigPath := filepath.Join(api.dataDir, "instances", instanceName, "kubeconfig")
|
kubeconfigPath := tools.GetKubeconfigPath(api.dataDir, instanceName)
|
||||||
|
|
||||||
status, err := utilities.GetClusterHealth(kubeconfigPath)
|
status, err := utilities.GetClusterHealth(kubeconfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -50,12 +36,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) {
|
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 := tools.GetKubeconfigPath(api.dataDir, instanceName)
|
||||||
|
|
||||||
|
token, err := utilities.GetDashboardToken(kubeconfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Try fallback method
|
// Try fallback method
|
||||||
token, err = utilities.GetDashboardTokenFromSecret()
|
token, err = utilities.GetDashboardTokenFromSecret(kubeconfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, http.StatusInternalServerError, "Failed to get dashboard token")
|
respondError(w, http.StatusInternalServerError, "Failed to get dashboard token")
|
||||||
return
|
return
|
||||||
@@ -70,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
|
||||||
@@ -86,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
|
||||||
@@ -103,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"`
|
||||||
@@ -120,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
|
||||||
}
|
}
|
||||||
@@ -133,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
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package apps
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -31,12 +32,15 @@ func NewManager(dataDir, appsDir string) *Manager {
|
|||||||
|
|
||||||
// App represents an application
|
// App represents an application
|
||||||
type App struct {
|
type App struct {
|
||||||
Name string `json:"name" yaml:"name"`
|
Name string `json:"name" yaml:"name"`
|
||||||
Description string `json:"description" yaml:"description"`
|
Description string `json:"description" yaml:"description"`
|
||||||
Version string `json:"version" yaml:"version"`
|
Version string `json:"version" yaml:"version"`
|
||||||
Category string `json:"category" yaml:"category"`
|
Category string `json:"category,omitempty" yaml:"category,omitempty"`
|
||||||
Dependencies []string `json:"dependencies" yaml:"dependencies"`
|
Icon string `json:"icon,omitempty" yaml:"icon,omitempty"`
|
||||||
Config map[string]string `json:"config,omitempty" yaml:"config,omitempty"`
|
Dependencies []string `json:"dependencies" yaml:"dependencies"`
|
||||||
|
Config map[string]string `json:"config,omitempty" yaml:"config,omitempty"`
|
||||||
|
DefaultConfig map[string]interface{} `json:"defaultConfig,omitempty" yaml:"defaultConfig,omitempty"`
|
||||||
|
RequiredSecrets []string `json:"requiredSecrets,omitempty" yaml:"requiredSecrets,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeployedApp represents a deployed application instance
|
// DeployedApp represents a deployed application instance
|
||||||
@@ -78,12 +82,30 @@ func (m *Manager) ListAvailable() ([]App, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var app App
|
var manifest AppManifest
|
||||||
if err := yaml.Unmarshal(data, &app); err != nil {
|
if err := yaml.Unmarshal(data, &manifest); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
app.Name = entry.Name() // Use directory name as app name
|
// Convert manifest to App struct
|
||||||
|
app := App{
|
||||||
|
Name: entry.Name(), // Use directory name as app name
|
||||||
|
Description: manifest.Description,
|
||||||
|
Version: manifest.Version,
|
||||||
|
Category: manifest.Category,
|
||||||
|
Icon: manifest.Icon,
|
||||||
|
DefaultConfig: manifest.DefaultConfig,
|
||||||
|
RequiredSecrets: manifest.RequiredSecrets,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract dependencies from Requires field
|
||||||
|
if len(manifest.Requires) > 0 {
|
||||||
|
app.Dependencies = make([]string, len(manifest.Requires))
|
||||||
|
for i, dep := range manifest.Requires {
|
||||||
|
app.Dependencies[i] = dep.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
apps = append(apps, app)
|
apps = append(apps, app)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,19 +125,37 @@ func (m *Manager) Get(appName string) (*App, error) {
|
|||||||
return nil, fmt.Errorf("failed to read app file: %w", err)
|
return nil, fmt.Errorf("failed to read app file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var app App
|
var manifest AppManifest
|
||||||
if err := yaml.Unmarshal(data, &app); err != nil {
|
if err := yaml.Unmarshal(data, &manifest); err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse app file: %w", err)
|
return nil, fmt.Errorf("failed to parse app file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.Name = appName
|
// Convert manifest to App struct
|
||||||
return &app, nil
|
app := &App{
|
||||||
|
Name: appName,
|
||||||
|
Description: manifest.Description,
|
||||||
|
Version: manifest.Version,
|
||||||
|
Category: manifest.Category,
|
||||||
|
Icon: manifest.Icon,
|
||||||
|
DefaultConfig: manifest.DefaultConfig,
|
||||||
|
RequiredSecrets: manifest.RequiredSecrets,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract dependencies from Requires field
|
||||||
|
if len(manifest.Requires) > 0 {
|
||||||
|
app.Dependencies = make([]string, len(manifest.Requires))
|
||||||
|
for i, dep := range manifest.Requires {
|
||||||
|
app.Dependencies[i] = dep.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return app, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListDeployed lists deployed apps for an instance
|
// ListDeployed lists deployed apps for an instance
|
||||||
func (m *Manager) ListDeployed(instanceName string) ([]DeployedApp, error) {
|
func (m *Manager) ListDeployed(instanceName string) ([]DeployedApp, error) {
|
||||||
kubeconfigPath := tools.GetKubeconfigPath(m.dataDir, instanceName)
|
kubeconfigPath := tools.GetKubeconfigPath(m.dataDir, instanceName)
|
||||||
instancePath := filepath.Join(m.dataDir, "instances", instanceName)
|
instancePath := tools.GetInstancePath(m.dataDir, instanceName)
|
||||||
appsDir := filepath.Join(instancePath, "apps")
|
appsDir := filepath.Join(instancePath, "apps")
|
||||||
|
|
||||||
apps := []DeployedApp{}
|
apps := []DeployedApp{}
|
||||||
@@ -173,6 +213,66 @@ func (m *Manager) ListDeployed(instanceName string) ([]DeployedApp, error) {
|
|||||||
if yaml.Unmarshal(output, &ns) == nil && ns.Status.Phase == "Active" {
|
if yaml.Unmarshal(output, &ns) == nil && ns.Status.Phase == "Active" {
|
||||||
// Namespace is active - app is deployed
|
// Namespace is active - app is deployed
|
||||||
app.Status = "deployed"
|
app.Status = "deployed"
|
||||||
|
|
||||||
|
// Get ingress URL if available
|
||||||
|
// Try Traefik IngressRoute first
|
||||||
|
ingressCmd := exec.Command("kubectl", "get", "ingressroute", "-n", appName, "-o", "json")
|
||||||
|
tools.WithKubeconfig(ingressCmd, kubeconfigPath)
|
||||||
|
ingressOutput, err := ingressCmd.CombinedOutput()
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
var ingressList struct {
|
||||||
|
Items []struct {
|
||||||
|
Spec struct {
|
||||||
|
Routes []struct {
|
||||||
|
Match string `json:"match"`
|
||||||
|
} `json:"routes"`
|
||||||
|
} `json:"spec"`
|
||||||
|
} `json:"items"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(ingressOutput, &ingressList) == nil && len(ingressList.Items) > 0 {
|
||||||
|
// Extract host from the first route match (format: Host(`example.com`))
|
||||||
|
if len(ingressList.Items[0].Spec.Routes) > 0 {
|
||||||
|
match := ingressList.Items[0].Spec.Routes[0].Match
|
||||||
|
// Parse Host(`domain.com`) format
|
||||||
|
if strings.Contains(match, "Host(`") {
|
||||||
|
start := strings.Index(match, "Host(`") + 6
|
||||||
|
end := strings.Index(match[start:], "`")
|
||||||
|
if end > 0 {
|
||||||
|
host := match[start : start+end]
|
||||||
|
app.URL = "https://" + host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no IngressRoute, try standard Ingress
|
||||||
|
if app.URL == "" {
|
||||||
|
ingressCmd := exec.Command("kubectl", "get", "ingress", "-n", appName, "-o", "json")
|
||||||
|
tools.WithKubeconfig(ingressCmd, kubeconfigPath)
|
||||||
|
ingressOutput, err := ingressCmd.CombinedOutput()
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
var ingressList struct {
|
||||||
|
Items []struct {
|
||||||
|
Spec struct {
|
||||||
|
Rules []struct {
|
||||||
|
Host string `json:"host"`
|
||||||
|
} `json:"rules"`
|
||||||
|
} `json:"spec"`
|
||||||
|
} `json:"items"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(ingressOutput, &ingressList) == nil && len(ingressList.Items) > 0 {
|
||||||
|
if len(ingressList.Items[0].Spec.Rules) > 0 {
|
||||||
|
host := ingressList.Items[0].Spec.Rules[0].Host
|
||||||
|
if host != "" {
|
||||||
|
app.URL = "https://" + host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,9 +290,9 @@ func (m *Manager) Add(instanceName, appName string, config map[string]string) er
|
|||||||
return fmt.Errorf("app %s not found at %s", appName, manifestPath)
|
return fmt.Errorf("app %s not found at %s", appName, manifestPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
instancePath := filepath.Join(m.dataDir, "instances", instanceName)
|
instancePath := tools.GetInstancePath(m.dataDir, instanceName)
|
||||||
configFile := filepath.Join(instancePath, "config.yaml")
|
configFile := tools.GetInstanceConfigPath(m.dataDir, instanceName)
|
||||||
secretsFile := filepath.Join(instancePath, "secrets.yaml")
|
secretsFile := tools.GetInstanceSecretsPath(m.dataDir, instanceName)
|
||||||
appDestDir := filepath.Join(instancePath, "apps", appName)
|
appDestDir := filepath.Join(instancePath, "apps", appName)
|
||||||
|
|
||||||
// Check instance config exists
|
// Check instance config exists
|
||||||
@@ -306,8 +406,8 @@ func (m *Manager) Add(instanceName, appName string, config map[string]string) er
|
|||||||
// Deploy deploys an app to the cluster
|
// Deploy deploys an app to the cluster
|
||||||
func (m *Manager) Deploy(instanceName, appName string) error {
|
func (m *Manager) Deploy(instanceName, appName string) error {
|
||||||
kubeconfigPath := tools.GetKubeconfigPath(m.dataDir, instanceName)
|
kubeconfigPath := tools.GetKubeconfigPath(m.dataDir, instanceName)
|
||||||
instancePath := filepath.Join(m.dataDir, "instances", instanceName)
|
instancePath := tools.GetInstancePath(m.dataDir, instanceName)
|
||||||
secretsFile := filepath.Join(instancePath, "secrets.yaml")
|
secretsFile := tools.GetInstanceSecretsPath(m.dataDir, instanceName)
|
||||||
|
|
||||||
// Get compiled app manifests from instance directory
|
// Get compiled app manifests from instance directory
|
||||||
appDir := filepath.Join(instancePath, "apps", appName)
|
appDir := filepath.Join(instancePath, "apps", appName)
|
||||||
@@ -369,9 +469,9 @@ func (m *Manager) Deploy(instanceName, appName string) error {
|
|||||||
// Delete removes an app from the cluster and configuration
|
// Delete removes an app from the cluster and configuration
|
||||||
func (m *Manager) Delete(instanceName, appName string) error {
|
func (m *Manager) Delete(instanceName, appName string) error {
|
||||||
kubeconfigPath := tools.GetKubeconfigPath(m.dataDir, instanceName)
|
kubeconfigPath := tools.GetKubeconfigPath(m.dataDir, instanceName)
|
||||||
instancePath := filepath.Join(m.dataDir, "instances", instanceName)
|
instancePath := tools.GetInstancePath(m.dataDir, instanceName)
|
||||||
configFile := filepath.Join(instancePath, "config.yaml")
|
configFile := tools.GetInstanceConfigPath(m.dataDir, instanceName)
|
||||||
secretsFile := filepath.Join(instancePath, "secrets.yaml")
|
secretsFile := tools.GetInstanceSecretsPath(m.dataDir, instanceName)
|
||||||
|
|
||||||
// Get compiled app manifests from instance directory
|
// Get compiled app manifests from instance directory
|
||||||
appDir := filepath.Join(instancePath, "apps", appName)
|
appDir := filepath.Join(instancePath, "apps", appName)
|
||||||
@@ -425,7 +525,7 @@ func (m *Manager) Delete(instanceName, appName string) error {
|
|||||||
// GetStatus returns the status of a deployed app
|
// GetStatus returns the status of a deployed app
|
||||||
func (m *Manager) GetStatus(instanceName, appName string) (*DeployedApp, error) {
|
func (m *Manager) GetStatus(instanceName, appName string) (*DeployedApp, error) {
|
||||||
kubeconfigPath := tools.GetKubeconfigPath(m.dataDir, instanceName)
|
kubeconfigPath := tools.GetKubeconfigPath(m.dataDir, instanceName)
|
||||||
instancePath := filepath.Join(m.dataDir, "instances", instanceName)
|
instancePath := tools.GetInstancePath(m.dataDir, instanceName)
|
||||||
appDir := filepath.Join(instancePath, "apps", appName)
|
appDir := filepath.Join(instancePath, "apps", appName)
|
||||||
|
|
||||||
app := &DeployedApp{
|
app := &DeployedApp{
|
||||||
@@ -526,3 +626,214 @@ func (m *Manager) GetStatus(instanceName, appName string) (*DeployedApp, error)
|
|||||||
|
|
||||||
return app, nil
|
return app, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetEnhanced returns enhanced app information with runtime status
|
||||||
|
func (m *Manager) GetEnhanced(instanceName, appName string) (*EnhancedApp, error) {
|
||||||
|
kubeconfigPath := tools.GetKubeconfigPath(m.dataDir, instanceName)
|
||||||
|
instancePath := tools.GetInstancePath(m.dataDir, instanceName)
|
||||||
|
configFile := tools.GetInstanceConfigPath(m.dataDir, instanceName)
|
||||||
|
appDir := filepath.Join(instancePath, "apps", appName)
|
||||||
|
|
||||||
|
enhanced := &EnhancedApp{
|
||||||
|
Name: appName,
|
||||||
|
Status: "not-added",
|
||||||
|
Namespace: appName,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if app was added to instance
|
||||||
|
if !storage.FileExists(appDir) {
|
||||||
|
return enhanced, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
enhanced.Status = "not-deployed"
|
||||||
|
|
||||||
|
// Load manifest
|
||||||
|
manifestPath := filepath.Join(appDir, "manifest.yaml")
|
||||||
|
if storage.FileExists(manifestPath) {
|
||||||
|
manifestData, _ := os.ReadFile(manifestPath)
|
||||||
|
var manifest AppManifest
|
||||||
|
if yaml.Unmarshal(manifestData, &manifest) == nil {
|
||||||
|
enhanced.Version = manifest.Version
|
||||||
|
enhanced.Description = manifest.Description
|
||||||
|
enhanced.Icon = manifest.Icon
|
||||||
|
enhanced.Manifest = &manifest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: README content is now served via dedicated /readme endpoint
|
||||||
|
// No need to populate readme/documentation fields here
|
||||||
|
|
||||||
|
// Load config
|
||||||
|
yq := tools.NewYQ()
|
||||||
|
configJSON, err := yq.Get(configFile, fmt.Sprintf(".apps.%s | @json", appName))
|
||||||
|
if err == nil && configJSON != "" && configJSON != "null" {
|
||||||
|
var config map[string]string
|
||||||
|
if json.Unmarshal([]byte(configJSON), &config) == nil {
|
||||||
|
enhanced.Config = config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if namespace exists
|
||||||
|
checkNsCmd := exec.Command("kubectl", "get", "namespace", appName, "-o", "json")
|
||||||
|
tools.WithKubeconfig(checkNsCmd, kubeconfigPath)
|
||||||
|
nsOutput, err := checkNsCmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
// Namespace doesn't exist - not deployed
|
||||||
|
return enhanced, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse namespace to check if it's active
|
||||||
|
var ns struct {
|
||||||
|
Status struct {
|
||||||
|
Phase string `json:"phase"`
|
||||||
|
} `json:"status"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(nsOutput, &ns); err != nil || ns.Status.Phase != "Active" {
|
||||||
|
return enhanced, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
enhanced.Status = "deployed"
|
||||||
|
|
||||||
|
// Get URL (ingress)
|
||||||
|
enhanced.URL = m.getAppURL(kubeconfigPath, appName)
|
||||||
|
|
||||||
|
// Get runtime status
|
||||||
|
runtime, err := m.getRuntimeStatus(kubeconfigPath, appName)
|
||||||
|
if err == nil {
|
||||||
|
enhanced.Runtime = runtime
|
||||||
|
|
||||||
|
// Update status based on runtime
|
||||||
|
if runtime.Pods != nil && len(runtime.Pods) > 0 {
|
||||||
|
allRunning := true
|
||||||
|
allReady := true
|
||||||
|
for _, pod := range runtime.Pods {
|
||||||
|
if pod.Status != "Running" {
|
||||||
|
allRunning = false
|
||||||
|
}
|
||||||
|
// Check ready ratio
|
||||||
|
parts := strings.Split(pod.Ready, "/")
|
||||||
|
if len(parts) == 2 && parts[0] != parts[1] {
|
||||||
|
allReady = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if allRunning && allReady {
|
||||||
|
enhanced.Status = "running"
|
||||||
|
} else if allRunning {
|
||||||
|
enhanced.Status = "starting"
|
||||||
|
} else {
|
||||||
|
enhanced.Status = "unhealthy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return enhanced, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEnhancedStatus returns just the runtime status for an app
|
||||||
|
func (m *Manager) GetEnhancedStatus(instanceName, appName string) (*RuntimeStatus, error) {
|
||||||
|
kubeconfigPath := tools.GetKubeconfigPath(m.dataDir, instanceName)
|
||||||
|
|
||||||
|
// Check if namespace exists
|
||||||
|
checkNsCmd := exec.Command("kubectl", "get", "namespace", appName, "-o", "json")
|
||||||
|
tools.WithKubeconfig(checkNsCmd, kubeconfigPath)
|
||||||
|
if err := checkNsCmd.Run(); err != nil {
|
||||||
|
return nil, fmt.Errorf("namespace not found or not deployed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.getRuntimeStatus(kubeconfigPath, appName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getRuntimeStatus fetches runtime information from kubernetes
|
||||||
|
func (m *Manager) getRuntimeStatus(kubeconfigPath, namespace string) (*RuntimeStatus, error) {
|
||||||
|
kubectl := tools.NewKubectl(kubeconfigPath)
|
||||||
|
|
||||||
|
runtime := &RuntimeStatus{}
|
||||||
|
|
||||||
|
// Get pods (with detailed info for app status display)
|
||||||
|
pods, err := kubectl.GetPods(namespace, true)
|
||||||
|
if err == nil {
|
||||||
|
runtime.Pods = pods
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get replicas
|
||||||
|
replicas, err := kubectl.GetReplicas(namespace)
|
||||||
|
if err == nil && (replicas.Desired > 0 || replicas.Current > 0) {
|
||||||
|
runtime.Replicas = replicas
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get resources
|
||||||
|
resources, err := kubectl.GetResources(namespace)
|
||||||
|
if err == nil {
|
||||||
|
runtime.Resources = resources
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get recent events (last 10)
|
||||||
|
events, err := kubectl.GetRecentEvents(namespace, 10)
|
||||||
|
if err == nil {
|
||||||
|
runtime.RecentEvents = events
|
||||||
|
}
|
||||||
|
|
||||||
|
return runtime, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAppURL extracts the ingress URL for an app
|
||||||
|
func (m *Manager) getAppURL(kubeconfigPath, appName string) string {
|
||||||
|
// Try Traefik IngressRoute first
|
||||||
|
ingressCmd := exec.Command("kubectl", "get", "ingressroute", "-n", appName, "-o", "json")
|
||||||
|
tools.WithKubeconfig(ingressCmd, kubeconfigPath)
|
||||||
|
ingressOutput, err := ingressCmd.CombinedOutput()
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
var ingressList struct {
|
||||||
|
Items []struct {
|
||||||
|
Spec struct {
|
||||||
|
Routes []struct {
|
||||||
|
Match string `json:"match"`
|
||||||
|
} `json:"routes"`
|
||||||
|
} `json:"spec"`
|
||||||
|
} `json:"items"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(ingressOutput, &ingressList) == nil && len(ingressList.Items) > 0 {
|
||||||
|
if len(ingressList.Items[0].Spec.Routes) > 0 {
|
||||||
|
match := ingressList.Items[0].Spec.Routes[0].Match
|
||||||
|
// Parse Host(`domain.com`) format
|
||||||
|
if strings.Contains(match, "Host(`") {
|
||||||
|
start := strings.Index(match, "Host(`") + 6
|
||||||
|
end := strings.Index(match[start:], "`")
|
||||||
|
if end > 0 {
|
||||||
|
host := match[start : start+end]
|
||||||
|
return "https://" + host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no IngressRoute, try standard Ingress
|
||||||
|
ingressCmd = exec.Command("kubectl", "get", "ingress", "-n", appName, "-o", "json")
|
||||||
|
tools.WithKubeconfig(ingressCmd, kubeconfigPath)
|
||||||
|
ingressOutput, err = ingressCmd.CombinedOutput()
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
var ingressList struct {
|
||||||
|
Items []struct {
|
||||||
|
Spec struct {
|
||||||
|
Rules []struct {
|
||||||
|
Host string `json:"host"`
|
||||||
|
} `json:"rules"`
|
||||||
|
} `json:"spec"`
|
||||||
|
} `json:"items"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(ingressOutput, &ingressList) == nil && len(ingressList.Items) > 0 {
|
||||||
|
if len(ingressList.Items[0].Spec.Rules) > 0 {
|
||||||
|
host := ingressList.Items[0].Spec.Rules[0].Host
|
||||||
|
if host != "" {
|
||||||
|
return "https://" + host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|||||||
55
internal/apps/models.go
Normal file
55
internal/apps/models.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package apps
|
||||||
|
|
||||||
|
import "github.com/wild-cloud/wild-central/daemon/internal/tools"
|
||||||
|
|
||||||
|
// AppManifest represents the complete app manifest from manifest.yaml
|
||||||
|
type AppManifest struct {
|
||||||
|
Name string `json:"name" yaml:"name"`
|
||||||
|
Description string `json:"description" yaml:"description"`
|
||||||
|
Version string `json:"version" yaml:"version"`
|
||||||
|
Icon string `json:"icon,omitempty" yaml:"icon,omitempty"`
|
||||||
|
Category string `json:"category,omitempty" yaml:"category,omitempty"`
|
||||||
|
Requires []AppDependency `json:"requires,omitempty" yaml:"requires,omitempty"`
|
||||||
|
DefaultConfig map[string]interface{} `json:"defaultConfig,omitempty" yaml:"defaultConfig,omitempty"`
|
||||||
|
RequiredSecrets []string `json:"requiredSecrets,omitempty" yaml:"requiredSecrets,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppDependency represents a dependency on another app
|
||||||
|
type AppDependency struct {
|
||||||
|
Name string `json:"name" yaml:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnhancedApp extends DeployedApp with runtime status information
|
||||||
|
type EnhancedApp struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Icon string `json:"icon,omitempty"`
|
||||||
|
Manifest *AppManifest `json:"manifest,omitempty"`
|
||||||
|
Runtime *RuntimeStatus `json:"runtime,omitempty"`
|
||||||
|
Config map[string]string `json:"config,omitempty"`
|
||||||
|
Readme string `json:"readme,omitempty"`
|
||||||
|
Documentation string `json:"documentation,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RuntimeStatus contains runtime information from kubernetes
|
||||||
|
type RuntimeStatus struct {
|
||||||
|
Pods []PodInfo `json:"pods,omitempty"`
|
||||||
|
Replicas *ReplicaInfo `json:"replicas,omitempty"`
|
||||||
|
Resources *ResourceUsage `json:"resources,omitempty"`
|
||||||
|
RecentEvents []KubernetesEvent `json:"recentEvents,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type aliases for kubectl wrapper types
|
||||||
|
// These types are defined in internal/tools and shared across the codebase
|
||||||
|
type PodInfo = tools.PodInfo
|
||||||
|
type ContainerInfo = tools.ContainerInfo
|
||||||
|
type ContainerState = tools.ContainerState
|
||||||
|
type PodCondition = tools.PodCondition
|
||||||
|
type ReplicaInfo = tools.ReplicaInfo
|
||||||
|
type ResourceUsage = tools.ResourceUsage
|
||||||
|
type KubernetesEvent = tools.KubernetesEvent
|
||||||
|
type LogEntry = tools.LogEntry
|
||||||
@@ -46,7 +46,7 @@ func NewManager(dataDir string) *Manager {
|
|||||||
|
|
||||||
// GetBackupDir returns the backup directory for an instance
|
// GetBackupDir returns the backup directory for an instance
|
||||||
func (m *Manager) GetBackupDir(instanceName string) string {
|
func (m *Manager) GetBackupDir(instanceName string) string {
|
||||||
return filepath.Join(m.dataDir, "instances", instanceName, "backups")
|
return tools.GetInstanceBackupsPath(m.dataDir, instanceName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStagingDir returns the staging directory for backups
|
// GetStagingDir returns the staging directory for backups
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ type ClusterStatus struct {
|
|||||||
|
|
||||||
// GetTalosDir returns the talos directory for an instance
|
// GetTalosDir returns the talos directory for an instance
|
||||||
func (m *Manager) GetTalosDir(instanceName string) string {
|
func (m *Manager) GetTalosDir(instanceName string) string {
|
||||||
return filepath.Join(m.dataDir, "instances", instanceName, "talos")
|
return tools.GetInstanceTalosPath(m.dataDir, instanceName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetGeneratedDir returns the generated config directory
|
// GetGeneratedDir returns the generated config directory
|
||||||
@@ -99,8 +99,7 @@ func (m *Manager) GenerateConfig(instanceName string, config *ClusterConfig) err
|
|||||||
// Bootstrap bootstraps the cluster on the specified node
|
// Bootstrap bootstraps the cluster on the specified node
|
||||||
func (m *Manager) Bootstrap(instanceName, nodeName string) error {
|
func (m *Manager) Bootstrap(instanceName, nodeName string) error {
|
||||||
// Get node configuration to find the target IP
|
// Get node configuration to find the target IP
|
||||||
instancePath := filepath.Join(m.dataDir, "instances", instanceName)
|
configPath := tools.GetInstanceConfigPath(m.dataDir, instanceName)
|
||||||
configPath := filepath.Join(instancePath, "config.yaml")
|
|
||||||
|
|
||||||
yq := tools.NewYQ()
|
yq := tools.NewYQ()
|
||||||
|
|
||||||
@@ -183,8 +182,7 @@ func (m *Manager) retrieveKubeconfigFromCluster(instanceName, nodeIP string, tim
|
|||||||
|
|
||||||
// RegenerateKubeconfig regenerates the kubeconfig by retrieving it from the cluster
|
// RegenerateKubeconfig regenerates the kubeconfig by retrieving it from the cluster
|
||||||
func (m *Manager) RegenerateKubeconfig(instanceName string) error {
|
func (m *Manager) RegenerateKubeconfig(instanceName string) error {
|
||||||
instancePath := filepath.Join(m.dataDir, "instances", instanceName)
|
configPath := tools.GetInstanceConfigPath(m.dataDir, instanceName)
|
||||||
configPath := filepath.Join(instancePath, "config.yaml")
|
|
||||||
|
|
||||||
yq := tools.NewYQ()
|
yq := tools.NewYQ()
|
||||||
|
|
||||||
@@ -206,8 +204,7 @@ func (m *Manager) RegenerateKubeconfig(instanceName string) error {
|
|||||||
|
|
||||||
// ConfigureEndpoints updates talosconfig to use VIP and retrieves kubeconfig
|
// ConfigureEndpoints updates talosconfig to use VIP and retrieves kubeconfig
|
||||||
func (m *Manager) ConfigureEndpoints(instanceName string, includeNodes bool) error {
|
func (m *Manager) ConfigureEndpoints(instanceName string, includeNodes bool) error {
|
||||||
instancePath := filepath.Join(m.dataDir, "instances", instanceName)
|
configPath := tools.GetInstanceConfigPath(m.dataDir, instanceName)
|
||||||
configPath := filepath.Join(instancePath, "config.yaml")
|
|
||||||
talosconfigPath := tools.GetTalosconfigPath(m.dataDir, instanceName)
|
talosconfigPath := tools.GetTalosconfigPath(m.dataDir, instanceName)
|
||||||
|
|
||||||
yq := tools.NewYQ()
|
yq := tools.NewYQ()
|
||||||
@@ -276,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"
|
||||||
@@ -359,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"
|
||||||
|
|||||||
@@ -152,16 +152,19 @@ func (m *Manager) CopyConfig(srcPath, dstPath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetInstanceConfigPath returns the path to an instance's config file
|
// GetInstanceConfigPath returns the path to an instance's config file
|
||||||
|
// Deprecated: Use tools.GetInstanceConfigPath instead
|
||||||
func GetInstanceConfigPath(dataDir, instanceName string) string {
|
func GetInstanceConfigPath(dataDir, instanceName string) string {
|
||||||
return filepath.Join(dataDir, "instances", instanceName, "config.yaml")
|
return tools.GetInstanceConfigPath(dataDir, instanceName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInstanceSecretsPath returns the path to an instance's secrets file
|
// GetInstanceSecretsPath returns the path to an instance's secrets file
|
||||||
|
// Deprecated: Use tools.GetInstanceSecretsPath instead
|
||||||
func GetInstanceSecretsPath(dataDir, instanceName string) string {
|
func GetInstanceSecretsPath(dataDir, instanceName string) string {
|
||||||
return filepath.Join(dataDir, "instances", instanceName, "secrets.yaml")
|
return tools.GetInstanceSecretsPath(dataDir, instanceName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInstancePath returns the path to an instance directory
|
// GetInstancePath returns the path to an instance directory
|
||||||
|
// Deprecated: Use tools.GetInstancePath instead
|
||||||
func GetInstancePath(dataDir, instanceName string) string {
|
func GetInstancePath(dataDir, instanceName string) string {
|
||||||
return filepath.Join(dataDir, "instances", instanceName)
|
return tools.GetInstancePath(dataDir, instanceName)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/wild-cloud/wild-central/daemon/internal/storage"
|
"github.com/wild-cloud/wild-central/daemon/internal/storage"
|
||||||
|
"github.com/wild-cloud/wild-central/daemon/internal/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Manager handles current instance context tracking
|
// Manager handles current instance context tracking
|
||||||
@@ -53,7 +54,7 @@ func (m *Manager) SetCurrentContext(instanceName string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify instance exists
|
// Verify instance exists
|
||||||
instancePath := filepath.Join(m.dataDir, "instances", instanceName)
|
instancePath := tools.GetInstancePath(m.dataDir, instanceName)
|
||||||
if !storage.FileExists(instancePath) {
|
if !storage.FileExists(instancePath) {
|
||||||
return fmt.Errorf("instance %s does not exist", instanceName)
|
return fmt.Errorf("instance %s does not exist", instanceName)
|
||||||
}
|
}
|
||||||
@@ -101,7 +102,7 @@ func (m *Manager) ValidateContext() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
instancePath := filepath.Join(m.dataDir, "instances", contextName)
|
instancePath := tools.GetInstancePath(m.dataDir, contextName)
|
||||||
if !storage.FileExists(instancePath) {
|
if !storage.FileExists(instancePath) {
|
||||||
return fmt.Errorf("current context %s points to non-existent instance", contextName)
|
return fmt.Errorf("current context %s points to non-existent instance", contextName)
|
||||||
}
|
}
|
||||||
@@ -116,7 +117,7 @@ func (m *Manager) GetCurrentInstancePath() (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return filepath.Join(m.dataDir, "instances", contextName), nil
|
return tools.GetInstancePath(m.dataDir, contextName), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentInstanceConfigPath returns the path to the current instance's config file
|
// GetCurrentInstanceConfigPath returns the path to the current instance's config file
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ type Manager struct {
|
|||||||
// NewManager creates a new discovery manager
|
// NewManager creates a new discovery manager
|
||||||
func NewManager(dataDir string, instanceName string) *Manager {
|
func NewManager(dataDir string, instanceName string) *Manager {
|
||||||
// Get talosconfig path for the instance
|
// Get talosconfig path for the instance
|
||||||
talosconfigPath := filepath.Join(dataDir, "instances", instanceName, "setup", "cluster-nodes", "generated", "talosconfig")
|
talosconfigPath := tools.GetTalosconfigPath(dataDir, instanceName)
|
||||||
|
|
||||||
return &Manager{
|
return &Manager{
|
||||||
dataDir: dataDir,
|
dataDir: dataDir,
|
||||||
@@ -53,7 +53,7 @@ type DiscoveryStatus struct {
|
|||||||
|
|
||||||
// GetDiscoveryDir returns the discovery directory for an instance
|
// GetDiscoveryDir returns the discovery directory for an instance
|
||||||
func (m *Manager) GetDiscoveryDir(instanceName string) string {
|
func (m *Manager) GetDiscoveryDir(instanceName string) string {
|
||||||
return filepath.Join(m.dataDir, "instances", instanceName, "discovery")
|
return tools.GetInstanceDiscoveryPath(m.dataDir, instanceName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDiscoveryStatusPath returns the path to discovery status file
|
// GetDiscoveryStatusPath returns the path to discovery status file
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/wild-cloud/wild-central/daemon/internal/context"
|
"github.com/wild-cloud/wild-central/daemon/internal/context"
|
||||||
"github.com/wild-cloud/wild-central/daemon/internal/secrets"
|
"github.com/wild-cloud/wild-central/daemon/internal/secrets"
|
||||||
"github.com/wild-cloud/wild-central/daemon/internal/storage"
|
"github.com/wild-cloud/wild-central/daemon/internal/storage"
|
||||||
|
"github.com/wild-cloud/wild-central/daemon/internal/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Manager handles instance lifecycle operations
|
// Manager handles instance lifecycle operations
|
||||||
@@ -38,18 +39,21 @@ type Instance struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetInstancePath returns the path to an instance directory
|
// GetInstancePath returns the path to an instance directory
|
||||||
|
// Deprecated: Use tools.GetInstancePath instead
|
||||||
func (m *Manager) GetInstancePath(name string) string {
|
func (m *Manager) GetInstancePath(name string) string {
|
||||||
return filepath.Join(m.dataDir, "instances", name)
|
return tools.GetInstancePath(m.dataDir, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInstanceConfigPath returns the path to an instance's config file
|
// GetInstanceConfigPath returns the path to an instance's config file
|
||||||
|
// Deprecated: Use tools.GetInstanceConfigPath instead
|
||||||
func (m *Manager) GetInstanceConfigPath(name string) string {
|
func (m *Manager) GetInstanceConfigPath(name string) string {
|
||||||
return filepath.Join(m.GetInstancePath(name), "config.yaml")
|
return tools.GetInstanceConfigPath(m.dataDir, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInstanceSecretsPath returns the path to an instance's secrets file
|
// GetInstanceSecretsPath returns the path to an instance's secrets file
|
||||||
|
// Deprecated: Use tools.GetInstanceSecretsPath instead
|
||||||
func (m *Manager) GetInstanceSecretsPath(name string) string {
|
func (m *Manager) GetInstanceSecretsPath(name string) string {
|
||||||
return filepath.Join(m.GetInstancePath(name), "secrets.yaml")
|
return tools.GetInstanceSecretsPath(m.dataDir, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// InstanceExists checks if an instance exists
|
// InstanceExists checks if an instance exists
|
||||||
@@ -71,7 +75,7 @@ func (m *Manager) CreateInstance(name string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Acquire lock for instance creation
|
// Acquire lock for instance creation
|
||||||
lockPath := filepath.Join(m.dataDir, "instances", ".lock")
|
lockPath := tools.GetInstancesLockPath(m.dataDir)
|
||||||
return storage.WithLock(lockPath, func() error {
|
return storage.WithLock(lockPath, func() error {
|
||||||
// Create instance directory
|
// Create instance directory
|
||||||
if err := storage.EnsureDir(instancePath, 0755); err != nil {
|
if err := storage.EnsureDir(instancePath, 0755); err != nil {
|
||||||
@@ -123,7 +127,7 @@ func (m *Manager) DeleteInstance(name string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Acquire lock for instance deletion
|
// Acquire lock for instance deletion
|
||||||
lockPath := filepath.Join(m.dataDir, "instances", ".lock")
|
lockPath := tools.GetInstancesLockPath(m.dataDir)
|
||||||
return storage.WithLock(lockPath, func() error {
|
return storage.WithLock(lockPath, func() error {
|
||||||
// Remove instance directory
|
// Remove instance directory
|
||||||
if err := os.RemoveAll(instancePath); err != nil {
|
if err := os.RemoveAll(instancePath); err != nil {
|
||||||
@@ -136,7 +140,7 @@ func (m *Manager) DeleteInstance(name string) error {
|
|||||||
|
|
||||||
// ListInstances returns a list of all instance names
|
// ListInstances returns a list of all instance names
|
||||||
func (m *Manager) ListInstances() ([]string, error) {
|
func (m *Manager) ListInstances() ([]string, error) {
|
||||||
instancesDir := filepath.Join(m.dataDir, "instances")
|
instancesDir := tools.GetInstancesPath(m.dataDir)
|
||||||
|
|
||||||
// Ensure instances directory exists
|
// Ensure instances directory exists
|
||||||
if !storage.FileExists(instancesDir) {
|
if !storage.FileExists(instancesDir) {
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ type ApplyOptions struct {
|
|||||||
|
|
||||||
// GetInstancePath returns the path to an instance's nodes directory
|
// GetInstancePath returns the path to an instance's nodes directory
|
||||||
func (m *Manager) GetInstancePath(instanceName string) string {
|
func (m *Manager) GetInstancePath(instanceName string) string {
|
||||||
return filepath.Join(m.dataDir, "instances", instanceName)
|
return tools.GetInstancePath(m.dataDir, instanceName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// List returns all nodes for an instance
|
// List returns all nodes for an instance
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/wild-cloud/wild-central/daemon/internal/storage"
|
"github.com/wild-cloud/wild-central/daemon/internal/storage"
|
||||||
|
"github.com/wild-cloud/wild-central/daemon/internal/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Manager handles async operation tracking
|
// Manager handles async operation tracking
|
||||||
@@ -38,7 +39,7 @@ type Operation struct {
|
|||||||
|
|
||||||
// GetOperationsDir returns the operations directory for an instance
|
// GetOperationsDir returns the operations directory for an instance
|
||||||
func (m *Manager) GetOperationsDir(instanceName string) string {
|
func (m *Manager) GetOperationsDir(instanceName string) string {
|
||||||
return filepath.Join(m.dataDir, "instances", instanceName, "operations")
|
return tools.GetInstanceOperationsPath(m.dataDir, instanceName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateID generates a unique operation ID
|
// generateID generates a unique operation ID
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/wild-cloud/wild-central/daemon/internal/storage"
|
"github.com/wild-cloud/wild-central/daemon/internal/storage"
|
||||||
|
"github.com/wild-cloud/wild-central/daemon/internal/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Manager handles PXE boot asset management
|
// Manager handles PXE boot asset management
|
||||||
@@ -35,7 +36,7 @@ type Asset struct {
|
|||||||
|
|
||||||
// GetPXEDir returns the PXE directory for an instance
|
// GetPXEDir returns the PXE directory for an instance
|
||||||
func (m *Manager) GetPXEDir(instanceName string) string {
|
func (m *Manager) GetPXEDir(instanceName string) string {
|
||||||
return filepath.Join(m.dataDir, "instances", instanceName, "pxe")
|
return tools.GetInstancePXEPath(m.dataDir, instanceName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListAssets returns available PXE assets for an instance
|
// ListAssets returns available PXE assets for an instance
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package services
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
@@ -11,6 +10,7 @@ import (
|
|||||||
"github.com/wild-cloud/wild-central/daemon/internal/contracts"
|
"github.com/wild-cloud/wild-central/daemon/internal/contracts"
|
||||||
"github.com/wild-cloud/wild-central/daemon/internal/operations"
|
"github.com/wild-cloud/wild-central/daemon/internal/operations"
|
||||||
"github.com/wild-cloud/wild-central/daemon/internal/storage"
|
"github.com/wild-cloud/wild-central/daemon/internal/storage"
|
||||||
|
"github.com/wild-cloud/wild-central/daemon/internal/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UpdateConfig updates service configuration and optionally redeploys
|
// UpdateConfig updates service configuration and optionally redeploys
|
||||||
@@ -27,8 +27,7 @@ func (m *Manager) UpdateConfig(instanceName, serviceName string, update contract
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Load instance config
|
// 2. Load instance config
|
||||||
instanceDir := filepath.Join(m.dataDir, "instances", instanceName)
|
configPath := tools.GetInstanceConfigPath(m.dataDir, instanceName)
|
||||||
configPath := filepath.Join(instanceDir, "config.yaml")
|
|
||||||
|
|
||||||
if !storage.FileExists(configPath) {
|
if !storage.FileExists(configPath) {
|
||||||
return nil, fmt.Errorf("config file not found for instance %s", instanceName)
|
return nil, fmt.Errorf("config file not found for instance %s", instanceName)
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/wild-cloud/wild-central/daemon/internal/contracts"
|
"github.com/wild-cloud/wild-central/daemon/internal/contracts"
|
||||||
@@ -41,7 +40,7 @@ func (m *Manager) GetLogs(instanceName, serviceName string, opts contracts.Servi
|
|||||||
podName, err = kubectl.GetFirstPodName(namespace)
|
podName, err = kubectl.GetFirstPodName(namespace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Check if it's because there are no pods
|
// Check if it's because there are no pods
|
||||||
pods, _ := kubectl.GetPods(namespace)
|
pods, _ := kubectl.GetPods(namespace, false)
|
||||||
if len(pods) == 0 {
|
if len(pods) == 0 {
|
||||||
// Return empty logs response instead of error when no pods exist
|
// Return empty logs response instead of error when no pods exist
|
||||||
return &contracts.ServiceLogsResponse{
|
return &contracts.ServiceLogsResponse{
|
||||||
@@ -61,7 +60,7 @@ func (m *Manager) GetLogs(instanceName, serviceName string, opts contracts.Servi
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Find pod with specified container
|
// Find pod with specified container
|
||||||
pods, err := kubectl.GetPods(namespace)
|
pods, err := kubectl.GetPods(namespace, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to list pods: %w", err)
|
return nil, fmt.Errorf("failed to list pods: %w", err)
|
||||||
}
|
}
|
||||||
@@ -83,19 +82,24 @@ func (m *Manager) GetLogs(instanceName, serviceName string, opts contracts.Servi
|
|||||||
|
|
||||||
// 5. Get logs
|
// 5. Get logs
|
||||||
logOpts := tools.LogOptions{
|
logOpts := tools.LogOptions{
|
||||||
Container: opts.Container,
|
Container: opts.Container,
|
||||||
Tail: opts.Tail,
|
Tail: opts.Tail,
|
||||||
Previous: opts.Previous,
|
Previous: opts.Previous,
|
||||||
Since: opts.Since,
|
Since: opts.Since,
|
||||||
|
SinceSeconds: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
logs, err := kubectl.GetLogs(namespace, podName, logOpts)
|
logEntries, err := kubectl.GetLogs(namespace, podName, logOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get logs: %w", err)
|
return nil, fmt.Errorf("failed to get logs: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Parse logs into lines
|
// 6. Convert structured logs to string lines
|
||||||
lines := strings.Split(strings.TrimSpace(logs), "\n")
|
lines := make([]string, 0, len(logEntries))
|
||||||
|
for _, entry := range logEntries {
|
||||||
|
lines = append(lines, entry.Message)
|
||||||
|
}
|
||||||
|
|
||||||
truncated := false
|
truncated := false
|
||||||
if len(lines) > opts.Tail {
|
if len(lines) > opts.Tail {
|
||||||
lines = lines[len(lines)-opts.Tail:]
|
lines = lines[len(lines)-opts.Tail:]
|
||||||
@@ -139,7 +143,7 @@ func (m *Manager) StreamLogs(instanceName, serviceName string, opts contracts.Se
|
|||||||
podName, err = kubectl.GetFirstPodName(namespace)
|
podName, err = kubectl.GetFirstPodName(namespace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Check if it's because there are no pods
|
// Check if it's because there are no pods
|
||||||
pods, _ := kubectl.GetPods(namespace)
|
pods, _ := kubectl.GetPods(namespace, false)
|
||||||
if len(pods) == 0 {
|
if len(pods) == 0 {
|
||||||
// Send a message event indicating no pods
|
// Send a message event indicating no pods
|
||||||
fmt.Fprintf(writer, "data: No pods found for service. The service may not be deployed yet.\n\n")
|
fmt.Fprintf(writer, "data: No pods found for service. The service may not be deployed yet.\n\n")
|
||||||
@@ -157,7 +161,7 @@ func (m *Manager) StreamLogs(instanceName, serviceName string, opts contracts.Se
|
|||||||
opts.Container = containers[0]
|
opts.Container = containers[0]
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
pods, err := kubectl.GetPods(namespace)
|
pods, err := kubectl.GetPods(namespace, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to list pods: %w", err)
|
return fmt.Errorf("failed to list pods: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
@@ -264,7 +265,7 @@ func (m *Manager) Delete(instanceName, serviceName string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get manifests file from embedded setup or instance directory
|
// Get manifests file from embedded setup or instance directory
|
||||||
instanceServiceDir := filepath.Join(m.dataDir, "instances", instanceName, "setup", "cluster-services", serviceName)
|
instanceServiceDir := filepath.Join(tools.GetInstancePath(m.dataDir, instanceName), "setup", "cluster-services", serviceName)
|
||||||
manifestsFile := filepath.Join(instanceServiceDir, "manifests.yaml")
|
manifestsFile := filepath.Join(instanceServiceDir, "manifests.yaml")
|
||||||
|
|
||||||
if !storage.FileExists(manifestsFile) {
|
if !storage.FileExists(manifestsFile) {
|
||||||
@@ -332,7 +333,7 @@ func (m *Manager) Fetch(instanceName, serviceName string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Create instance service directory
|
// 2. Create instance service directory
|
||||||
instanceDir := filepath.Join(m.dataDir, "instances", instanceName,
|
instanceDir := filepath.Join(tools.GetInstancePath(m.dataDir, instanceName),
|
||||||
"setup", "cluster-services", serviceName)
|
"setup", "cluster-services", serviceName)
|
||||||
if err := os.MkdirAll(instanceDir, 0755); err != nil {
|
if err := os.MkdirAll(instanceDir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create service directory: %w", err)
|
return fmt.Errorf("failed to create service directory: %w", err)
|
||||||
@@ -376,7 +377,7 @@ func (m *Manager) Fetch(instanceName, serviceName string) error {
|
|||||||
|
|
||||||
// serviceFilesExist checks if service files exist in the instance
|
// serviceFilesExist checks if service files exist in the instance
|
||||||
func (m *Manager) serviceFilesExist(instanceName, serviceName string) bool {
|
func (m *Manager) serviceFilesExist(instanceName, serviceName string) bool {
|
||||||
serviceDir := filepath.Join(m.dataDir, "instances", instanceName,
|
serviceDir := filepath.Join(tools.GetInstancePath(m.dataDir, instanceName),
|
||||||
"setup", "cluster-services", serviceName)
|
"setup", "cluster-services", serviceName)
|
||||||
installSh := filepath.Join(serviceDir, "install.sh")
|
installSh := filepath.Join(serviceDir, "install.sh")
|
||||||
return fileExists(installSh)
|
return fileExists(installSh)
|
||||||
@@ -422,7 +423,7 @@ func extractFS(fsys fs.FS, dst string) error {
|
|||||||
|
|
||||||
// Compile processes gomplate templates into final Kubernetes manifests
|
// Compile processes gomplate templates into final Kubernetes manifests
|
||||||
func (m *Manager) Compile(instanceName, serviceName string) error {
|
func (m *Manager) Compile(instanceName, serviceName string) error {
|
||||||
instanceDir := filepath.Join(m.dataDir, "instances", instanceName)
|
instanceDir := tools.GetInstancePath(m.dataDir, instanceName)
|
||||||
serviceDir := filepath.Join(instanceDir, "setup", "cluster-services", serviceName)
|
serviceDir := filepath.Join(instanceDir, "setup", "cluster-services", serviceName)
|
||||||
templateDir := filepath.Join(serviceDir, "kustomize.template")
|
templateDir := filepath.Join(serviceDir, "kustomize.template")
|
||||||
outputDir := filepath.Join(serviceDir, "kustomize")
|
outputDir := filepath.Join(serviceDir, "kustomize")
|
||||||
@@ -500,7 +501,7 @@ func (m *Manager) Compile(instanceName, serviceName string) error {
|
|||||||
func (m *Manager) Deploy(instanceName, serviceName, opID string, broadcaster *operations.Broadcaster) error {
|
func (m *Manager) Deploy(instanceName, serviceName, opID string, broadcaster *operations.Broadcaster) error {
|
||||||
fmt.Printf("[DEBUG] Deploy() called for service=%s instance=%s opID=%s\n", serviceName, instanceName, opID)
|
fmt.Printf("[DEBUG] Deploy() called for service=%s instance=%s opID=%s\n", serviceName, instanceName, opID)
|
||||||
|
|
||||||
instanceDir := filepath.Join(m.dataDir, "instances", instanceName)
|
instanceDir := tools.GetInstancePath(m.dataDir, instanceName)
|
||||||
serviceDir := filepath.Join(instanceDir, "setup", "cluster-services", serviceName)
|
serviceDir := filepath.Join(instanceDir, "setup", "cluster-services", serviceName)
|
||||||
installScript := filepath.Join(serviceDir, "install.sh")
|
installScript := filepath.Join(serviceDir, "install.sh")
|
||||||
|
|
||||||
@@ -596,8 +597,7 @@ func (m *Manager) validateConfig(instanceName, serviceName string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load instance config
|
// Load instance config
|
||||||
instanceDir := filepath.Join(m.dataDir, "instances", instanceName)
|
configFile := tools.GetInstanceConfigPath(m.dataDir, instanceName)
|
||||||
configFile := filepath.Join(instanceDir, "config.yaml")
|
|
||||||
|
|
||||||
configData, err := os.ReadFile(configFile)
|
configData, err := os.ReadFile(configFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package services
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
@@ -73,7 +72,7 @@ func (m *Manager) GetDetailedStatus(instanceName, serviceName string) (*contract
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4. Get pod information
|
// 4. Get pod information
|
||||||
podInfos, err := kubectl.GetPods(namespace)
|
podInfos, err := kubectl.GetPods(namespace, false)
|
||||||
pods := make([]contracts.PodStatus, 0, len(podInfos))
|
pods := make([]contracts.PodStatus, 0, len(podInfos))
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -91,8 +90,7 @@ func (m *Manager) GetDetailedStatus(instanceName, serviceName string) (*contract
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 5. Load current config values
|
// 5. Load current config values
|
||||||
instanceDir := filepath.Join(m.dataDir, "instances", instanceName)
|
configPath := tools.GetInstanceConfigPath(m.dataDir, instanceName)
|
||||||
configPath := filepath.Join(instanceDir, "config.yaml")
|
|
||||||
configValues := make(map[string]interface{})
|
configValues := make(map[string]interface{})
|
||||||
|
|
||||||
if storage.FileExists(configPath) {
|
if storage.FileExists(configPath) {
|
||||||
|
|||||||
@@ -35,3 +35,53 @@ func GetTalosconfigPath(dataDir, instanceName string) string {
|
|||||||
func GetKubeconfigPath(dataDir, instanceName string) string {
|
func GetKubeconfigPath(dataDir, instanceName string) string {
|
||||||
return filepath.Join(dataDir, "instances", instanceName, "kubeconfig")
|
return filepath.Join(dataDir, "instances", instanceName, "kubeconfig")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetInstancePath returns the path to an instance directory
|
||||||
|
func GetInstancePath(dataDir, instanceName string) string {
|
||||||
|
return filepath.Join(dataDir, "instances", instanceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInstanceConfigPath returns the path to an instance's config file
|
||||||
|
func GetInstanceConfigPath(dataDir, instanceName string) string {
|
||||||
|
return filepath.Join(dataDir, "instances", instanceName, "config.yaml")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInstanceSecretsPath returns the path to an instance's secrets file
|
||||||
|
func GetInstanceSecretsPath(dataDir, instanceName string) string {
|
||||||
|
return filepath.Join(dataDir, "instances", instanceName, "secrets.yaml")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInstanceTalosPath returns the path to an instance's talos directory
|
||||||
|
func GetInstanceTalosPath(dataDir, instanceName string) string {
|
||||||
|
return filepath.Join(dataDir, "instances", instanceName, "talos")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInstancePXEPath returns the path to an instance's PXE directory
|
||||||
|
func GetInstancePXEPath(dataDir, instanceName string) string {
|
||||||
|
return filepath.Join(dataDir, "instances", instanceName, "pxe")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInstanceOperationsPath returns the path to an instance's operations directory
|
||||||
|
func GetInstanceOperationsPath(dataDir, instanceName string) string {
|
||||||
|
return filepath.Join(dataDir, "instances", instanceName, "operations")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInstanceBackupsPath returns the path to an instance's backups directory
|
||||||
|
func GetInstanceBackupsPath(dataDir, instanceName string) string {
|
||||||
|
return filepath.Join(dataDir, "instances", instanceName, "backups")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInstanceDiscoveryPath returns the path to an instance's discovery directory
|
||||||
|
func GetInstanceDiscoveryPath(dataDir, instanceName string) string {
|
||||||
|
return filepath.Join(dataDir, "instances", instanceName, "discovery")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInstancesPath returns the path to the instances directory
|
||||||
|
func GetInstancesPath(dataDir string) string {
|
||||||
|
return filepath.Join(dataDir, "instances")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInstancesLockPath returns the path to the instances directory lock file
|
||||||
|
func GetInstancesLockPath(dataDir string) string {
|
||||||
|
return filepath.Join(dataDir, "instances", ".lock")
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Kubectl provides a thin wrapper around the kubectl command-line tool
|
// Kubectl provides a comprehensive wrapper around the kubectl command-line tool
|
||||||
type Kubectl struct {
|
type Kubectl struct {
|
||||||
kubeconfigPath string
|
kubeconfigPath string
|
||||||
}
|
}
|
||||||
@@ -20,6 +22,115 @@ func NewKubectl(kubeconfigPath string) *Kubectl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pod Information Structures
|
||||||
|
|
||||||
|
// PodInfo represents pod information from kubectl
|
||||||
|
type PodInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Ready string `json:"ready"`
|
||||||
|
Restarts int `json:"restarts"`
|
||||||
|
Age string `json:"age"`
|
||||||
|
Node string `json:"node,omitempty"`
|
||||||
|
IP string `json:"ip,omitempty"`
|
||||||
|
Containers []ContainerInfo `json:"containers,omitempty"`
|
||||||
|
Conditions []PodCondition `json:"conditions,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainerInfo represents detailed container information
|
||||||
|
type ContainerInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Image string `json:"image"`
|
||||||
|
Ready bool `json:"ready"`
|
||||||
|
RestartCount int `json:"restartCount"`
|
||||||
|
State ContainerState `json:"state"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainerState represents the state of a container
|
||||||
|
type ContainerState struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
Since time.Time `json:"since,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PodCondition represents a pod condition
|
||||||
|
type PodCondition struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
Since time.Time `json:"since,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deployment Information Structures
|
||||||
|
|
||||||
|
// DeploymentInfo represents deployment information
|
||||||
|
type DeploymentInfo struct {
|
||||||
|
Desired int32 `json:"desired"`
|
||||||
|
Current int32 `json:"current"`
|
||||||
|
Ready int32 `json:"ready"`
|
||||||
|
Available int32 `json:"available"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplicaInfo represents aggregated replica information
|
||||||
|
type ReplicaInfo struct {
|
||||||
|
Desired int `json:"desired"`
|
||||||
|
Current int `json:"current"`
|
||||||
|
Ready int `json:"ready"`
|
||||||
|
Available int `json:"available"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resource Information Structures
|
||||||
|
|
||||||
|
// ResourceMetric represents resource usage for a specific resource type
|
||||||
|
type ResourceMetric struct {
|
||||||
|
Used string `json:"used"`
|
||||||
|
Requested string `json:"requested"`
|
||||||
|
Limit string `json:"limit"`
|
||||||
|
Percentage float64 `json:"percentage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceUsage represents aggregated resource usage
|
||||||
|
type ResourceUsage struct {
|
||||||
|
CPU *ResourceMetric `json:"cpu,omitempty"`
|
||||||
|
Memory *ResourceMetric `json:"memory,omitempty"`
|
||||||
|
Storage *ResourceMetric `json:"storage,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event Information Structures
|
||||||
|
|
||||||
|
// KubernetesEvent represents a Kubernetes event
|
||||||
|
type KubernetesEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
FirstSeen time.Time `json:"firstSeen"`
|
||||||
|
LastSeen time.Time `json:"lastSeen"`
|
||||||
|
Object string `json:"object"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logging Structures
|
||||||
|
|
||||||
|
// LogOptions configures log retrieval
|
||||||
|
type LogOptions struct {
|
||||||
|
Container string
|
||||||
|
Tail int
|
||||||
|
Previous bool
|
||||||
|
Since string
|
||||||
|
SinceSeconds int
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogEntry represents a structured log entry
|
||||||
|
type LogEntry struct {
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Pod string `json:"pod"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pod Operations
|
||||||
|
|
||||||
// DeploymentExists checks if a deployment exists in the specified namespace
|
// DeploymentExists checks if a deployment exists in the specified namespace
|
||||||
func (k *Kubectl) DeploymentExists(name, namespace string) bool {
|
func (k *Kubectl) DeploymentExists(name, namespace string) bool {
|
||||||
args := []string{
|
args := []string{
|
||||||
@@ -36,27 +147,9 @@ func (k *Kubectl) DeploymentExists(name, namespace string) bool {
|
|||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PodInfo represents pod information from kubectl
|
|
||||||
type PodInfo struct {
|
|
||||||
Name string
|
|
||||||
Status string
|
|
||||||
Ready string
|
|
||||||
Restarts int
|
|
||||||
Age string
|
|
||||||
Node string
|
|
||||||
IP string
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeploymentInfo represents deployment information
|
|
||||||
type DeploymentInfo struct {
|
|
||||||
Desired int32
|
|
||||||
Current int32
|
|
||||||
Ready int32
|
|
||||||
Available int32
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPods retrieves pod information for a namespace
|
// GetPods retrieves pod information for a namespace
|
||||||
func (k *Kubectl) GetPods(namespace string) ([]PodInfo, error) {
|
// If detailed is true, includes containers and conditions
|
||||||
|
func (k *Kubectl) GetPods(namespace string, detailed bool) ([]PodInfo, error) {
|
||||||
args := []string{
|
args := []string{
|
||||||
"get", "pods",
|
"get", "pods",
|
||||||
"-n", namespace,
|
"-n", namespace,
|
||||||
@@ -80,14 +173,36 @@ func (k *Kubectl) GetPods(namespace string) ([]PodInfo, error) {
|
|||||||
CreationTimestamp time.Time `json:"creationTimestamp"`
|
CreationTimestamp time.Time `json:"creationTimestamp"`
|
||||||
} `json:"metadata"`
|
} `json:"metadata"`
|
||||||
Spec struct {
|
Spec struct {
|
||||||
NodeName string `json:"nodeName"`
|
NodeName string `json:"nodeName"`
|
||||||
|
Containers []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Image string `json:"image"`
|
||||||
|
} `json:"containers"`
|
||||||
} `json:"spec"`
|
} `json:"spec"`
|
||||||
Status struct {
|
Status struct {
|
||||||
Phase string `json:"phase"`
|
Phase string `json:"phase"`
|
||||||
PodIP string `json:"podIP"`
|
PodIP string `json:"podIP"`
|
||||||
|
Conditions []struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
LastTransitionTime time.Time `json:"lastTransitionTime"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
} `json:"conditions"`
|
||||||
ContainerStatuses []struct {
|
ContainerStatuses []struct {
|
||||||
Ready bool `json:"ready"`
|
Name string `json:"name"`
|
||||||
RestartCount int `json:"restartCount"`
|
Image string `json:"image"`
|
||||||
|
Ready bool `json:"ready"`
|
||||||
|
RestartCount int `json:"restartCount"`
|
||||||
|
State struct {
|
||||||
|
Running *struct{ StartedAt time.Time } `json:"running,omitempty"`
|
||||||
|
Waiting *struct{ Reason, Message string } `json:"waiting,omitempty"`
|
||||||
|
Terminated *struct {
|
||||||
|
Reason string
|
||||||
|
Message string
|
||||||
|
FinishedAt time.Time
|
||||||
|
} `json:"terminated,omitempty"`
|
||||||
|
} `json:"state"`
|
||||||
} `json:"containerStatuses"`
|
} `json:"containerStatuses"`
|
||||||
} `json:"status"`
|
} `json:"status"`
|
||||||
} `json:"items"`
|
} `json:"items"`
|
||||||
@@ -102,31 +217,117 @@ func (k *Kubectl) GetPods(namespace string) ([]PodInfo, error) {
|
|||||||
// Calculate ready containers
|
// Calculate ready containers
|
||||||
readyCount := 0
|
readyCount := 0
|
||||||
totalCount := len(pod.Status.ContainerStatuses)
|
totalCount := len(pod.Status.ContainerStatuses)
|
||||||
restarts := 0
|
totalRestarts := 0
|
||||||
|
|
||||||
for _, cs := range pod.Status.ContainerStatuses {
|
for _, cs := range pod.Status.ContainerStatuses {
|
||||||
if cs.Ready {
|
if cs.Ready {
|
||||||
readyCount++
|
readyCount++
|
||||||
}
|
}
|
||||||
restarts += cs.RestartCount
|
totalRestarts += cs.RestartCount
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate age
|
// Ensure status is never empty
|
||||||
age := formatAge(time.Since(pod.Metadata.CreationTimestamp))
|
status := pod.Status.Phase
|
||||||
|
if status == "" {
|
||||||
|
status = "Unknown"
|
||||||
|
}
|
||||||
|
|
||||||
pods = append(pods, PodInfo{
|
podInfo := PodInfo{
|
||||||
Name: pod.Metadata.Name,
|
Name: pod.Metadata.Name,
|
||||||
Status: pod.Status.Phase,
|
Status: status,
|
||||||
Ready: fmt.Sprintf("%d/%d", readyCount, totalCount),
|
Ready: fmt.Sprintf("%d/%d", readyCount, totalCount),
|
||||||
Restarts: restarts,
|
Restarts: totalRestarts,
|
||||||
Age: age,
|
Age: formatAge(time.Since(pod.Metadata.CreationTimestamp)),
|
||||||
Node: pod.Spec.NodeName,
|
Node: pod.Spec.NodeName,
|
||||||
IP: pod.Status.PodIP,
|
IP: pod.Status.PodIP,
|
||||||
})
|
}
|
||||||
|
|
||||||
|
// Include detailed information if requested
|
||||||
|
if detailed {
|
||||||
|
// Add container details
|
||||||
|
containers := make([]ContainerInfo, 0, len(pod.Status.ContainerStatuses))
|
||||||
|
for _, cs := range pod.Status.ContainerStatuses {
|
||||||
|
containerState := ContainerState{Status: "unknown"}
|
||||||
|
if cs.State.Running != nil {
|
||||||
|
containerState.Status = "running"
|
||||||
|
containerState.Since = cs.State.Running.StartedAt
|
||||||
|
} else if cs.State.Waiting != nil {
|
||||||
|
containerState.Status = "waiting"
|
||||||
|
containerState.Reason = cs.State.Waiting.Reason
|
||||||
|
containerState.Message = cs.State.Waiting.Message
|
||||||
|
} else if cs.State.Terminated != nil {
|
||||||
|
containerState.Status = "terminated"
|
||||||
|
containerState.Reason = cs.State.Terminated.Reason
|
||||||
|
containerState.Message = cs.State.Terminated.Message
|
||||||
|
containerState.Since = cs.State.Terminated.FinishedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
containers = append(containers, ContainerInfo{
|
||||||
|
Name: cs.Name,
|
||||||
|
Image: cs.Image,
|
||||||
|
Ready: cs.Ready,
|
||||||
|
RestartCount: cs.RestartCount,
|
||||||
|
State: containerState,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
podInfo.Containers = containers
|
||||||
|
|
||||||
|
// Add condition details
|
||||||
|
conditions := make([]PodCondition, 0, len(pod.Status.Conditions))
|
||||||
|
for _, cond := range pod.Status.Conditions {
|
||||||
|
conditions = append(conditions, PodCondition{
|
||||||
|
Type: cond.Type,
|
||||||
|
Status: cond.Status,
|
||||||
|
Reason: cond.Reason,
|
||||||
|
Message: cond.Message,
|
||||||
|
Since: cond.LastTransitionTime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
podInfo.Conditions = conditions
|
||||||
|
}
|
||||||
|
|
||||||
|
pods = append(pods, podInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
return pods, nil
|
return pods, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetFirstPodName returns the name of the first pod in a namespace
|
||||||
|
func (k *Kubectl) GetFirstPodName(namespace string) (string, error) {
|
||||||
|
pods, err := k.GetPods(namespace, false)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(pods) == 0 {
|
||||||
|
return "", fmt.Errorf("no pods found in namespace %s", namespace)
|
||||||
|
}
|
||||||
|
return pods[0].Name, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPodContainers returns container names for a pod
|
||||||
|
func (k *Kubectl) GetPodContainers(namespace, podName string) ([]string, error) {
|
||||||
|
args := []string{
|
||||||
|
"get", "pod", podName,
|
||||||
|
"-n", namespace,
|
||||||
|
"-o", "jsonpath={.spec.containers[*].name}",
|
||||||
|
}
|
||||||
|
|
||||||
|
if k.kubeconfigPath != "" {
|
||||||
|
args = append([]string{"--kubeconfig", k.kubeconfigPath}, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("kubectl", args...)
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get pod containers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
containerNames := strings.Fields(string(output))
|
||||||
|
return containerNames, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deployment Operations
|
||||||
|
|
||||||
// GetDeployment retrieves deployment information
|
// GetDeployment retrieves deployment information
|
||||||
func (k *Kubectl) GetDeployment(name, namespace string) (*DeploymentInfo, error) {
|
func (k *Kubectl) GetDeployment(name, namespace string) (*DeploymentInfo, error) {
|
||||||
args := []string{
|
args := []string{
|
||||||
@@ -169,25 +370,235 @@ func (k *Kubectl) GetDeployment(name, namespace string) (*DeploymentInfo, error)
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLogs retrieves logs from a pod
|
// GetReplicas retrieves aggregated replica information for a namespace
|
||||||
func (k *Kubectl) GetLogs(namespace, podName string, opts LogOptions) (string, error) {
|
func (k *Kubectl) GetReplicas(namespace string) (*ReplicaInfo, error) {
|
||||||
args := []string{
|
info := &ReplicaInfo{}
|
||||||
"logs", podName,
|
|
||||||
"-n", namespace,
|
// Get deployments
|
||||||
|
deployCmd := exec.Command("kubectl", "get", "deployments", "-n", namespace, "-o", "json")
|
||||||
|
WithKubeconfig(deployCmd, k.kubeconfigPath)
|
||||||
|
|
||||||
|
deployOutput, err := deployCmd.Output()
|
||||||
|
if err == nil {
|
||||||
|
var deployList struct {
|
||||||
|
Items []struct {
|
||||||
|
Spec struct {
|
||||||
|
Replicas int `json:"replicas"`
|
||||||
|
} `json:"spec"`
|
||||||
|
Status struct {
|
||||||
|
Replicas int `json:"replicas"`
|
||||||
|
ReadyReplicas int `json:"readyReplicas"`
|
||||||
|
AvailableReplicas int `json:"availableReplicas"`
|
||||||
|
} `json:"status"`
|
||||||
|
} `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if json.Unmarshal(deployOutput, &deployList) == nil {
|
||||||
|
for _, deploy := range deployList.Items {
|
||||||
|
info.Desired += deploy.Spec.Replicas
|
||||||
|
info.Current += deploy.Status.Replicas
|
||||||
|
info.Ready += deploy.Status.ReadyReplicas
|
||||||
|
info.Available += deploy.Status.AvailableReplicas
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get statefulsets
|
||||||
|
stsCmd := exec.Command("kubectl", "get", "statefulsets", "-n", namespace, "-o", "json")
|
||||||
|
WithKubeconfig(stsCmd, k.kubeconfigPath)
|
||||||
|
|
||||||
|
stsOutput, err := stsCmd.Output()
|
||||||
|
if err == nil {
|
||||||
|
var stsList struct {
|
||||||
|
Items []struct {
|
||||||
|
Spec struct {
|
||||||
|
Replicas int `json:"replicas"`
|
||||||
|
} `json:"spec"`
|
||||||
|
Status struct {
|
||||||
|
Replicas int `json:"replicas"`
|
||||||
|
ReadyReplicas int `json:"readyReplicas"`
|
||||||
|
} `json:"status"`
|
||||||
|
} `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if json.Unmarshal(stsOutput, &stsList) == nil {
|
||||||
|
for _, sts := range stsList.Items {
|
||||||
|
info.Desired += sts.Spec.Replicas
|
||||||
|
info.Current += sts.Status.Replicas
|
||||||
|
info.Ready += sts.Status.ReadyReplicas
|
||||||
|
// StatefulSets don't have availableReplicas, use ready as proxy
|
||||||
|
info.Available += sts.Status.ReadyReplicas
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resource Monitoring
|
||||||
|
|
||||||
|
// GetResources retrieves aggregated resource usage for a namespace
|
||||||
|
func (k *Kubectl) GetResources(namespace string) (*ResourceUsage, error) {
|
||||||
|
cmd := exec.Command("kubectl", "get", "pods", "-n", namespace, "-o", "json")
|
||||||
|
WithKubeconfig(cmd, k.kubeconfigPath)
|
||||||
|
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get pods: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var podList struct {
|
||||||
|
Items []struct {
|
||||||
|
Spec struct {
|
||||||
|
Containers []struct {
|
||||||
|
Resources struct {
|
||||||
|
Requests map[string]string `json:"requests,omitempty"`
|
||||||
|
Limits map[string]string `json:"limits,omitempty"`
|
||||||
|
} `json:"resources"`
|
||||||
|
} `json:"containers"`
|
||||||
|
} `json:"spec"`
|
||||||
|
} `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(output, &podList); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse pod list: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregate resources
|
||||||
|
cpuRequests := int64(0)
|
||||||
|
cpuLimits := int64(0)
|
||||||
|
memRequests := int64(0)
|
||||||
|
memLimits := int64(0)
|
||||||
|
|
||||||
|
for _, pod := range podList.Items {
|
||||||
|
for _, container := range pod.Spec.Containers {
|
||||||
|
if req, ok := container.Resources.Requests["cpu"]; ok {
|
||||||
|
cpuRequests += parseResourceQuantity(req)
|
||||||
|
}
|
||||||
|
if lim, ok := container.Resources.Limits["cpu"]; ok {
|
||||||
|
cpuLimits += parseResourceQuantity(lim)
|
||||||
|
}
|
||||||
|
if req, ok := container.Resources.Requests["memory"]; ok {
|
||||||
|
memRequests += parseResourceQuantity(req)
|
||||||
|
}
|
||||||
|
if lim, ok := container.Resources.Limits["memory"]; ok {
|
||||||
|
memLimits += parseResourceQuantity(lim)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build resource usage with metrics
|
||||||
|
usage := &ResourceUsage{}
|
||||||
|
|
||||||
|
// CPU metrics (if any resources defined)
|
||||||
|
if cpuRequests > 0 || cpuLimits > 0 {
|
||||||
|
cpuUsed := cpuRequests // Approximate "used" as requests for now
|
||||||
|
cpuPercentage := 0.0
|
||||||
|
if cpuLimits > 0 {
|
||||||
|
cpuPercentage = float64(cpuUsed) / float64(cpuLimits) * 100
|
||||||
|
}
|
||||||
|
usage.CPU = &ResourceMetric{
|
||||||
|
Used: formatCPU(cpuUsed),
|
||||||
|
Requested: formatCPU(cpuRequests),
|
||||||
|
Limit: formatCPU(cpuLimits),
|
||||||
|
Percentage: cpuPercentage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory metrics (if any resources defined)
|
||||||
|
if memRequests > 0 || memLimits > 0 {
|
||||||
|
memUsed := memRequests // Approximate "used" as requests for now
|
||||||
|
memPercentage := 0.0
|
||||||
|
if memLimits > 0 {
|
||||||
|
memPercentage = float64(memUsed) / float64(memLimits) * 100
|
||||||
|
}
|
||||||
|
usage.Memory = &ResourceMetric{
|
||||||
|
Used: formatMemory(memUsed),
|
||||||
|
Requested: formatMemory(memRequests),
|
||||||
|
Limit: formatMemory(memLimits),
|
||||||
|
Percentage: memPercentage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return usage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRecentEvents retrieves recent events for a namespace
|
||||||
|
func (k *Kubectl) GetRecentEvents(namespace string, limit int) ([]KubernetesEvent, error) {
|
||||||
|
cmd := exec.Command("kubectl", "get", "events", "-n", namespace,
|
||||||
|
"--sort-by=.lastTimestamp", "-o", "json")
|
||||||
|
WithKubeconfig(cmd, k.kubeconfigPath)
|
||||||
|
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get events: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var eventList struct {
|
||||||
|
Items []struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
FirstTimestamp time.Time `json:"firstTimestamp"`
|
||||||
|
LastTimestamp time.Time `json:"lastTimestamp"`
|
||||||
|
InvolvedObject struct {
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"involvedObject"`
|
||||||
|
} `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(output, &eventList); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse events: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by last timestamp (most recent first)
|
||||||
|
sort.Slice(eventList.Items, func(i, j int) bool {
|
||||||
|
return eventList.Items[i].LastTimestamp.After(eventList.Items[j].LastTimestamp)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Limit results
|
||||||
|
if limit > 0 && len(eventList.Items) > limit {
|
||||||
|
eventList.Items = eventList.Items[:limit]
|
||||||
|
}
|
||||||
|
|
||||||
|
events := make([]KubernetesEvent, 0, len(eventList.Items))
|
||||||
|
for _, event := range eventList.Items {
|
||||||
|
events = append(events, KubernetesEvent{
|
||||||
|
Type: event.Type,
|
||||||
|
Reason: event.Reason,
|
||||||
|
Message: event.Message,
|
||||||
|
Count: event.Count,
|
||||||
|
FirstSeen: event.FirstTimestamp,
|
||||||
|
LastSeen: event.LastTimestamp,
|
||||||
|
Object: fmt.Sprintf("%s/%s", event.InvolvedObject.Kind, event.InvolvedObject.Name),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return events, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logging Operations
|
||||||
|
|
||||||
|
// GetLogs retrieves logs from a pod
|
||||||
|
func (k *Kubectl) GetLogs(namespace, podName string, opts LogOptions) ([]LogEntry, error) {
|
||||||
|
args := []string{"logs", podName, "-n", namespace}
|
||||||
|
|
||||||
if opts.Container != "" {
|
if opts.Container != "" {
|
||||||
args = append(args, "-c", opts.Container)
|
args = append(args, "-c", opts.Container)
|
||||||
}
|
}
|
||||||
if opts.Tail > 0 {
|
if opts.Tail > 0 {
|
||||||
args = append(args, "--tail", fmt.Sprintf("%d", opts.Tail))
|
args = append(args, "--tail", strconv.Itoa(opts.Tail))
|
||||||
|
}
|
||||||
|
if opts.SinceSeconds > 0 {
|
||||||
|
args = append(args, "--since", fmt.Sprintf("%ds", opts.SinceSeconds))
|
||||||
|
} else if opts.Since != "" {
|
||||||
|
args = append(args, "--since", opts.Since)
|
||||||
}
|
}
|
||||||
if opts.Previous {
|
if opts.Previous {
|
||||||
args = append(args, "--previous")
|
args = append(args, "--previous")
|
||||||
}
|
}
|
||||||
if opts.Since != "" {
|
|
||||||
args = append(args, "--since", opts.Since)
|
|
||||||
}
|
|
||||||
|
|
||||||
if k.kubeconfigPath != "" {
|
if k.kubeconfigPath != "" {
|
||||||
args = append([]string{"--kubeconfig", k.kubeconfigPath}, args...)
|
args = append([]string{"--kubeconfig", k.kubeconfigPath}, args...)
|
||||||
@@ -196,18 +607,24 @@ func (k *Kubectl) GetLogs(namespace, podName string, opts LogOptions) (string, e
|
|||||||
cmd := exec.Command("kubectl", 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 logs: %w", err)
|
return nil, fmt.Errorf("failed to get logs: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(output), nil
|
lines := strings.Split(string(output), "\n")
|
||||||
}
|
entries := make([]LogEntry, 0, len(lines))
|
||||||
|
|
||||||
// LogOptions configures log retrieval
|
for _, line := range lines {
|
||||||
type LogOptions struct {
|
if line == "" {
|
||||||
Container string
|
continue
|
||||||
Tail int
|
}
|
||||||
Previous bool
|
entries = append(entries, LogEntry{
|
||||||
Since string
|
Timestamp: time.Now(), // Best effort - kubectl doesn't provide structured timestamps
|
||||||
|
Message: line,
|
||||||
|
Pod: podName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// StreamLogs streams logs from a pod
|
// StreamLogs streams logs from a pod
|
||||||
@@ -236,6 +653,8 @@ func (k *Kubectl) StreamLogs(namespace, podName string, opts LogOptions) (*exec.
|
|||||||
return cmd, nil
|
return cmd, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper Functions
|
||||||
|
|
||||||
// formatAge converts a duration to a human-readable age string
|
// formatAge converts a duration to a human-readable age string
|
||||||
func formatAge(d time.Duration) string {
|
func formatAge(d time.Duration) string {
|
||||||
if d < time.Minute {
|
if d < time.Minute {
|
||||||
@@ -250,36 +669,71 @@ func formatAge(d time.Duration) string {
|
|||||||
return fmt.Sprintf("%dd", int(d.Hours()/24))
|
return fmt.Sprintf("%dd", int(d.Hours()/24))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFirstPodName returns the name of the first pod in a namespace
|
// parseResourceQuantity converts kubernetes resource quantities to millicores/bytes
|
||||||
func (k *Kubectl) GetFirstPodName(namespace string) (string, error) {
|
func parseResourceQuantity(quantity string) int64 {
|
||||||
pods, err := k.GetPods(namespace)
|
quantity = strings.TrimSpace(quantity)
|
||||||
if err != nil {
|
if quantity == "" {
|
||||||
return "", err
|
return 0
|
||||||
}
|
}
|
||||||
if len(pods) == 0 {
|
|
||||||
return "", fmt.Errorf("no pods found in namespace %s", namespace)
|
// Handle CPU (cores)
|
||||||
|
if strings.HasSuffix(quantity, "m") {
|
||||||
|
val, _ := strconv.ParseInt(strings.TrimSuffix(quantity, "m"), 10, 64)
|
||||||
|
return val
|
||||||
}
|
}
|
||||||
return pods[0].Name, nil
|
|
||||||
|
// Handle memory (bytes)
|
||||||
|
multipliers := map[string]int64{
|
||||||
|
"Ki": 1024,
|
||||||
|
"Mi": 1024 * 1024,
|
||||||
|
"Gi": 1024 * 1024 * 1024,
|
||||||
|
"Ti": 1024 * 1024 * 1024 * 1024,
|
||||||
|
"K": 1000,
|
||||||
|
"M": 1000 * 1000,
|
||||||
|
"G": 1000 * 1000 * 1000,
|
||||||
|
"T": 1000 * 1000 * 1000 * 1000,
|
||||||
|
}
|
||||||
|
|
||||||
|
for suffix, mult := range multipliers {
|
||||||
|
if strings.HasSuffix(quantity, suffix) {
|
||||||
|
val, _ := strconv.ParseInt(strings.TrimSuffix(quantity, suffix), 10, 64)
|
||||||
|
return val * mult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plain number
|
||||||
|
val, _ := strconv.ParseInt(quantity, 10, 64)
|
||||||
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPodContainers returns container names for a pod
|
// formatCPU formats millicores to human-readable format
|
||||||
func (k *Kubectl) GetPodContainers(namespace, podName string) ([]string, error) {
|
func formatCPU(millicores int64) string {
|
||||||
args := []string{
|
if millicores == 0 {
|
||||||
"get", "pod", podName,
|
return "0"
|
||||||
"-n", namespace,
|
|
||||||
"-o", "jsonpath={.spec.containers[*].name}",
|
|
||||||
}
|
}
|
||||||
|
if millicores < 1000 {
|
||||||
if k.kubeconfigPath != "" {
|
return fmt.Sprintf("%dm", millicores)
|
||||||
args = append([]string{"--kubeconfig", k.kubeconfigPath}, args...)
|
|
||||||
}
|
}
|
||||||
|
return fmt.Sprintf("%.1f", float64(millicores)/1000.0)
|
||||||
cmd := exec.Command("kubectl", args...)
|
}
|
||||||
output, err := cmd.Output()
|
|
||||||
if err != nil {
|
// formatMemory formats bytes to human-readable format
|
||||||
return nil, fmt.Errorf("failed to get pod containers: %w", err)
|
func formatMemory(bytes int64) string {
|
||||||
}
|
if bytes == 0 {
|
||||||
|
return "0"
|
||||||
containerNames := strings.Fields(string(output))
|
}
|
||||||
return containerNames, nil
|
|
||||||
|
const unit = 1024
|
||||||
|
if bytes < unit {
|
||||||
|
return fmt.Sprintf("%dB", bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
div, exp := int64(unit), 0
|
||||||
|
for n := bytes / unit; n >= unit; n /= unit {
|
||||||
|
div *= unit
|
||||||
|
exp++
|
||||||
|
}
|
||||||
|
|
||||||
|
units := []string{"Ki", "Mi", "Gi", "Ti"}
|
||||||
|
return fmt.Sprintf("%.1f%s", float64(bytes)/float64(div), units[exp])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/wild-cloud/wild-central/daemon/internal/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HealthStatus represents cluster health information
|
// HealthStatus represents cluster health information
|
||||||
@@ -82,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)
|
||||||
@@ -127,15 +125,17 @@ func checkComponent(kubeconfigPath, namespace, selector string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetDashboardToken retrieves or creates a Kubernetes dashboard token
|
// GetDashboardToken retrieves or creates a Kubernetes dashboard token
|
||||||
func GetDashboardToken() (*DashboardToken, error) {
|
func GetDashboardToken(kubeconfigPath string) (*DashboardToken, error) {
|
||||||
// Check if service account exists
|
// Check if service account exists
|
||||||
cmd := exec.Command("kubectl", "get", "serviceaccount", "-n", "kubernetes-dashboard", "dashboard-admin")
|
cmd := exec.Command("kubectl", "get", "serviceaccount", "-n", "kubernetes-dashboard", "dashboard-admin")
|
||||||
|
tools.WithKubeconfig(cmd, kubeconfigPath)
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
return nil, fmt.Errorf("dashboard-admin service account not found")
|
return nil, fmt.Errorf("dashboard-admin service account not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create token
|
// Create token
|
||||||
cmd = exec.Command("kubectl", "-n", "kubernetes-dashboard", "create", "token", "dashboard-admin")
|
cmd = exec.Command("kubectl", "-n", "kubernetes-dashboard", "create", "token", "dashboard-admin")
|
||||||
|
tools.WithKubeconfig(cmd, kubeconfigPath)
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create token: %w", err)
|
return nil, fmt.Errorf("failed to create token: %w", err)
|
||||||
@@ -148,9 +148,10 @@ func GetDashboardToken() (*DashboardToken, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetDashboardTokenFromSecret retrieves dashboard token from secret (fallback method)
|
// 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",
|
cmd := exec.Command("kubectl", "-n", "kubernetes-dashboard", "get", "secret",
|
||||||
"dashboard-admin-token", "-o", "jsonpath={.data.token}")
|
"dashboard-admin-token", "-o", "jsonpath={.data.token}")
|
||||||
|
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 token secret: %w", err)
|
return nil, fmt.Errorf("failed to get token secret: %w", err)
|
||||||
@@ -167,8 +168,9 @@ func GetDashboardTokenFromSecret() (*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)
|
||||||
@@ -212,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)
|
||||||
@@ -229,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)
|
||||||
@@ -259,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))
|
||||||
@@ -268,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)
|
||||||
|
|||||||
Reference in New Issue
Block a user