Files
kubesolo-os/update/cmd/apply.go
Adolfo Delorenzo 49a37e30e8 feat: add production hardening — Ed25519 signing, Portainer Edge, SSH extension (Phase 4)
Image signing:
- Ed25519 sign/verify package (pure Go stdlib, zero deps)
- genkey and sign CLI subcommands for build system
- Optional --pubkey flag for verifying updates on apply
- Signature URLs in update metadata (latest.json)

Portainer Edge Agent:
- cloud-init portainer.go module writes K8s manifest
- Auto-deploys Edge Agent when portainer.edge-agent.enabled
- Full RBAC (ServiceAccount, ClusterRoleBinding, Deployment)
- 5 Portainer tests in portainer_test.go

Production tooling:
- SSH debug extension builder (hack/build-ssh-extension.sh)
- Boot performance benchmark (test/benchmark/bench-boot.sh)
- Resource usage benchmark (test/benchmark/bench-resources.sh)
- Deployment guide (docs/deployment-guide.md)

Test results: 50 update agent tests + 22 cloud-init tests passing.

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

77 lines
2.1 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"
)
// 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 := grubenv.New(opts.GrubenvPath)
// 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
}