Files
kubesolo-os/cloud-init/parser_test.go
Adolfo Delorenzo d900fa920e 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>
2026-02-11 10:39:05 -06:00

239 lines
5.3 KiB
Go

package cloudinit
import (
"testing"
)
func TestParseDHCP(t *testing.T) {
yaml := []byte(`
hostname: test-node
network:
mode: dhcp
kubesolo:
local-storage: true
`)
cfg, err := ParseBytes(yaml)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cfg.Hostname != "test-node" {
t.Errorf("hostname = %q, want %q", cfg.Hostname, "test-node")
}
if cfg.Network.Mode != "dhcp" {
t.Errorf("network.mode = %q, want %q", cfg.Network.Mode, "dhcp")
}
}
func TestParseStatic(t *testing.T) {
yaml := []byte(`
hostname: edge-01
network:
mode: static
interface: eth0
address: 192.168.1.100/24
gateway: 192.168.1.1
dns:
- 8.8.8.8
- 8.8.4.4
kubesolo:
extra-flags: "--disable traefik"
local-storage: true
apiserver-extra-sans:
- edge-01.local
`)
cfg, err := ParseBytes(yaml)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cfg.Hostname != "edge-01" {
t.Errorf("hostname = %q, want %q", cfg.Hostname, "edge-01")
}
if cfg.Network.Mode != "static" {
t.Errorf("network.mode = %q, want %q", cfg.Network.Mode, "static")
}
if cfg.Network.Address != "192.168.1.100/24" {
t.Errorf("network.address = %q, want %q", cfg.Network.Address, "192.168.1.100/24")
}
if cfg.Network.Gateway != "192.168.1.1" {
t.Errorf("network.gateway = %q, want %q", cfg.Network.Gateway, "192.168.1.1")
}
if len(cfg.Network.DNS) != 2 {
t.Fatalf("dns count = %d, want 2", len(cfg.Network.DNS))
}
if cfg.Network.DNS[0] != "8.8.8.8" {
t.Errorf("dns[0] = %q, want %q", cfg.Network.DNS[0], "8.8.8.8")
}
if cfg.KubeSolo.ExtraFlags != "--disable traefik" {
t.Errorf("extra-flags = %q, want %q", cfg.KubeSolo.ExtraFlags, "--disable traefik")
}
if len(cfg.KubeSolo.ExtraSANs) != 1 || cfg.KubeSolo.ExtraSANs[0] != "edge-01.local" {
t.Errorf("extra-sans = %v, want [edge-01.local]", cfg.KubeSolo.ExtraSANs)
}
}
func TestParseDefaultMode(t *testing.T) {
yaml := []byte(`
hostname: default-node
`)
cfg, err := ParseBytes(yaml)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cfg.Network.Mode != "dhcp" {
t.Errorf("network.mode = %q, want %q (default)", cfg.Network.Mode, "dhcp")
}
}
func TestParseStaticMissingAddress(t *testing.T) {
yaml := []byte(`
network:
mode: static
gateway: 192.168.1.1
`)
_, err := ParseBytes(yaml)
if err == nil {
t.Fatal("expected error for static mode without address")
}
}
func TestParseStaticMissingGateway(t *testing.T) {
yaml := []byte(`
network:
mode: static
address: 192.168.1.100/24
`)
_, err := ParseBytes(yaml)
if err == nil {
t.Fatal("expected error for static mode without gateway")
}
}
func TestParseUnknownMode(t *testing.T) {
yaml := []byte(`
network:
mode: ppp
`)
_, err := ParseBytes(yaml)
if err == nil {
t.Fatal("expected error for unknown network mode")
}
}
func TestParseAirgap(t *testing.T) {
yaml := []byte(`
hostname: airgap-node
network:
mode: static
address: 10.0.0.50/24
gateway: 10.0.0.1
airgap:
import-images: true
images-dir: /mnt/data/images
`)
cfg, err := ParseBytes(yaml)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !cfg.Airgap.ImportImages {
t.Error("airgap.import-images should be true")
}
if cfg.Airgap.ImagesDir != "/mnt/data/images" {
t.Errorf("airgap.images-dir = %q, want %q", cfg.Airgap.ImagesDir, "/mnt/data/images")
}
}
func TestParsePortainer(t *testing.T) {
yaml := []byte(`
hostname: edge-node
network:
mode: dhcp
portainer:
edge-agent:
enabled: true
edge-id: test-id
edge-key: test-key
portainer-url: https://portainer.example.com
`)
cfg, err := ParseBytes(yaml)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !cfg.Portainer.EdgeAgent.Enabled {
t.Error("portainer.edge-agent.enabled should be true")
}
if cfg.Portainer.EdgeAgent.EdgeID != "test-id" {
t.Errorf("edge-id = %q, want %q", cfg.Portainer.EdgeAgent.EdgeID, "test-id")
}
if cfg.Portainer.EdgeAgent.PortainerURL != "https://portainer.example.com" {
t.Errorf("portainer-url = %q", cfg.Portainer.EdgeAgent.PortainerURL)
}
}
func TestParseNTP(t *testing.T) {
yaml := []byte(`
hostname: ntp-node
ntp:
servers:
- pool.ntp.org
- time.google.com
`)
cfg, err := ParseBytes(yaml)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(cfg.NTP.Servers) != 2 {
t.Fatalf("ntp.servers count = %d, want 2", len(cfg.NTP.Servers))
}
if cfg.NTP.Servers[0] != "pool.ntp.org" {
t.Errorf("ntp.servers[0] = %q, want %q", cfg.NTP.Servers[0], "pool.ntp.org")
}
}
func TestParseBoolPointer(t *testing.T) {
yaml := []byte(`
kubesolo:
local-storage: false
`)
cfg, err := ParseBytes(yaml)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cfg.KubeSolo.LocalStorage == nil {
t.Fatal("local-storage should not be nil")
}
if *cfg.KubeSolo.LocalStorage {
t.Error("local-storage should be false")
}
}
func TestParseEmptyConfig(t *testing.T) {
yaml := []byte(``)
cfg, err := ParseBytes(yaml)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cfg.Network.Mode != "dhcp" {
t.Errorf("empty config should default to dhcp, got %q", cfg.Network.Mode)
}
}
func TestParseExampleFiles(t *testing.T) {
examples := []string{
"examples/dhcp.yaml",
"examples/static-ip.yaml",
"examples/portainer-edge.yaml",
"examples/airgapped.yaml",
}
for _, path := range examples {
t.Run(path, func(t *testing.T) {
_, err := Parse(path)
if err != nil {
t.Errorf("failed to parse %s: %v", path, err)
}
})
}
}