Skip to main content
Version: 0.4.x

XTenantDatabase

Tenant-facing PostgreSQL database with dynamic tier resolution, Vault-backed credentials, and ESO sync. Supports two deployment models: shared (auto-assign to a least-loaded pool cluster) and dedicated (compose an isolated CNPG cluster as a child resource).

Groupplatform.wxops.cloud
KindXTenantDatabase
Pluralxtenantdatabases
ScopeCluster
API versionv1alpha1 (served, storage)
Composition functionsfunction-extra-resources (shared cluster discovery) + function-kcl

Tier model​

Shared β€” function-extra-resources discovers all XPlatformDatabaseCluster resources labeled wxops.cloud/managed-by: platform-database-clusters and shared: true, counts existing tenant databases per cluster, and assigns to the least-loaded one matching the claim's environment. The assignment is sticky β€” once resolved, subsequent reconciles reuse the same cluster.

Dedicated β€” composes a child XPlatformDatabaseCluster for full isolation. Sized via dedicatedCluster.* fields.

spec.parameters​

Tier resolution​

FieldTypeRequiredDefaultDescription
tierstring"shared"shared or dedicated.
environmentstring"dev"One of dev, staging, prod. For shared, filters the pool to clusters matching this environment. For dedicated, passed to the child cluster.
dedicatedCluster.instancesinteger (1–9)1PostgreSQL instances. 1 = standalone, 3 = HA. Only used when tier: dedicated.
dedicatedCluster.storageSizestring"8Gi"PVC storage per instance. Only used when tier: dedicated.
dedicatedCluster.postgresVersioninteger16PostgreSQL major version. Only used when tier: dedicated.
dedicatedCluster.enablePoolerbooleantrueDeploy a PgBouncer RW pooler. Only used when tier: dedicated.
dedicatedCluster.namespacestring"cnpg-system"Namespace for the dedicated cluster. Only used when tier: dedicated.
clusterRefstringName of an XPlatformDatabaseCluster to target directly. When set (with clusterNamespace), bypasses all tier logic.
clusterNamespacestringNamespace of the target CNPG cluster. Required when clusterRef is set.

Database​

FieldTypeRequiredDefaultDescription
dbNamestringyesPostgreSQL database name. Must be unique per cluster.
ownerstringyesTeam or project identifier (e.g. "rocket-team"). Scopes the Vault path and PostgreSQL role name.
extensionsarray<string>[]PostgreSQL extensions to enable (e.g. [uuid-ossp, pgcrypto, postgis]).
databaseReclaimPolicystring"retain"retain or delete. Controls what happens to the database, credentials Secret, and Vault entry when the XR is deleted. See below.
vaultSecretStoreNamestring"vault-cluster-store"Name of the ESO ClusterSecretStore used by the PushSecret that writes credentials to Vault.

Reclaim policy​

PolicyOn XR deletion
retain (default)Database, role, and credentials Secret survive XR deletion β€” manual cleanup required if the tenant is decommissioned. Stale Vault entry must also be manually deleted.
deleteCNPG drops the database and role; credentials Secret and Vault entry are removed. Two ClusterUsage ordering constraints ensure DROP DATABASE completes before DROP ROLE, and DROP ROLE completes before credentials cleanup β€” preventing the 2BP01 parallel-deletion race.
Vault entry on retain→delete transition

Switching from retain to delete and then deleting the XR cleans up Kubernetes resources but does not remove the Vault entry β€” the PushSecret's deletionPolicy update is not retroactive for entries written under None. Manual cleanup required:

vault kv metadata delete tenants/{owner}/databases/{dbName}/connection-creds

Credential flow​

XTenantDatabase
└─ PushSecret (provider-kubernetes)
└─ Vault KV: tenants/{owner}/databases/{dbName}/connection-creds
└─ ExternalSecret (ESO)
└─ K8s Secret: {appName}-db-creds (in the tenant namespace)

Wire to XTenantApp via secretsFrom.database.enabled: true β€” XTenantApp adds envFrom.secretRef: {appName}-db-creds without managing the Secret itself.

Go-live ordering

XTenantDatabase must be applied and reach Ready before the XTenantApp Deployment starts β€” otherwise Pods fail with CreateContainerConfigError: secret not found. GitOps sync wave ordering or an ArgoCD sync-wave annotation on the XTenantApp Application ensures this.

Required discovery labels​

function-extra-resources requires these labels in the XR manifests β€” they cannot be set by the Composition:

ResourceRequired labelPurpose
XPlatformDatabaseClusterwxops.cloud/managed-by: platform-database-clustersDiscovered as a candidate for tier: shared pool
XTenantDatabasewxops.cloud/tenant-database: "true"Counted for per-cluster load balancing and dbName collision detection

Without the XTenantDatabase label, single-database scenarios still work but collision detection and load balancing across clusters are disabled.

Examples​

Shared tier (default)​

apiVersion: platform.wxops.cloud/v1alpha1
kind: XTenantDatabase
metadata:
name: payment-api-db
labels:
wxops.cloud/tenant-database: "true"
spec:
parameters:
tier: shared
environment: dev
dbName: payment_api
owner: rocket-team
extensions:
- uuid-ossp
- pgcrypto

Dedicated tier​

apiVersion: platform.wxops.cloud/v1alpha1
kind: XTenantDatabase
metadata:
name: analytics-db
labels:
wxops.cloud/tenant-database: "true"
spec:
parameters:
tier: dedicated
environment: staging
dbName: analytics
owner: data-team
databaseReclaimPolicy: retain
dedicatedCluster:
instances: 3
storageSize: 50Gi
postgresVersion: 16
enablePooler: true

See also​