Skip to content

docs: add control plane upgrade workflow design doc - #567

Draft
Philip Lombardi (plombardi89) wants to merge 1 commit into
mainfrom
docs/upgrade-workflow
Draft

docs: add control plane upgrade workflow design doc#567
Philip Lombardi (plombardi89) wants to merge 1 commit into
mainfrom
docs/upgrade-workflow

Conversation

@plombardi89

Copy link
Copy Markdown
Collaborator

Summary

Adds designs/upgrade-workflow.md, documenting 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.

This is descriptive of current behavior, not a proposal. Every file:line citation was verified against the source. Scope is deliberately limited to the operator and what it manages; the host unbounded-agent and 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 install be renamed to kubectl unbounded apply?
    The command is already idempotent and is already the upgrade path: nightly.yaml computes MODE=init|upgrade and runs install for both (.github/workflows/nightly.yaml:490), as does release-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 -f and does not behave like kubectl apply, so the analogy is partial. Cost is breadth - two workflows, hack/operator-upgrade-e2e/e2e.py:611, and six docs pages. Cobra Aliases offers a non-breaking path; the command is registered at cmd/kubectl-unbounded/app/cmd.go:29 and 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 resources explicitly (net/controller:123, net/node:100, machina:57, gantry:141 and :222, storage-supervisor:93), and the operator applies the full object with ForceOwnership (internal/operator/component/env.go:232-239), so resources is operator-owned. Reversion is near-immediate: ManagedWorkloadPredicate fires on generation change (internal/operator/component/watch.go:129-140), so a kubectl edit enqueues the reconcile that undoes it. No pause, opt-out, or unmanaged annotation exists in internal/operator/.
    Two precedents already exist but are not generalized: ConfigMaps are create-only and preserved (ensureConfig), and metalman.replicas is a typed workload-shape knob on the Site API. Relevant SSA detail: a field omitted from the applied configuration is not removed if another field manager owns it, so dropping resources from 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_REGISTRY in the ConfigMap, and the operator's own image:. There is no single override. Edited after applying, envFrom is read once at pod start, so it also needs kubectl rollout restart deployment/unbounded-operator.
    Related: UNBOUNDED_OPERATOR_IMAGE_REGISTRY ?= ghcr.io (Makefile:88) does not default from CONTAINER_REGISTRY ?= ghcr.io/azure (Makefile:13), and the release workflow passes only CONTAINER_REGISTRY (.github/workflows/release.yaml:805-809). With Config.Image hardcoding /azure/ (internal/operator/component/env.go:74-76), a fork publishing to ghcr.io/myorg gets an operator at ghcr.io/myorg/unbounded-operator:<TAG> resolving components to ghcr.io/azure/<component>:<TAG>. This works today only because the repo owner is azure. 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.

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.
Copilot AI review requested due to automatic review settings August 3, 2026 22:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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.
Comment on lines +141 to +142
Most components ship as rendered YAML compiled into the operator binary with
`go:embed` (`deploy/net/embed.go`, `deploy/machina/embed.go`,
@plombardi89

Copy link
Copy Markdown
Collaborator Author

Relatively minor but I think we should rename the command to apply and alias it to install. Then it can be used for both install and upgrade path.

@jveski

Copy link
Copy Markdown
Contributor

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be in scope? Could the operator use the underlying MachineOperations to orchestrate the agent upgrade?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants