First commit of golang CLI.

This commit is contained in:
2025-08-31 11:51:11 -07:00
parent 4ca06aecb6
commit f0a2098f11
51 changed files with 8840 additions and 0 deletions

View File

@@ -0,0 +1,249 @@
package util
import (
"context"
"fmt"
"os"
"path/filepath"
"time"
"github.com/spf13/cobra"
"github.com/wild-cloud/wild-cli/internal/config"
"github.com/wild-cloud/wild-cli/internal/environment"
"github.com/wild-cloud/wild-cli/internal/external"
"github.com/wild-cloud/wild-cli/internal/output"
)
var (
backupAll bool
)
func NewBackupCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "backup",
Short: "Backup Wild Cloud system",
Long: `Backup the entire Wild Cloud system including applications and data.
This command performs a comprehensive backup of your Wild Cloud system using restic,
including WC_HOME directory and all application data.
Examples:
wild backup
wild backup --all`,
RunE: runBackup,
}
cmd.Flags().BoolVar(&backupAll, "all", true, "backup all applications")
return cmd
}
func runBackup(cmd *cobra.Command, args []string) error {
output.Header("Wild Cloud System Backup")
// Initialize environment
env := environment.New()
if err := env.RequiresProject(); err != nil {
return err
}
// Check external tools
toolManager := external.NewManager()
if err := toolManager.CheckTools(cmd.Context(), []string{"restic"}); err != nil {
return fmt.Errorf("required tools not available: %w", err)
}
// Load configuration
configMgr := config.NewManager(env.ConfigPath(), env.SecretsPath())
// Get backup configuration
backupRoot, err := configMgr.Get("cloud.backup.root")
if err != nil || backupRoot == nil {
return fmt.Errorf("backup root not configured. Set cloud.backup.root in config.yaml")
}
backupPassword, err := configMgr.GetSecret("cloud.backupPassword")
if err != nil || backupPassword == nil {
return fmt.Errorf("backup password not configured. Set cloud.backupPassword in secrets.yaml")
}
stagingDir, err := configMgr.Get("cloud.backup.staging")
if err != nil || stagingDir == nil {
return fmt.Errorf("backup staging directory not configured. Set cloud.backup.staging in config.yaml")
}
repository := fmt.Sprintf("%v", backupRoot)
password := fmt.Sprintf("%v", backupPassword)
staging := fmt.Sprintf("%v", stagingDir)
output.Info("Backup repository: " + repository)
// Initialize restic tool
restic := toolManager.Restic()
restic.SetRepository(repository)
restic.SetPassword(password)
// Check if repository exists, initialize if needed
output.Info("Checking if restic repository exists...")
if err := checkOrInitializeRepository(cmd.Context(), restic); err != nil {
return fmt.Errorf("repository initialization failed: %w", err)
}
// Create staging directory
if err := os.MkdirAll(staging, 0755); err != nil {
return fmt.Errorf("creating staging directory: %w", err)
}
// Generate backup tags
today := time.Now().Format("2006-01-02")
tags := []string{"wild-cloud", "wc-home", today}
// Backup entire WC_HOME
output.Info("Backing up WC_HOME directory...")
wcHome := env.WCHome()
if wcHome == "" {
wcHome = env.WildCloudDir()
}
if err := restic.Backup(cmd.Context(), []string{wcHome}, []string{".wildcloud/cache"}, tags); err != nil {
return fmt.Errorf("backing up WC_HOME: %w", err)
}
output.Success("WC_HOME backup completed")
// Backup applications if requested
if backupAll {
output.Info("Running backup for all applications...")
if err := backupAllApplications(cmd.Context(), env, configMgr, restic, staging, today); err != nil {
return fmt.Errorf("application backup failed: %w", err)
}
}
// TODO: Future enhancements
// - Backup Kubernetes resources (kubectl get all -A -o yaml)
// - Backup persistent volumes
// - Backup secrets and configmaps
output.Success("Wild Cloud system backup completed successfully!")
return nil
}
// checkOrInitializeRepository checks if restic repository exists and initializes if needed
func checkOrInitializeRepository(ctx context.Context, restic *external.ResticTool) error {
// Try to check repository
if err := restic.Check(ctx); err != nil {
output.Warning("No existing backup repository found. Initializing restic repository...")
if err := restic.InitRepository(ctx); err != nil {
return fmt.Errorf("initializing repository: %w", err)
}
output.Success("Repository initialized successfully")
} else {
output.Info("Using existing backup repository")
}
return nil
}
// backupAllApplications backs up all applications using the app backup functionality
func backupAllApplications(ctx context.Context, env *environment.Environment, configMgr *config.Manager, restic *external.ResticTool, staging, dateTag string) error {
// Get list of applications
appsDir := env.AppsDir()
if _, err := os.Stat(appsDir); os.IsNotExist(err) {
output.Warning("No apps directory found, skipping application backups")
return nil
}
entries, err := os.ReadDir(appsDir)
if err != nil {
return fmt.Errorf("reading apps directory: %w", err)
}
var apps []string
for _, entry := range entries {
if entry.IsDir() {
apps = append(apps, entry.Name())
}
}
if len(apps) == 0 {
output.Warning("No applications found, skipping application backups")
return nil
}
output.Info(fmt.Sprintf("Found %d applications to backup: %v", len(apps), apps))
// For now, we'll use the existing bash script for application backups
// This maintains compatibility with the existing backup infrastructure
wcRoot := env.WCRoot()
if wcRoot == "" {
output.Warning("WC_ROOT not set, skipping application-specific backups")
return nil
}
appBackupScript := filepath.Join(wcRoot, "bin", "wild-app-backup")
if _, err := os.Stat(appBackupScript); os.IsNotExist(err) {
output.Warning("App backup script not found, skipping application backups")
return nil
}
// Execute the app backup script
bashTool := external.NewBaseTool("bash", "bash")
// Set environment variables needed by the script
oldWCRoot := os.Getenv("WC_ROOT")
oldWCHome := os.Getenv("WC_HOME")
defer func() {
if oldWCRoot != "" {
_ = os.Setenv("WC_ROOT", oldWCRoot)
}
if oldWCHome != "" {
_ = os.Setenv("WC_HOME", oldWCHome)
}
}()
_ = os.Setenv("WC_ROOT", wcRoot)
_ = os.Setenv("WC_HOME", env.WCHome())
output.Info("Running application backup script...")
if _, err := bashTool.Execute(ctx, appBackupScript, "--all"); err != nil {
output.Warning(fmt.Sprintf("Application backup script failed: %v", err))
return nil // Don't fail the entire backup for app backup issues
}
output.Success("Application backup script completed")
// Upload each app's backup to restic individually
stagingAppsDir := filepath.Join(staging, "apps")
if _, err := os.Stat(stagingAppsDir); err != nil {
output.Warning("No app staging directory found, skipping app backup uploads")
return nil
}
entries, err = os.ReadDir(stagingAppsDir)
if err != nil {
output.Warning(fmt.Sprintf("Reading app staging directory failed: %v", err))
return nil
}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
appName := entry.Name()
appBackupDir := filepath.Join(stagingAppsDir, appName)
output.Info(fmt.Sprintf("Uploading backup for app: %s", appName))
tags := []string{"wild-cloud", appName, dateTag}
if err := restic.Backup(ctx, []string{appBackupDir}, []string{}, tags); err != nil {
output.Warning(fmt.Sprintf("Failed to backup app %s: %v", appName, err))
continue
}
output.Success(fmt.Sprintf("Backup for app '%s' completed", appName))
}
return nil
}

