L3 — GitOps bootstrap (Day-1)

Once a cluster exists, GitOps takes over. Hub is driven directly; spokes via the ACM pull model.

Flow

platform/gitops (GitLab, private)  ── Argo auths with read-only deploy token
   │
   ▼
Hub Argo (openshift-gitops) ── syncs hub/  (from bootstrap/root-app.yaml, app-of-apps)
   │
   ├─ GitOpsCluster + Placement + ApplicationSet(spoke-config)   ← ACM pull model
   │        │
   │        ▼   ManifestWork
   │   Spoke Argo (openshift-gitops on wl / wl-dr) ── syncs spoke/overlays/<cluster>
   │
   └─ ACM policies install the gitops operator + repo secret on each spoke

Key mechanics

Piece Requirement
Root app bootstrap/root-app.yaml (hub-root, app-of-apps, path hub)
Pull model GitOpsCluster CR (without it, 0 apps generated) + appset-controller RBAC for placementdecisions + acm-placement CM (statusListKey: decisions)
Per-cluster spoke config spoke/base + spoke/overlays/{wl,wl-dr} (Argo ApplicationSet spoke/overlays/{{name}})
Sync waves order the platform bring-up

Out-of-band bootstrap (per cluster, in order)

These are not GitOps (secrets / stale runtime state) — idempotent scripts in scripts/:

Step Action
1 Install OpenShift GitOps operator + oc apply -f bootstrap/root-app.yaml
2 Seed gitops-repo deploy-token secret (Vault apps/gitlab/argo-deploy) so Argo can pull
3 scripts/reintegrate-vault-k8s-auth.sh <hub\|spoke> — reviewer SA + rewrite the stale Vault k8s-auth mount
4 Node labels: scripts/label-odf-nodes.sh, label-infra-nodes.sh

Pull-model gotchas (see Gotchas)

GitOps vs. out-of-band

The root app + ApplicationSet are GitOps; the 4 bootstrap steps above are out-of-band per-cluster seeds (see Rebuild Phase 1–2).