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>
This commit is contained in:
56
update/cmd/healthcheck.go
Normal file
56
update/cmd/healthcheck.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/kubesolo-os/update/pkg/grubenv"
|
||||
"github.com/portainer/kubesolo-os/update/pkg/health"
|
||||
)
|
||||
|
||||
// Healthcheck performs post-boot health verification.
|
||||
// If all checks pass, it marks the boot as successful in GRUB.
|
||||
// This should be run after every boot (typically via a systemd unit or
|
||||
// init script) to confirm the system is healthy.
|
||||
func Healthcheck(args []string) error {
|
||||
opts := parseOpts(args)
|
||||
env := grubenv.New(opts.GrubenvPath)
|
||||
|
||||
// Check if already marked successful
|
||||
success, err := env.BootSuccess()
|
||||
if err != nil {
|
||||
slog.Warn("could not read boot_success", "error", err)
|
||||
}
|
||||
if success {
|
||||
fmt.Println("Boot already marked successful")
|
||||
return nil
|
||||
}
|
||||
|
||||
timeout := time.Duration(opts.TimeoutSecs) * time.Second
|
||||
checker := health.NewChecker("", "", timeout)
|
||||
|
||||
slog.Info("running post-boot health checks", "timeout", timeout)
|
||||
|
||||
status, err := checker.WaitForHealthy()
|
||||
if err != nil {
|
||||
fmt.Printf("Health check FAILED: %s\n", status.Message)
|
||||
fmt.Printf(" containerd: %v\n", status.Containerd)
|
||||
fmt.Printf(" apiserver: %v\n", status.APIServer)
|
||||
fmt.Printf(" node_ready: %v\n", status.NodeReady)
|
||||
fmt.Println("\nBoot NOT marked successful — system may roll back on next reboot")
|
||||
return err
|
||||
}
|
||||
|
||||
// Mark boot as successful
|
||||
if err := env.MarkBootSuccess(); err != nil {
|
||||
return fmt.Errorf("marking boot success: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Health check PASSED — boot marked successful")
|
||||
fmt.Printf(" containerd: %v\n", status.Containerd)
|
||||
fmt.Printf(" apiserver: %v\n", status.APIServer)
|
||||
fmt.Printf(" node_ready: %v\n", status.NodeReady)
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user