ADR-0002: spec.owner on the Workshop CRD
Status: Accepted
Date: 2026-04-15
Context
Section titled “Context”Workshop resources needed an ownership model so that users can only see and manage their own workshops, and so the future per-pod oauth2-proxy sidecar knows which email is allowed access to each workshop.
Options considered:
spec.ownerfield on the CRD — a required, immutable email field in the Workshop spec.- Label/annotation only — store
orchestra.io/owner=<encoded-email>as a label; no CRD schema change. - External database — a Postgres/SQLite table relating users to workshops.
Decision
Section titled “Decision”Option 1: spec.owner on the CRD, supplemented by an orchestra.io/owner-hash
label for efficient server-side label selector filtering (label values can’t
contain @).
Consequences
Section titled “Consequences”Positive:
kubectl get workshopsshows owners; ownership is self-documenting.spec.owneris validated by the API server (email pattern) and marked immutable via a CEL rule (!has(oldSelf.owner) || self.owner == oldSelf.owner). The first-set guard permits the initial write, then locks the field; ownership cannot drift after creation.- The operator already needs to know the owner email to provision the per-pod oauth2-proxy sidecar (future work). Putting it on the CR is the natural home.
- No new persistence layer required; the Kubernetes API is the single source of truth.
Negative/trade-offs:
- Changing the CRD schema is a coordinated operation (upgrade
orchestra-crdschart beforeorchestrachart). - The label value is a SHA-256 prefix (not the email itself) which requires
documentation. The original email is always in
spec.owner. - If requirements grow to include sharing (multiple owners) or group memberships, a DB will be needed. That migration path is left for a future ADR.
Implementation notes
Section titled “Implementation notes”- API stamps
spec.ownerfromX-Auth-Request-Emailat create time; callers cannot set it via the request body. - List filtering:
list_workshops(owner_email=email)builds a label selectororchestra.io/owner-hash=<hash>and passes it to the k8s client. - Get/delete: fetch the CR, compare
workshop.owner == current_user.email; mismatches return404(no existence leak). - Admins (configured via
ORCHESTRA_ADMIN_EMAILS) bypass the filter entirely.
As implemented (note added later): Two details diverged from this record. (1) The
orchestra.io/owner-hashlabel was never shipped — instance ownership is filtered by theowner_emailcolumn on theworkshop_instancesPostgres table, not a k8s label selector (see Authorization and Data Model). (2) The CEL immutability rule includes a first-set guard:!has(oldSelf.owner) || self.owner == oldSelf.owner. The “future per-pod oauth2-proxy sidecar” shipped as the Orchestra Goorchestra-sidecar, which enforces owner-only access usingspec.owner.