feat(catalyst-chart): land Organization CRD orgs.openova.io/v1 (slice B1, #1095) (#1106)
Realizes the Organization CRD spec from docs/EPICS-1-6-unified-design.md §3.2.1.
Per ADR-0001 §2.7 a tenant is namespace + vCluster + Keycloak group; this CRD
is the K8s-native parent of those three artifacts plus billing/identity
attributes. Customer (real billing) and internal (chargeback/showback) Orgs
share the SAME shape and SAME code path — billingMode is the only dimension
that differs.
Cluster-scoped resource (Organizations span vClusters and host clusters; not
namespace-bound).
Spec carries:
- slug — pattern-validated lowercase 3-32 chars; `not.enum` rejects reserved
names (system, flux, crossplane, catalyst, gitea, hetzner, etc., per
NAMING-CONVENTION.md §2.5)
- displayName — minLength=1
- kind — enum customer | internal
- tier — enum sme | corporate
- billingMode — enum real | chargeback | showback
- sovereignRef — FQDN pattern
- parentOrg — optional, for nested orgs in corporate Sovereigns
- defaultEnvironmentType — enum prod|stg|uat|dev|poc, default prod
- owners[] — minItems=1, role enum owner|admin|developer|viewer
- identity — federationProvider enum (azure-sso|okta|generic-oidc) +
clientSecretRef (SealedSecret name+key — plaintext NEVER on the CR)
Status carries vcluster.{name,hostCluster,phase}, keycloakGroup.{id,path,realm},
giteaOrg.{name,repos[]}, conditions[], observedGeneration.
additionalPrinterColumns surface slug, kind, tier, billing, sovereign, vcluster
phase, age via `kubectl get org`.
Validated against a real k3s control plane:
- 2 valid samples accepted (corporate Org with Azure-SSO + internal Org with
parentOrg/chargeback)
- 2 invalid samples REJECTED with all 12 seeded error vectors:
* slug=system → not.enum reserved-name rejection
* slug=AC → pattern + length rejection
* displayName="" → minLength=1
* displayName missing → required
* kind=vendor → enum
* tier=premium → enum
* billingMode=invoice → enum
* sovereignRef="not a domain" → FQDN pattern
* sovereignRef missing → required
* defaultEnvironmentType=production → enum
* owners=[] → minItems=1
* identity.federationProvider=saml → enum
Refs: #1094, #1095, docs/EPICS-1-6-unified-design.md §3.2.1, NAMING-CONVENTION.md §1.5/§2.5/§4.6, ADR-0001 §2.7
Co-authored-by: hatiyildiz <hatiyildiz@noreply.openova.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>