docs: add control plane upgrade workflow design doc - #567
docs: add control plane upgrade workflow design doc#567Philip Lombardi (plombardi89) wants to merge 1 commit into
Conversation
Document how the Unbounded control plane is upgraded: the unbounded-operator and the five components it manages (net, machina, gantry, metalman, storage), plus the operator-owned CRDs. Covers the version model, image resolution, embedded manifests, server-side apply semantics, CRD bootstrap ordering, config preservation and config-hash driven rollouts, per-workload rollout strategies, verification, and the design constraints that follow from the model. Descriptive of current behavior rather than a proposal. The host-resident unbounded-agent and node repave are separate update planes and are noted as out of scope.
There was a problem hiding this comment.
🟢 Ready to approve
The change is documentation-only and the identified issues are minor wording/accuracy fixes that don’t affect implementation behavior.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Pull request overview
Adds a new design document describing the current Unbounded control plane upgrade mechanism driven by unbounded-operator, including version/image stamping, CRD bootstrap/maintenance, apply semantics (SSA + ForceOwnership), rollout behaviors, and verification gates.
Changes:
- Documented the “operator is the upgrade unit” model and its implications (lockstep upgrades, no per-component versions).
- Detailed mechanics: image resolution, embedded manifests, SSA semantics, CRD lifecycle, and config-hash-driven rollouts.
- Summarized rollout strategies and availability characteristics per managed workload, plus CI upgrade gates and release artifacts.
File summaries
| File | Description |
|---|---|
| designs/upgrade-workflow.md | New design doc capturing current operator-managed upgrade workflow, constraints, and verification/rollout behavior. |
Review details
- Files reviewed: 1/1 changed files
- Comments generated: 2
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
| A release tag produces the inputs this workflow consumes (`Makefile:1387-1425`): | ||
|
|
||
| - `build/unbounded-operator-<VERSION>.yaml`, the rendered operator manifests | ||
| concatenated into one directly appliable file. |
| Most components ship as rendered YAML compiled into the operator binary with | ||
| `go:embed` (`deploy/net/embed.go`, `deploy/machina/embed.go`, |
|
Relatively minor but I think we should rename the command to |
|
I think most of the current ambiguity comes from the unbounded-agent and its nspawn containers being separate from the operator workflow. I don't have a sense for how much surface area there is between agent and non-agent (operator-managed?) components. So my only concerns are around regressions within that surface, otherwise this LGTM. Scoping out the agent here is fair but I think it deserves a separate doc and maybe n-1 backcompat style e2e tests. |
| The operator also owns twelve CRDs, six in the `unbounded-cloud.io` group and six | ||
| in `net.unbounded-cloud.io` (`internal/operator/bootstrap.go:49-62`). | ||
|
|
||
| **Out of scope for this document.** The host-resident `unbounded-agent` binary |
There was a problem hiding this comment.
Should this be in scope? Could the operator use the underlying MachineOperations to orchestrate the agent upgrade?
There was a problem hiding this comment.
I think it's conceptually simpler for a user to just have manage upgrades from the operator interface/config. Not sure how feasible that is for agent though.
|
|
||
| ```go | ||
| func (c Config) Image(repository string) string { | ||
| return strings.TrimRight(c.ImageRegistry, "/") + "/azure/" + repository + ":" + c.ImageTag |
There was a problem hiding this comment.
Is /azure/ just an requirement because unbounded is in the Azure org currently?
| Once the operator applies the new images, Kubernetes drives each rollout using | ||
| that workload's own strategy. These differ meaningfully. | ||
|
|
||
| | Workload | Strategy | Availability during upgrade | |
There was a problem hiding this comment.
I think these may need to be tunable per cluster. For example, with huge clusters, running a daemonset upgrade one pod at a time may not complete in a reasonable amount of time so might need to bump up maxUnavailable/maxSure. Some folks might want pit stop vs rolling upgrades too to minimize impact while updates apply.
Summary
Adds
designs/upgrade-workflow.md, documenting how the Unbounded control plane is upgraded: theunbounded-operatorand the five components it manages (net,machina,gantry,metalman,storage), plus the operator-owned CRDs.This is descriptive of current behavior, not a proposal. Every
file:linecitation was verified against the source. Scope is deliberately limited to the operator and what it manages; the hostunbounded-agentand node repave are separate update planes and are called out as out of scope.Covers: the version model (operator version is the fleet version), image resolution, embedded manifests, SSA with
ForceOwnership, CRD bootstrap ordering, config preservation and config-hash-driven rollouts, per-workload rollout strategies and their availability implications, verification, and six design constraints.Review checklist
Three items I'd like the team to weigh in on. Each states current behavior with citations; none has a recommendation attached.
Should
kubectl unbounded installbe renamed tokubectl unbounded apply?The command is already idempotent and is already the upgrade path:
nightly.yamlcomputesMODE=init|upgradeand runsinstallfor both (.github/workflows/nightly.yaml:490), as doesrelease-upgrade.yaml:361. It preserves existing config across re-runs (cmd/kubectl-unbounded/app/install.go:284-338), and it no longer installs CRDs since the operator owns them (install.go:140-143). Against: it takes no-fand does not behave likekubectl apply, so the analogy is partial. Cost is breadth - two workflows,hack/operator-upgrade-e2e/e2e.py:611, and six docs pages. CobraAliasesoffers a non-breaking path; the command is registered atcmd/kubectl-unbounded/app/cmd.go:29and declares no aliases today.Do we need a mechanism to preserve user customizations of managed workloads (e.g. custom requests/limits)?
Today there is no way to do this. Every managed manifest sets
resourcesexplicitly (net/controller:123,net/node:100,machina:57,gantry:141and:222,storage-supervisor:93), and the operator applies the full object withForceOwnership(internal/operator/component/env.go:232-239), soresourcesis operator-owned. Reversion is near-immediate:ManagedWorkloadPredicatefires on generation change (internal/operator/component/watch.go:129-140), so akubectl editenqueues the reconcile that undoes it. No pause, opt-out, or unmanaged annotation exists ininternal/operator/.Two precedents already exist but are not generalized: ConfigMaps are create-only and preserved (
ensureConfig), andmetalman.replicasis a typed workload-shape knob on theSiteAPI. Relevant SSA detail: a field omitted from the applied configuration is not removed if another field manager owns it, so droppingresourcesfrom the embedded manifests would make user-set values stick.How does a user set the registry override when installing or upgrading via plain
kubectl apply?Currently two separate edits to
unbounded-operator-<TAG>.yaml:UNBOUNDED_IMAGE_REGISTRYin the ConfigMap, and the operator's ownimage:. There is no single override. Edited after applying,envFromis read once at pod start, so it also needskubectl rollout restart deployment/unbounded-operator.Related:
UNBOUNDED_OPERATOR_IMAGE_REGISTRY ?= ghcr.io(Makefile:88) does not default fromCONTAINER_REGISTRY ?= ghcr.io/azure(Makefile:13), and the release workflow passes onlyCONTAINER_REGISTRY(.github/workflows/release.yaml:805-809). WithConfig.Imagehardcoding/azure/(internal/operator/component/env.go:74-76), a fork publishing toghcr.io/myorggets an operator atghcr.io/myorg/unbounded-operator:<TAG>resolving components toghcr.io/azure/<component>:<TAG>. This works today only because the repo owner isazure. There is no Helm or Kustomize for Unbounded, so GitOps users have no standard patch mechanism.Notes
Documentation only - no code, manifest, or workflow changes.