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>
41 lines
1.2 KiB
Go
41 lines
1.2 KiB
Go
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
|
|
"github.com/portainer/kubesolo-os/update/pkg/grubenv"
|
|
)
|
|
|
|
// Activate switches the boot target to the passive partition.
|
|
// After activation, the next reboot will boot from the new partition
|
|
// with boot_counter=3. If health checks fail 3 times, GRUB auto-rolls back.
|
|
func Activate(args []string) error {
|
|
opts := parseOpts(args)
|
|
env := grubenv.New(opts.GrubenvPath)
|
|
|
|
// Get passive slot (the one we want to boot into)
|
|
passiveSlot, err := env.PassiveSlot()
|
|
if err != nil {
|
|
return fmt.Errorf("reading passive slot: %w", err)
|
|
}
|
|
|
|
activeSlot, err := env.ActiveSlot()
|
|
if err != nil {
|
|
return fmt.Errorf("reading active slot: %w", err)
|
|
}
|
|
|
|
slog.Info("activating slot", "from", activeSlot, "to", passiveSlot)
|
|
|
|
// Set the passive slot as active with fresh boot counter
|
|
if err := env.ActivateSlot(passiveSlot); err != nil {
|
|
return fmt.Errorf("activating slot %s: %w", passiveSlot, err)
|
|
}
|
|
|
|
fmt.Printf("Slot %s activated (was %s)\n", passiveSlot, activeSlot)
|
|
fmt.Println("Boot counter set to 3. Reboot to start the new version.")
|
|
fmt.Println("The system will automatically roll back if health checks fail 3 times.")
|
|
|
|
return nil
|
|
}
|