Per docs/EPICS-1-6-unified-design.md §3.5 and ADR-0001 §2.3 amendment,
K8s-to-K8s reconciliation belongs to thin in-cluster controllers, not
Crossplane Compositions. The existing useraccess.compose.openova.io
Composition writes RoleBindings via provider-kubernetes — but
provider-kubernetes is NOT installed on any production Sovereign
(caught in the EPIC-0 audit). Every UserAccess CR has been silently
no-op'd. This controller fixes that.
What lands:
- core/controllers/useraccess/cmd/main.go — controller-runtime Manager
with leader election + signal handling, environment-only config
- internal/controller/{reconciler,desired,spec,status,types}.go — the
reconciler. Watches UserAccess.access.openova.io/v1alpha1 (cluster-
scoped, unstructured client) and owns RoleBinding +
ClusterRoleBinding via Owns() so drift triggers reconcile via
ownerRef indexing
- internal/labels/scope.go — Manara DNA scope matcher: AND-within /
OR-across, wildcard scopes, EnforcedScopes() per catalog tier (the
developer auto-injection of openova.io/env-type=dev)
- internal/controller/*_test.go + internal/labels/scope_test.go —
26 unit tests with the controller-runtime fake client. Covers
happy-path, multi-app/multi-ns fan-out, namespaces:["*"]→CRB,
group subjects, drift detection+restore, orphan deletion on spec
shrink, idempotency, invalid spec, ownerRef shape, NotFound no-op,
and the 5-catalog-tier matrix
- deploy/{rbac,deployment}.yaml — ClusterRole/SA/Deployment with
non-root, read-only-rootfs, drop-ALL caps, leader-election Role
- Containerfile — Alpine 3.20 final stage, CGO_ENABLED=0, UID 65534
- .github/workflows/useraccess-controller-build.yaml — event-driven
build (push-on-main + PR test job), SHA-pinned image tags
Behaviour:
- Per UserAccess CR, materialises RoleBindings (per namespace) or
ClusterRoleBindings (when namespaces:["*"]) referencing the
canonical openova:application-{admin,editor,viewer} ClusterRoles
- ownerRef back to the UserAccess CR with controller=true +
blockOwnerDeletion=true so K8s GC cascades deletes
- Drift detection: hand-mutated bindings are restored on next pass +
Condition Drift=True surfaced for the UI
- Idempotent: steady-state reconcile = 0 K8s writes
- Status: phase (Pending|Active|Failed), rolebindingsCreated,
observedGeneration, conditions[]
Out of scope per the brief:
- Crossplane Composition deletion (operator retires post-verify)
- 5-catalog-tier role inheritance (lands with EPIC-3 #1098)
- Keycloak realm-role sync (slice D1b, this controller is consumer)
Tests:
go vet ./... # clean
go test -count=1 -race ./... # 26/26 pass
go test ./internal/labels/... -run TestScope # full 5-tier matrix
Co-authored-by: Hatice Yildiz <hatiyildiz@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>