feat: add cloud-init Go parser (Phase 2)
Implement a lightweight cloud-init system for first-boot configuration: - Go parser for YAML config (hostname, network, KubeSolo settings) - Static/DHCP network modes with DNS override - KubeSolo extra flags and API server SAN configuration - Portainer Edge Agent and air-gapped deployment support - New init stage 45-cloud-init.sh runs before network/hostname stages - Stages 50/60 skip gracefully when cloud-init has already applied - Build script compiles static Linux/amd64 binary (~2.7 MB) - 17 unit tests covering parsing, validation, and example files - Full documentation at docs/cloud-init.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
132
cloud-init/cmd/main.go
Normal file
132
cloud-init/cmd/main.go
Normal file
@@ -0,0 +1,132 @@
|
||||
// kubesolo-cloudinit is a lightweight cloud-init parser for KubeSolo OS.
|
||||
//
|
||||
// It reads a YAML configuration file and applies hostname, network, and
|
||||
// KubeSolo settings during the init sequence. Designed to run as a static
|
||||
// binary on BusyBox-based systems.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// kubesolo-cloudinit apply <config.yaml>
|
||||
// kubesolo-cloudinit validate <config.yaml>
|
||||
// kubesolo-cloudinit dump <config.yaml>
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
cloudinit "github.com/portainer/kubesolo-os/cloud-init"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultConfigPath = "/mnt/data/etc-kubesolo/cloud-init.yaml"
|
||||
persistDataDir = "/mnt/data"
|
||||
configDir = "/etc/kubesolo"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Set up structured logging to stderr (captured by init)
|
||||
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
})))
|
||||
|
||||
if len(os.Args) < 2 {
|
||||
usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
cmd := os.Args[1]
|
||||
|
||||
// Determine config path
|
||||
configPath := defaultConfigPath
|
||||
if len(os.Args) >= 3 {
|
||||
configPath = os.Args[2]
|
||||
}
|
||||
|
||||
switch cmd {
|
||||
case "apply":
|
||||
if err := cmdApply(configPath); err != nil {
|
||||
slog.Error("cloud-init apply failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
case "validate":
|
||||
if err := cmdValidate(configPath); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "validation failed: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("OK")
|
||||
case "dump":
|
||||
if err := cmdDump(configPath); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown command: %s\n", cmd)
|
||||
usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func cmdApply(configPath string) error {
|
||||
slog.Info("applying cloud-init", "config", configPath)
|
||||
|
||||
cfg, err := cloudinit.Parse(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 1. Apply hostname
|
||||
if err := cloudinit.ApplyHostname(cfg); err != nil {
|
||||
return fmt.Errorf("hostname: %w", err)
|
||||
}
|
||||
|
||||
// 2. Apply network configuration
|
||||
if err := cloudinit.ApplyNetwork(cfg); err != nil {
|
||||
return fmt.Errorf("network: %w", err)
|
||||
}
|
||||
|
||||
// 3. Apply KubeSolo settings
|
||||
if err := cloudinit.ApplyKubeSolo(cfg, configDir); err != nil {
|
||||
return fmt.Errorf("kubesolo config: %w", err)
|
||||
}
|
||||
|
||||
// 4. Save persistent configs for next boot
|
||||
if err := cloudinit.SaveHostname(cfg, persistDataDir+"/etc-kubesolo"); err != nil {
|
||||
slog.Warn("failed to save hostname", "error", err)
|
||||
}
|
||||
if err := cloudinit.SaveNetworkConfig(cfg, persistDataDir+"/network"); err != nil {
|
||||
slog.Warn("failed to save network config", "error", err)
|
||||
}
|
||||
|
||||
slog.Info("cloud-init applied successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
func cmdValidate(configPath string) error {
|
||||
_, err := cloudinit.Parse(configPath)
|
||||
return err
|
||||
}
|
||||
|
||||
func cmdDump(configPath string) error {
|
||||
cfg, err := cloudinit.Parse(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(cfg)
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintf(os.Stderr, `Usage: kubesolo-cloudinit <command> [config.yaml]
|
||||
|
||||
Commands:
|
||||
apply Parse and apply cloud-init configuration
|
||||
validate Check config file for errors
|
||||
dump Parse and print config as JSON
|
||||
|
||||
If config path is omitted, defaults to %s
|
||||
`, defaultConfigPath)
|
||||
}
|
||||
Reference in New Issue
Block a user