Skip to content

Consolidate components into a single unbounded-system namespace with operator-driven migration#383

Open
plombardi89 wants to merge 10 commits into
mainfrom
unbounded-operator-poc
Open

Consolidate components into a single unbounded-system namespace with operator-driven migration#383
plombardi89 wants to merge 10 commits into
mainfrom
unbounded-operator-poc

Conversation

@plombardi89

Copy link
Copy Markdown
Collaborator

Problem

Unbounded spreads its components across several namespaces: machina, metalman, and unbounded-storage land in unbounded-kube, unbounded-net lands in unbounded-net, and the operator itself runs in unbounded-kube (orca, inventory, machine-ops, and gantry add still more). The split is historical rather than meaningful. It fragments RBAC, namespace-scoped discovery, NetworkPolicy, and day-2 operations, and there is no single namespace that represents "Unbounded" on a cluster.

This PR consolidates everything - the operator and every component it manages - onto one configurable namespace, unbounded-system, and provides a safe, automated path to move existing clusters (already running the split layout) onto it. Kubernetes namespaces cannot be renamed in place, so existing clusters need a deliberate migration, not a config flip.

Components are deployed by unbounded-operator, which reconciles Site.spec.components into the corresponding workloads, so the operator is the natural owner of the migration.

Scope note: this branch is the delivery vehicle for the unbounded-operator / Site model and the cross-component namespace unification it builds on (neither merged independently), so the diff includes those alongside the consolidation and migration described here.

What this PR delivers

  1. A single namespace for fresh installs: all components, plus the operator, default to unbounded-system.
  2. Operator-driven migration for existing clusters: it moves component state and workloads onto unbounded-system and decommissions the operator-owned resources left behind in the old namespaces.

Single-namespace model

  • internal/unbounded.SystemNamespace is the Go source of truth for the default namespace; binary defaults and fallbacks derive from it.
  • The Makefile UNBOUNDED_NAMESPACE variable (default unbounded-system) feeds every per-component *_NAMESPACE variable, and manifest templates default via {{ default "unbounded-system" .Namespace }}. One override moves everything; individual components can still be overridden.
  • A drift-guard test renders each component's namespace template with no override and asserts the result equals unbounded.SystemNamespace, so the Go constant and the manifest defaults cannot silently diverge.
  • net keeps its own --net-namespace knob for operators who want it isolated, but it now defaults to the shared namespace.
  • Genuine kube-system / kube-public reads (bootstrap tokens, cluster-info, kube-proxy) are deliberately left untouched.

Because the operator's namespace constants now equal the namespace baked into the manifests it applies, the operator's namespace rewrite is a no-op on default installs and only engages when an operator overrides the namespace.

Migration workflow

Migration runs either continuously (start the operator with --reap-legacy-resources, off by default) or as a one-shot (unbounded-operator migrate-legacy, suitable for a Job). Both run the same idempotent routine; the one-shot variant exits once the migration is complete. Per pass:

  1. Copy non-regenerable state from each legacy namespace (unbounded-kube, unbounded-net) into unbounded-system: operator/user Secrets and the machina-config ConfigMap. Server-managed metadata is stripped, existing target copies are never overwritten, and resources that are regenerated or auto-managed are skipped (the net serving cert, ServiceAccount tokens, Helm release secrets).
  2. Rewrite cluster-scoped secret references so they point at the moved secrets: Machine.spec.pxe.redfish.passwordRef.namespace and MachineOperationCredential.spec.auth.secretRef.namespace.
  3. Reap operator-owned resources per component: once a component's target workloads report healthy in unbounded-system, delete the resources it left behind in the legacy namespace. Components are matched by the label each manifest actually carries, and deletion is scoped to those labels, so unrelated workloads sharing the namespace are never touched. net is reaped last, because removing the old net data plane before the new one is Ready is briefly disruptive and that cutover should happen after everything else has moved. A component with no legacy footprint is skipped, so the routine never blocks on a workload that will never exist.

The operator never deletes the legacy Namespace objects. Deleting a shared namespace is the one irreversible, broad-blast-radius action in the flow (it cascades to any not-yet-migrated workloads and their data), so it is left to a human once the namespaces are confirmed empty:

kubectl delete namespace unbounded-kube unbounded-net

Every step is create-if-absent / patch / delete-if-exists, so an interrupted run is safe to re-run to completion.

Safety properties

  • The operator needs no namespaces: delete permission; it deletes only the component kinds it owns.
  • Reaping is gated on target health, scoped to operator-owned labels, and idempotent, so a stale cache read can at worst re-delete an already-gone resource on the next pass, never a namespace.
  • Non-regenerable user state (e.g. BMC password secrets referenced by Machines) is copied and its references repointed before the old copies are abandoned.

Testing

  • internal/operator/migrate_test.go covers the copy/skip rules, idempotency, the cluster-scoped reference rewrites, health-gating, label-scoped reaping, the no-footprint skip, and the invariant that the operator never issues a namespace deletion.
  • operator-reap in hack/smoke-namespace-migration.py is an end-to-end kind test: it stages a legacy split-namespace install plus operator-owned state and cluster-scoped CRs, stands up a neutralized unbounded-system target, runs migrate-legacy, and asserts the full end state (legacy workloads reaped; secrets and config copied while regenerable/auto-managed are skipped; apiServerEndpoint preserved; CR references rewritten; target workloads intact; legacy namespaces retained; idempotency on re-run). The standalone migrate-namespace.sh path (air-gapped / non-operator) is covered by the existing scenarios. The smoke workflow now also triggers on operator source changes.

