package cluster import ( "fmt" "net/http" "net/url" "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" ) func newNodesCommand() *cobra.Command { cmd := &cobra.Command{ Use: "nodes", Short: "Manage cluster nodes", Long: `Manage Kubernetes cluster nodes.`, } cmd.AddCommand( newNodesListCommand(), newNodesBootCommand(), ) return cmd } func newNodesListCommand() *cobra.Command { return &cobra.Command{ Use: "list", Short: "List cluster nodes", Long: `List and show status of cluster nodes. This command shows the status of both configured nodes and running cluster nodes. Examples: wild cluster nodes list`, RunE: runNodesList, } } func newNodesBootCommand() *cobra.Command { return &cobra.Command{ Use: "boot", Short: "Boot cluster nodes", Long: `Boot and configure cluster nodes by downloading boot assets. This command downloads Talos boot assets including kernel, initramfs, and ISO images for PXE booting or USB creation. Examples: wild cluster nodes boot`, RunE: runNodesBoot, } } func runNodesList(cmd *cobra.Command, args []string) error { output.Header("Cluster Nodes Status") // Initialize environment env := environment.New() if err := env.RequiresProject(); err != nil { return err } // Load configuration configMgr := config.NewManager(env.ConfigPath(), env.SecretsPath()) // Show configured nodes output.Info("=== Configured Nodes ===") 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\"}]'") } else { nodes, ok := nodesConfig.([]interface{}) if !ok || len(nodes) == 0 { output.Warning("No nodes configured") } else { for i, nodeConfig := range nodes { nodeMap, ok := nodeConfig.(map[string]interface{}) if !ok { output.Warning(fmt.Sprintf("Invalid node %d configuration", i)) continue } nodeIP := nodeMap["ip"] nodeRole := nodeMap["role"] if nodeRole == nil { nodeRole = "worker" } output.Info(fmt.Sprintf(" Node %d: %v (%v)", i+1, nodeIP, nodeRole)) } } } // Try to show running cluster nodes if kubectl is available toolManager := external.NewManager() if err := toolManager.CheckTools(cmd.Context(), []string{"kubectl"}); err == nil { kubectl := toolManager.Kubectl() if nodesOutput, err := kubectl.GetNodes(cmd.Context()); err == nil { output.Info("\n=== Running Cluster Nodes ===") output.Info(string(nodesOutput)) } else { output.Info("\n=== Running Cluster Nodes ===") output.Warning("Could not connect to cluster: " + err.Error()) } } else { output.Info("\n=== Running Cluster Nodes ===") output.Warning("kubectl not available - cannot show running cluster status") } return nil } func runNodesBoot(cmd *cobra.Command, args []string) error { output.Header("Talos Installer Image Generation and Asset Download") // Initialize environment env := environment.New() if err := env.RequiresProject(); err != nil { return err } // Load configuration configMgr := config.NewManager(env.ConfigPath(), env.SecretsPath()) // Check for required configuration talosVersion, err := configMgr.Get("cluster.nodes.talos.version") if err != nil || talosVersion == nil { return fmt.Errorf("missing required configuration: cluster.nodes.talos.version") } schematicID, err := configMgr.Get("cluster.nodes.talos.schematicId") if err != nil || schematicID == nil { return fmt.Errorf("missing required configuration: cluster.nodes.talos.schematicId") } talosVersionStr := talosVersion.(string) schematicIDStr := schematicID.(string) if talosVersionStr == "" || schematicIDStr == "" { return fmt.Errorf("talos version and schematic ID cannot be empty") } output.Info("Creating custom Talos installer image...") output.Info("Talos version: " + talosVersionStr) output.Info("Schematic ID: " + schematicIDStr) // Show schematic extensions if available if extensions, err := configMgr.Get("cluster.nodes.talos.schematic.customization.systemExtensions.officialExtensions"); err == nil && extensions != nil { if extList, ok := extensions.([]interface{}); ok && len(extList) > 0 { output.Info("\nSchematic includes:") for _, ext := range extList { output.Info(" - " + fmt.Sprintf("%v", ext)) } output.Info("") } } // Generate installer image URL installerURL := fmt.Sprintf("factory.talos.dev/metal-installer/%s:%s", schematicIDStr, talosVersionStr) output.Success("Custom installer image URL generated!") output.Info("") output.Info("Installer URL: " + installerURL) // Download and cache assets output.Header("Downloading and Caching PXE Boot Assets") // Create cache directories organized by schematic ID cacheDir := filepath.Join(env.WildCloudDir()) schematicCacheDir := filepath.Join(cacheDir, "node-boot-assets", schematicIDStr) pxeCacheDir := filepath.Join(schematicCacheDir, "pxe") ipxeCacheDir := filepath.Join(schematicCacheDir, "ipxe") isoCacheDir := filepath.Join(schematicCacheDir, "iso") if err := os.MkdirAll(filepath.Join(pxeCacheDir, "amd64"), 0755); err != nil { return fmt.Errorf("creating cache directories: %w", err) } if err := os.MkdirAll(ipxeCacheDir, 0755); err != nil { return fmt.Errorf("creating cache directories: %w", err) } if err := os.MkdirAll(isoCacheDir, 0755); err != nil { return fmt.Errorf("creating cache directories: %w", err) } // Download Talos kernel and initramfs for PXE boot output.Info("Downloading Talos PXE assets...") kernelURL := fmt.Sprintf("https://pxe.factory.talos.dev/image/%s/%s/kernel-amd64", schematicIDStr, talosVersionStr) initramfsURL := fmt.Sprintf("https://pxe.factory.talos.dev/image/%s/%s/initramfs-amd64.xz", schematicIDStr, talosVersionStr) kernelPath := filepath.Join(pxeCacheDir, "amd64", "vmlinuz") initramfsPath := filepath.Join(pxeCacheDir, "amd64", "initramfs.xz") // Download assets if err := downloadAsset(kernelURL, kernelPath, "Talos kernel"); err != nil { return fmt.Errorf("downloading kernel: %w", err) } if err := downloadAsset(initramfsURL, initramfsPath, "Talos initramfs"); err != nil { return fmt.Errorf("downloading initramfs: %w", err) } // Download iPXE bootloader files output.Info("Downloading iPXE bootloader assets...") ipxeAssets := map[string]string{ "http://boot.ipxe.org/ipxe.efi": filepath.Join(ipxeCacheDir, "ipxe.efi"), "http://boot.ipxe.org/undionly.kpxe": filepath.Join(ipxeCacheDir, "undionly.kpxe"), "http://boot.ipxe.org/arm64-efi/ipxe.efi": filepath.Join(ipxeCacheDir, "ipxe-arm64.efi"), } for downloadURL, path := range ipxeAssets { description := fmt.Sprintf("iPXE %s", filepath.Base(path)) if err := downloadAsset(downloadURL, path, description); err != nil { output.Warning(fmt.Sprintf("Failed to download %s: %v", description, err)) } } // Download Talos ISO output.Info("Downloading Talos ISO...") isoURL := fmt.Sprintf("https://factory.talos.dev/image/%s/%s/metal-amd64.iso", schematicIDStr, talosVersionStr) isoFilename := fmt.Sprintf("talos-%s-metal-amd64.iso", talosVersionStr) isoPath := filepath.Join(isoCacheDir, isoFilename) if err := downloadAsset(isoURL, isoPath, "Talos ISO"); err != nil { return fmt.Errorf("downloading ISO: %w", err) } output.Success("All assets downloaded and cached!") output.Info("") output.Info(fmt.Sprintf("Cached assets for schematic %s:", schematicIDStr)) output.Info(fmt.Sprintf(" Talos kernel: %s", kernelPath)) output.Info(fmt.Sprintf(" Talos initramfs: %s", initramfsPath)) output.Info(fmt.Sprintf(" Talos ISO: %s", isoPath)) output.Info(fmt.Sprintf(" iPXE EFI: %s", filepath.Join(ipxeCacheDir, "ipxe.efi"))) output.Info(fmt.Sprintf(" iPXE BIOS: %s", filepath.Join(ipxeCacheDir, "undionly.kpxe"))) output.Info(fmt.Sprintf(" iPXE ARM64: %s", filepath.Join(ipxeCacheDir, "ipxe-arm64.efi"))) output.Info("") output.Info(fmt.Sprintf("Cache location: %s", schematicCacheDir)) output.Info("") output.Info("Use these assets for:") output.Info(" - PXE boot: Use kernel and initramfs from cache") output.Info(" - USB creation: Use ISO file for dd or imaging tools") output.Info(fmt.Sprintf(" Example: sudo dd if=%s of=/dev/sdX bs=4M status=progress", isoPath)) output.Info(fmt.Sprintf(" - Custom installer: https://%s", installerURL)) output.Success("Installer image generation and asset caching completed!") return nil } // downloadAsset downloads a file with progress indication func downloadAsset(downloadURL, path, description string) error { // Check if file already exists if _, err := os.Stat(path); err == nil { output.Info(fmt.Sprintf("%s already cached at %s", description, path)) return nil } output.Info(fmt.Sprintf("Downloading %s...", description)) output.Info(fmt.Sprintf("URL: %s", downloadURL)) // Parse URL to validate parsedURL, err := url.Parse(downloadURL) if err != nil { return fmt.Errorf("invalid URL: %w", err) } // Create HTTP client client := &http.Client{} req, err := http.NewRequest("GET", parsedURL.String(), nil) if err != nil { return fmt.Errorf("creating request: %w", err) } resp, err := client.Do(req) if err != nil { return fmt.Errorf("downloading: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return fmt.Errorf("download failed with status: %s", resp.Status) } // Create destination file if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { return fmt.Errorf("creating directory: %w", err) } file, err := os.Create(path) if err != nil { return fmt.Errorf("creating file: %w", err) } defer func() { _ = file.Close() }() // Copy data if _, err := file.ReadFrom(resp.Body); err != nil { return fmt.Errorf("writing file: %w", err) } // Verify download if stat, err := os.Stat(path); err != nil || stat.Size() == 0 { _ = os.Remove(path) return fmt.Errorf("download failed or file is empty") } output.Success(fmt.Sprintf("%s downloaded successfully", description)) return nil }