ADR-0001: CRD structure and API group choice
- Status: Accepted
- Date: 2026-07-20
- Deciders: Postgres operator team (CPI)
- Related: CPI-2030 (API group migration), ADR-0003, ADR-0005, RFC summary
Context
The PostgreSQL feature adds managed PostgreSQL to the Splunk Operator. It has to express three distinct concerns, each with a different owner and lifecycle:
- A platform policy / template — sizing, versions, backup policy, pooler policy, provider choice — owned by the platform/operations team and shared across many workloads.
- A cluster instance — one running PostgreSQL cluster, owned by a service team, that references a template and applies a small set of guard-railed overrides.
- Application databases — the logical databases, roles, and credentials a service actually consumes, which may live in a shared cluster alongside other tenants.
The original ERD modeled this with a two-CRD split and role naming (_migration/_runtime) that conflated cluster infrastructure with the databases living inside it. Two questions had to be settled before 10.9:
- How many CRDs, and where is the seam? A single fat CRD would couple infrastructure provisioning to per-database lifecycle and force one RBAC boundary onto two teams.
- Which API group and version? The operator already ships an established
enterprise.splunk.comgroup (currently atv4) for its Splunk Enterprise resources. A greenfield group such asdatabase.splunk.comwas possible.
Decision
Three CRDs, split by concern:
| CRD | Scope | Owner | Purpose |
|---|---|---|---|
PostgresClusterClass | Cluster-scoped | Platform | Reusable template + immutable platform policy (provisioner, sizing defaults, backup/pooler policy, CNPG-specific config). |
PostgresCluster | Namespaced | Service team | One PostgreSQL cluster. References a class by name; applies guard-railed overrides. |
PostgresDatabase | Namespaced | Service team | Logical databases, roles, extensions, and credentials on a referenced cluster. |
API group and version: all three types live in the existing enterprise.splunk.com/v4 group (api/enterprise/v4/, api/enterprise/v4/groupversion_info.go), not a new group. They register into the operator’s existing scheme alongside the Splunk Enterprise types.
Key structural rules that fall out of this split:
PostgresClusterClassis cluster-scoped because a template is a platform asset shared across namespaces; the two instance CRDs are namespaced.PostgresCluster.spec.classis a plain name reference (the class is cluster-scoped, so no namespace is needed) and is immutable (self == oldSelf).PostgresDatabase.spec.clusterRefis aLocalObjectReference— same-namespace only — and is immutable. Cross-namespace references are intentionally not supported (see Consequences).- Config scope is strictly partitioned: platform-only policy lives under
spec.cnpgon the class and cannot be overridden per cluster; overridable fields live underspec.configon the class and are echoed onPostgresClusterSpecwith merge semantics (see ADR-0005).
Alternatives considered
- Single CRD (“PostgresCluster does everything”). Rejected: it forces one RBAC boundary onto the platform team and the service teams, couples infrastructure changes (version, storage) to database churn, and makes multi-tenant shared clusters awkward — every database edit would reconcile the whole cluster.
- Two CRDs (class + one instance CRD holding both cluster and databases). This was the ERD-era shape. Rejected because a shared cluster hosting several service teams’ databases needs independent lifecycles per database (create/delete a database without touching the cluster), and because the cluster and database reconcilers have genuinely different status models and failure modes (see ADR-0002).
- New
database.splunk.comAPI group. A clean group would read well in isolation, but it would mean a second scheme, second set of RBAC group rules, second conversion-webhook story, and a separate versioning timeline to maintain. The resources are shipped by, and only meaningful within, the Splunk Operator, so reusingenterprise.splunk.comkeeps one scheme, one RBAC group, and one release cadence. Chosen: reuseenterprise.splunk.com/v4(tracked in CPI-2030, which migrated the types onto the shared group). - A dedicated
spec.dedicated: trueflag on PostgresCluster to distinguish shared vs dedicated clusters. Rejected: shared-vs-dedicated is emergent from how manyPostgresDatabaseobjects point at a cluster, and encoding it as policy would duplicate a fact the reference graph already carries. See ADR-0005.
Consequences
- Positive: clean RBAC seam (platform owns classes cluster-wide; service teams own instances in their namespace); independent database lifecycles on a shared cluster; infrastructure changes don’t churn database state; one scheme and one release cadence by staying on
enterprise.splunk.com. - Negative / costs:
clusterRefbeing a same-namespaceLocalObjectReferencemeans aPostgresDatabasecannot target a cluster in another namespace. This is a known limitation; cross-namespace database provisioning is out of scope for 10.9.- Reusing
v4couples the Postgres types’ served version to the enterprise group’s version timeline. A future breaking change to the Postgres types would ride the enterprise group’s version bump. - Three CRDs is more surface area for docs, samples, and validation than one.
- Follow-ups: CPI-2030 (completed API group migration). Cross-namespace
clusterRefremains unaddressed by design.
References
- Code:
api/enterprise/v4/postgresclusterclass_types.go,api/enterprise/v4/postgrescluster_types.go,api/enterprise/v4/postgresdatabase_types.go,api/enterprise/v4/groupversion_info.go - Design docs: “ERD improvement — ClusterClass CRD”, “PostgreSQL on k8s + SOK”
- Jira: CPI-2030, CPI-2066