Files
kubesolo-os/update/cmd/check.go
Adolfo Delorenzo 8d25e1890e feat: add A/B partition updates with GRUB and Go update agent (Phase 3)
Implement atomic OS updates via A/B partition scheme with automatic
rollback. GRUB bootloader manages slot selection with a 3-attempt
boot counter that auto-rolls back on repeated health check failures.

GRUB boot config:
- A/B slot selection with boot_counter/boot_success env vars
- Automatic rollback when counter reaches 0 (3 failed boots)
- Debug, emergency shell, and manual slot-switch menu entries

Disk image (refactored):
- 4-partition GPT layout: EFI + System A + System B + Data
- GRUB EFI/BIOS installation with graceful fallbacks
- Both system partitions populated during image creation

Update agent (Go, zero external deps):
- pkg/grubenv: read/write GRUB env vars (grub-editenv + manual fallback)
- pkg/partition: find/mount/write system partitions by label
- pkg/image: HTTP download with SHA256 verification
- pkg/health: post-boot checks (containerd, API server, node Ready)
- 6 CLI commands: check, apply, activate, rollback, healthcheck, status
- 37 unit tests across all 4 packages

Deployment:
- K8s CronJob for automatic update checks (every 6 hours)
- ConfigMap for update server URL
- Health check Job for post-boot verification

Build pipeline:
- build-update-agent.sh compiles static Linux binary (~5.9 MB)
- inject-kubesolo.sh includes update agent in initramfs
- Makefile: build-update-agent, test-update-agent, test-update targets

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 11:12:46 -06:00

66 lines
1.7 KiB
Go

package cmd
import (
"fmt"
"log/slog"
"github.com/portainer/kubesolo-os/update/pkg/grubenv"
"github.com/portainer/kubesolo-os/update/pkg/image"
"github.com/portainer/kubesolo-os/update/pkg/partition"
)
// Check queries the update server for available updates and compares
// against the currently running version.
func Check(args []string) error {
opts := parseOpts(args)
if opts.ServerURL == "" {
return fmt.Errorf("--server is required (no default update server configured)")
}
// Get current version from active partition
env := grubenv.New(opts.GrubenvPath)
activeSlot, err := env.ActiveSlot()
if err != nil {
return fmt.Errorf("reading active slot: %w", err)
}
partInfo, err := partition.GetSlotPartition(activeSlot)
if err != nil {
return fmt.Errorf("finding active partition: %w", err)
}
mountPoint := "/tmp/kubesolo-check-" + activeSlot
if err := partition.MountReadOnly(partInfo.Device, mountPoint); err != nil {
return fmt.Errorf("mounting active partition: %w", err)
}
defer partition.Unmount(mountPoint)
currentVersion, err := partition.ReadVersion(mountPoint)
if err != nil {
slog.Warn("could not read current version", "error", err)
currentVersion = "unknown"
}
// Check update server
client := image.NewClient(opts.ServerURL, "")
meta, err := client.CheckForUpdate()
if err != nil {
return fmt.Errorf("checking for update: %w", err)
}
fmt.Printf("Current version: %s (slot %s)\n", currentVersion, activeSlot)
fmt.Printf("Latest version: %s\n", meta.Version)
if meta.Version == currentVersion {
fmt.Println("Status: up to date")
} else {
fmt.Println("Status: update available")
if meta.ReleaseNotes != "" {
fmt.Printf("Release notes: %s\n", meta.ReleaseNotes)
}
}
return nil
}