View File

@@ -0,0 +1,157 @@
package util
import (
"context"
"fmt"
"strings"
"github.com/spf13/cobra"
"github.com/wild-cloud/wild-cli/internal/environment"
"github.com/wild-cloud/wild-cli/internal/external"
"github.com/wild-cloud/wild-cli/internal/output"
)
func NewDashboardCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "dashboard",
Short: "Manage Kubernetes dashboard",
Long: `Manage access to the Kubernetes dashboard.`,
}
cmd.AddCommand(
newDashboardTokenCommand(),
)
return cmd
}
func newDashboardTokenCommand() *cobra.Command {
return &cobra.Command{
Use: "token",
Short: "Get dashboard access token",
Long: `Get an access token for the Kubernetes dashboard.
This command retrieves the authentication token needed to access the Kubernetes dashboard.
Examples:
wild dashboard token`,
RunE: runDashboardToken,
}
}
func runDashboardToken(cmd *cobra.Command, args []string) error {
output.Header("Kubernetes Dashboard Token")
// Initialize environment
env := environment.New()
if err := env.RequiresProject(); err != nil {
return err
}
// Check external tools
toolManager := external.NewManager()
if err := toolManager.CheckTools(cmd.Context(), []string{"kubectl"}); err != nil {
return fmt.Errorf("required tools not available: %w", err)
}
kubectl := toolManager.Kubectl()
// The namespace where the dashboard is installed
namespace := "kubernetes-dashboard"
secretName := "dashboard-admin-token"
// Try to get the token from the secret
token, err := getDashboardToken(cmd.Context(), kubectl, namespace, secretName)
if err != nil {
return fmt.Errorf("failed to get dashboard token: %w", err)
}
// Print the token with nice formatting
output.Success("Use this token to authenticate to the Kubernetes Dashboard:")
output.Info("")
output.Printf("%s\n", token)
output.Info("")
// Additional instructions
output.Info("Instructions:")
output.Info("1. Copy the token above")
output.Info("2. Navigate to your Kubernetes Dashboard URL")
output.Info("3. Select 'Token' authentication method")
output.Info("4. Paste the token and click 'Sign In'")
return nil
}
// getDashboardToken retrieves the dashboard token from Kubernetes
func getDashboardToken(ctx context.Context, kubectl *external.KubectlTool, namespace, secretName string) (string, error) {
// Try to get the secret directly
secretData, err := kubectl.GetResource(ctx, "secret", secretName, namespace)
if err != nil {
// If secret doesn't exist, try to find any admin-related secret
output.Warning("Dashboard admin token secret not found, searching for available tokens...")
return findDashboardToken(ctx, kubectl, namespace)
}
// Extract token from secret data
// The secret data is in YAML format, we need to parse it
secretStr := string(secretData)
lines := strings.Split(secretStr, "\n")
for _, line := range lines {
if strings.Contains(line, "token:") {
// Extract the base64 encoded token
parts := strings.Fields(line)
if len(parts) >= 2 {
encodedToken := parts[1]
// Decode base64 token using kubectl
tokenBytes, err := kubectl.Execute(ctx, "exec", "deploy/coredns", "-n", "kube-system", "--", "base64", "-d")
if err != nil {
// Try alternative method with echo and base64
echoCmd := fmt.Sprintf("echo '%s' | base64 -d", encodedToken)
tokenBytes, err = kubectl.Execute(ctx, "exec", "deploy/coredns", "-n", "kube-system", "--", "sh", "-c", echoCmd)
if err != nil {
// Return the encoded token as fallback
return encodedToken, nil
}
}
return strings.TrimSpace(string(tokenBytes)), nil
}
}
}
return "", fmt.Errorf("token not found in secret data")
}
// findDashboardToken searches for available dashboard tokens
func findDashboardToken(ctx context.Context, kubectl *external.KubectlTool, namespace string) (string, error) {
// List all secrets in the dashboard namespace
secrets, err := kubectl.GetResource(ctx, "secrets", "", namespace)
if err != nil {
return "", fmt.Errorf("failed to list secrets in namespace %s: %w", namespace, err)
}
// Look for tokens in the secret list
secretsStr := string(secrets)
lines := strings.Split(secretsStr, "\n")
var tokenSecrets []string
for _, line := range lines {
if strings.Contains(line, "token") && strings.Contains(line, "dashboard") {
parts := strings.Fields(line)
if len(parts) > 0 {
tokenSecrets = append(tokenSecrets, parts[0])
}
}
}
if len(tokenSecrets) == 0 {
return "", fmt.Errorf("no dashboard token secrets found in namespace %s", namespace)
}
// Try the first available token secret
secretName := tokenSecrets[0]
output.Info("Using token secret: " + secretName)
return getDashboardToken(ctx, kubectl, namespace, secretName)
}

