Initial commit.
This commit is contained in:
186
cmd/app.go
Normal file
186
cmd/app.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// App commands
|
||||
var appCmd = &cobra.Command{
|
||||
Use: "app",
|
||||
Short: "Manage applications",
|
||||
}
|
||||
|
||||
var appListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List available apps",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
resp, err := apiClient.Get("/api/v1/apps")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
|
||||
apps := resp.GetArray("apps")
|
||||
if len(apps) == 0 {
|
||||
fmt.Println("No apps found")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("%-20s %-30s\n", "NAME", "DESCRIPTION")
|
||||
fmt.Println("-----------------------------------------------------")
|
||||
for _, app := range apps {
|
||||
if m, ok := app.(map[string]interface{}); ok {
|
||||
fmt.Printf("%-20s %-30s\n", m["name"], m["description"])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var appListDeployedCmd = &cobra.Command{
|
||||
Use: "list-deployed",
|
||||
Short: "List deployed apps",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := apiClient.Get(fmt.Sprintf("/api/v1/instances/%s/apps", inst))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
|
||||
apps := resp.GetArray("apps")
|
||||
if len(apps) == 0 {
|
||||
fmt.Println("No deployed apps found")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("%-20s %-12s\n", "NAME", "STATUS")
|
||||
fmt.Println("----------------------------------")
|
||||
for _, app := range apps {
|
||||
if m, ok := app.(map[string]interface{}); ok {
|
||||
fmt.Printf("%-20s %-12s\n", m["name"], m["status"])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var appAddCmd = &cobra.Command{
|
||||
Use: "add <app>",
|
||||
Short: "Add app to instance",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = apiClient.Post(fmt.Sprintf("/api/v1/instances/%s/apps", inst), map[string]string{
|
||||
"name": args[0],
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("App added: %s\n", args[0])
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var appDeployCmd = &cobra.Command{
|
||||
Use: "deploy <app>",
|
||||
Short: "Deploy an app",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := apiClient.Post(fmt.Sprintf("/api/v1/instances/%s/apps/%s/deploy", inst, args[0]), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("App deployment started: %s\n", args[0])
|
||||
if opID := resp.GetString("operation_id"); opID != "" {
|
||||
fmt.Printf("Operation ID: %s\n", opID)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var appDeleteCmd = &cobra.Command{
|
||||
Use: "delete <app>",
|
||||
Short: "Delete an app",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = apiClient.Delete(fmt.Sprintf("/api/v1/instances/%s/apps/%s", inst, args[0]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("App deleted: %s\n", args[0])
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var appStatusCmd = &cobra.Command{
|
||||
Use: "status <app>",
|
||||
Short: "Get app status",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := apiClient.Get(fmt.Sprintf("/api/v1/instances/%s/apps/%s/status", inst, args[0]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
|
||||
// Print status in text format
|
||||
fmt.Printf("App: %s\n", resp.GetString("name"))
|
||||
fmt.Printf("Status: %s\n", resp.GetString("status"))
|
||||
if version := resp.GetString("version"); version != "" {
|
||||
fmt.Printf("Version: %s\n", version)
|
||||
}
|
||||
fmt.Printf("Namespace: %s\n", resp.GetString("namespace"))
|
||||
if url := resp.GetString("url"); url != "" {
|
||||
fmt.Printf("URL: %s\n", url)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
appCmd.AddCommand(appListCmd)
|
||||
appCmd.AddCommand(appListDeployedCmd)
|
||||
appCmd.AddCommand(appAddCmd)
|
||||
appCmd.AddCommand(appDeployCmd)
|
||||
appCmd.AddCommand(appDeleteCmd)
|
||||
appCmd.AddCommand(appStatusCmd)
|
||||
}
|
||||
|
||||
55
cmd/backup.go
Normal file
55
cmd/backup.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Backup/Restore commands
|
||||
var backupCmd = &cobra.Command{
|
||||
Use: "backup <app>",
|
||||
Short: "Backup an app",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := apiClient.Post(fmt.Sprintf("/api/v1/instances/%s/apps/%s/backup", inst, args[0]), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Backup started: %s\n", args[0])
|
||||
if opID := resp.GetString("operation_id"); opID != "" {
|
||||
fmt.Printf("Operation ID: %s\n", opID)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var restoreCmd = &cobra.Command{
|
||||
Use: "restore <app>",
|
||||
Short: "Restore an app",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := apiClient.Post(fmt.Sprintf("/api/v1/instances/%s/apps/%s/restore", inst, args[0]), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Restore started: %s\n", args[0])
|
||||
if opID := resp.GetString("operation_id"); opID != "" {
|
||||
fmt.Printf("Operation ID: %s\n", opID)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
284
cmd/cluster.go
Normal file
284
cmd/cluster.go
Normal file
@@ -0,0 +1,284 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/wild-cloud/wild-central/wild/internal/config"
|
||||
)
|
||||
|
||||
// Cluster commands
|
||||
var clusterCmd = &cobra.Command{
|
||||
Use: "cluster",
|
||||
Short: "Manage cluster",
|
||||
}
|
||||
|
||||
var clusterBootstrapCmd = &cobra.Command{
|
||||
Use: "bootstrap <node>",
|
||||
Short: "Bootstrap cluster on a control plane node",
|
||||
Long: `Bootstrap the Kubernetes cluster by initializing etcd on a control plane node.
|
||||
|
||||
This should be run once after the first control plane node is configured.
|
||||
|
||||
Example:
|
||||
wild cluster bootstrap test-control-1`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nodeName := args[0]
|
||||
|
||||
resp, err := apiClient.Post(fmt.Sprintf("/api/v1/instances/%s/cluster/bootstrap", inst), map[string]string{
|
||||
"node": nodeName,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Cluster bootstrap started on node: %s\n", nodeName)
|
||||
if opID := resp.GetString("operation_id"); opID != "" {
|
||||
fmt.Printf("Operation ID: %s\n", opID)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var clusterStatusCmd = &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Get cluster status",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := apiClient.Get(fmt.Sprintf("/api/v1/instances/%s/cluster/status", inst))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
|
||||
return printYAML(resp.Data)
|
||||
},
|
||||
}
|
||||
|
||||
var clusterHealthCmd = &cobra.Command{
|
||||
Use: "health",
|
||||
Short: "Check cluster health",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := apiClient.Get(fmt.Sprintf("/api/v1/instances/%s/cluster/health", inst))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
|
||||
return printYAML(resp.GetMap("health"))
|
||||
},
|
||||
}
|
||||
|
||||
var clusterKubeconfigCmd = &cobra.Command{
|
||||
Use: "kubeconfig",
|
||||
Short: "Get or generate kubeconfig",
|
||||
Long: `Get the cluster kubeconfig or regenerate it from the cluster.
|
||||
|
||||
By default, retrieves the existing kubeconfig file. Use --generate to
|
||||
regenerate it from the cluster (useful if the file was lost or corrupted).
|
||||
|
||||
Examples:
|
||||
wild cluster kubeconfig # Get existing kubeconfig
|
||||
wild cluster kubeconfig --persist # Get and save locally
|
||||
wild cluster kubeconfig --generate # Regenerate from cluster
|
||||
wild cluster kubeconfig --generate --persist # Regenerate and save locally`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
persist, _ := cmd.Flags().GetBool("persist")
|
||||
generate, _ := cmd.Flags().GetBool("generate")
|
||||
|
||||
var kubeconfigContent string
|
||||
|
||||
// If --generate flag is set, trigger regeneration
|
||||
if generate {
|
||||
fmt.Println("Regenerating kubeconfig from cluster...")
|
||||
_, err := apiClient.Post(fmt.Sprintf("/api/v1/instances/%s/cluster/kubeconfig/generate", inst), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("Kubeconfig regenerated successfully")
|
||||
|
||||
// Now fetch the newly generated kubeconfig
|
||||
resp, err := apiClient.Get(fmt.Sprintf("/api/v1/instances/%s/cluster/kubeconfig", inst))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
kubeconfigContent = resp.GetString("kubeconfig")
|
||||
} else {
|
||||
// Get existing kubeconfig
|
||||
resp, err := apiClient.Get(fmt.Sprintf("/api/v1/instances/%s/cluster/kubeconfig", inst))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
kubeconfigContent = resp.GetString("kubeconfig")
|
||||
}
|
||||
|
||||
// If --persist flag is set, save to instance directory
|
||||
if persist {
|
||||
dataDir := config.GetWildCLIDataDir()
|
||||
instanceDir := fmt.Sprintf("%s/instances/%s", dataDir, inst)
|
||||
|
||||
// Create instance directory if it doesn't exist
|
||||
if err := os.MkdirAll(instanceDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create instance directory: %w", err)
|
||||
}
|
||||
|
||||
kubeconfigPath := fmt.Sprintf("%s/kubeconfig", instanceDir)
|
||||
if err := os.WriteFile(kubeconfigPath, []byte(kubeconfigContent), 0600); err != nil {
|
||||
return fmt.Errorf("failed to write kubeconfig: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Kubeconfig saved to %s\n", kubeconfigPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Default behavior: print to stdout
|
||||
fmt.Println(kubeconfigContent)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var clusterConfigGenerateCmd = &cobra.Command{
|
||||
Use: "config generate",
|
||||
Short: "Generate cluster configuration",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := apiClient.Post(fmt.Sprintf("/api/v1/instances/%s/cluster/config/generate", inst), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("Cluster configuration generated successfully")
|
||||
if msg := resp.GetString("message"); msg != "" {
|
||||
fmt.Println(msg)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var clusterTalosconfigCmd = &cobra.Command{
|
||||
Use: "talosconfig",
|
||||
Short: "Get talosconfig",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
persist, _ := cmd.Flags().GetBool("persist")
|
||||
|
||||
resp, err := apiClient.Get(fmt.Sprintf("/api/v1/instances/%s/cluster/talosconfig", inst))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
talosconfigContent := resp.GetString("talosconfig")
|
||||
|
||||
// If --persist flag is set, save to instance directory
|
||||
if persist {
|
||||
dataDir := config.GetWildCLIDataDir()
|
||||
instanceDir := fmt.Sprintf("%s/instances/%s", dataDir, inst)
|
||||
|
||||
// Create instance directory if it doesn't exist
|
||||
if err := os.MkdirAll(instanceDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create instance directory: %w", err)
|
||||
}
|
||||
|
||||
talosconfigPath := fmt.Sprintf("%s/talosconfig", instanceDir)
|
||||
if err := os.WriteFile(talosconfigPath, []byte(talosconfigContent), 0600); err != nil {
|
||||
return fmt.Errorf("failed to write talosconfig: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Talosconfig saved to %s\n", talosconfigPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Default behavior: print to stdout
|
||||
fmt.Println(talosconfigContent)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var clusterEndpointsCmd = &cobra.Command{
|
||||
Use: "endpoints",
|
||||
Short: "Configure cluster endpoints to use VIP",
|
||||
Long: `Configure talosconfig endpoints to use the control plane VIP.
|
||||
|
||||
Run this after all control plane nodes are added and running.
|
||||
This updates the talosconfig to use the VIP as the primary endpoint
|
||||
and retrieves the kubeconfig for cluster access.
|
||||
|
||||
Examples:
|
||||
# Configure endpoints to use VIP only
|
||||
wild cluster endpoints
|
||||
|
||||
# Include all control node IPs as fallback endpoints
|
||||
wild cluster endpoints --nodes`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
includeNodes, _ := cmd.Flags().GetBool("nodes")
|
||||
|
||||
_, err = apiClient.Post(fmt.Sprintf("/api/v1/instances/%s/cluster/endpoints", inst), map[string]bool{
|
||||
"include_nodes": includeNodes,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("✓ Endpoints configured to use control plane VIP")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
clusterCmd.AddCommand(clusterBootstrapCmd)
|
||||
clusterCmd.AddCommand(clusterStatusCmd)
|
||||
clusterCmd.AddCommand(clusterHealthCmd)
|
||||
clusterCmd.AddCommand(clusterKubeconfigCmd)
|
||||
clusterCmd.AddCommand(clusterConfigGenerateCmd)
|
||||
clusterCmd.AddCommand(clusterTalosconfigCmd)
|
||||
clusterCmd.AddCommand(clusterEndpointsCmd)
|
||||
|
||||
clusterEndpointsCmd.Flags().Bool("nodes", false, "Include all control node IPs as fallback endpoints")
|
||||
|
||||
// Add --persist flags to config commands
|
||||
clusterTalosconfigCmd.Flags().Bool("persist", false, "Save talosconfig to instance directory")
|
||||
clusterKubeconfigCmd.Flags().Bool("persist", false, "Save kubeconfig to instance directory")
|
||||
clusterKubeconfigCmd.Flags().Bool("generate", false, "Regenerate kubeconfig from the cluster")
|
||||
}
|
||||
|
||||
1
cmd/commands.go
Normal file
1
cmd/commands.go
Normal file
@@ -0,0 +1 @@
|
||||
package cmd
|
||||
93
cmd/config.go
Normal file
93
cmd/config.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/wild-cloud/wild-central/wild/internal/config"
|
||||
)
|
||||
|
||||
// Config commands
|
||||
var configCmd = &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "Manage instance configuration",
|
||||
}
|
||||
|
||||
var configGetCmd = &cobra.Command{
|
||||
Use: "get <key>",
|
||||
Short: "Get configuration value",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key := args[0]
|
||||
|
||||
resp, err := apiClient.Get(fmt.Sprintf("/api/v1/instances/%s/config", inst))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Use nested path lookup for dot notation (e.g., certManager.cloudflare.zoneId)
|
||||
val := config.GetValue(resp.Data, key)
|
||||
if val != nil {
|
||||
fmt.Println(val)
|
||||
} else {
|
||||
return fmt.Errorf("key '%s' not found", key)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var configSetCmd = &cobra.Command{
|
||||
Use: "set <key> <value>",
|
||||
Short: "Set configuration value",
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key, value := args[0], args[1]
|
||||
|
||||
_, err = apiClient.Put(fmt.Sprintf("/api/v1/instances/%s/config", inst), map[string]string{
|
||||
key: value,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Configuration updated: %s = %s\n", key, value)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var configShowCmd = &cobra.Command{
|
||||
Use: "show",
|
||||
Short: "Show full configuration",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := apiClient.Get(fmt.Sprintf("/api/v1/instances/%s/config", inst))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
|
||||
return printYAML(resp.Data)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
configCmd.AddCommand(configGetCmd)
|
||||
configCmd.AddCommand(configSetCmd)
|
||||
configCmd.AddCommand(configShowCmd)
|
||||
}
|
||||
67
cmd/daemon.go
Normal file
67
cmd/daemon.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var daemonCmd = &cobra.Command{
|
||||
Use: "daemon",
|
||||
Short: "Daemon operations",
|
||||
Long: `Check daemon status and perform daemon-related operations.`,
|
||||
}
|
||||
|
||||
var daemonStatusCmd = &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Check daemon status",
|
||||
Long: `Check if the Wild Central daemon is running and accessible.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Try to get status from daemon
|
||||
resp, err := apiClient.Get("/api/v1/status")
|
||||
if err != nil {
|
||||
return fmt.Errorf("daemon is not accessible: %w", err)
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
|
||||
if outputFormat == "yaml" {
|
||||
return printYAML(resp.Data)
|
||||
}
|
||||
|
||||
// Print status
|
||||
fmt.Println("✓ Daemon is running")
|
||||
fmt.Printf(" URL: %s\n", apiClient.BaseURL())
|
||||
|
||||
if version := resp.GetString("version"); version != "" {
|
||||
fmt.Printf(" Version: %s\n", version)
|
||||
}
|
||||
|
||||
if uptime := resp.GetString("uptime"); uptime != "" {
|
||||
fmt.Printf(" Uptime: %s\n", uptime)
|
||||
}
|
||||
|
||||
if dataDir := resp.GetString("dataDir"); dataDir != "" {
|
||||
fmt.Printf(" Data Directory: %s\n", dataDir)
|
||||
}
|
||||
|
||||
if directoryPath := resp.GetString("directoryPath"); directoryPath != "" {
|
||||
fmt.Printf(" Wild Directory: %s\n", directoryPath)
|
||||
}
|
||||
|
||||
// Show instance info
|
||||
if instances := resp.GetMap("instances"); instances != nil {
|
||||
if count, ok := instances["count"].(float64); ok {
|
||||
fmt.Printf(" Instances: %d\n", int(count))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
daemonCmd.AddCommand(daemonStatusCmd)
|
||||
}
|
||||
276
cmd/instance.go
Normal file
276
cmd/instance.go
Normal file
@@ -0,0 +1,276 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/wild-cloud/wild-central/wild/internal/config"
|
||||
)
|
||||
|
||||
var instanceCmd = &cobra.Command{
|
||||
Use: "instance",
|
||||
Short: "Manage instances",
|
||||
Long: `Create, list, and manage Wild Cloud instances.`,
|
||||
}
|
||||
|
||||
var instanceCreateCmd = &cobra.Command{
|
||||
Use: "create <name>",
|
||||
Short: "Create a new instance",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
|
||||
resp, err := apiClient.Post("/api/v1/instances", map[string]string{
|
||||
"name": name,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Instance '%s' created successfully\n", name)
|
||||
if msg, ok := resp.Data["message"].(string); ok && msg != "" {
|
||||
fmt.Println(msg)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var instanceListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all instances",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
resp, err := apiClient.Get("/api/v1/instances")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
|
||||
if outputFormat == "yaml" {
|
||||
return printYAML(resp.Data)
|
||||
}
|
||||
|
||||
instances := resp.GetArray("instances")
|
||||
if len(instances) == 0 {
|
||||
fmt.Println("No instances found")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Println("INSTANCES:")
|
||||
for _, inst := range instances {
|
||||
if name, ok := inst.(string); ok {
|
||||
fmt.Printf(" - %s\n", name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var instanceShowCmd = &cobra.Command{
|
||||
Use: "show <name>",
|
||||
Short: "Show instance details",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
|
||||
resp, err := apiClient.Get(fmt.Sprintf("/api/v1/instances/%s", name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
|
||||
if outputFormat == "yaml" {
|
||||
return printYAML(resp.Data)
|
||||
}
|
||||
|
||||
// Pretty print instance info
|
||||
instanceName := resp.GetString("name")
|
||||
if instanceName != "" {
|
||||
fmt.Printf("Instance: %s\n", instanceName)
|
||||
}
|
||||
|
||||
// Show key config values
|
||||
if config := resp.GetMap("config"); config != nil {
|
||||
// Check for cloud config (test-cloud structure)
|
||||
if cloud, ok := config["cloud"].(map[string]interface{}); ok {
|
||||
if domain, ok := cloud["domain"].(string); ok && domain != "" {
|
||||
fmt.Printf("Domain: %s\n", domain)
|
||||
}
|
||||
if baseDomain, ok := cloud["baseDomain"].(string); ok && baseDomain != "" {
|
||||
fmt.Printf("Base Domain: %s\n", baseDomain)
|
||||
}
|
||||
} else {
|
||||
// Check for direct config fields (test-cli structure)
|
||||
if domain, ok := config["domain"].(string); ok && domain != "" {
|
||||
fmt.Printf("Domain: %s\n", domain)
|
||||
}
|
||||
if baseDomain, ok := config["baseDomain"].(string); ok && baseDomain != "" {
|
||||
fmt.Printf("Base Domain: %s\n", baseDomain)
|
||||
}
|
||||
}
|
||||
|
||||
// Show cluster info if available
|
||||
if cluster, ok := config["cluster"].(map[string]interface{}); ok {
|
||||
if clusterName, ok := cluster["name"].(string); ok && clusterName != "" {
|
||||
fmt.Printf("Cluster Name: %s\n", clusterName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("\nUse -o json or -o yaml for full configuration")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var instanceDeleteCmd = &cobra.Command{
|
||||
Use: "delete <name>",
|
||||
Short: "Delete an instance",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
|
||||
// Confirm deletion
|
||||
fmt.Printf("Are you sure you want to delete instance '%s'? (yes/no): ", name)
|
||||
var confirm string
|
||||
fmt.Scanln(&confirm)
|
||||
if confirm != "yes" {
|
||||
fmt.Println("Deletion cancelled")
|
||||
return nil
|
||||
}
|
||||
|
||||
resp, err := apiClient.Delete(fmt.Sprintf("/api/v1/instances/%s", name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Instance '%s' deleted successfully\n", name)
|
||||
if msg, ok := resp.Data["message"].(string); ok && msg != "" {
|
||||
fmt.Println(msg)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var instanceCurrentCmd = &cobra.Command{
|
||||
Use: "current",
|
||||
Short: "Show current instance",
|
||||
Long: `Display the instance that would be used by commands.
|
||||
|
||||
Resolution order:
|
||||
1. --instance flag
|
||||
2. ~/.wildcloud/current_instance file
|
||||
3. Auto-select first available instance`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println(inst)
|
||||
},
|
||||
}
|
||||
|
||||
var instanceUseCmd = &cobra.Command{
|
||||
Use: "use <name>",
|
||||
Short: "Set the default instance",
|
||||
Long: `Set the default instance to use for all commands.
|
||||
|
||||
This persists the instance selection to ~/.wildcloud/current_instance.
|
||||
The instance can still be overridden with the --instance flag.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
instanceToSet := args[0]
|
||||
|
||||
// Validate instance exists by calling API
|
||||
resp, err := apiClient.Get(fmt.Sprintf("/api/v1/instances/%s", instanceToSet))
|
||||
if err != nil {
|
||||
return fmt.Errorf("instance '%s' not found: %w", instanceToSet, err)
|
||||
}
|
||||
|
||||
// Verify we got a valid response
|
||||
if name := resp.GetString("name"); name != instanceToSet {
|
||||
return fmt.Errorf("instance '%s' not found", instanceToSet)
|
||||
}
|
||||
|
||||
// Persist the selection
|
||||
if err := config.SetCurrentInstance(instanceToSet); err != nil {
|
||||
return fmt.Errorf("failed to set current instance: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Switched to instance: %s\n", instanceToSet)
|
||||
|
||||
// Check for config files and provide hint
|
||||
dataDir := config.GetWildCLIDataDir()
|
||||
instanceDir := filepath.Join(dataDir, "instances", instanceToSet)
|
||||
|
||||
var hasConfigs bool
|
||||
if _, err := os.Stat(filepath.Join(instanceDir, "talosconfig")); err == nil {
|
||||
hasConfigs = true
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(instanceDir, "kubeconfig")); err == nil {
|
||||
hasConfigs = true
|
||||
}
|
||||
|
||||
if hasConfigs {
|
||||
fmt.Println("\nTo configure your environment, run:")
|
||||
fmt.Println(" source <(wild instance env)")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var instanceEnvCmd = &cobra.Command{
|
||||
Use: "env",
|
||||
Short: "Output environment variables for current instance",
|
||||
Long: `Output export commands for TALOSCONFIG and KUBECONFIG.
|
||||
|
||||
Usage:
|
||||
source <(wild instance env)
|
||||
|
||||
This will set environment variables for the current instance's talosconfig and kubeconfig files if they exist.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Get current instance
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check for talosconfig and kubeconfig files
|
||||
dataDir := config.GetWildCLIDataDir()
|
||||
instanceDir := filepath.Join(dataDir, "instances", inst)
|
||||
|
||||
// Check for talosconfig
|
||||
talosconfigPath := filepath.Join(instanceDir, "talosconfig")
|
||||
if _, err := os.Stat(talosconfigPath); err == nil {
|
||||
fmt.Printf("export TALOSCONFIG=%s\n", talosconfigPath)
|
||||
}
|
||||
|
||||
// Check for kubeconfig
|
||||
kubeconfigPath := filepath.Join(instanceDir, "kubeconfig")
|
||||
if _, err := os.Stat(kubeconfigPath); err == nil {
|
||||
fmt.Printf("export KUBECONFIG=%s\n", kubeconfigPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
instanceCmd.AddCommand(instanceCreateCmd)
|
||||
instanceCmd.AddCommand(instanceListCmd)
|
||||
instanceCmd.AddCommand(instanceShowCmd)
|
||||
instanceCmd.AddCommand(instanceDeleteCmd)
|
||||
instanceCmd.AddCommand(instanceCurrentCmd)
|
||||
instanceCmd.AddCommand(instanceUseCmd)
|
||||
instanceCmd.AddCommand(instanceEnvCmd)
|
||||
}
|
||||
482
cmd/node.go
Normal file
482
cmd/node.go
Normal file
@@ -0,0 +1,482 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Node commands
|
||||
var nodeCmd = &cobra.Command{
|
||||
Use: "node",
|
||||
Short: "Manage nodes",
|
||||
}
|
||||
|
||||
var nodeDiscoverCmd = &cobra.Command{
|
||||
Use: "discover <ip>...",
|
||||
Short: "Discover nodes on network",
|
||||
Long: "Discover nodes on the network by scanning the provided IP addresses or ranges",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Starting discovery for %d IP(s)...\n", len(args))
|
||||
_, err = apiClient.Post(fmt.Sprintf("/api/v1/instances/%s/nodes/discover", inst), map[string]interface{}{
|
||||
"ip_list": args,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Poll for completion
|
||||
fmt.Println("Scanning nodes...")
|
||||
for {
|
||||
resp, err := apiClient.Get(fmt.Sprintf("/api/v1/instances/%s/discovery", inst))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
active, _ := resp.Data["active"].(bool)
|
||||
if !active {
|
||||
// Discovery complete
|
||||
nodesFound := resp.GetArray("nodes_found")
|
||||
if len(nodesFound) == 0 {
|
||||
fmt.Println("\nNo Talos nodes found")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("\nFound %d node(s):\n\n", len(nodesFound))
|
||||
fmt.Printf("%-15s %-12s %-10s\n", "IP", "INTERFACE", "VERSION")
|
||||
fmt.Println("-----------------------------------------------")
|
||||
for _, node := range nodesFound {
|
||||
if m, ok := node.(map[string]interface{}); ok {
|
||||
fmt.Printf("%-15s %-12s %-10s\n",
|
||||
m["ip"], m["interface"], m["version"])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Still running, wait a bit
|
||||
fmt.Print(".")
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var nodeDetectCmd = &cobra.Command{
|
||||
Use: "detect <ip>",
|
||||
Short: "Detect hardware on a single node",
|
||||
Long: `Detect hardware configuration on a single node in maintenance mode.
|
||||
|
||||
This queries the node for available network interfaces and disks, helping you
|
||||
decide which hardware to use when adding the node to the cluster.
|
||||
|
||||
Example:
|
||||
wild node detect 192.168.1.31
|
||||
|
||||
Output shows:
|
||||
- Available network interfaces
|
||||
- Available disks with sizes
|
||||
- Recommended selections`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nodeIP := args[0]
|
||||
|
||||
// Call API to detect hardware
|
||||
resp, err := apiClient.Post(fmt.Sprintf("/api/v1/instances/%s/nodes/detect", inst), map[string]string{
|
||||
"ip": nodeIP,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
|
||||
if outputFormat == "yaml" {
|
||||
return printYAML(resp.Data)
|
||||
}
|
||||
|
||||
// Text format - show hardware details
|
||||
fmt.Printf("Hardware detected for node at %s:\n\n", nodeIP)
|
||||
|
||||
if iface := resp.GetString("interface"); iface != "" {
|
||||
fmt.Printf("Interface: %s\n", iface)
|
||||
}
|
||||
|
||||
if disks := resp.GetArray("disks"); len(disks) > 0 {
|
||||
fmt.Printf("\nAvailable Disks:\n")
|
||||
for _, diskData := range disks {
|
||||
diskMap, ok := diskData.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
path, _ := diskMap["path"].(string)
|
||||
size, _ := diskMap["size"].(float64) // JSON numbers are float64
|
||||
|
||||
// Format size in GB/TB
|
||||
sizeGB := size / (1024 * 1024 * 1024)
|
||||
var sizeStr string
|
||||
if sizeGB >= 1000 {
|
||||
sizeStr = fmt.Sprintf("%.1f TB", sizeGB/1024)
|
||||
} else {
|
||||
sizeStr = fmt.Sprintf("%.1f GB", sizeGB)
|
||||
}
|
||||
|
||||
fmt.Printf(" - %s (%s)\n", path, sizeStr)
|
||||
}
|
||||
}
|
||||
|
||||
if selected := resp.GetString("selected_disk"); selected != "" {
|
||||
fmt.Printf("\nRecommended Disk: %s\n", selected)
|
||||
}
|
||||
|
||||
fmt.Printf("\nTo add this node:\n")
|
||||
fmt.Printf(" wild node add <hostname> <role> --current-ip %s --target-ip <target-ip> --disk <disk> --interface <interface>\n", nodeIP)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var nodeListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List configured nodes",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := apiClient.Get(fmt.Sprintf("/api/v1/instances/%s/nodes", inst))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
|
||||
if outputFormat == "yaml" {
|
||||
return printYAML(resp.Data)
|
||||
}
|
||||
|
||||
nodes := resp.GetArray("nodes")
|
||||
if len(nodes) == 0 {
|
||||
fmt.Println("No nodes found")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("%-20s %-12s %-15s\n", "HOSTNAME", "ROLE", "TARGET IP")
|
||||
fmt.Println("-----------------------------------------------------")
|
||||
for _, node := range nodes {
|
||||
if m, ok := node.(map[string]interface{}); ok {
|
||||
fmt.Printf("%-20s %-12s %-15s\n",
|
||||
m["hostname"], m["role"], m["target_ip"])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var nodeShowCmd = &cobra.Command{
|
||||
Use: "show <hostname>",
|
||||
Short: "Show node details",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := apiClient.Get(fmt.Sprintf("/api/v1/instances/%s/nodes/%s", inst, args[0]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
|
||||
if outputFormat == "yaml" {
|
||||
return printYAML(resp.Data)
|
||||
}
|
||||
|
||||
// Text format - show node details
|
||||
fmt.Printf("Hostname: %s\n", resp.GetString("hostname"))
|
||||
fmt.Printf("Role: %s\n", resp.GetString("role"))
|
||||
fmt.Printf("Target IP: %s\n", resp.GetString("target_ip"))
|
||||
fmt.Printf("Disk: %s\n", resp.GetString("disk"))
|
||||
fmt.Printf("Interface: %s\n", resp.GetString("interface"))
|
||||
fmt.Printf("Version: %s\n", resp.GetString("version"))
|
||||
fmt.Printf("Schematic ID: %s\n", resp.GetString("schematic_id"))
|
||||
fmt.Printf("Configured: %v\n", resp.Data["configured"])
|
||||
fmt.Printf("Deployed: %v\n", resp.Data["deployed"])
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var nodeAddCmd = &cobra.Command{
|
||||
Use: "add <hostname> <role>",
|
||||
Short: "Add a node to cluster configuration",
|
||||
Long: `Add a node to the cluster configuration with required hardware details.
|
||||
|
||||
Role must be either 'controlplane' or 'worker'.
|
||||
|
||||
The node configuration will be stored in the instance config and used during apply.
|
||||
|
||||
Examples:
|
||||
# Node in maintenance mode (PXE booted)
|
||||
wild node add control-1 controlplane --current-ip 192.168.1.100 --target-ip 192.168.1.31 --disk /dev/sda
|
||||
|
||||
# Node already applied (unusual, only if config was removed manually)
|
||||
wild node add worker-1 worker --target-ip 192.168.1.32 --disk /dev/nvme0n1`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get flags
|
||||
targetIP, _ := cmd.Flags().GetString("target-ip")
|
||||
currentIP, _ := cmd.Flags().GetString("current-ip")
|
||||
disk, _ := cmd.Flags().GetString("disk")
|
||||
iface, _ := cmd.Flags().GetString("interface")
|
||||
schematicID, _ := cmd.Flags().GetString("schematic-id")
|
||||
maintenance, _ := cmd.Flags().GetBool("maintenance")
|
||||
|
||||
// Build request body
|
||||
body := map[string]interface{}{
|
||||
"hostname": args[0],
|
||||
"role": args[1],
|
||||
}
|
||||
|
||||
if targetIP != "" {
|
||||
body["target_ip"] = targetIP
|
||||
}
|
||||
if currentIP != "" {
|
||||
body["current_ip"] = currentIP
|
||||
}
|
||||
if disk != "" {
|
||||
body["disk"] = disk
|
||||
}
|
||||
if iface != "" {
|
||||
body["interface"] = iface
|
||||
}
|
||||
if schematicID != "" {
|
||||
body["schematic_id"] = schematicID
|
||||
}
|
||||
if maintenance {
|
||||
body["maintenance"] = true
|
||||
}
|
||||
|
||||
_, err = apiClient.Post(fmt.Sprintf("/api/v1/instances/%s/nodes", inst), body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Node added: %s (%s)\n", args[0], args[1])
|
||||
if targetIP != "" {
|
||||
fmt.Printf(" Target IP: %s\n", targetIP)
|
||||
}
|
||||
if disk != "" {
|
||||
fmt.Printf(" Disk: %s\n", disk)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var nodeApplyCmd = &cobra.Command{
|
||||
Use: "apply <hostname>",
|
||||
Short: "Apply Talos configuration to node",
|
||||
Long: `Generate and apply Talos configuration to a node.
|
||||
|
||||
This command:
|
||||
1. Auto-fetches patch templates if missing
|
||||
2. Generates node-specific configuration from templates
|
||||
3. Merges base config with node patch
|
||||
4. Applies configuration to node (using --insecure if in maintenance mode)
|
||||
5. Updates node state after successful application
|
||||
|
||||
Examples:
|
||||
# Apply to node in maintenance mode (PXE booted)
|
||||
wild node apply control-1
|
||||
|
||||
# Re-apply to production node (updates configuration)
|
||||
wild node apply worker-1`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := apiClient.Post(fmt.Sprintf("/api/v1/instances/%s/nodes/%s/apply", inst, args[0]), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Node configuration applied: %s\n", args[0])
|
||||
if msg := resp.GetString("message"); msg != "" {
|
||||
fmt.Printf("%s\n", msg)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var nodeUpdateCmd = &cobra.Command{
|
||||
Use: "update <hostname>",
|
||||
Short: "Update node configuration",
|
||||
Long: `Update existing node configuration with partial updates.
|
||||
|
||||
This command modifies node properties without requiring all fields.
|
||||
|
||||
Examples:
|
||||
# Update disk after hardware change
|
||||
wild node update worker-1 --disk /dev/sdb
|
||||
|
||||
# Move node to maintenance mode
|
||||
wild node update control-1 --current-ip 192.168.1.100 --maintenance
|
||||
|
||||
# Clear maintenance after successful apply
|
||||
wild node update control-1 --no-maintenance`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get flags
|
||||
targetIP, _ := cmd.Flags().GetString("target-ip")
|
||||
currentIP, _ := cmd.Flags().GetString("current-ip")
|
||||
disk, _ := cmd.Flags().GetString("disk")
|
||||
iface, _ := cmd.Flags().GetString("interface")
|
||||
schematicID, _ := cmd.Flags().GetString("schematic-id")
|
||||
maintenance, _ := cmd.Flags().GetBool("maintenance")
|
||||
noMaintenance, _ := cmd.Flags().GetBool("no-maintenance")
|
||||
|
||||
// Build request body with only provided fields
|
||||
body := map[string]interface{}{}
|
||||
|
||||
if targetIP != "" {
|
||||
body["target_ip"] = targetIP
|
||||
}
|
||||
if currentIP != "" {
|
||||
body["current_ip"] = currentIP
|
||||
}
|
||||
if disk != "" {
|
||||
body["disk"] = disk
|
||||
}
|
||||
if iface != "" {
|
||||
body["interface"] = iface
|
||||
}
|
||||
if schematicID != "" {
|
||||
body["schematic_id"] = schematicID
|
||||
}
|
||||
if maintenance {
|
||||
body["maintenance"] = true
|
||||
}
|
||||
if noMaintenance {
|
||||
body["maintenance"] = false
|
||||
}
|
||||
|
||||
if len(body) == 0 {
|
||||
return fmt.Errorf("no updates specified")
|
||||
}
|
||||
|
||||
_, err = apiClient.Put(fmt.Sprintf("/api/v1/instances/%s/nodes/%s", inst, args[0]), body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Node updated: %s\n", args[0])
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var nodeFetchTemplatesCmd = &cobra.Command{
|
||||
Use: "fetch-patch-templates",
|
||||
Short: "Fetch patch templates from directory",
|
||||
Long: `Copy latest patch templates from directory/setup/cluster-nodes/patch.templates to instance.
|
||||
|
||||
This command is automatically called by 'apply' if templates are missing.
|
||||
You can use it manually to update templates.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = apiClient.Post(fmt.Sprintf("/api/v1/instances/%s/nodes/fetch-templates", inst), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("Templates fetched successfully")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var nodeDeleteCmd = &cobra.Command{
|
||||
Use: "delete <hostname>",
|
||||
Short: "Delete a node",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = apiClient.Delete(fmt.Sprintf("/api/v1/instances/%s/nodes/%s", inst, args[0]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Node deleted: %s\n", args[0])
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
nodeCmd.AddCommand(nodeDiscoverCmd)
|
||||
nodeCmd.AddCommand(nodeDetectCmd)
|
||||
nodeCmd.AddCommand(nodeListCmd)
|
||||
nodeCmd.AddCommand(nodeShowCmd)
|
||||
nodeCmd.AddCommand(nodeAddCmd)
|
||||
nodeCmd.AddCommand(nodeApplyCmd)
|
||||
nodeCmd.AddCommand(nodeUpdateCmd)
|
||||
nodeCmd.AddCommand(nodeFetchTemplatesCmd)
|
||||
nodeCmd.AddCommand(nodeDeleteCmd)
|
||||
|
||||
// Add flags to node add command
|
||||
nodeAddCmd.Flags().String("target-ip", "", "Target IP address for production")
|
||||
nodeAddCmd.Flags().String("current-ip", "", "Current IP address (for maintenance mode)")
|
||||
nodeAddCmd.Flags().String("disk", "", "Disk device (required, e.g., /dev/sda)")
|
||||
nodeAddCmd.Flags().String("interface", "", "Network interface (optional, e.g., eth0)")
|
||||
nodeAddCmd.Flags().String("schematic-id", "", "Talos schematic ID (optional, uses instance default)")
|
||||
nodeAddCmd.Flags().Bool("maintenance", false, "Mark node as in maintenance mode")
|
||||
|
||||
// Add flags to node update command
|
||||
nodeUpdateCmd.Flags().String("target-ip", "", "Update target IP address")
|
||||
nodeUpdateCmd.Flags().String("current-ip", "", "Update current IP address")
|
||||
nodeUpdateCmd.Flags().String("disk", "", "Update disk device")
|
||||
nodeUpdateCmd.Flags().String("interface", "", "Update network interface")
|
||||
nodeUpdateCmd.Flags().String("schematic-id", "", "Update Talos schematic ID")
|
||||
nodeUpdateCmd.Flags().Bool("maintenance", false, "Set maintenance mode")
|
||||
nodeUpdateCmd.Flags().Bool("no-maintenance", false, "Clear maintenance mode")
|
||||
}
|
||||
|
||||
142
cmd/operation.go
Normal file
142
cmd/operation.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/r3labs/sse/v2"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/wild-cloud/wild-central/wild/internal/config"
|
||||
)
|
||||
|
||||
// Operation commands
|
||||
var operationCmd = &cobra.Command{
|
||||
Use: "operation",
|
||||
Short: "Manage operations",
|
||||
}
|
||||
|
||||
var operationGetCmd = &cobra.Command{
|
||||
Use: "get <id>",
|
||||
Short: "Get operation status",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := apiClient.Get(fmt.Sprintf("/api/v1/operations/%s?instance=%s", args[0], inst))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
|
||||
return printYAML(resp.Data)
|
||||
},
|
||||
}
|
||||
|
||||
var operationListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List operations",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
resp, err := apiClient.Get("/api/v1/operations")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
|
||||
ops := resp.GetArray("operations")
|
||||
if len(ops) == 0 {
|
||||
fmt.Println("No operations found")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("%-10s %-12s %-12s %-10s\n", "ID", "TYPE", "STATUS", "PROGRESS")
|
||||
fmt.Println("--------------------------------------------------------")
|
||||
for _, op := range ops {
|
||||
if m, ok := op.(map[string]interface{}); ok {
|
||||
fmt.Printf("%-10s %-12s %-12s %d%%\n",
|
||||
m["id"], m["type"], m["status"], int(m["progress"].(float64)))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
operationCmd.AddCommand(operationGetCmd)
|
||||
operationCmd.AddCommand(operationListCmd)
|
||||
}
|
||||
|
||||
// streamOperationOutput streams operation output via SSE
|
||||
func streamOperationOutput(opID string) error {
|
||||
// Get instance name
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get base URL
|
||||
baseURL := daemonURL
|
||||
if baseURL == "" {
|
||||
baseURL = config.GetDaemonURL()
|
||||
}
|
||||
|
||||
// Connect to SSE stream
|
||||
url := fmt.Sprintf("%s/api/v1/operations/%s/stream?instance=%s", baseURL, opID, inst)
|
||||
client := sse.NewClient(url)
|
||||
events := make(chan *sse.Event)
|
||||
|
||||
err = client.SubscribeChan("messages", events)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to subscribe to SSE: %w", err)
|
||||
}
|
||||
|
||||
// Poll for completion in background
|
||||
done := make(chan bool, 1)
|
||||
go func() {
|
||||
ticker := time.NewTicker(500 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
resp, err := apiClient.Get(fmt.Sprintf("/api/v1/operations/%s?instance=%s", opID, inst))
|
||||
if err == nil {
|
||||
status := resp.GetString("status")
|
||||
if status == "completed" || status == "failed" {
|
||||
time.Sleep(500 * time.Millisecond) // Give SSE time to flush
|
||||
done <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Stream events
|
||||
for {
|
||||
select {
|
||||
case msg, ok := <-events:
|
||||
if !ok {
|
||||
// Channel closed
|
||||
return nil
|
||||
}
|
||||
if msg != nil {
|
||||
// Check for completion event
|
||||
if string(msg.Event) == "complete" {
|
||||
return nil
|
||||
}
|
||||
// Print data with newline
|
||||
if msg.Data != nil {
|
||||
fmt.Println(string(msg.Data))
|
||||
}
|
||||
}
|
||||
case <-done:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
78
cmd/pxe.go
Normal file
78
cmd/pxe.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// PXE commands
|
||||
var pxeCmd = &cobra.Command{
|
||||
Use: "pxe",
|
||||
Short: "Manage PXE assets",
|
||||
}
|
||||
|
||||
var pxeListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List PXE assets",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := apiClient.Get(fmt.Sprintf("/api/v1/instances/%s/pxe/assets", inst))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
|
||||
assets := resp.GetArray("assets")
|
||||
if len(assets) == 0 {
|
||||
fmt.Println("No PXE assets found")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("%-20s %-15s %-12s\n", "NAME", "VERSION", "STATUS")
|
||||
fmt.Println("--------------------------------------------------")
|
||||
for _, asset := range assets {
|
||||
if m, ok := asset.(map[string]interface{}); ok {
|
||||
fmt.Printf("%-20s %-15s %-12s\n",
|
||||
m["name"], m["version"], m["status"])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var pxeDownloadCmd = &cobra.Command{
|
||||
Use: "download <asset>",
|
||||
Short: "Download PXE asset",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := apiClient.Post(fmt.Sprintf("/api/v1/instances/%s/pxe/assets/%s/download", inst, args[0]), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Download started for: %s\n", args[0])
|
||||
if opID := resp.GetString("operation_id"); opID != "" {
|
||||
fmt.Printf("Operation ID: %s\n", opID)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
pxeCmd.AddCommand(pxeListCmd)
|
||||
pxeCmd.AddCommand(pxeDownloadCmd)
|
||||
}
|
||||
|
||||
132
cmd/root.go
Normal file
132
cmd/root.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/wild-cloud/wild-central/wild/internal/client"
|
||||
"github.com/wild-cloud/wild-central/wild/internal/config"
|
||||
)
|
||||
|
||||
var (
|
||||
apiClient *client.Client
|
||||
|
||||
// Global flags
|
||||
daemonURL string
|
||||
instanceName string
|
||||
outputFormat string
|
||||
)
|
||||
|
||||
// rootCmd represents the base command
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "wild",
|
||||
Short: "Wild Cloud CLI",
|
||||
Long: `wild-cli is the command-line interface for Wild Central.
|
||||
|
||||
It provides a simple way to manage Wild Cloud instances, nodes, clusters,
|
||||
services, and applications through the Wild Central daemon.`,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Skip for commands that don't need API client
|
||||
if cmd.Name() == "version" || cmd.Name() == "help" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get daemon URL: flag > env > default
|
||||
url := daemonURL
|
||||
if url == "" {
|
||||
url = config.GetDaemonURL()
|
||||
}
|
||||
|
||||
// Create API client
|
||||
apiClient = client.NewClient(url)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Execute runs the root command
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Global flags
|
||||
rootCmd.PersistentFlags().StringVar(&daemonURL, "daemon-url", "", "Daemon URL (default: $WILD_DAEMON_URL or http://localhost:5055)")
|
||||
rootCmd.PersistentFlags().StringVar(&instanceName, "instance", "", "Instance name (overrides current instance)")
|
||||
rootCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", "text", "Output format (text, json, yaml)")
|
||||
|
||||
// Add subcommands
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
rootCmd.AddCommand(daemonCmd)
|
||||
rootCmd.AddCommand(instanceCmd)
|
||||
rootCmd.AddCommand(configCmd)
|
||||
rootCmd.AddCommand(secretCmd)
|
||||
rootCmd.AddCommand(nodeCmd)
|
||||
rootCmd.AddCommand(pxeCmd)
|
||||
rootCmd.AddCommand(clusterCmd)
|
||||
rootCmd.AddCommand(serviceCmd)
|
||||
rootCmd.AddCommand(appCmd)
|
||||
rootCmd.AddCommand(backupCmd)
|
||||
rootCmd.AddCommand(restoreCmd)
|
||||
rootCmd.AddCommand(healthCmd)
|
||||
rootCmd.AddCommand(dashboardCmd)
|
||||
rootCmd.AddCommand(nodeIPCmd)
|
||||
rootCmd.AddCommand(operationCmd)
|
||||
}
|
||||
|
||||
// getInstanceName returns the current instance name using the priority cascade
|
||||
func getInstanceName() (string, error) {
|
||||
// Create instance lister adapter for API client
|
||||
var lister config.InstanceLister
|
||||
if apiClient != nil {
|
||||
lister = &instanceListerAdapter{client: apiClient}
|
||||
}
|
||||
|
||||
instance, _, err := config.GetCurrentInstance(instanceName, lister)
|
||||
return instance, err
|
||||
}
|
||||
|
||||
// instanceListerAdapter adapts the API client to the InstanceLister interface
|
||||
type instanceListerAdapter struct {
|
||||
client *client.Client
|
||||
}
|
||||
|
||||
func (a *instanceListerAdapter) ListInstances() ([]string, error) {
|
||||
resp, err := a.client.Get("/api/v1/instances")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
instances := resp.GetArray("instances")
|
||||
result := make([]string, 0, len(instances))
|
||||
for _, inst := range instances {
|
||||
if name, ok := inst.(string); ok {
|
||||
result = append(result, name)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// printJSON prints data as JSON
|
||||
func printJSON(data interface{}) error {
|
||||
encoder := json.NewEncoder(os.Stdout)
|
||||
encoder.SetIndent("", " ")
|
||||
return encoder.Encode(data)
|
||||
}
|
||||
|
||||
// printYAML prints data as YAML
|
||||
func printYAML(data interface{}) error {
|
||||
yamlData, err := yaml.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(yamlData))
|
||||
return nil
|
||||
}
|
||||
72
cmd/secret.go
Normal file
72
cmd/secret.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/wild-cloud/wild-central/wild/internal/config"
|
||||
)
|
||||
|
||||
// Secret commands
|
||||
var secretCmd = &cobra.Command{
|
||||
Use: "secret",
|
||||
Short: "Manage secrets",
|
||||
}
|
||||
|
||||
var secretGetCmd = &cobra.Command{
|
||||
Use: "get <key>",
|
||||
Short: "Get secret value (redacted)",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key := args[0]
|
||||
|
||||
// Request raw secrets (not redacted)
|
||||
resp, err := apiClient.Get(fmt.Sprintf("/api/v1/instances/%s/secrets?raw=true", inst))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Secrets are returned directly at top level, not nested under "secrets" key
|
||||
// Use nested path lookup for dot notation (e.g., cloudflare.token)
|
||||
val := config.GetValue(resp.Data, key)
|
||||
if val != nil {
|
||||
fmt.Println(val)
|
||||
} else {
|
||||
return fmt.Errorf("secret '%s' not found", key)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var secretSetCmd = &cobra.Command{
|
||||
Use: "set <key> <value>",
|
||||
Short: "Set secret value",
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key, value := args[0], args[1]
|
||||
|
||||
_, err = apiClient.Put(fmt.Sprintf("/api/v1/instances/%s/secrets", inst), map[string]string{
|
||||
key: value,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Secret updated: %s\n", key)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
secretCmd.AddCommand(secretGetCmd)
|
||||
secretCmd.AddCommand(secretSetCmd)
|
||||
}
|
||||
325
cmd/service.go
Normal file
325
cmd/service.go
Normal file
@@ -0,0 +1,325 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/wild-cloud/wild-central/wild/internal/config"
|
||||
"github.com/wild-cloud/wild-central/wild/internal/prompt"
|
||||
)
|
||||
|
||||
// Service commands
|
||||
var serviceCmd = &cobra.Command{
|
||||
Use: "service",
|
||||
Short: "Manage services",
|
||||
}
|
||||
|
||||
var serviceListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List services",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := apiClient.Get(fmt.Sprintf("/api/v1/instances/%s/services", inst))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
|
||||
services := resp.GetArray("services")
|
||||
if len(services) == 0 {
|
||||
fmt.Println("No services found")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("%-20s %-12s\n", "NAME", "STATUS")
|
||||
fmt.Println("----------------------------------")
|
||||
for _, svc := range services {
|
||||
if m, ok := svc.(map[string]interface{}); ok {
|
||||
fmt.Printf("%-20s %-12s\n", m["name"], m["status"])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// ServiceManifest matches the daemon's ServiceManifest structure
|
||||
type ServiceManifest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Namespace string `json:"namespace"`
|
||||
ConfigReferences []string `json:"configReferences"`
|
||||
ServiceConfig map[string]ConfigDefinition `json:"serviceConfig"`
|
||||
}
|
||||
|
||||
// ConfigDefinition defines config that should be prompted during service setup
|
||||
type ConfigDefinition struct {
|
||||
Path string `json:"path"`
|
||||
Prompt string `json:"prompt"`
|
||||
Default string `json:"default"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// ConfigUpdate represents a single configuration update
|
||||
type ConfigUpdate struct {
|
||||
Path string `json:"path"`
|
||||
Value interface{} `json:"value"`
|
||||
}
|
||||
|
||||
var (
|
||||
fetchFlag bool
|
||||
noDeployFlag bool
|
||||
)
|
||||
|
||||
var serviceInstallCmd = &cobra.Command{
|
||||
Use: "install <service>",
|
||||
Short: "Install a service with interactive configuration",
|
||||
Long: `Install and configure a cluster service.
|
||||
|
||||
This command orchestrates the complete service installation lifecycle:
|
||||
1. Fetch service files from Wild Cloud Directory (if needed or --fetch)
|
||||
2. Validate configuration requirements
|
||||
3. Prompt for any missing service configuration
|
||||
4. Update instance configuration
|
||||
5. Compile templates using gomplate
|
||||
6. Deploy service to cluster (unless --no-deploy)
|
||||
|
||||
Examples:
|
||||
# Configure and deploy (most common)
|
||||
wild service install metallb
|
||||
|
||||
# Fetch fresh templates and deploy
|
||||
wild service install metallb --fetch
|
||||
|
||||
# Configure only, skip deployment
|
||||
wild service install metallb --no-deploy
|
||||
|
||||
# Fetch fresh templates, configure only
|
||||
wild service install metallb --fetch --no-deploy
|
||||
|
||||
# Use cached templates (default if files exist)
|
||||
wild service install traefik
|
||||
`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
serviceName := args[0]
|
||||
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Installing service: %s\n", serviceName)
|
||||
|
||||
// Step 1: Fetch service manifest
|
||||
fmt.Println("\nFetching service manifest...")
|
||||
manifestResp, err := apiClient.Get(fmt.Sprintf("/api/v1/services/%s/manifest", serviceName))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch manifest: %w", err)
|
||||
}
|
||||
|
||||
// Parse manifest
|
||||
var manifest ServiceManifest
|
||||
manifestData := manifestResp.Data
|
||||
// API returns camelCase field names
|
||||
if name, ok := manifestData["name"].(string); ok {
|
||||
manifest.Name = name
|
||||
}
|
||||
if desc, ok := manifestData["description"].(string); ok {
|
||||
manifest.Description = desc
|
||||
}
|
||||
if namespace, ok := manifestData["namespace"].(string); ok {
|
||||
manifest.Namespace = namespace
|
||||
}
|
||||
if refs, ok := manifestData["configReferences"].([]interface{}); ok {
|
||||
manifest.ConfigReferences = make([]string, len(refs))
|
||||
for i, ref := range refs {
|
||||
if s, ok := ref.(string); ok {
|
||||
manifest.ConfigReferences[i] = s
|
||||
}
|
||||
}
|
||||
}
|
||||
if svcConfig, ok := manifestData["serviceConfig"].(map[string]interface{}); ok {
|
||||
manifest.ServiceConfig = make(map[string]ConfigDefinition)
|
||||
for key, val := range svcConfig {
|
||||
if cfgMap, ok := val.(map[string]interface{}); ok {
|
||||
cfg := ConfigDefinition{}
|
||||
if path, ok := cfgMap["path"].(string); ok {
|
||||
cfg.Path = path
|
||||
}
|
||||
if prompt, ok := cfgMap["prompt"].(string); ok {
|
||||
cfg.Prompt = prompt
|
||||
}
|
||||
if def, ok := cfgMap["default"].(string); ok {
|
||||
cfg.Default = def
|
||||
}
|
||||
if typ, ok := cfgMap["type"].(string); ok {
|
||||
cfg.Type = typ
|
||||
}
|
||||
manifest.ServiceConfig[key] = cfg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("Service: %s - %s\n", manifest.Name, manifest.Description)
|
||||
|
||||
// Step 2: Fetch current config
|
||||
fmt.Println("\nFetching instance configuration...")
|
||||
configResp, err := apiClient.Get(fmt.Sprintf("/api/v1/instances/%s/config", inst))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch config: %w", err)
|
||||
}
|
||||
|
||||
instanceConfig := configResp.Data
|
||||
|
||||
// Step 3: Validate configReferences
|
||||
if len(manifest.ConfigReferences) > 0 {
|
||||
fmt.Println("\nValidating configuration references...")
|
||||
missingPaths := config.ValidatePaths(instanceConfig, manifest.ConfigReferences)
|
||||
if len(missingPaths) > 0 {
|
||||
fmt.Println("\nERROR: Missing required configuration values:")
|
||||
for _, path := range missingPaths {
|
||||
fmt.Printf(" - %s\n", path)
|
||||
}
|
||||
fmt.Println("\nPlease set these configuration values before installing this service.")
|
||||
fmt.Printf("Use: wild config set <key> <value>\n")
|
||||
return fmt.Errorf("missing required configuration")
|
||||
}
|
||||
fmt.Println("All required configuration references are present.")
|
||||
}
|
||||
|
||||
// Step 4: Process serviceConfig - prompt for missing values
|
||||
var updates []ConfigUpdate
|
||||
|
||||
if len(manifest.ServiceConfig) > 0 {
|
||||
fmt.Println("\nConfiguring service parameters...")
|
||||
|
||||
for key, cfg := range manifest.ServiceConfig {
|
||||
// Check if path already set
|
||||
existingValue := config.GetValue(instanceConfig, cfg.Path)
|
||||
if existingValue != nil && existingValue != "" && existingValue != "null" {
|
||||
fmt.Printf(" %s: %v (already set)\n", cfg.Path, existingValue)
|
||||
continue
|
||||
}
|
||||
|
||||
// Expand default template
|
||||
defaultValue := cfg.Default
|
||||
if defaultValue != "" {
|
||||
expanded, err := config.ExpandTemplate(defaultValue, instanceConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to expand template for %s: %w", key, err)
|
||||
}
|
||||
defaultValue = expanded
|
||||
}
|
||||
|
||||
// Prompt user
|
||||
var value string
|
||||
switch cfg.Type {
|
||||
case "int":
|
||||
intVal, err := prompt.Int(cfg.Prompt, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read input for %s: %w", key, err)
|
||||
}
|
||||
value = fmt.Sprintf("%d", intVal)
|
||||
case "bool":
|
||||
boolVal, err := prompt.Bool(cfg.Prompt, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read input for %s: %w", key, err)
|
||||
}
|
||||
if boolVal {
|
||||
value = "true"
|
||||
} else {
|
||||
value = "false"
|
||||
}
|
||||
default: // string
|
||||
var err error
|
||||
value, err = prompt.String(cfg.Prompt, defaultValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read input for %s: %w", key, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Add to updates
|
||||
updates = append(updates, ConfigUpdate{
|
||||
Path: cfg.Path,
|
||||
Value: value,
|
||||
})
|
||||
|
||||
fmt.Printf(" %s: %s\n", cfg.Path, value)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: Update configuration if needed
|
||||
if len(updates) > 0 {
|
||||
fmt.Println("\nUpdating instance configuration...")
|
||||
_, err = apiClient.Patch(
|
||||
fmt.Sprintf("/api/v1/instances/%s/config", inst),
|
||||
map[string]interface{}{
|
||||
"updates": updates,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update configuration: %w", err)
|
||||
}
|
||||
fmt.Printf("Configuration updated (%d values)\n", len(updates))
|
||||
}
|
||||
|
||||
// Step 6: Install service with lifecycle control
|
||||
if noDeployFlag {
|
||||
fmt.Println("\nConfiguring service...")
|
||||
} else {
|
||||
fmt.Println("\nInstalling service...")
|
||||
}
|
||||
|
||||
installResp, err := apiClient.Post(
|
||||
fmt.Sprintf("/api/v1/instances/%s/services", inst),
|
||||
map[string]interface{}{
|
||||
"name": serviceName,
|
||||
"fetch": fetchFlag,
|
||||
"deploy": !noDeployFlag,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to install service: %w", err)
|
||||
}
|
||||
|
||||
// Show appropriate success message
|
||||
if noDeployFlag {
|
||||
fmt.Printf("\n✓ Service configured: %s\n", serviceName)
|
||||
fmt.Printf(" Templates compiled and ready to deploy\n")
|
||||
fmt.Printf(" To deploy later, run: wild service install %s\n", serviceName)
|
||||
} else {
|
||||
// Stream installation output
|
||||
opID := installResp.GetString("operation_id")
|
||||
if opID != "" {
|
||||
fmt.Printf("Installing service: %s\n\n", serviceName)
|
||||
if err := streamOperationOutput(opID); err != nil {
|
||||
// If streaming fails, show operation ID for manual monitoring
|
||||
fmt.Printf("\nCouldn't stream output: %v\n", err)
|
||||
fmt.Printf("Operation ID: %s\n", opID)
|
||||
fmt.Printf("Monitor with: wild operation get %s\n", opID)
|
||||
} else {
|
||||
fmt.Printf("\n✓ Service installed successfully: %s\n", serviceName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
serviceInstallCmd.Flags().BoolVar(&fetchFlag, "fetch", false, "Fetch fresh templates from directory before installing")
|
||||
serviceInstallCmd.Flags().BoolVar(&noDeployFlag, "no-deploy", false, "Configure and compile only, skip deployment")
|
||||
|
||||
serviceCmd.AddCommand(serviceListCmd)
|
||||
serviceCmd.AddCommand(serviceInstallCmd)
|
||||
}
|
||||
|
||||
68
cmd/utility.go
Normal file
68
cmd/utility.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Utility commands
|
||||
var healthCmd = &cobra.Command{
|
||||
Use: "health",
|
||||
Short: "Check cluster health",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := apiClient.Get(fmt.Sprintf("/api/v1/instances/%s/utilities/health", inst))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
|
||||
return printYAML(resp.Data)
|
||||
},
|
||||
}
|
||||
|
||||
var dashboardCmd = &cobra.Command{
|
||||
Use: "dashboard",
|
||||
Short: "Dashboard operations",
|
||||
}
|
||||
|
||||
var dashboardTokenCmd = &cobra.Command{
|
||||
Use: "token",
|
||||
Short: "Get dashboard token",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
resp, err := apiClient.Get("/api/v1/utilities/dashboard/token")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(resp.GetString("token"))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
dashboardCmd.AddCommand(dashboardTokenCmd)
|
||||
}
|
||||
|
||||
var nodeIPCmd = &cobra.Command{
|
||||
Use: "node-ip",
|
||||
Short: "Get control plane IP",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
resp, err := apiClient.Get("/api/v1/utilities/controlplane/ip")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(resp.GetString("ip"))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
38
cmd/version.go
Normal file
38
cmd/version.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
// Version information set during build
|
||||
Version = "dev"
|
||||
GitCommit = "unknown"
|
||||
BuildTime = "unknown"
|
||||
)
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Show version information",
|
||||
Long: `Display version information for the CLI and optionally for the cluster.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("wild-cli version: %s\n", Version)
|
||||
fmt.Printf("Git commit: %s\n", GitCommit)
|
||||
fmt.Printf("Build time: %s\n", BuildTime)
|
||||
|
||||
// If connected to daemon, show cluster versions
|
||||
if apiClient != nil {
|
||||
resp, err := apiClient.Get("/api/v1/utilities/version")
|
||||
if err == nil {
|
||||
if k8s, ok := resp.Data["kubernetes"].(string); ok {
|
||||
fmt.Printf("Kubernetes: %s\n", k8s)
|
||||
}
|
||||
if talos, ok := resp.Data["talos"].(string); ok && talos != "" {
|
||||
fmt.Printf("Talos: %s\n", talos)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user