package setup import ( "context" "fmt" "os" "path/filepath" "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 ( skipInstaller bool skipHardware bool ) func newClusterCommand() *cobra.Command { cmd := &cobra.Command{ Use: "cluster", Short: "Set up Kubernetes cluster", Long: `Set up the Kubernetes cluster infrastructure using Talos Linux. This command configures Talos Linux nodes and bootstraps the Kubernetes cluster. Examples: wild setup cluster wild setup cluster --skip-installer wild setup cluster --skip-hardware`, RunE: runCluster, } cmd.Flags().BoolVar(&skipInstaller, "skip-installer", false, "skip installer image generation") cmd.Flags().BoolVar(&skipHardware, "skip-hardware", false, "skip node hardware detection") return cmd } func runCluster(cmd *cobra.Command, args []string) error { output.Header("Wild Cloud Cluster Setup") // 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{"talosctl"}); err != nil { return fmt.Errorf("required tools not available: %w", err) } // Load configuration configMgr := config.NewManager(env.ConfigPath(), env.SecretsPath()) // Get cluster configuration clusterName, err := getConfigString(configMgr, "cluster.name") if err != nil { return fmt.Errorf("cluster name not configured: %w", err) } vip, err := getConfigString(configMgr, "cluster.vip") if err != nil { return fmt.Errorf("cluster VIP not configured: %w", err) } output.Info("Cluster: " + clusterName) output.Info("VIP: " + vip) // Phase 1: Generate Talos configuration output.Info("\n=== Phase 1: Generating Talos Configuration ===") configDir := filepath.Join(env.WildCloudDir(), "talos") if err := os.MkdirAll(configDir, 0755); err != nil { return fmt.Errorf("creating config directory: %w", err) } talosctl := toolManager.Talosctl() clusterEndpoint := "https://" + vip + ":6443" if err := talosctl.GenerateConfig(cmd.Context(), clusterName, clusterEndpoint, configDir); err != nil { return fmt.Errorf("generating talos config: %w", err) } output.Success("Talos configuration generated") // Phase 2: Node configuration if !skipHardware { output.Info("\n=== Phase 2: Detecting Nodes ===") if err := detectAndConfigureNodes(cmd.Context(), configMgr, talosctl, configDir); err != nil { return fmt.Errorf("configuring nodes: %w", err) } } else { output.Info("Skipping node hardware detection") } // Phase 3: Bootstrap cluster output.Info("\n=== Phase 3: Bootstrapping Cluster ===") if err := bootstrapCluster(cmd.Context(), configMgr, talosctl, configDir); err != nil { return fmt.Errorf("bootstrapping cluster: %w", err) } output.Success("Cluster setup completed successfully!") output.Info("") output.Info("Next steps:") output.Info(" wild setup services # Install cluster services") output.Info(" kubectl get nodes # Verify cluster") return nil } // detectAndConfigureNodes detects and configures cluster nodes func detectAndConfigureNodes(ctx context.Context, configMgr *config.Manager, talosctl *external.TalosctlTool, configDir string) error { // Get nodes from configuration nodesConfig, err := configMgr.Get("cluster.nodes") if err != nil || nodesConfig == nil { output.Warning("No nodes configured") output.Info("Add nodes to config: wild config set cluster.nodes '[{\"ip\": \"192.168.1.10\", \"role\": \"controlplane\"}]'") return nil } nodes, ok := nodesConfig.([]interface{}) if !ok { return fmt.Errorf("invalid nodes configuration") } if len(nodes) == 0 { output.Warning("No nodes configured") return nil } output.Info(fmt.Sprintf("Found %d nodes in configuration", len(nodes))) // Configure each node for i, nodeConfig := range nodes { nodeMap, ok := nodeConfig.(map[string]interface{}) if !ok { output.Warning(fmt.Sprintf("Invalid node %d configuration", i)) continue } nodeIP, exists := nodeMap["ip"] if !exists { output.Warning(fmt.Sprintf("Node %d missing IP address", i)) continue } nodeRole, exists := nodeMap["role"] if !exists { nodeRole = "worker" } output.Info(fmt.Sprintf("Configuring node %s (%s)", nodeIP, nodeRole)) // Apply configuration to node var configFile string if nodeRole == "controlplane" { configFile = filepath.Join(configDir, "controlplane.yaml") } else { configFile = filepath.Join(configDir, "worker.yaml") } talosctl.SetEndpoints([]string{fmt.Sprintf("%v", nodeIP)}) if err := talosctl.ApplyConfig(ctx, configFile, true); err != nil { output.Warning(fmt.Sprintf("Failed to configure node %s: %v", nodeIP, err)) } else { output.Success(fmt.Sprintf("Node %s configured", nodeIP)) } } return nil } // bootstrapCluster bootstraps the Kubernetes cluster func bootstrapCluster(ctx context.Context, configMgr *config.Manager, talosctl *external.TalosctlTool, configDir string) error { // Get first controlplane node nodesConfig, err := configMgr.Get("cluster.nodes") if err != nil || nodesConfig == nil { return fmt.Errorf("no nodes configured") } nodes, ok := nodesConfig.([]interface{}) if !ok || len(nodes) == 0 { return fmt.Errorf("invalid nodes configuration") } // Find first controlplane node var bootstrapNode string for _, nodeConfig := range nodes { nodeMap, ok := nodeConfig.(map[string]interface{}) if !ok { continue } nodeIP, exists := nodeMap["ip"] if !exists { continue } nodeRole, exists := nodeMap["role"] if exists && nodeRole == "controlplane" { bootstrapNode = fmt.Sprintf("%v", nodeIP) break } } if bootstrapNode == "" { return fmt.Errorf("no controlplane node found") } output.Info("Bootstrap node: " + bootstrapNode) // Set talosconfig talosconfig := filepath.Join(configDir, "talosconfig") talosctl.SetTalosconfig(talosconfig) talosctl.SetEndpoints([]string{bootstrapNode}) talosctl.SetNodes([]string{bootstrapNode}) // Bootstrap cluster if err := talosctl.Bootstrap(ctx); err != nil { return fmt.Errorf("bootstrapping cluster: %w", err) } output.Success("Cluster bootstrapped") // Generate kubeconfig output.Info("Generating kubeconfig...") kubeconfigPath := filepath.Join(configDir, "kubeconfig") if err := talosctl.Kubeconfig(ctx, kubeconfigPath, true); err != nil { output.Warning("Failed to generate kubeconfig: " + err.Error()) } else { output.Success("Kubeconfig generated: " + kubeconfigPath) output.Info("Set KUBECONFIG=" + kubeconfigPath) } return nil } // getConfigString gets a string value from config with validation func getConfigString(configMgr *config.Manager, path string) (string, error) { value, err := configMgr.Get(path) if err != nil { return "", err } if value == nil { return "", fmt.Errorf("config value '%s' not set", path) } strValue, ok := value.(string) if !ok { return "", fmt.Errorf("config value '%s' is not a string", path) } if strValue == "" { return "", fmt.Errorf("config value '%s' is empty", path) } return strValue, nil }