Security hardening: bind kubeconfig server to localhost, mount hardening (noexec/nosuid/nodev on tmpfs), sysctl network hardening, kernel module loading lock after boot, SHA256 checksum verification for downloads, kernel AppArmor + Audit support, complain-mode AppArmor profiles for containerd and kubelet, and security integration test. ARM64 Raspberry Pi support: piCore64 base extraction, RPi kernel build from raspberrypi/linux fork, RPi firmware fetch, SD card image with 4- partition GPT and tryboot A/B mechanism, BootEnv Go interface abstracting GRUB vs RPi boot environments, architecture-aware build scripts, QEMU aarch64 dev VM and boot test. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
76 lines
2.1 KiB
Go
76 lines
2.1 KiB
Go
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
|
|
"github.com/portainer/kubesolo-os/update/pkg/image"
|
|
"github.com/portainer/kubesolo-os/update/pkg/partition"
|
|
)
|
|
|
|
// Apply downloads a new OS image and writes it to the passive partition.
|
|
// It does NOT activate the new partition — use 'activate' for that.
|
|
func Apply(args []string) error {
|
|
opts := parseOpts(args)
|
|
|
|
if opts.ServerURL == "" {
|
|
return fmt.Errorf("--server is required")
|
|
}
|
|
|
|
env := opts.NewBootEnv()
|
|
|
|
// Determine passive slot
|
|
passiveSlot, err := env.PassiveSlot()
|
|
if err != nil {
|
|
return fmt.Errorf("reading passive slot: %w", err)
|
|
}
|
|
|
|
slog.Info("applying update", "target_slot", passiveSlot)
|
|
|
|
// Check for update
|
|
stageDir := "/tmp/kubesolo-update-stage"
|
|
client := image.NewClient(opts.ServerURL, stageDir)
|
|
defer client.Cleanup()
|
|
|
|
// Enable signature verification if public key is configured
|
|
if opts.PubKeyPath != "" {
|
|
client.SetPublicKeyPath(opts.PubKeyPath)
|
|
slog.Info("signature verification enabled", "pubkey", opts.PubKeyPath)
|
|
}
|
|
|
|
meta, err := client.CheckForUpdate()
|
|
if err != nil {
|
|
return fmt.Errorf("checking for update: %w", err)
|
|
}
|
|
|
|
slog.Info("update available", "version", meta.Version)
|
|
|
|
// Download and verify
|
|
staged, err := client.Download(meta)
|
|
if err != nil {
|
|
return fmt.Errorf("downloading update: %w", err)
|
|
}
|
|
|
|
// Mount passive partition
|
|
partInfo, err := partition.GetSlotPartition(passiveSlot)
|
|
if err != nil {
|
|
return fmt.Errorf("finding passive partition: %w", err)
|
|
}
|
|
|
|
mountPoint := "/tmp/kubesolo-passive-" + passiveSlot
|
|
if err := partition.MountReadWrite(partInfo.Device, mountPoint); err != nil {
|
|
return fmt.Errorf("mounting passive partition: %w", err)
|
|
}
|
|
defer partition.Unmount(mountPoint)
|
|
|
|
// Write image to passive partition
|
|
if err := partition.WriteSystemImage(mountPoint, staged.VmlinuzPath, staged.InitramfsPath, staged.Version); err != nil {
|
|
return fmt.Errorf("writing system image: %w", err)
|
|
}
|
|
|
|
fmt.Printf("Update v%s written to slot %s (%s)\n", staged.Version, passiveSlot, partInfo.Device)
|
|
fmt.Println("Run 'kubesolo-update activate' to boot into the new version")
|
|
|
|
return nil
|
|
}
|