Reviewer guide

  • internal/operator/migrate.go - the migration routine (copy, rewrite, health-gate, reap).
  • cmd/unbounded-operator/main.go - the --reap-legacy-resources flag and the migrate-legacy subcommand.
  • deploy/unbounded-operator/02-rbac.yaml.tmpl - operator permissions (note: no namespace delete).
  • internal/unbounded/ - the namespace source of truth and drift-guard.
  • hack/smoke-namespace-migration.py - the end-to-end coverage.

Out of scope / follow-ups

  • The final kubectl delete namespace of the emptied legacy namespaces is intentionally manual.
  • Continuous reaping is an explicit operator opt-in (--reap-legacy-resources); it is not enabled by default.

jveski and others added 10 commits June 25, 2026 17:09
Fold the namespace-unification foundation from #372 onto the operator
(shared-site) line:

- internal/unbounded.SystemNamespace as the Go-side source of truth,
  with the drift-guard test extended to cover the unbounded-operator
  and storage-supervisor namespace templates.
- Makefile UNBOUNDED_NAMESPACE (default unbounded-system) feeding every
  per-component *_NAMESPACE var, including UNBOUNDED_OPERATOR_NAMESPACE.
- Flip the unbounded-operator manifest template defaults
  (namespace + --net-namespace) from unbounded-kube/unbounded-net to
  unbounded-system.
- Carry in #372's component templatization (gantry, inventory, orca,
  machine-ops, storage), scripts, smoke test, and migrate-namespace.sh.

Conflicts with the operator redesign resolved in favor of the operator
model: the deleted kubectl install paths stay deleted and the stale
ensureMachinaIsRunning helper is dropped.
Point the operator and kubectl install Go defaults at
unbounded.SystemNamespace:

- internal/operator: DefaultNamespace and DefaultNetNamespace both
  resolve to unbounded-system (net keeps a distinct knob via
  --net-namespace).
- cmd/unbounded-operator: --leader-elect-namespace default.
- kubectl install: machinaNamespace and netNamespace constants.

Because the Go defaults now equal the rendered manifest namespace, the
operator's namespace string-rewrite is an inert no-op on default
installs and only engages for explicit overrides.
Add internal/operator/migrate.go: a leader-elected LegacyReaper Runnable,
gated behind --reap-legacy-resources (default off), that consolidates an
existing split-namespace install onto the unified namespace:

- copies non-regenerable state (user/operator Secrets, the machina-config
  ConfigMap) from unbounded-kube/unbounded-net into the target namespace,
  skipping auto-managed (SA-token, helm) and regenerable (net serving
  cert) secrets and never clobbering existing target copies;
- rewrites the namespace embedded in cluster-scoped secret references
  (Machine.spec.pxe.redfish.passwordRef, MachineOperationCredential
  spec.auth.secretRef);
- gates each component on its target workloads being healthy, reaps net
  last, and deletes only operator-owned resources (matched by
  app.kubernetes.io/name) in the legacy namespaces;
- NEVER deletes the legacy Namespace objects: an operator removes those
  manually once empty.

Grant the operator ClusterRole delete on the component kinds (not on
namespaces) plus machine/machineoperationcredential patch for the secret
ref rewrites. Idempotent throughout; unit tests cover copy/skip,
idempotency, ref rewrite, health-gating, label-scoped reaping, and the
namespace-preservation invariant.
- release-upgrade.yaml: rollout waits, image summaries, and the
  orca-credentials secret check now target unbounded-system (this PR is
  the release vehicle, so the #374 follow-up is folded in here).
- cli.md: document the unbounded-system defaults and a migration runbook
  for the operator-driven --reap-legacy-resources path plus the manual
  'kubectl delete ns' step.
- migrate-namespace.sh: note the operator-driven path as preferred for
  operator-managed clusters.
Make the legacy migration testable end-to-end and fix two reaper gaps
surfaced by the e2e:

- Add a one-shot 'unbounded-operator migrate-legacy' subcommand that runs
  the same migrate-then-reap logic to completion and exits (suitable for a
  migration Job and for deterministic e2e). Refactor the reaper loop into
  RunToCompletion, shared by the always-on Runnable and the subcommand.
- Reap by the labels components actually carry: machina uses 'app', while
  net/storage/metalman use 'app.kubernetes.io/name' (machina would have
  been missed before). Selectors are now per-component.
- Skip components with no legacy footprint so the reaper never blocks on a
  target workload that will never exist (e.g. storage never installed).

Extend hack/smoke-namespace-migration.py with an 'operator-reap' kind
scenario that stages a legacy install plus a neutralized unbounded-system
target, runs 'migrate-legacy', and asserts: legacy operator-owned
workloads reaped, secrets/machina-config copied (serving-cert and
SA-token skipped), apiServerEndpoint preserved, cluster-scoped CR secret
refs rewritten, target workloads intact, legacy namespaces retained, and
idempotency on re-run. Wire the operator paths into the smoke workflow
triggers.

Validated on a real kind cluster (operator-reap and happy scenarios both
pass).
@plombardi89 plombardi89 requested a review from a team June 30, 2026 20:03
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.

2 participants