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>
87 lines
2.1 KiB
Go
87 lines
2.1 KiB
Go
package health
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestStatusIsHealthy(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
status Status
|
|
wantHealth bool
|
|
}{
|
|
{
|
|
name: "all healthy",
|
|
status: Status{Containerd: true, APIServer: true, NodeReady: true},
|
|
wantHealth: true,
|
|
},
|
|
{
|
|
name: "containerd down",
|
|
status: Status{Containerd: false, APIServer: true, NodeReady: true},
|
|
wantHealth: false,
|
|
},
|
|
{
|
|
name: "apiserver down",
|
|
status: Status{Containerd: true, APIServer: false, NodeReady: true},
|
|
wantHealth: false,
|
|
},
|
|
{
|
|
name: "node not ready",
|
|
status: Status{Containerd: true, APIServer: true, NodeReady: false},
|
|
wantHealth: false,
|
|
},
|
|
{
|
|
name: "all down",
|
|
status: Status{Containerd: false, APIServer: false, NodeReady: false},
|
|
wantHealth: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := tt.status.IsHealthy(); got != tt.wantHealth {
|
|
t.Errorf("IsHealthy() = %v, want %v", got, tt.wantHealth)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewChecker(t *testing.T) {
|
|
// Test defaults
|
|
c := NewChecker("", "", 0)
|
|
if c.kubeconfigPath != "/var/lib/kubesolo/pki/admin/admin.kubeconfig" {
|
|
t.Errorf("unexpected default kubeconfig: %s", c.kubeconfigPath)
|
|
}
|
|
if c.apiServerAddr != "127.0.0.1:6443" {
|
|
t.Errorf("unexpected default apiserver addr: %s", c.apiServerAddr)
|
|
}
|
|
if c.timeout != 120*time.Second {
|
|
t.Errorf("unexpected default timeout: %v", c.timeout)
|
|
}
|
|
|
|
// Test custom values
|
|
c = NewChecker("/custom/kubeconfig", "10.0.0.1:6443", 30*time.Second)
|
|
if c.kubeconfigPath != "/custom/kubeconfig" {
|
|
t.Errorf("expected custom kubeconfig, got %s", c.kubeconfigPath)
|
|
}
|
|
if c.apiServerAddr != "10.0.0.1:6443" {
|
|
t.Errorf("expected custom addr, got %s", c.apiServerAddr)
|
|
}
|
|
if c.timeout != 30*time.Second {
|
|
t.Errorf("expected 30s timeout, got %v", c.timeout)
|
|
}
|
|
}
|
|
|
|
func TestStatusMessage(t *testing.T) {
|
|
s := &Status{
|
|
Containerd: true,
|
|
APIServer: true,
|
|
NodeReady: true,
|
|
Message: "all checks passed",
|
|
}
|
|
if s.Message != "all checks passed" {
|
|
t.Errorf("unexpected message: %s", s.Message)
|
|
}
|
|
}
|