320 lines
10 KiB
Go
320 lines
10 KiB
Go
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
|
|
}
|