Streamline CLI: remove Wild Central commands, merge overlapping features
Remove commands that belong to Wild Central (dns, proxy, firewall, ddns, crowdsec, secret central) since those APIs live on Central now. Merge service into app (install, logs, config), merge iso into asset (delete), move orphan commands (health, node-ip) under cluster. Clean up redundant commands (backup start, node cancel-discovery) and standardize deletion confirmation on --yes/-y across all commands.
This commit is contained in:
359
cli/cmd/app.go
359
cli/cmd/app.go
@@ -1,9 +1,18 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/wild-cloud/wild-central/wild/internal/config"
|
||||
)
|
||||
|
||||
// App commands
|
||||
@@ -252,14 +261,364 @@ var appUpgradePlanCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
appFetchFlag bool
|
||||
appNoDeployFlag bool
|
||||
// App logs flags
|
||||
appTailLines int
|
||||
appFollowLogs bool
|
||||
appContainerName string
|
||||
appPreviousLogs bool
|
||||
appSinceDuration string
|
||||
// App config update flags
|
||||
appSetFlags []string
|
||||
appNoRedeploy bool
|
||||
)
|
||||
|
||||
var appInstallCmd = &cobra.Command{
|
||||
Use: "install <app>",
|
||||
Short: "Add and deploy an app in one step",
|
||||
Long: `Install an app by adding it to the instance and deploying it.
|
||||
|
||||
This command combines 'app add' and 'app deploy' into a single step:
|
||||
1. Add app to instance (fetches from Wild Directory, compiles templates)
|
||||
2. Deploy app to cluster (unless --no-deploy)
|
||||
|
||||
Examples:
|
||||
wild app install gitea
|
||||
wild app install gitea --no-deploy
|
||||
wild app install gitea --fetch`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
appName := args[0]
|
||||
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Installing app: %s\n", appName)
|
||||
|
||||
// Fetch manifest for display
|
||||
manifestResp, err := apiClient.Get(fmt.Sprintf("/api/v1/apps/%s/manifest", appName))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch manifest: %w", err)
|
||||
}
|
||||
name := manifestResp.GetString("name")
|
||||
desc := manifestResp.GetString("description")
|
||||
if name != "" {
|
||||
fmt.Printf("App: %s - %s\n", name, desc)
|
||||
}
|
||||
|
||||
// Check if config overrides needed
|
||||
if defaultConfig, ok := manifestResp.Data["defaultConfig"].(map[string]any); ok && len(defaultConfig) > 0 {
|
||||
configResp, err := apiClient.Get(fmt.Sprintf("/api/v1/instances/%s/config", inst))
|
||||
if err == nil {
|
||||
existingConfig := config.GetValue(configResp.Data, fmt.Sprintf("apps.%s", appName))
|
||||
if existingConfig == nil || existingConfig == "null" {
|
||||
fmt.Println("\nDefault configuration will be applied:")
|
||||
for key, val := range defaultConfig {
|
||||
fmt.Printf(" %s: %v\n", key, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add app to instance
|
||||
fmt.Println("\nAdding app to instance...")
|
||||
_, err = apiClient.Post(
|
||||
fmt.Sprintf("/api/v1/instances/%s/apps", inst),
|
||||
map[string]any{
|
||||
"name": appName,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add app: %w", err)
|
||||
}
|
||||
|
||||
// Re-fetch if requested
|
||||
if appFetchFlag {
|
||||
fmt.Println("Fetching fresh templates...")
|
||||
_, err = apiClient.Post(fmt.Sprintf("/api/v1/instances/%s/apps/%s/fetch", inst, appName), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch app: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if appNoDeployFlag {
|
||||
fmt.Printf("\nApp added: %s\n", appName)
|
||||
fmt.Printf(" Templates compiled and ready to deploy\n")
|
||||
fmt.Printf(" To deploy: wild app deploy %s\n", appName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deploy
|
||||
fmt.Println("\nDeploying app...")
|
||||
deployResp, err := apiClient.Post(
|
||||
fmt.Sprintf("/api/v1/instances/%s/apps/%s/deploy", inst, appName),
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to deploy app: %w", err)
|
||||
}
|
||||
|
||||
opID := deployResp.GetString("operation_id")
|
||||
if opID != "" {
|
||||
if err := streamOperationOutput(opID); err != nil {
|
||||
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("\nApp installed successfully: %s\n", appName)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var appLogsCmd = &cobra.Command{
|
||||
Use: "logs <app>",
|
||||
Short: "View app logs",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
appName := args[0]
|
||||
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Build query parameters
|
||||
params := []string{}
|
||||
if appTailLines > 0 {
|
||||
params = append(params, fmt.Sprintf("tail=%d", appTailLines))
|
||||
}
|
||||
if appContainerName != "" {
|
||||
params = append(params, fmt.Sprintf("container=%s", appContainerName))
|
||||
}
|
||||
if appPreviousLogs {
|
||||
params = append(params, "previous=true")
|
||||
}
|
||||
if appSinceDuration != "" {
|
||||
params = append(params, fmt.Sprintf("since=%s", appSinceDuration))
|
||||
}
|
||||
|
||||
queryString := ""
|
||||
if len(params) > 0 {
|
||||
queryString = "?" + strings.Join(params, "&")
|
||||
}
|
||||
|
||||
if appFollowLogs {
|
||||
return streamAppLogs(inst, appName, queryString)
|
||||
}
|
||||
|
||||
// Buffered mode
|
||||
resp, err := apiClient.Get(fmt.Sprintf("/api/v1/instances/%s/apps/%s/logs%s", inst, appName, queryString))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if lines, ok := resp.Data["lines"].([]any); ok {
|
||||
for _, line := range lines {
|
||||
if lineStr, ok := line.(string); ok {
|
||||
fmt.Println(lineStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var appConfigUpdateCmd = &cobra.Command{
|
||||
Use: "config <app>",
|
||||
Short: "Update app configuration",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
appName := args[0]
|
||||
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updates := make(map[string]string)
|
||||
|
||||
// Parse --set flags
|
||||
for _, flag := range appSetFlags {
|
||||
parts := strings.SplitN(flag, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("invalid --set format: %s (expected key=value)", flag)
|
||||
}
|
||||
updates[parts[0]] = parts[1]
|
||||
}
|
||||
|
||||
// Interactive mode if no --set flags provided
|
||||
if len(updates) == 0 {
|
||||
fmt.Printf("Updating app config: %s\n", appName)
|
||||
fmt.Println("Enter configuration values (key=value), empty line to finish:")
|
||||
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
for {
|
||||
fmt.Print("> ")
|
||||
if !scanner.Scan() {
|
||||
break
|
||||
}
|
||||
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" {
|
||||
break
|
||||
}
|
||||
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
fmt.Println("Invalid format, expected key=value")
|
||||
continue
|
||||
}
|
||||
|
||||
updates[parts[0]] = parts[1]
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return fmt.Errorf("error reading input: %w", err)
|
||||
}
|
||||
|
||||
if len(updates) == 0 {
|
||||
fmt.Println("No updates provided")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Build request body
|
||||
requestBody := map[string]any{
|
||||
"config": updates,
|
||||
"redeploy": !appNoRedeploy,
|
||||
}
|
||||
|
||||
fmt.Printf("Updating configuration for app: %s\n", appName)
|
||||
resp, err := apiClient.Patch(
|
||||
fmt.Sprintf("/api/v1/instances/%s/apps/%s/config", inst, appName),
|
||||
requestBody,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Configuration updated (%d values)\n", len(updates))
|
||||
for key, value := range updates {
|
||||
fmt.Printf(" %s: %s\n", key, value)
|
||||
}
|
||||
|
||||
if !appNoRedeploy {
|
||||
if opID := resp.GetString("operation_id"); opID != "" {
|
||||
fmt.Println("\nRedeploying app...")
|
||||
if err := streamOperationOutput(opID); err != nil {
|
||||
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("\nApp redeployed successfully\n")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Println("\nConfiguration updated without redeployment")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func streamAppLogs(instance, appName, queryString string) error {
|
||||
baseURL := daemonURL
|
||||
if baseURL == "" {
|
||||
baseURL = config.GetDaemonURL()
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/api/v1/instances/%s/apps/%s/logs%s", baseURL, instance, appName, queryString)
|
||||
if strings.Contains(url, "?") {
|
||||
url += "&follow=true"
|
||||
} else {
|
||||
url += "?follow=true"
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-sigChan
|
||||
cancel()
|
||||
}()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 0}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("request failed with status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
for scanner.Scan() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
default:
|
||||
line := scanner.Text()
|
||||
if data, ok := strings.CutPrefix(line, "data: "); ok {
|
||||
fmt.Println(data)
|
||||
} else if line != "" {
|
||||
fmt.Println(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
if ctx.Err() == context.Canceled {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("error reading stream: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
appCmd.AddCommand(appListCmd)
|
||||
appCmd.AddCommand(appListDeployedCmd)
|
||||
appCmd.AddCommand(appAddCmd)
|
||||
appAddCmd.Flags().String("version", "", "Install a specific version from .versions/ (e.g. '1.0.0-1')")
|
||||
appCmd.AddCommand(appDeployCmd)
|
||||
appCmd.AddCommand(appInstallCmd)
|
||||
appCmd.AddCommand(appUpdateCmd)
|
||||
appCmd.AddCommand(appDeleteCmd)
|
||||
appCmd.AddCommand(appStatusCmd)
|
||||
appCmd.AddCommand(appLogsCmd)
|
||||
appCmd.AddCommand(appConfigUpdateCmd)
|
||||
appCmd.AddCommand(appUpgradePlanCmd)
|
||||
|
||||
// install flags
|
||||
appInstallCmd.Flags().BoolVar(&appFetchFlag, "fetch", false, "Fetch fresh templates from directory before installing")
|
||||
appInstallCmd.Flags().BoolVar(&appNoDeployFlag, "no-deploy", false, "Configure and compile only, skip deployment")
|
||||
|
||||
// logs flags
|
||||
appLogsCmd.Flags().IntVar(&appTailLines, "tail", 100, "Number of lines to show")
|
||||
appLogsCmd.Flags().BoolVarP(&appFollowLogs, "follow", "f", false, "Stream logs in real-time")
|
||||
appLogsCmd.Flags().StringVar(&appContainerName, "container", "", "Specific container (if app has multiple)")
|
||||
appLogsCmd.Flags().BoolVar(&appPreviousLogs, "previous", false, "Show logs from previous container instance")
|
||||
appLogsCmd.Flags().StringVar(&appSinceDuration, "since", "", "Show logs since duration (e.g., \"5m\", \"1h\")")
|
||||
|
||||
// config update flags
|
||||
appConfigUpdateCmd.Flags().StringArrayVar(&appSetFlags, "set", []string{}, "Set a configuration value (key=value), can be repeated")
|
||||
appConfigUpdateCmd.Flags().BoolVar(&appNoRedeploy, "no-redeploy", false, "Don't trigger redeployment after update")
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -163,12 +165,54 @@ var assetInfoCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
var assetDeleteForce bool
|
||||
|
||||
var assetDeleteCmd = &cobra.Command{
|
||||
Use: "delete <schematic-id> <version>",
|
||||
Short: "Delete an asset and all its files",
|
||||
Long: `Delete a specific schematic@version asset and all its downloaded files.
|
||||
This operation cannot be undone.`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
schematicID := args[0]
|
||||
version := args[1]
|
||||
|
||||
if !assetDeleteForce {
|
||||
fmt.Printf("Are you sure you want to delete %s@%s and all its assets? (y/N): ", schematicID, version)
|
||||
reader := bufio.NewReader(cmd.InOrStdin())
|
||||
response, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response = strings.TrimSpace(strings.ToLower(response))
|
||||
if response != "yes" && response != "y" {
|
||||
fmt.Println("Deletion cancelled")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := apiClient.Delete(fmt.Sprintf("/api/v1/assets/%s/%s", schematicID, version))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Asset deleted: %s@%s\n", schematicID, version)
|
||||
if msg := resp.GetString("message"); msg != "" {
|
||||
fmt.Printf("Status: %s\n", msg)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
assetDownloadCmd.Flags().StringVarP(&assetPlatform, "platform", "p", "amd64", "Platform architecture (amd64, arm64)")
|
||||
assetDownloadCmd.Flags().StringSliceVarP(&assetAssetTypes, "assets", "a", []string{}, "Asset types to download (kernel, initramfs, iso). Default: all")
|
||||
assetDeleteCmd.Flags().BoolVarP(&assetDeleteForce, "yes", "y", false, "Skip confirmation prompt")
|
||||
|
||||
assetCmd.AddCommand(assetListCmd)
|
||||
assetCmd.AddCommand(assetDownloadCmd)
|
||||
assetCmd.AddCommand(assetStatusCmd)
|
||||
assetCmd.AddCommand(assetInfoCmd)
|
||||
assetCmd.AddCommand(assetDeleteCmd)
|
||||
}
|
||||
|
||||
@@ -45,31 +45,6 @@ Use subcommands for other operations (list, verify, delete, etc.).`,
|
||||
},
|
||||
}
|
||||
|
||||
// Backup start command (default action when app name provided)
|
||||
var backupStartCmd = &cobra.Command{
|
||||
Use: "start <app>",
|
||||
Short: "Start a backup for 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 for app: %s\n", args[0])
|
||||
if opID := resp.GetString("operation_id"); opID != "" {
|
||||
fmt.Printf(" Operation ID: %s\n", opID)
|
||||
fmt.Printf("\nUse 'wild operation status %s' to monitor progress\n", opID)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Backup list command
|
||||
var backupListCmd = &cobra.Command{
|
||||
Use: "list <app>",
|
||||
@@ -863,7 +838,6 @@ func capitalize(s string) string {
|
||||
// init registers all backup subcommands and flags
|
||||
func init() {
|
||||
// Add subcommands to backup
|
||||
backupCmd.AddCommand(backupStartCmd)
|
||||
backupCmd.AddCommand(backupListCmd)
|
||||
backupCmd.AddCommand(backupVerifyCmd)
|
||||
backupCmd.AddCommand(backupDeleteCmd)
|
||||
|
||||
@@ -287,6 +287,25 @@ Examples:
|
||||
},
|
||||
}
|
||||
|
||||
var clusterControlplaneIPCmd = &cobra.Command{
|
||||
Use: "controlplane-ip",
|
||||
Short: "Get control plane IP",
|
||||
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/controlplane/ip", inst))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(resp.GetString("ip"))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
clusterCmd.AddCommand(clusterBootstrapCmd)
|
||||
clusterCmd.AddCommand(clusterStatusCmd)
|
||||
@@ -296,6 +315,7 @@ func init() {
|
||||
clusterCmd.AddCommand(clusterTalosconfigCmd)
|
||||
clusterTalosconfigCmd.AddCommand(clusterTalosconfigRenewCmd)
|
||||
clusterCmd.AddCommand(clusterEndpointsCmd)
|
||||
clusterCmd.AddCommand(clusterControlplaneIPCmd)
|
||||
|
||||
clusterEndpointsCmd.Flags().Bool("nodes", false, "Include all control node IPs as fallback endpoints")
|
||||
|
||||
|
||||
@@ -1,259 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// crowdsec commands — manage Wild Central's CrowdSec LAPI
|
||||
var crowdsecCmd = &cobra.Command{
|
||||
Use: "crowdsec",
|
||||
Short: "Manage CrowdSec LAPI",
|
||||
Long: `Manage Wild Central's CrowdSec Local API (LAPI).
|
||||
|
||||
Wild Central runs the CrowdSec LAPI — the hub that k8s cluster agents report
|
||||
threats to and that bouncers (e.g. Traefik) query for blocklists.`,
|
||||
}
|
||||
|
||||
var crowdsecStatusCmd = &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Show LAPI status, agents, and bouncers",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
resp, err := apiClient.Get("/api/v1/crowdsec/status")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get CrowdSec status: %w", err)
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
if outputFormat == "yaml" {
|
||||
return printYAML(resp.Data)
|
||||
}
|
||||
|
||||
active, _ := resp.Data["active"].(bool)
|
||||
if !active {
|
||||
fmt.Println("✗ CrowdSec LAPI is not running")
|
||||
fmt.Println(" Install with: apt install crowdsec")
|
||||
return nil
|
||||
}
|
||||
fmt.Println("✓ CrowdSec LAPI is running")
|
||||
|
||||
machines, _ := resp.Data["machines"].([]any)
|
||||
fmt.Printf("\nAgents (%d registered):\n", len(machines))
|
||||
for _, m := range machines {
|
||||
machine, ok := m.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
id, _ := machine["machineId"].(string)
|
||||
validated, _ := machine["isValidated"].(bool)
|
||||
heartbeat, _ := machine["last_heartbeat"].(string)
|
||||
status := "pending"
|
||||
if validated {
|
||||
status = "validated"
|
||||
}
|
||||
if heartbeat != "" {
|
||||
fmt.Printf(" %-30s %s (heartbeat: %s)\n", id, status, heartbeat)
|
||||
} else {
|
||||
fmt.Printf(" %-30s %s\n", id, status)
|
||||
}
|
||||
}
|
||||
|
||||
bouncers, _ := resp.Data["bouncers"].([]any)
|
||||
fmt.Printf("\nBouncers (%d registered):\n", len(bouncers))
|
||||
for _, b := range bouncers {
|
||||
bouncer, ok := b.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
name, _ := bouncer["name"].(string)
|
||||
revoked, _ := bouncer["revoked"].(bool)
|
||||
status := "active"
|
||||
if revoked {
|
||||
status = "revoked"
|
||||
}
|
||||
fmt.Printf(" %-40s %s\n", name, status)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var crowdsecSummaryCmd = &cobra.Command{
|
||||
Use: "summary",
|
||||
Short: "Show active ban counts by threat category",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
resp, err := apiClient.Get("/api/v1/crowdsec/summary")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get ban summary: %w", err)
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
if outputFormat == "yaml" {
|
||||
return printYAML(resp.Data)
|
||||
}
|
||||
|
||||
total, _ := resp.Data["total"].(float64)
|
||||
fmt.Printf("Active bans: %d\n\n", int(total))
|
||||
|
||||
byReason, _ := resp.Data["byReason"].(map[string]any)
|
||||
if len(byReason) == 0 {
|
||||
fmt.Println("No active bans.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sort by count descending
|
||||
type entry struct {
|
||||
reason string
|
||||
count int
|
||||
}
|
||||
entries := make([]entry, 0, len(byReason))
|
||||
for reason, v := range byReason {
|
||||
count, _ := v.(float64)
|
||||
entries = append(entries, entry{reason, int(count)})
|
||||
}
|
||||
sort.Slice(entries, func(i, j int) bool {
|
||||
return entries[i].count > entries[j].count
|
||||
})
|
||||
|
||||
fmt.Printf("%-50s %s\n", "Reason", "Count")
|
||||
fmt.Printf("%-50s %s\n", "------", "-----")
|
||||
for _, e := range entries {
|
||||
fmt.Printf("%-50s %d\n", e.reason, e.count)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var crowdsecMachinesCmd = &cobra.Command{
|
||||
Use: "machines",
|
||||
Short: "List registered CrowdSec agents",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
resp, err := apiClient.Get("/api/v1/crowdsec/machines")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get machines: %w", err)
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
if outputFormat == "yaml" {
|
||||
return printYAML(resp.Data)
|
||||
}
|
||||
|
||||
machines := resp.GetArray("machines")
|
||||
if len(machines) == 0 {
|
||||
fmt.Println("No agents registered.")
|
||||
return nil
|
||||
}
|
||||
for _, m := range machines {
|
||||
machine, ok := m.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
id, _ := machine["machineId"].(string)
|
||||
validated, _ := machine["isValidated"].(bool)
|
||||
heartbeat, _ := machine["last_heartbeat"].(string)
|
||||
version, _ := machine["version"].(string)
|
||||
status := "pending"
|
||||
if validated {
|
||||
status = "validated"
|
||||
}
|
||||
fmt.Printf("%-30s %-10s %-25s %s\n", id, status, heartbeat, version)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var crowdsecBouncersCmd = &cobra.Command{
|
||||
Use: "bouncers",
|
||||
Short: "List registered bouncers",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
resp, err := apiClient.Get("/api/v1/crowdsec/bouncers")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get bouncers: %w", err)
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
if outputFormat == "yaml" {
|
||||
return printYAML(resp.Data)
|
||||
}
|
||||
|
||||
bouncers := resp.GetArray("bouncers")
|
||||
if len(bouncers) == 0 {
|
||||
fmt.Println("No bouncers registered.")
|
||||
return nil
|
||||
}
|
||||
for _, b := range bouncers {
|
||||
bouncer, ok := b.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
name, _ := bouncer["name"].(string)
|
||||
revoked, _ := bouncer["revoked"].(bool)
|
||||
btype, _ := bouncer["type"].(string)
|
||||
version, _ := bouncer["version"].(string)
|
||||
status := "active"
|
||||
if revoked {
|
||||
status = "revoked"
|
||||
}
|
||||
fmt.Printf("%-40s %-8s %-15s %s\n", name, status, btype, version)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var crowdsecProvisionCmd = &cobra.Command{
|
||||
Use: "provision <instance>",
|
||||
Short: "Provision a Wild Cloud instance as a CrowdSec agent",
|
||||
Long: `Provision connects a Wild Cloud instance's CrowdSec agent to this LAPI.
|
||||
|
||||
It generates credentials, registers the instance as both an agent and a bouncer,
|
||||
and writes the LAPI URL into the instance's CrowdSec app config.
|
||||
|
||||
After provisioning, redeploy the CrowdSec app on the instance:
|
||||
wild app deploy crowdsec
|
||||
|
||||
Safe to run multiple times — credentials are preserved, registrations refreshed.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
instance := args[0]
|
||||
resp, err := apiClient.Post(fmt.Sprintf("/api/v1/crowdsec/provision/%s", instance), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to provision %s: %w", instance, err)
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
if outputFormat == "yaml" {
|
||||
return printYAML(resp.Data)
|
||||
}
|
||||
|
||||
fmt.Printf("✓ %s\n", resp.GetString("message"))
|
||||
if url := resp.GetString("centralLapiUrl"); url != "" {
|
||||
fmt.Printf(" LAPI URL: %s\n", url)
|
||||
}
|
||||
if username := resp.GetString("agentUsername"); username != "" {
|
||||
fmt.Printf(" Agent username: %s\n", username)
|
||||
}
|
||||
fmt.Println("\nNext step: redeploy the crowdsec app on the instance:")
|
||||
fmt.Printf(" wild --instance %s app deploy crowdsec\n", instance)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
crowdsecCmd.AddCommand(crowdsecStatusCmd)
|
||||
crowdsecCmd.AddCommand(crowdsecSummaryCmd)
|
||||
crowdsecCmd.AddCommand(crowdsecMachinesCmd)
|
||||
crowdsecCmd.AddCommand(crowdsecBouncersCmd)
|
||||
crowdsecCmd.AddCommand(crowdsecProvisionCmd)
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ddns commands — manage Wild Central's built-in dynamic DNS updater
|
||||
var ddnsCmd = &cobra.Command{
|
||||
Use: "ddns",
|
||||
Short: "Manage dynamic DNS",
|
||||
Long: `Manage Wild Central's built-in dynamic DNS service, which keeps your public DNS A records updated when your IP address changes.`,
|
||||
}
|
||||
|
||||
var ddnsStatusCmd = &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Show DDNS status",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
resp, err := apiClient.Get("/api/v1/ddns/status")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get DDNS status: %w", err)
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
if outputFormat == "yaml" {
|
||||
return printYAML(resp.Data)
|
||||
}
|
||||
|
||||
enabled, _ := resp.Data["enabled"].(bool)
|
||||
if !enabled {
|
||||
fmt.Println("DDNS is disabled")
|
||||
if msg := resp.GetString("message"); msg != "" {
|
||||
fmt.Printf(" %s\n", msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Println("✓ DDNS is active")
|
||||
if ip := resp.GetString("currentIP"); ip != "" {
|
||||
fmt.Printf(" Public IP: %s\n", ip)
|
||||
}
|
||||
if t := resp.GetString("lastChecked"); t != "" {
|
||||
fmt.Printf(" Last checked: %s\n", t)
|
||||
}
|
||||
if t := resp.GetString("lastUpdated"); t != "" {
|
||||
fmt.Printf(" Last updated: %s\n", t)
|
||||
}
|
||||
if e := resp.GetString("lastError"); e != "" {
|
||||
fmt.Printf(" Last error: %s\n", e)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var ddnsTriggerCmd = &cobra.Command{
|
||||
Use: "trigger",
|
||||
Short: "Force an immediate IP check and update",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
resp, err := apiClient.Post("/api/v1/ddns/trigger", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to trigger DDNS update: %w", err)
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
if outputFormat == "yaml" {
|
||||
return printYAML(resp.Data)
|
||||
}
|
||||
|
||||
fmt.Printf("✓ %s\n", resp.GetString("message"))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
ddnsCmd.AddCommand(ddnsStatusCmd)
|
||||
ddnsCmd.AddCommand(ddnsTriggerCmd)
|
||||
}
|
||||
279
cli/cmd/dns.go
279
cli/cmd/dns.go
@@ -1,279 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// DNS commands
|
||||
var dnsCmd = &cobra.Command{
|
||||
Use: "dns",
|
||||
Short: "Manage DNS services",
|
||||
Long: `Manage the dnsmasq DNS service that provides network bootstrapping for Wild Cloud nodes.`,
|
||||
}
|
||||
|
||||
var dnsStatusCmd = &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Show DNS service status",
|
||||
Long: `Display the current status of the dnsmasq service including health, process info, and configuration state.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
resp, err := apiClient.Get("/api/v1/dnsmasq/status")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get DNS status: %w", err)
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
|
||||
if outputFormat == "yaml" {
|
||||
return printYAML(resp.Data)
|
||||
}
|
||||
|
||||
// Text output - follow daemon.go pattern
|
||||
status := resp.GetString("status")
|
||||
if status == "active" {
|
||||
fmt.Println("✓ DNS service is running")
|
||||
} else {
|
||||
fmt.Printf("DNS service status: %s\n", status)
|
||||
}
|
||||
|
||||
// PID
|
||||
if pid, ok := resp.Data["pid"].(float64); ok && pid > 0 {
|
||||
fmt.Printf(" PID: %d\n", int(pid))
|
||||
}
|
||||
|
||||
// Instances configured
|
||||
if instances, ok := resp.Data["instances_configured"].(float64); ok {
|
||||
fmt.Printf(" Instances: %d\n", int(instances))
|
||||
}
|
||||
|
||||
// Config file
|
||||
if configFile := resp.GetString("config_file"); configFile != "" {
|
||||
fmt.Printf(" Config: %s\n", configFile)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var dnsConfigCmd = &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "View DNS configuration",
|
||||
Long: `Display the current dnsmasq configuration file.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
resp, err := apiClient.Get("/api/v1/dnsmasq/config")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get DNS config: %w", err)
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
|
||||
if outputFormat == "yaml" {
|
||||
return printYAML(resp.Data)
|
||||
}
|
||||
|
||||
// Text output
|
||||
if configFile := resp.GetString("config_file"); configFile != "" {
|
||||
fmt.Printf("Config file: %s\n\n", configFile)
|
||||
}
|
||||
|
||||
if content := resp.GetString("content"); content != "" {
|
||||
fmt.Println(content)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var dnsRestartCmd = &cobra.Command{
|
||||
Use: "restart",
|
||||
Short: "Restart DNS service",
|
||||
Long: `Restart the dnsmasq service. This will briefly interrupt DNS resolution on the network.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
resp, err := apiClient.Post("/api/v1/dnsmasq/restart", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to restart DNS service: %w", err)
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
|
||||
if outputFormat == "yaml" {
|
||||
return printYAML(resp.Data)
|
||||
}
|
||||
|
||||
// Text output
|
||||
if message := resp.GetString("message"); message != "" {
|
||||
fmt.Printf("✓ %s\n", message)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var dnsUpdateCmd = &cobra.Command{
|
||||
Use: "update",
|
||||
Short: "Update DNS configuration",
|
||||
Long: `Regenerate dnsmasq configuration from all instances and restart the service.
|
||||
|
||||
--dry-run See what config would be applied without writing it.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
dryRun, _ := cmd.Flags().GetBool("dry-run")
|
||||
|
||||
var endpoint string
|
||||
if dryRun {
|
||||
endpoint = "/api/v1/dnsmasq/generate"
|
||||
} else {
|
||||
endpoint = "/api/v1/dnsmasq/generate?overwrite=true"
|
||||
}
|
||||
|
||||
resp, err := apiClient.Post(endpoint, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate DNS configuration: %w", err)
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
|
||||
if outputFormat == "yaml" {
|
||||
return printYAML(resp.Data)
|
||||
}
|
||||
|
||||
// Text output
|
||||
if dryRun {
|
||||
fmt.Println("Dry-run mode: Configuration preview")
|
||||
fmt.Println("======================================")
|
||||
if config := resp.GetString("config"); config != "" {
|
||||
fmt.Println(config)
|
||||
}
|
||||
} else {
|
||||
if message := resp.GetString("message"); message != "" {
|
||||
fmt.Printf("✓ %s\n", message)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// dhcp subcommand group
|
||||
var dnsDhcpCmd = &cobra.Command{
|
||||
Use: "dhcp",
|
||||
Short: "Manage DHCP leases",
|
||||
Long: `View active DHCP leases and manage static assignments.`,
|
||||
}
|
||||
|
||||
var dnsDhcpLeasesCmd = &cobra.Command{
|
||||
Use: "leases",
|
||||
Short: "List active DHCP leases",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
resp, err := apiClient.Get("/api/v1/dnsmasq/dhcp/leases")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get DHCP leases: %w", err)
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
if outputFormat == "yaml" {
|
||||
return printYAML(resp.Data)
|
||||
}
|
||||
|
||||
leases, _ := resp.Data["leases"].([]any)
|
||||
if len(leases) == 0 {
|
||||
fmt.Println("No active leases")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("%-20s %-18s %-16s %s\n", "MAC", "IP", "HOSTNAME", "EXPIRES")
|
||||
fmt.Println("------------------------------------------------------------------------")
|
||||
for _, l := range leases {
|
||||
m, ok := l.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
mac, _ := m["mac"].(string)
|
||||
ip, _ := m["ip"].(string)
|
||||
hostname, _ := m["hostname"].(string)
|
||||
if hostname == "" {
|
||||
hostname = "—"
|
||||
}
|
||||
expiry, _ := m["expiry"].(string)
|
||||
fmt.Printf("%-20s %-18s %-16s %s\n", mac, ip, hostname, expiry)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var dnsDhcpAddStaticCmd = &cobra.Command{
|
||||
Use: "add-static <mac> <ip> [hostname]",
|
||||
Short: "Add a static DHCP lease",
|
||||
Args: cobra.RangeArgs(2, 3),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
body := map[string]string{
|
||||
"mac": args[0],
|
||||
"ip": args[1],
|
||||
}
|
||||
if len(args) == 3 {
|
||||
body["hostname"] = args[2]
|
||||
}
|
||||
|
||||
resp, err := apiClient.Post("/api/v1/dnsmasq/dhcp/static", body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add static lease: %w", err)
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
if outputFormat == "yaml" {
|
||||
return printYAML(resp.Data)
|
||||
}
|
||||
|
||||
fmt.Printf("✓ %s\n", resp.GetString("message"))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var dnsDhcpDeleteStaticCmd = &cobra.Command{
|
||||
Use: "delete-static <mac>",
|
||||
Short: "Remove a static DHCP lease",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
resp, err := apiClient.Delete(fmt.Sprintf("/api/v1/dnsmasq/dhcp/static/%s", args[0]))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete static lease: %w", err)
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
if outputFormat == "yaml" {
|
||||
return printYAML(resp.Data)
|
||||
}
|
||||
|
||||
fmt.Printf("✓ %s\n", resp.GetString("message"))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
dnsCmd.AddCommand(dnsStatusCmd)
|
||||
dnsCmd.AddCommand(dnsConfigCmd)
|
||||
dnsCmd.AddCommand(dnsRestartCmd)
|
||||
dnsCmd.AddCommand(dnsUpdateCmd)
|
||||
dnsCmd.AddCommand(dnsDhcpCmd)
|
||||
|
||||
dnsDhcpCmd.AddCommand(dnsDhcpLeasesCmd)
|
||||
dnsDhcpCmd.AddCommand(dnsDhcpAddStaticCmd)
|
||||
dnsDhcpCmd.AddCommand(dnsDhcpDeleteStaticCmd)
|
||||
|
||||
// Add --dry-run flag to update command
|
||||
dnsUpdateCmd.Flags().Bool("dry-run", false, "Preview configuration without applying changes")
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// firewall commands — manage the nftables firewall on Wild Central
|
||||
var firewallCmd = &cobra.Command{
|
||||
Use: "firewall",
|
||||
Short: "Manage the Wild Central firewall",
|
||||
Long: `Manage the nftables firewall that protects Wild Central.
|
||||
|
||||
Extra allowed ports and WAN interface filtering are configured through the
|
||||
global config. Changing these settings automatically regenerates and applies
|
||||
the firewall rules.`,
|
||||
}
|
||||
|
||||
var firewallStatusCmd = &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Show current firewall rules",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
resp, err := apiClient.Get("/api/v1/nftables/status")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get firewall status: %w", err)
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
if outputFormat == "yaml" {
|
||||
return printYAML(resp.Data)
|
||||
}
|
||||
|
||||
rules := resp.GetString("rules")
|
||||
if rules == "" {
|
||||
fmt.Println("No firewall rules loaded.")
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("Rules file: %s\n\n", resp.GetString("rulesFile"))
|
||||
fmt.Println(rules)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var firewallEnableCmd = &cobra.Command{
|
||||
Use: "enable",
|
||||
Short: "Enable the firewall (apply rules)",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return setFirewallEnabled(true)
|
||||
},
|
||||
}
|
||||
|
||||
var firewallDisableCmd = &cobra.Command{
|
||||
Use: "disable",
|
||||
Short: "Disable the firewall (flush all Wild Cloud rules)",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return setFirewallEnabled(false)
|
||||
},
|
||||
}
|
||||
|
||||
var firewallApplyCmd = &cobra.Command{
|
||||
Use: "apply",
|
||||
Short: "Reapply the current firewall rules from disk",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
resp, err := apiClient.Post("/api/v1/nftables/apply", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to apply firewall rules: %w", err)
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
if outputFormat == "yaml" {
|
||||
return printYAML(resp.Data)
|
||||
}
|
||||
|
||||
fmt.Printf("✓ %s\n", resp.GetString("message"))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// setFirewallEnabled reads the current global config, sets enabled, and saves it.
|
||||
// The API auto-applies nftables rules after saving.
|
||||
func setFirewallEnabled(enabled bool) error {
|
||||
resp, err := apiClient.Get("/api/v1/config")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get config: %w", err)
|
||||
}
|
||||
|
||||
cfg := resp.GetMap("config")
|
||||
if cfg == nil {
|
||||
cfg = map[string]any{}
|
||||
}
|
||||
|
||||
cloud, _ := cfg["cloud"].(map[string]any)
|
||||
if cloud == nil {
|
||||
cloud = map[string]any{}
|
||||
}
|
||||
nftables, _ := cloud["nftables"].(map[string]any)
|
||||
if nftables == nil {
|
||||
nftables = map[string]any{}
|
||||
}
|
||||
nftables["enabled"] = enabled
|
||||
cloud["nftables"] = nftables
|
||||
cfg["cloud"] = cloud
|
||||
|
||||
if _, err := apiClient.Put("/api/v1/config", cfg); err != nil {
|
||||
return fmt.Errorf("failed to update config: %w", err)
|
||||
}
|
||||
|
||||
if enabled {
|
||||
fmt.Println("✓ Firewall enabled — rules applied")
|
||||
} else {
|
||||
fmt.Println("✓ Firewall disabled — Wild Cloud nftables rules flushed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
firewallCmd.AddCommand(firewallStatusCmd)
|
||||
firewallCmd.AddCommand(firewallEnableCmd)
|
||||
firewallCmd.AddCommand(firewallDisableCmd)
|
||||
firewallCmd.AddCommand(firewallApplyCmd)
|
||||
}
|
||||
@@ -129,6 +129,8 @@ var instanceShowCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
var instanceDeleteConfirm bool
|
||||
|
||||
var instanceDeleteCmd = &cobra.Command{
|
||||
Use: "delete <name>",
|
||||
Short: "Delete an instance",
|
||||
@@ -136,13 +138,14 @@ var instanceDeleteCmd = &cobra.Command{
|
||||
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
|
||||
if !instanceDeleteConfirm {
|
||||
fmt.Printf("Are you sure you want to delete instance '%s'? (y/N): ", name)
|
||||
var confirm string
|
||||
fmt.Scanln(&confirm)
|
||||
if confirm != "y" && confirm != "Y" {
|
||||
fmt.Println("Deletion cancelled")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := apiClient.Delete(fmt.Sprintf("/api/v1/instances/%s", name))
|
||||
@@ -267,6 +270,7 @@ func init() {
|
||||
instanceCmd.AddCommand(instanceListCmd)
|
||||
instanceCmd.AddCommand(instanceShowCmd)
|
||||
instanceCmd.AddCommand(instanceDeleteCmd)
|
||||
instanceDeleteCmd.Flags().BoolVarP(&instanceDeleteConfirm, "yes", "y", false, "Skip confirmation prompt")
|
||||
instanceCmd.AddCommand(instanceCurrentCmd)
|
||||
instanceCmd.AddCommand(instanceUseCmd)
|
||||
instanceCmd.AddCommand(instanceEnvCmd)
|
||||
|
||||
255
cli/cmd/iso.go
255
cli/cmd/iso.go
@@ -1,255 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ISO-specific commands for managing Talos ISO images
|
||||
var isoCmd = &cobra.Command{
|
||||
Use: "iso",
|
||||
Short: "Manage Talos ISO images",
|
||||
Long: `Manage Talos ISO images for booting bare metal machines.
|
||||
ISOs are organized by schematic ID and can be downloaded for different platforms.`,
|
||||
}
|
||||
|
||||
var (
|
||||
isoPlatform string
|
||||
isoForce bool
|
||||
)
|
||||
|
||||
var isoListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all downloaded ISO images",
|
||||
Long: `List all downloaded ISO images with their schematic IDs, versions, and platforms.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
resp, err := apiClient.Get("/api/v1/assets")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
|
||||
assets := resp.GetArray("assets")
|
||||
if len(assets) == 0 {
|
||||
fmt.Println("No assets found")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Collect all ISO assets
|
||||
type isoInfo struct {
|
||||
SchematicID string
|
||||
Version string
|
||||
Platform string
|
||||
Size int64
|
||||
}
|
||||
var isos []isoInfo
|
||||
|
||||
for _, asset := range assets {
|
||||
if m, ok := asset.(map[string]any); ok {
|
||||
schematicID := fmt.Sprintf("%v", m["schematic_id"])
|
||||
version := fmt.Sprintf("%v", m["version"])
|
||||
assetsList := m["assets"]
|
||||
if assetsArray, ok := assetsList.([]any); ok {
|
||||
for _, assetItem := range assetsArray {
|
||||
if assetMap, ok := assetItem.(map[string]any); ok {
|
||||
if assetType, _ := assetMap["type"].(string); assetType == "iso" {
|
||||
if downloaded, _ := assetMap["downloaded"].(bool); downloaded {
|
||||
path := fmt.Sprintf("%v", assetMap["path"])
|
||||
platform := extractPlatform(path)
|
||||
size := int64(0)
|
||||
if s, ok := assetMap["size"].(float64); ok {
|
||||
size = int64(s)
|
||||
}
|
||||
|
||||
isos = append(isos, isoInfo{
|
||||
SchematicID: schematicID,
|
||||
Version: version,
|
||||
Platform: platform,
|
||||
Size: size,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(isos) == 0 {
|
||||
fmt.Println("No ISOs downloaded")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("%-66s %-10s %-10s %-10s\n", "SCHEMATIC ID", "VERSION", "PLATFORM", "SIZE")
|
||||
fmt.Println("--------------------------------------------------------------------------------------------------------")
|
||||
for _, iso := range isos {
|
||||
sizeMB := float64(iso.Size) / 1024 / 1024
|
||||
fmt.Printf("%-66s %-10s %-10s %.2f MB\n", iso.SchematicID, iso.Version, iso.Platform, sizeMB)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var isoDownloadCmd = &cobra.Command{
|
||||
Use: "download <schematic-id> <version>",
|
||||
Short: "Download an ISO image",
|
||||
Long: `Download a Talos ISO image for a given schematic ID and version.
|
||||
The ISO can be used to boot bare metal machines.`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
schematicID := args[0]
|
||||
version := args[1]
|
||||
|
||||
payload := map[string]any{
|
||||
"platform": isoPlatform,
|
||||
"asset_types": []string{"iso"},
|
||||
}
|
||||
|
||||
if isoForce {
|
||||
payload["force"] = true
|
||||
}
|
||||
|
||||
resp, err := apiClient.Post(fmt.Sprintf("/api/v1/assets/%s/%s/download", schematicID, version), payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Downloading ISO:\n")
|
||||
fmt.Printf(" Schematic: %s\n", schematicID)
|
||||
fmt.Printf(" Version: %s\n", version)
|
||||
fmt.Printf(" Platform: %s\n", isoPlatform)
|
||||
|
||||
if msg := resp.GetString("message"); msg != "" {
|
||||
fmt.Printf("\nStatus: %s\n", msg)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var isoDeleteCmd = &cobra.Command{
|
||||
Use: "delete <schematic-id> <version>",
|
||||
Short: "Delete an asset and all its files",
|
||||
Long: `Delete a specific schematic@version asset and all its downloaded files including ISOs.
|
||||
This operation cannot be undone.`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
schematicID := args[0]
|
||||
version := args[1]
|
||||
|
||||
// Prompt for confirmation unless --force is used
|
||||
if !isoForce {
|
||||
fmt.Printf("Are you sure you want to delete %s@%s and all its assets? (yes/no): ", schematicID, version)
|
||||
reader := bufio.NewReader(cmd.InOrStdin())
|
||||
response, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response = strings.TrimSpace(strings.ToLower(response))
|
||||
if response != "yes" && response != "y" {
|
||||
fmt.Println("Deletion cancelled")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := apiClient.Delete(fmt.Sprintf("/api/v1/assets/%s/%s", schematicID, version))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Asset deleted: %s@%s\n", schematicID, version)
|
||||
if msg := resp.GetString("message"); msg != "" {
|
||||
fmt.Printf("Status: %s\n", msg)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var isoInfoCmd = &cobra.Command{
|
||||
Use: "info <schematic-id> <version>",
|
||||
Short: "Show detailed information about a specific asset",
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
schematicID := args[0]
|
||||
version := args[1]
|
||||
|
||||
resp, err := apiClient.Get(fmt.Sprintf("/api/v1/assets/%s/%s", schematicID, version))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
|
||||
fmt.Printf("Schematic ID: %s\n", resp.GetString("schematic_id"))
|
||||
fmt.Printf("Version: %s\n", resp.GetString("version"))
|
||||
fmt.Printf("Path: %s\n", resp.GetString("path"))
|
||||
|
||||
if assets := resp.GetArray("assets"); len(assets) > 0 {
|
||||
fmt.Println("\nAssets:")
|
||||
for _, asset := range assets {
|
||||
if a, ok := asset.(map[string]any); ok {
|
||||
assetType, _ := a["type"].(string)
|
||||
downloaded, _ := a["downloaded"].(bool)
|
||||
path := fmt.Sprintf("%v", a["path"])
|
||||
|
||||
if downloaded {
|
||||
size := int64(0)
|
||||
if s, ok := a["size"].(float64); ok {
|
||||
size = int64(s)
|
||||
}
|
||||
sizeMB := float64(size) / 1024 / 1024
|
||||
|
||||
if assetType == "iso" {
|
||||
platform := extractPlatform(path)
|
||||
fmt.Printf(" ✓ ISO (%s): %.2f MB\n", platform, sizeMB)
|
||||
} else {
|
||||
fmt.Printf(" ✓ %s: %.2f MB\n", assetType, sizeMB)
|
||||
}
|
||||
fmt.Printf(" Path: %s\n", path)
|
||||
} else {
|
||||
fmt.Printf(" ✗ %s: Not downloaded\n", assetType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// extractPlatform extracts platform from filename (e.g., "metal-amd64.iso" -> "amd64")
|
||||
func extractPlatform(path string) string {
|
||||
filename := filepath.Base(path)
|
||||
re := regexp.MustCompile(`-(amd64|arm64)\.`)
|
||||
matches := re.FindStringSubmatch(filename)
|
||||
if len(matches) > 1 {
|
||||
return matches[1]
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Flags for download command
|
||||
isoDownloadCmd.Flags().StringVarP(&isoPlatform, "platform", "p", "amd64", "Platform architecture (amd64, arm64)")
|
||||
isoDownloadCmd.Flags().BoolVarP(&isoForce, "force", "f", false, "Force re-download if already exists")
|
||||
|
||||
// Flags for delete command
|
||||
isoDeleteCmd.Flags().BoolVarP(&isoForce, "force", "f", false, "Skip confirmation prompt")
|
||||
|
||||
isoCmd.AddCommand(isoListCmd)
|
||||
isoCmd.AddCommand(isoDownloadCmd)
|
||||
isoCmd.AddCommand(isoDeleteCmd)
|
||||
isoCmd.AddCommand(isoInfoCmd)
|
||||
}
|
||||
@@ -501,30 +501,7 @@ You can use it manually to update templates.`,
|
||||
},
|
||||
}
|
||||
|
||||
var nodeCancelDiscoveryCmd = &cobra.Command{
|
||||
Use: "cancel-discovery",
|
||||
Short: "Cancel active node discovery",
|
||||
Long: `Cancel an active node discovery operation.
|
||||
|
||||
Use this if discovery gets stuck or you want to stop a running scan.
|
||||
|
||||
Example:
|
||||
wild node cancel-discovery`,
|
||||
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/discovery/cancel", inst), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("Discovery cancelled successfully")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
var nodeDeleteConfirm bool
|
||||
|
||||
var nodeDeleteCmd = &cobra.Command{
|
||||
Use: "delete <hostname>",
|
||||
@@ -536,6 +513,16 @@ var nodeDeleteCmd = &cobra.Command{
|
||||
return err
|
||||
}
|
||||
|
||||
if !nodeDeleteConfirm {
|
||||
fmt.Printf("Are you sure you want to delete node '%s'? (y/N): ", args[0])
|
||||
var response string
|
||||
fmt.Scanln(&response)
|
||||
if response != "y" && response != "Y" {
|
||||
fmt.Println("Deletion cancelled")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
_, err = apiClient.Delete(fmt.Sprintf("/api/v1/instances/%s/nodes/%s", inst, args[0]))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -752,7 +739,6 @@ Examples:
|
||||
|
||||
func init() {
|
||||
nodeCmd.AddCommand(nodeDiscoverCmd)
|
||||
nodeCmd.AddCommand(nodeCancelDiscoveryCmd)
|
||||
nodeCmd.AddCommand(nodeDetectCmd)
|
||||
nodeCmd.AddCommand(nodeListCmd)
|
||||
nodeCmd.AddCommand(nodeShowCmd)
|
||||
@@ -761,6 +747,7 @@ func init() {
|
||||
nodeCmd.AddCommand(nodeUpdateCmd)
|
||||
nodeCmd.AddCommand(nodeFetchTemplatesCmd)
|
||||
nodeCmd.AddCommand(nodeDeleteCmd)
|
||||
nodeDeleteCmd.Flags().BoolVarP(&nodeDeleteConfirm, "yes", "y", false, "Skip confirmation prompt")
|
||||
nodeCmd.AddCommand(nodeHealthCmd)
|
||||
nodeCmd.AddCommand(nodeRebootCmd)
|
||||
nodeCmd.AddCommand(nodeResetCmd)
|
||||
|
||||
143
cli/cmd/proxy.go
143
cli/cmd/proxy.go
@@ -1,143 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// proxy commands — manage the HAProxy ingress proxy on Wild Central
|
||||
var proxyCmd = &cobra.Command{
|
||||
Use: "proxy",
|
||||
Short: "Manage the ingress proxy",
|
||||
Long: `Manage the HAProxy ingress proxy that routes external traffic to Wild Cloud instances by SNI hostname.`,
|
||||
}
|
||||
|
||||
var proxyStatusCmd = &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Show ingress proxy status",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
resp, err := apiClient.Get("/api/v1/haproxy/status")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get proxy status: %w", err)
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
if outputFormat == "yaml" {
|
||||
return printYAML(resp.Data)
|
||||
}
|
||||
|
||||
status := resp.GetString("status")
|
||||
if status == "active" {
|
||||
fmt.Println("✓ Ingress proxy is running")
|
||||
} else {
|
||||
fmt.Printf("Ingress proxy status: %s\n", status)
|
||||
}
|
||||
if pid, ok := resp.Data["pid"].(float64); ok && pid > 0 {
|
||||
fmt.Printf(" PID: %d\n", int(pid))
|
||||
}
|
||||
if cf := resp.GetString("configFile"); cf != "" {
|
||||
fmt.Printf(" Config: %s\n", cf)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var proxyGenerateCmd = &cobra.Command{
|
||||
Use: "generate",
|
||||
Short: "Regenerate proxy config and apply",
|
||||
Long: `Rebuild the ingress proxy configuration from all Wild Cloud instances and custom routes, then reload the proxy and update firewall rules.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
resp, err := apiClient.Post("/api/v1/haproxy/generate", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate proxy config: %w", err)
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
if outputFormat == "yaml" {
|
||||
return printYAML(resp.Data)
|
||||
}
|
||||
|
||||
fmt.Printf("✓ %s\n", resp.GetString("message"))
|
||||
if routes, ok := resp.Data["routes"].(float64); ok {
|
||||
fmt.Printf(" Instance routes: %d\n", int(routes))
|
||||
}
|
||||
if custom, ok := resp.Data["customRoutes"].(float64); ok && custom > 0 {
|
||||
fmt.Printf(" Custom routes: %d\n", int(custom))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var proxyRestartCmd = &cobra.Command{
|
||||
Use: "restart",
|
||||
Short: "Restart the ingress proxy",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
resp, err := apiClient.Post("/api/v1/haproxy/restart", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to restart proxy: %w", err)
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
if outputFormat == "yaml" {
|
||||
return printYAML(resp.Data)
|
||||
}
|
||||
|
||||
fmt.Printf("✓ %s\n", resp.GetString("message"))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var proxyStatsCmd = &cobra.Command{
|
||||
Use: "stats",
|
||||
Short: "Show live backend connection stats",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
resp, err := apiClient.Get("/api/v1/haproxy/stats")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get proxy stats: %w", err)
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
if outputFormat == "yaml" {
|
||||
return printYAML(resp.Data)
|
||||
}
|
||||
|
||||
backends, _ := resp.Data["backends"].([]any)
|
||||
if len(backends) == 0 {
|
||||
fmt.Println("No backends (proxy may not be running)")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("%-30s %-10s %-8s %-12s %-8s\n", "BACKEND", "STATUS", "ACTIVE", "TOTAL", "ERRORS")
|
||||
fmt.Println("-----------------------------------------------------------------------")
|
||||
for _, b := range backends {
|
||||
m, ok := b.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
name, _ := m["name"].(string)
|
||||
status, _ := m["status"].(string)
|
||||
active, _ := m["activeConns"].(float64)
|
||||
total, _ := m["totalConns"].(float64)
|
||||
errors, _ := m["errors"].(float64)
|
||||
fmt.Printf("%-30s %-10s %-8d %-12d %-8d\n",
|
||||
name, status, int(active), int(total), int(errors))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
proxyCmd.AddCommand(proxyStatusCmd)
|
||||
proxyCmd.AddCommand(proxyGenerateCmd)
|
||||
proxyCmd.AddCommand(proxyRestartCmd)
|
||||
proxyCmd.AddCommand(proxyStatsCmd)
|
||||
}
|
||||
@@ -25,11 +25,9 @@ var (
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "wild",
|
||||
Short: "Wild Cloud CLI",
|
||||
Long: `wild-cli is the command-line interface for Wild Central.
|
||||
Long: `wild-cli is the command-line interface for Wild Cloud.
|
||||
|
||||
Manage Wild Cloud instances, nodes, clusters, services, and applications,
|
||||
as well as Wild Central's built-in network appliance features: DNS/DHCP
|
||||
(dnsmasq), ingress proxy (HAProxy), dynamic DNS (DDNS), and secrets.`,
|
||||
Manage Wild Cloud instances, nodes, clusters, and applications.`,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Skip for commands that don't need API client
|
||||
if cmd.Name() == "version" || cmd.Name() == "help" {
|
||||
@@ -70,33 +68,24 @@ func Execute() {
|
||||
|
||||
func init() {
|
||||
// Global flags
|
||||
rootCmd.PersistentFlags().StringVar(&daemonURL, "daemon-url", "", "Daemon URL (default: $WILD_API_URI or http://localhost:5055)")
|
||||
rootCmd.PersistentFlags().StringVar(&daemonURL, "daemon-url", "", "Daemon URL (default: $WILD_API_URI or http://localhost:5056)")
|
||||
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(dnsCmd)
|
||||
rootCmd.AddCommand(instanceCmd)
|
||||
rootCmd.AddCommand(configCmd)
|
||||
rootCmd.AddCommand(secretCmd)
|
||||
rootCmd.AddCommand(nodeCmd)
|
||||
rootCmd.AddCommand(assetCmd)
|
||||
rootCmd.AddCommand(isoCmd)
|
||||
rootCmd.AddCommand(clusterCmd)
|
||||
rootCmd.AddCommand(serviceCmd)
|
||||
rootCmd.AddCommand(appCmd)
|
||||
rootCmd.AddCommand(backupCmd)
|
||||
rootCmd.AddCommand(restoreCmd)
|
||||
rootCmd.AddCommand(healthCmd)
|
||||
rootCmd.AddCommand(nodeIPCmd)
|
||||
rootCmd.AddCommand(operationCmd)
|
||||
rootCmd.AddCommand(talosCmd)
|
||||
rootCmd.AddCommand(proxyCmd)
|
||||
rootCmd.AddCommand(ddnsCmd)
|
||||
rootCmd.AddCommand(crowdsecCmd)
|
||||
rootCmd.AddCommand(firewallCmd)
|
||||
}
|
||||
|
||||
// getInstanceName returns the current instance name using the priority cascade
|
||||
|
||||
@@ -2,7 +2,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@@ -67,69 +66,7 @@ var secretSetCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
// secretCentralCmd manages Wild Central's own secrets (e.g., DDNS API tokens).
|
||||
var secretCentralCmd = &cobra.Command{
|
||||
Use: "central",
|
||||
Short: "Manage Wild Central secrets",
|
||||
Long: `Manage Wild Central's own secrets (e.g., DDNS API tokens). These are separate from instance secrets.`,
|
||||
}
|
||||
|
||||
var secretCentralGetCmd = &cobra.Command{
|
||||
Use: "get <key>",
|
||||
Short: "Get a Wild Central secret value (raw)",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
key := args[0]
|
||||
|
||||
resp, err := apiClient.Get("/api/v1/secrets?raw=true")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get secrets: %w", err)
|
||||
}
|
||||
|
||||
val := config.GetValue(resp.Data, key)
|
||||
if val != nil {
|
||||
fmt.Println(val)
|
||||
} else {
|
||||
return fmt.Errorf("secret '%s' not found", key)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var secretCentralSetCmd = &cobra.Command{
|
||||
Use: "set <key> <value>",
|
||||
Short: "Set a Wild Central secret value",
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
key, value := args[0], args[1]
|
||||
|
||||
// Build nested map from dot-notation key (e.g., ddns.cloudflare.apiToken)
|
||||
body := buildNestedSecretMap(key, value)
|
||||
|
||||
_, err := apiClient.Put("/api/v1/secrets", body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set secret: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Secret updated: %s\n", key)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// buildNestedSecretMap converts a dot-notation key and value to a nested map.
|
||||
// e.g., "ddns.cloudflare.apiToken", "abc" → {"ddns": {"cloudflare": {"apiToken": "abc"}}}
|
||||
func buildNestedSecretMap(key, value string) map[string]any {
|
||||
parts := strings.SplitN(key, ".", 2)
|
||||
if len(parts) == 1 {
|
||||
return map[string]any{key: value}
|
||||
}
|
||||
return map[string]any{parts[0]: buildNestedSecretMap(parts[1], value)}
|
||||
}
|
||||
|
||||
func init() {
|
||||
secretCmd.AddCommand(secretGetCmd)
|
||||
secretCmd.AddCommand(secretSetCmd)
|
||||
secretCmd.AddCommand(secretCentralCmd)
|
||||
secretCentralCmd.AddCommand(secretCentralGetCmd)
|
||||
secretCentralCmd.AddCommand(secretCentralSetCmd)
|
||||
}
|
||||
|
||||
@@ -1,515 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/wild-cloud/wild-central/wild/internal/config"
|
||||
)
|
||||
|
||||
// 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/apps?category=infrastructure", inst))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
|
||||
services := resp.GetArray("apps")
|
||||
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]any); ok {
|
||||
fmt.Printf("%-20s %-12s\n", m["name"], m["status"])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
fetchFlag bool
|
||||
noDeployFlag bool
|
||||
// Service logs flags
|
||||
tailLines int
|
||||
followLogs bool
|
||||
containerName string
|
||||
previousLogs bool
|
||||
sinceDuration string
|
||||
// Service update flags
|
||||
setFlags []string
|
||||
noRedeployFlag bool
|
||||
)
|
||||
|
||||
var serviceInstallCmd = &cobra.Command{
|
||||
Use: "install <service>",
|
||||
Short: "Install a service",
|
||||
Long: `Install and configure a cluster service.
|
||||
|
||||
This command adds a service to the instance and optionally deploys it:
|
||||
1. Add service to instance (fetches from Wild Directory, compiles templates)
|
||||
2. Deploy service to cluster (unless --no-deploy)
|
||||
|
||||
Examples:
|
||||
# Add and deploy (most common)
|
||||
wild service install metallb
|
||||
|
||||
# Add only, skip deployment
|
||||
wild service install metallb --no-deploy
|
||||
|
||||
# Force re-fetch from Wild Directory
|
||||
wild service install metallb --fetch
|
||||
`,
|
||||
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)
|
||||
|
||||
// Fetch manifest for display
|
||||
manifestResp, err := apiClient.Get(fmt.Sprintf("/api/v1/apps/%s/manifest", serviceName))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch manifest: %w", err)
|
||||
}
|
||||
name := manifestResp.GetString("name")
|
||||
desc := manifestResp.GetString("description")
|
||||
if name != "" {
|
||||
fmt.Printf("Service: %s - %s\n", name, desc)
|
||||
}
|
||||
|
||||
// Check if config overrides needed
|
||||
if defaultConfig, ok := manifestResp.Data["defaultConfig"].(map[string]any); ok && len(defaultConfig) > 0 {
|
||||
// Check existing config for this service
|
||||
configResp, err := apiClient.Get(fmt.Sprintf("/api/v1/instances/%s/config", inst))
|
||||
if err == nil {
|
||||
existingConfig := config.GetValue(configResp.Data, fmt.Sprintf("apps.%s", serviceName))
|
||||
if existingConfig == nil || existingConfig == "null" {
|
||||
fmt.Println("\nDefault configuration will be applied:")
|
||||
for key, val := range defaultConfig {
|
||||
fmt.Printf(" %s: %v\n", key, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add service to instance
|
||||
fmt.Println("\nAdding service to instance...")
|
||||
_, err = apiClient.Post(
|
||||
fmt.Sprintf("/api/v1/instances/%s/apps", inst),
|
||||
map[string]any{
|
||||
"name": serviceName,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add service: %w", err)
|
||||
}
|
||||
|
||||
// Re-fetch if requested
|
||||
if fetchFlag {
|
||||
fmt.Println("Fetching fresh templates...")
|
||||
_, err = apiClient.Post(fmt.Sprintf("/api/v1/instances/%s/apps/%s/fetch", inst, serviceName), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch service: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if noDeployFlag {
|
||||
fmt.Printf("\nService added: %s\n", serviceName)
|
||||
fmt.Printf(" Templates compiled and ready to deploy\n")
|
||||
fmt.Printf(" To deploy: wild service install %s\n", serviceName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deploy
|
||||
fmt.Println("\nDeploying service...")
|
||||
deployResp, err := apiClient.Post(
|
||||
fmt.Sprintf("/api/v1/instances/%s/apps/%s/deploy", inst, serviceName),
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to deploy service: %w", err)
|
||||
}
|
||||
|
||||
opID := deployResp.GetString("operation_id")
|
||||
if opID != "" {
|
||||
if err := streamOperationOutput(opID); err != nil {
|
||||
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("\nService installed successfully: %s\n", serviceName)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var serviceStatusCmd = &cobra.Command{
|
||||
Use: "status <service>",
|
||||
Short: "Show detailed status of a service",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runServiceStatus,
|
||||
}
|
||||
|
||||
var serviceLogsCmd = &cobra.Command{
|
||||
Use: "logs <service>",
|
||||
Short: "View service logs",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runServiceLogs,
|
||||
}
|
||||
|
||||
var serviceUpdateCmd = &cobra.Command{
|
||||
Use: "update <service>",
|
||||
Short: "Update service configuration",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runServiceUpdate,
|
||||
}
|
||||
|
||||
func runServiceStatus(cmd *cobra.Command, args []string) error {
|
||||
serviceName := args[0]
|
||||
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := apiClient.Get(fmt.Sprintf("/api/v1/instances/%s/apps/%s/status", inst, serviceName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if outputFormat == "json" {
|
||||
return printJSON(resp.Data)
|
||||
}
|
||||
|
||||
if outputFormat == "yaml" {
|
||||
return printYAML(resp.Data)
|
||||
}
|
||||
|
||||
// Pretty print status
|
||||
fmt.Printf("Service: %s\n", resp.GetString("name"))
|
||||
if namespace := resp.GetString("namespace"); namespace != "" {
|
||||
fmt.Printf("Namespace: %s\n", namespace)
|
||||
}
|
||||
if status := resp.GetString("status"); status != "" {
|
||||
fmt.Printf("Status: %s\n", status)
|
||||
}
|
||||
|
||||
// Show replica information
|
||||
if replicas := resp.GetMap("replicas"); replicas != nil {
|
||||
fmt.Println("\nReplicas:")
|
||||
if desired, ok := replicas["desired"].(float64); ok {
|
||||
fmt.Printf(" Desired: %.0f\n", desired)
|
||||
}
|
||||
if current, ok := replicas["current"].(float64); ok {
|
||||
fmt.Printf(" Current: %.0f\n", current)
|
||||
}
|
||||
if ready, ok := replicas["ready"].(float64); ok {
|
||||
fmt.Printf(" Ready: %.0f\n", ready)
|
||||
}
|
||||
if available, ok := replicas["available"].(float64); ok {
|
||||
fmt.Printf(" Available: %.0f\n", available)
|
||||
}
|
||||
}
|
||||
|
||||
// Show pod information
|
||||
if pods := resp.GetArray("pods"); len(pods) > 0 {
|
||||
fmt.Println("\nPods:")
|
||||
fmt.Printf(" %-40s %-12s %-8s %-10s %-10s\n", "NAME", "STATUS", "READY", "RESTARTS", "AGE")
|
||||
fmt.Println(" " + strings.Repeat("-", 90))
|
||||
for _, pod := range pods {
|
||||
if p, ok := pod.(map[string]any); ok {
|
||||
name := p["name"]
|
||||
status := p["status"]
|
||||
ready := p["ready"]
|
||||
restarts := p["restarts"]
|
||||
age := p["age"]
|
||||
fmt.Printf(" %-40s %-12s %-8v %-10v %-10s\n", name, status, ready, restarts, age)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show current configuration
|
||||
if configMap := resp.GetMap("config"); len(configMap) > 0 {
|
||||
fmt.Println("\nConfiguration:")
|
||||
for key, value := range configMap {
|
||||
fmt.Printf(" %s: %v\n", key, value)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runServiceLogs(cmd *cobra.Command, args []string) error {
|
||||
serviceName := args[0]
|
||||
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Build query parameters
|
||||
params := []string{}
|
||||
if tailLines > 0 {
|
||||
params = append(params, fmt.Sprintf("tail=%d", tailLines))
|
||||
}
|
||||
if containerName != "" {
|
||||
params = append(params, fmt.Sprintf("container=%s", containerName))
|
||||
}
|
||||
if previousLogs {
|
||||
params = append(params, "previous=true")
|
||||
}
|
||||
if sinceDuration != "" {
|
||||
params = append(params, fmt.Sprintf("since=%s", sinceDuration))
|
||||
}
|
||||
|
||||
queryString := ""
|
||||
if len(params) > 0 {
|
||||
queryString = "?" + strings.Join(params, "&")
|
||||
}
|
||||
|
||||
if followLogs {
|
||||
// Streaming mode
|
||||
return streamServiceLogs(inst, serviceName, queryString)
|
||||
}
|
||||
|
||||
// Buffered mode
|
||||
resp, err := apiClient.Get(fmt.Sprintf("/api/v1/instances/%s/apps/%s/logs%s", inst, serviceName, queryString))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Print logs - API returns logs as an array of lines
|
||||
if lines, ok := resp.Data["lines"].([]any); ok {
|
||||
for _, line := range lines {
|
||||
if lineStr, ok := line.(string); ok {
|
||||
fmt.Println(lineStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func streamServiceLogs(instance, serviceName, queryString string) error {
|
||||
// Get base URL
|
||||
baseURL := daemonURL
|
||||
if baseURL == "" {
|
||||
baseURL = config.GetDaemonURL()
|
||||
}
|
||||
|
||||
// Build URL with follow=true parameter
|
||||
url := fmt.Sprintf("%s/api/v1/instances/%s/apps/%s/logs%s", baseURL, instance, serviceName, queryString)
|
||||
if strings.Contains(url, "?") {
|
||||
url += "&follow=true"
|
||||
} else {
|
||||
url += "?follow=true"
|
||||
}
|
||||
|
||||
// Create context that can be cancelled
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Set up signal handling for graceful shutdown
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-sigChan
|
||||
cancel()
|
||||
}()
|
||||
|
||||
// Create HTTP request
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
// Make request
|
||||
client := &http.Client{Timeout: 0} // No timeout for streaming
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("request failed with status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Stream response line by line
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
for scanner.Scan() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
default:
|
||||
line := scanner.Text()
|
||||
// SSE events are prefixed with "data: "
|
||||
if data, ok := strings.CutPrefix(line, "data: "); ok {
|
||||
fmt.Println(data)
|
||||
} else if line != "" {
|
||||
fmt.Println(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
if ctx.Err() == context.Canceled {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("error reading stream: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runServiceUpdate(cmd *cobra.Command, args []string) error {
|
||||
serviceName := args[0]
|
||||
|
||||
inst, err := getInstanceName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updates := make(map[string]string)
|
||||
|
||||
// Parse --set flags
|
||||
for _, setFlag := range setFlags {
|
||||
parts := strings.SplitN(setFlag, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("invalid --set format: %s (expected key=value)", setFlag)
|
||||
}
|
||||
updates[parts[0]] = parts[1]
|
||||
}
|
||||
|
||||
// Interactive mode if no --set flags provided
|
||||
if len(updates) == 0 {
|
||||
fmt.Printf("Updating service: %s\n", serviceName)
|
||||
fmt.Println("Enter configuration values (key=value), empty line to finish:")
|
||||
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
for {
|
||||
fmt.Print("> ")
|
||||
if !scanner.Scan() {
|
||||
break
|
||||
}
|
||||
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" {
|
||||
break
|
||||
}
|
||||
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
fmt.Println("Invalid format, expected key=value")
|
||||
continue
|
||||
}
|
||||
|
||||
updates[parts[0]] = parts[1]
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return fmt.Errorf("error reading input: %w", err)
|
||||
}
|
||||
|
||||
if len(updates) == 0 {
|
||||
fmt.Println("No updates provided")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Build request body
|
||||
requestBody := map[string]any{
|
||||
"config": updates,
|
||||
"redeploy": !noRedeployFlag,
|
||||
}
|
||||
|
||||
// Make API call
|
||||
fmt.Printf("Updating configuration for service: %s\n", serviceName)
|
||||
resp, err := apiClient.Patch(
|
||||
fmt.Sprintf("/api/v1/instances/%s/apps/%s/config", inst, serviceName),
|
||||
requestBody,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Show result
|
||||
fmt.Printf("Configuration updated (%d values)\n", len(updates))
|
||||
for key, value := range updates {
|
||||
fmt.Printf(" %s: %s\n", key, value)
|
||||
}
|
||||
|
||||
// If redeployment triggered, show operation
|
||||
if !noRedeployFlag {
|
||||
if opID := resp.GetString("operation_id"); opID != "" {
|
||||
fmt.Println("\nRedeploying service...")
|
||||
if err := streamOperationOutput(opID); err != nil {
|
||||
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 redeployed successfully\n")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Println("\nConfiguration updated without redeployment")
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
serviceLogsCmd.Flags().IntVar(&tailLines, "tail", 100, "Number of lines to show")
|
||||
serviceLogsCmd.Flags().BoolVarP(&followLogs, "follow", "f", false, "Stream logs in real-time")
|
||||
serviceLogsCmd.Flags().StringVar(&containerName, "container", "", "Specific container (if service has multiple)")
|
||||
serviceLogsCmd.Flags().BoolVar(&previousLogs, "previous", false, "Show logs from previous container instance")
|
||||
serviceLogsCmd.Flags().StringVar(&sinceDuration, "since", "", "Show logs since duration (e.g., \"5m\", \"1h\")")
|
||||
|
||||
serviceUpdateCmd.Flags().StringArrayVar(&setFlags, "set", []string{}, "Set a configuration value (key=value), can be repeated")
|
||||
serviceUpdateCmd.Flags().BoolVar(&noRedeployFlag, "no-redeploy", false, "Don't trigger redeployment after update")
|
||||
|
||||
serviceCmd.AddCommand(serviceListCmd)
|
||||
serviceCmd.AddCommand(serviceInstallCmd)
|
||||
serviceCmd.AddCommand(serviceStatusCmd)
|
||||
serviceCmd.AddCommand(serviceLogsCmd)
|
||||
serviceCmd.AddCommand(serviceUpdateCmd)
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
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 nodeIPCmd = &cobra.Command{
|
||||
Use: "node-ip",
|
||||
Short: "Get control plane IP",
|
||||
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/controlplane/ip", inst))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(resp.GetString("ip"))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user