Adds cutover-step-11-crossplane-provider-pivot, modelled on step 10's
two-phase pattern, that rewrites every `pkg.crossplane.io/v1.Provider`
CR's `spec.package` host literal from `xpkg.upbound.io/...` to
`harbor.<SOVEREIGN_FQDN>/proxy-xpkg/...` and pushes the same edit to
local Gitea so the bootstrap-kit Kustomization reconcile doesn't
revert the live patch.
Why Step 04 (containerd registries.yaml.v2 mirror) does NOT cover this
even though it registers `xpkg.upbound.io → harbor.<sov>/proxy-xpkg`:
Crossplane's package manager uses `go-containerregistry`'s
`remote.Image()` DIRECTLY from inside the `crossplane-system`
controller Pod (source: `internal/xpkg/fetch.go`), NOT through the
kubelet/containerd CRI client. Containerd mirror config is irrelevant
to it. The ONLY way to redirect Provider package fetches is to
rewrite each Provider's `spec.package` host literal.
The bootstrap-kit ships THREE Provider CRs all carrying the upstream
xpkg literal (clusters/_template + clusters/omantel.omani.works +
clusters/otech.omani.works). None were patched by any prior cutover
step — so every Provider package fetch (initial install, version bump,
ProviderRevision reconcile of an inactive revision, Pod-restart-with-
evicted-cache, any new operator-installed Provider) hit
xpkg.upbound.io directly post-handover. Principle #11 violation.
Caught by the TBD-V24 empirical investigation 2026-05-20.
Step 11 changes:
- NEW templates/11-crossplane-provider-pivot-job.yaml (~270 lines):
Phase 1 kubectl patches every Provider CR (cluster-scoped, idempotent,
skip-if-CRD-absent for early-handover window); Phase 2 git push edits
every clusters/*/infrastructure/provider-*.yaml in local Gitea.
- 09-cutover-status-configmap.yaml: totalSteps "10" → "11" plus
step.crossplane-provider-pivot.* status keys.
- values.yaml: append `xpkg.upbound.io` to harbor.mothershipAuthsToStrip
(credential hygiene now covers the xpkg upstream too) and to
egressTest.blockedDomains (TBD-V23's deny-egress hold proof must
block xpkg.upbound.io alongside the other 3 mothership families);
add stepTimeouts.crossplaneProviderPivotSeconds (600s) and
crossplaneProviderPivot.{upstreamHost,registryPath} overlay knobs.
- rbac.yaml: ClusterRole gains pkg.crossplane.io.providers
[get,list,watch,update,patch] + apiextensions.k8s.io.
customresourcedefinitions [get,list,watch] (for CRD-presence probe).
- Chart.yaml: 0.1.36 → 0.1.37 with full changelog entry.
- blueprint.yaml: 0.1.36 → 0.1.37 lockstep.
- clusters/_template/bootstrap-kit/06a-bp-self-sovereign-cutover.yaml:
pin 0.1.36 → 0.1.37 with comment.
- chart/tests/cutover-contract.sh: bump step_count + mode_job_count
assertions 10 → 11 / 9 → 10; new Case 22 verifies Step 11 patches
Provider CRs, rewrites Gitea YAML, and the RBAC + values are wired.
Validation:
- `helm template platform/self-sovereign-cutover/chart` smoke-renders
cleanly with all 11 step ConfigMaps.
- `bash platform/self-sovereign-cutover/chart/tests/cutover-contract.sh`
green on all 22 cases.
- `go test ./products/catalyst/bootstrap/api/internal/handler/...
-count=1` passes (62.8s) — cutover handler reads steps dynamically
via label selector, no hardcoded list to update.
- Did NOT use --dry-run=server. Cluster-side validation deferred to
the operator walk on a fresh multi-region prov per anti-theater
discipline.
Refs #2034 (TBD-V24 — closes only after operator-walk-with-screenshot
on a fresh multi-region prov verifies Provider CRs reconcile from
harbor.<sov-fqdn>/proxy-xpkg, NOT from xpkg.upbound.io).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>