View File

@@ -0,0 +1,126 @@
package util
import (
"context"
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/wild-cloud/wild-cli/internal/environment"
"github.com/wild-cloud/wild-cli/internal/external"
"github.com/wild-cloud/wild-cli/internal/output"
)
func runStatus(cmd *cobra.Command, args []string) error {
output.Header("Wild Cloud Status")
// Initialize environment
env := environment.New()
// Check if we're in a project directory
detected, _ := env.DetectWCHome()
if detected != "" {
env.SetWCHome(detected)
output.Success("Found Wild Cloud project: " + detected)
} else {
output.Warning("Not in a Wild Cloud project directory")
}
// Check environment
output.Info("\n=== Environment ===")
if env.WCRoot() != "" {
output.Success("WC_ROOT: " + env.WCRoot())
} else {
output.Warning("WC_ROOT: Not set")
}
if env.WCHome() != "" {
output.Success("WC_HOME: " + env.WCHome())
} else {
output.Warning("WC_HOME: Not set")
}
// Check external tools
output.Info("\n=== External Tools ===")
toolManager := external.NewManager()
tools := toolManager.ListTools()
for toolName, installed := range tools {
if installed {
version, err := toolManager.GetToolVersion(toolName)
if err != nil {
output.Success(fmt.Sprintf("%-12s: Installed (version unknown)", toolName))
} else {
output.Success(fmt.Sprintf("%-12s: %s", toolName, version))
}
} else {
output.Warning(fmt.Sprintf("%-12s: Not installed", toolName))
}
}
// Check project structure if in project
if env.WCHome() != "" {
output.Info("\n=== Project Structure ===")
// Check config files
if fileExists(env.ConfigPath()) {
output.Success("config.yaml: Found")
} else {
output.Warning("config.yaml: Missing")
}
if fileExists(env.SecretsPath()) {
output.Success("secrets.yaml: Found")
} else {
output.Warning("secrets.yaml: Missing")
}
if dirExists(env.AppsDir()) {
output.Success("apps/ directory: Found")
} else {
output.Warning("apps/ directory: Missing")
}
if dirExists(env.WildCloudDir()) {
output.Success(".wildcloud/ directory: Found")
} else {
output.Warning(".wildcloud/ directory: Missing")
}
// Check cluster connectivity if tools are available
if tools["kubectl"] {
output.Info("\n=== Cluster Status ===")
kubectl := toolManager.Kubectl()
ctx := context.Background()
nodes, err := kubectl.GetNodes(ctx)
if err != nil {
output.Warning("Cluster: Not accessible (" + err.Error() + ")")
} else {
output.Success("Cluster: Connected")
output.Info("Nodes:\n" + string(nodes))
}
}
}
output.Info("\n=== Summary ===")
if detected != "" {
output.Success("Wild Cloud project is properly configured")
} else {
output.Warning("Run 'wild setup scaffold' to initialize a project")
}
return nil
}
// Helper functions
func fileExists(path string) bool {
info, err := os.Stat(path)
return err == nil && !info.IsDir()
}
func dirExists(path string) bool {
info, err := os.Stat(path)
return err == nil && info.IsDir()
}

