Files
kubesolo-os/update/main.go
Adolfo Delorenzo 28de656b97
Some checks failed
ARM64 Build / Build generic ARM64 disk image (push) Failing after 4s
CI / Go Tests (push) Successful in 1m28s
CI / Shellcheck (push) Successful in 45s
CI / Build Go Binaries (amd64, linux, linux-amd64) (push) Successful in 1m17s
CI / Build Go Binaries (arm64, linux, linux-arm64) (push) Successful in 1m13s
feat(update): OCI registry distribution for update artifacts
Phase 7 of v0.3. The update agent can now pull update artifacts from any
OCI-compliant registry (ghcr.io, quay.io, harbor, zot, etc.) alongside the
existing HTTP latest.json protocol. Multi-arch artifacts are resolved
through manifest indexes so the same tag (e.g. "stable") yields the
right kernel + initramfs for runtime.GOARCH.

New package update/pkg/oci (~280 LOC, 9 tests):
- Client wraps oras-go/v2's remote.Repository. NewClient parses
  host/path references; WithPlainHTTP toggle for httptest.
- FetchMetadata resolves a tag and returns image.UpdateMetadata from
  manifest annotations (io.kubesolo.os.{version,channel,architecture,
  min_compatible_version,release_notes,release_date}). No blobs fetched.
- Pull resolves the tag, walks index → arch-specific manifest, downloads
  kernel + initramfs layers identified by their custom media types
  (application/vnd.kubesolo.os.kernel.v1+octet-stream and
  application/vnd.kubesolo.os.initramfs.v1+gzip), verifies their digests
  against the manifest, returns the same image.StagedImage shape the
  HTTP client produces.
- Cross-arch single-arch manifests are refused via the AnnotArch check
  (defense in depth on top of the gates in cmd/apply.go).
- Tests use a hand-rolled httptest registry implementing /v2/probe,
  manifest fetch by tag-or-digest, blob fetch by digest. Cover index
  arch-selection, single-arch manifests, missing-arch error, tampered
  blob rejection (digest mismatch), and reference parsing.

Dependencies added: oras.land/oras-go/v2 v2.6.0 plus its transitive
opencontainers/{go-digest,image-spec} and golang.org/x/sync. All small
and well-maintained; total binary size impact is negligible relative to
the existing 6.1 MB update agent.

cmd/apply.go:
- New --registry and --tag flags; mutually exclusive with --server.
- applyMetadataGates extracted as a helper, called from both transports
  so channel/arch/min-version policy is enforced identically regardless
  of how metadata was fetched.
- State transitions identical to the HTTP path: Checking → Downloading
  → Staged, with RecordError on any failure.

cmd/opts.go: --registry, --tag CLI flags. update.conf "server=" already
accepts either an HTTP URL or an OCI ref; the agent distinguishes by
which CLI/conf field carries the value.

build/scripts/push-oci-artifact.sh: new tool that publishes a single-arch
update artifact via the oras CLI with our custom media types and
annotations. After running for each arch, the operator composes the
multi-arch index with `oras manifest index create`. Documented inline.

build/Dockerfile.builder: installs oras 1.2.3 from upstream releases so
the Gitea Actions build container can run the new script.

Signature verification on the OCI path is intentionally deferred — the
artifact format is digest-verified end-to-end via oras-go, and Ed25519
signature consumption via OCI referrers is a follow-up. Plain HTTP
clients keep their existing signature path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 18:58:38 -06:00

103 lines
3.6 KiB
Go

// kubesolo-update is the atomic update agent for KubeSolo OS.
//
// It manages A/B partition updates with automatic rollback:
//
// kubesolo-update check Check for available updates
// kubesolo-update apply Download + write update to passive partition
// kubesolo-update activate Set passive partition as next boot target
// kubesolo-update rollback Force rollback to other partition
// kubesolo-update healthcheck Post-boot health verification
// kubesolo-update status Show current A/B slot and boot status
// kubesolo-update sign Sign update artifacts with Ed25519 key
// kubesolo-update genkey Generate new Ed25519 signing key pair
// kubesolo-update metrics Start Prometheus-compatible metrics server
package main
import (
"fmt"
"log/slog"
"os"
"github.com/portainer/kubesolo-os/update/cmd"
)
func main() {
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
Level: slog.LevelInfo,
})))
if len(os.Args) < 2 {
usage()
os.Exit(1)
}
var err error
switch os.Args[1] {
case "check":
err = cmd.Check(os.Args[2:])
case "apply":
err = cmd.Apply(os.Args[2:])
case "activate":
err = cmd.Activate(os.Args[2:])
case "rollback":
err = cmd.Rollback(os.Args[2:])
case "healthcheck":
err = cmd.Healthcheck(os.Args[2:])
case "status":
err = cmd.Status(os.Args[2:])
case "sign":
err = cmd.Sign(os.Args[2:])
case "genkey":
err = cmd.GenKey(os.Args[2:])
case "metrics":
err = cmd.Metrics(os.Args[2:])
default:
fmt.Fprintf(os.Stderr, "unknown command: %s\n\n", os.Args[1])
usage()
os.Exit(1)
}
if err != nil {
slog.Error("command failed", "command", os.Args[1], "error", err)
os.Exit(1)
}
}
func usage() {
fmt.Fprintf(os.Stderr, `Usage: kubesolo-update <command> [options]
Commands:
check Check for available updates
apply Download and write update to passive partition
activate Set passive partition as next boot target
rollback Force rollback to other partition
healthcheck Post-boot health verification (marks boot successful)
status Show current A/B slot and boot status
sign Sign artifacts with Ed25519 private key (build system)
genkey Generate new Ed25519 signing key pair
metrics Start Prometheus-compatible metrics HTTP server
Options:
--server URL HTTP update server (mutually exclusive with --registry)
--registry REPO OCI registry repository, e.g. ghcr.io/portainer/kubesolo-os
(mutually exclusive with --server)
--tag TAG OCI tag to pull (default: channel name, then "stable")
--conf PATH update.conf path (default: /etc/kubesolo/update.conf)
--state PATH Update state file (default: /var/lib/kubesolo/update/state.json)
--channel NAME Update channel (default: "stable", or value from update.conf)
--maintenance-window HH:MM-HH:MM local time window; apply refuses outside it
--force Bypass maintenance-window check
--grubenv PATH Path to grubenv file (default: /boot/grub/grubenv)
--timeout SECS Health check timeout in seconds (default: 120)
--pubkey PATH Ed25519 public key for signature verification (optional)
--json For 'status': emit JSON instead of human-readable output
Examples:
kubesolo-update apply --server https://updates.example.com
kubesolo-update apply --registry ghcr.io/portainer/kubesolo-os --tag stable
kubesolo-update apply --force # uses /etc/kubesolo/update.conf
kubesolo-update healthcheck
kubesolo-update status --json
`)
}