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:
2026-02-11 10:39:05 -06:00
parent e372df578b
commit d900fa920e
17 changed files with 1217 additions and 12 deletions

156
docs/cloud-init.md Normal file
View File

@@ -0,0 +1,156 @@
# KubeSolo OS — Cloud-Init Configuration
KubeSolo OS uses a lightweight cloud-init system to configure the node on first boot. The configuration is a YAML file placed on the data partition before the first boot.
## Configuration File Location
The cloud-init config is loaded from (in priority order):
1. Path specified by `kubesolo.cloudinit=<path>` boot parameter
2. `/mnt/data/etc-kubesolo/cloud-init.yaml` (default)
## Boot Sequence Integration
Cloud-init runs as **init stage 45**, before network (stage 50) and hostname (stage 60). When cloud-init applies successfully, stages 50 and 60 detect this and skip their default behavior.
```
Stage 20: Mount persistent storage
Stage 30: Load kernel modules
Stage 40: Apply sysctl
Stage 45: Cloud-init (parse YAML, apply hostname + network + KubeSolo config) <--
Stage 50: Network fallback (skipped if cloud-init handled it)
Stage 60: Hostname fallback (skipped if cloud-init handled it)
Stage 70: Clock sync
Stage 80: Containerd prerequisites
Stage 90: Start KubeSolo
```
## YAML Schema
```yaml
# Hostname for the node
hostname: kubesolo-node
# Network configuration
network:
mode: dhcp | static # Default: dhcp
interface: eth0 # Optional: auto-detected if omitted
address: 192.168.1.100/24 # Required for static mode (CIDR notation)
gateway: 192.168.1.1 # Required for static mode
dns: # Optional: DNS nameservers
- 8.8.8.8
- 1.1.1.1
# KubeSolo settings
kubesolo:
extra-flags: "--disable traefik" # Extra CLI flags for KubeSolo binary
local-storage: true # Enable local-path provisioner (default: true)
apiserver-extra-sans: # Extra SANs for API server certificate
- node.example.com
- 10.0.0.50
# NTP servers (optional)
ntp:
servers:
- pool.ntp.org
# Air-gapped deployment (optional)
airgap:
import-images: true # Import container images from data partition
images-dir: /mnt/data/images # Directory containing .tar image files
# Portainer Edge Agent (optional)
portainer:
edge-agent:
enabled: true
edge-id: "your-edge-id"
edge-key: "your-edge-key"
portainer-url: "https://portainer.example.com"
```
## Network Modes
### DHCP (Default)
```yaml
network:
mode: dhcp
```
Uses BusyBox `udhcpc` on the first non-virtual interface. Optionally override DNS:
```yaml
network:
mode: dhcp
dns:
- 10.0.0.1
```
### Static IP
```yaml
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
```
Both `address` (CIDR) and `gateway` are required for static mode.
## Persistence
After applying, cloud-init saves its configuration to the data partition:
| File | Purpose |
|------|---------|
| `/mnt/data/network/interfaces.sh` | Shell script to restore network config on next boot |
| `/mnt/data/etc-kubesolo/hostname` | Saved hostname |
| `/etc/kubesolo/extra-flags` | KubeSolo CLI flags |
| `/etc/kubesolo/config.yaml` | KubeSolo configuration |
On subsequent boots, stage 50 (network) sources the saved `interfaces.sh` directly, skipping cloud-init parsing entirely. This is faster and doesn't require the cloud-init binary.
## CLI Usage
The cloud-init binary supports three commands:
```bash
# Apply configuration (run during boot by stage 45)
kubesolo-cloudinit apply /path/to/cloud-init.yaml
# Validate a config file
kubesolo-cloudinit validate /path/to/cloud-init.yaml
# Dump parsed config as JSON (for debugging)
kubesolo-cloudinit dump /path/to/cloud-init.yaml
```
## Examples
See `cloud-init/examples/` for complete configuration examples:
- `dhcp.yaml` — DHCP with defaults
- `static-ip.yaml` — Static IP configuration
- `portainer-edge.yaml` — Portainer Edge Agent integration
- `airgapped.yaml` — Air-gapped deployment with pre-loaded images
## Building
The cloud-init binary is built as part of the normal build process:
```bash
# Build just the cloud-init binary
make build-cloudinit
# Run cloud-init unit tests
make test-cloudinit
# Full build (includes cloud-init)
make iso
```
The binary is compiled as a static Linux/amd64 binary (`CGO_ENABLED=0`) and is approximately 2.7 MB.