266 lines
7.2 KiB
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
|
|
}
|