Files
wild-cloud/wild-cli/cmd/wild/setup/cluster.go

266 lines
7.2 KiB
Go

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
}