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>
81 lines
2.0 KiB
Go
81 lines
2.0 KiB
Go
package cloudinit
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// ApplyHostname sets the system hostname and updates /etc/hostname and /etc/hosts.
|
|
func ApplyHostname(cfg *Config) error {
|
|
hostname := cfg.Hostname
|
|
if hostname == "" {
|
|
slog.Info("no hostname in cloud-init, skipping")
|
|
return nil
|
|
}
|
|
|
|
// Set the running hostname
|
|
if err := os.WriteFile("/proc/sys/kernel/hostname", []byte(hostname), 0o644); err != nil {
|
|
// Fallback: use the hostname command
|
|
if err := run("hostname", hostname); err != nil {
|
|
return fmt.Errorf("setting hostname: %w", err)
|
|
}
|
|
}
|
|
|
|
// Write /etc/hostname
|
|
if err := os.WriteFile("/etc/hostname", []byte(hostname+"\n"), 0o644); err != nil {
|
|
return fmt.Errorf("writing /etc/hostname: %w", err)
|
|
}
|
|
|
|
// Ensure hostname is in /etc/hosts
|
|
if err := ensureHostsEntry(hostname); err != nil {
|
|
return fmt.Errorf("updating /etc/hosts: %w", err)
|
|
}
|
|
|
|
slog.Info("hostname set", "hostname", hostname)
|
|
return nil
|
|
}
|
|
|
|
// SaveHostname writes the hostname to the persistent data partition so it
|
|
// survives reboots (even without cloud-init on next boot).
|
|
func SaveHostname(cfg *Config, destDir string) error {
|
|
if cfg.Hostname == "" {
|
|
return nil
|
|
}
|
|
if err := os.MkdirAll(destDir, 0o755); err != nil {
|
|
return fmt.Errorf("creating hostname dir: %w", err)
|
|
}
|
|
dest := destDir + "/hostname"
|
|
if err := os.WriteFile(dest, []byte(cfg.Hostname+"\n"), 0o644); err != nil {
|
|
return fmt.Errorf("writing persistent hostname: %w", err)
|
|
}
|
|
slog.Info("hostname saved", "path", dest)
|
|
return nil
|
|
}
|
|
|
|
func ensureHostsEntry(hostname string) error {
|
|
data, err := os.ReadFile("/etc/hosts")
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
|
|
content := string(data)
|
|
entry := "127.0.0.1 " + hostname
|
|
|
|
// Check if already present
|
|
for _, line := range strings.Split(content, "\n") {
|
|
if strings.Contains(line, hostname) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Append
|
|
if !strings.HasSuffix(content, "\n") && content != "" {
|
|
content += "\n"
|
|
}
|
|
content += entry + "\n"
|
|
|
|
return os.WriteFile("/etc/hosts", []byte(content), 0o644)
|
|
}
|