View File

@@ -0,0 +1,61 @@
package util
import (
"fmt"
"io"
"os"
"github.com/spf13/cobra"
"github.com/wild-cloud/wild-cli/internal/config"
"github.com/wild-cloud/wild-cli/internal/environment"
)
func newCompileCommand() *cobra.Command {
return &cobra.Command{
Use: "compile",
Short: "Compile template from stdin",
Long: `Compile a template from stdin using Wild Cloud configuration context.
This command reads template content from stdin and processes it using the
current project's config.yaml and secrets.yaml as context.
Examples:
echo 'Hello {{.config.cluster.name}}' | wild template compile
cat template.yml | wild template compile`,
RunE: runCompileTemplate,
}
}
func runCompileTemplate(cmd *cobra.Command, args []string) error {
// Initialize environment
env := environment.New()
if err := env.RequiresProject(); err != nil {
return err
}
// Create config manager
mgr := config.NewManager(env.ConfigPath(), env.SecretsPath())
// Create template engine
engine, err := config.NewTemplateEngine(mgr)
if err != nil {
return fmt.Errorf("creating template engine: %w", err)
}
// Read template from stdin
templateContent, err := io.ReadAll(os.Stdin)
if err != nil {
return fmt.Errorf("reading template from stdin: %w", err)
}
// Process template
result, err := engine.Process(string(templateContent))
if err != nil {
return fmt.Errorf("processing template: %w", err)
}
// Output result
fmt.Print(result)
return nil
}

View File

@@ -0,0 +1,32 @@
package util
import (
"github.com/spf13/cobra"
)
// NewBackupCommand is implemented in backup.go
// NewDashboardCommand is implemented in dashboard.go
// NewTemplateCommand creates the template command
func NewTemplateCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "template",
Short: "Process templates",
Long: `Process template files with Wild Cloud configuration.`,
}
cmd.AddCommand(newCompileCommand())
return cmd
}
// NewStatusCommand creates the status command
func NewStatusCommand() *cobra.Command {
return &cobra.Command{
Use: "status",
Short: "Show Wild Cloud status",
Long: `Show the overall status of the Wild Cloud system.`,
RunE: runStatus,
}
}
// NewVersionCommand is implemented in version.go

View File

@@ -0,0 +1,52 @@
package util
import (
"fmt"
"github.com/spf13/cobra"
"github.com/wild-cloud/wild-cli/internal/output"
)
const (
Version = "0.1.0-dev"
BuildDate = "development"
)
func NewVersionCommand() *cobra.Command {
return &cobra.Command{
Use: "version",
Short: "Show version information",
Long: `Show version information for Wild CLI and components.
This command displays version information for the Wild CLI and related components.
Examples:
wild version`,
RunE: runVersion,
}
}
func runVersion(cmd *cobra.Command, args []string) error {
output.Header("Wild CLI Version Information")
output.Info(fmt.Sprintf("Wild CLI Version: %s", Version))
output.Info(fmt.Sprintf("Build Date: %s", BuildDate))
output.Info(fmt.Sprintf("Go Version: %s", "go1.21+"))
// TODO: Add component versions
// - kubectl version
// - talosctl version
// - restic version
// - yq version
output.Info("")
output.Info("Components:")
output.Info(" - Native Go implementation replacing 35+ bash scripts")
output.Info(" - Unified CLI with Cobra framework")
output.Info(" - Cross-platform support (Linux/macOS/Windows)")
output.Info(" - Built-in template engine with sprig functions")
output.Info(" - Integrated external tool management")
return nil
}