diff --git a/.github/actions/setup-test-environment/action.yml b/.github/actions/setup-test-environment/action.yml
index e63db423..05b5ce7d 100644
--- a/.github/actions/setup-test-environment/action.yml
+++ b/.github/actions/setup-test-environment/action.yml
@@ -707,8 +707,9 @@ runs:
spec:
nodeCount: ${{ inputs.node-count }}
instancesPerNode: ${{ inputs.instances-per-node }}
- documentDBImage: ${DOCUMENTDB_IMAGE_RESOLVED}
- gatewayImage: ${GATEWAY_IMAGE_RESOLVED}
+ image:
+ documentDB: ${DOCUMENTDB_IMAGE_RESOLVED}
+ gateway: ${GATEWAY_IMAGE_RESOLVED}
resource:
storage:
pvcSize: 5Gi
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5dbee113..5043f9c7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@
- **Two-Phase Extension Upgrade**: New `spec.schemaVersion` field separates binary upgrades (`spec.documentDBVersion`) from irreversible schema migrations (`ALTER EXTENSION UPDATE`). The default behavior gives you a rollback-safe window — update the binary first, validate, then finalize the schema. Set `schemaVersion: "auto"` for single-step upgrades in development environments. See the [upgrade guide](docs/operator-public-documentation/preview/operations/upgrades.md) for details.
### Breaking Changes
+- **CRD restructure into domain-grouped stanzas**: image, postgres and plugin fields have moved into dedicated groups. Migrate as follows: `spec.documentDBImage` → `spec.image.documentDB`, `spec.gatewayImage` → `spec.image.gateway`, `spec.postgresImage` → `spec.image.postgres`, `spec.sidecarInjectorPluginName` → `spec.plugins.sidecarInjectorName`. A new `spec.image.mode` enum (`layered` | `combined`, default `layered`) replaces the previous implicit "empty documentDBImage means layered" heuristic. A new `spec.postgres` group exposes `uid`, `gid`, `preloadLibraries` and `postInitSQL` (the operator's mandatory bootstrap statements always run first; user statements are appended after). A new root-level `spec.imagePullSecrets` is propagated to the underlying CNPG cluster.
- **Validating webhook added**: A new `ValidatingWebhookConfiguration` enforces that `spec.schemaVersion` never exceeds the binary version and blocks `spec.documentDBVersion` rollbacks below the committed schema version. This requires [cert-manager](https://cert-manager.io/) to be installed in the cluster (it is already a prerequisite for the sidecar injector). Existing clusters upgrading to this release will have the webhook activated automatically via `helm upgrade`.
- **Removed `Disabled` TLS gateway mode**: The `spec.tls.gateway.mode: Disabled` option has been removed to eliminate the security risk of plaintext Mongo wire protocol traffic. Previously, `Disabled` mode served connections in plaintext, contradicting the `Disabled` tab in `tls.md` which described the mode as a self-signed bootstrap. Empty or unset mode now defaults to `SelfSigned`, and the controller fails closed (also defaulting to `SelfSigned`) if a legacy `Disabled` value is encountered on a stored object. Users with `mode: Disabled` should remove this setting or explicitly set `mode: SelfSigned` — the gateway will automatically use a cert-manager generated self-signed certificate. See [issue #356](https://github.com/documentdb/documentdb-kubernetes-operator/issues/356) for details.
@@ -38,7 +39,7 @@
### Breaking Changes
- **Kubernetes 1.35+ required**: The legacy combined-image deployment mode for Kubernetes < 1.35 has been removed. Kubernetes 1.35+ is now required.
- **Deb-based container images**: Container images switched from source-compiled builds to deb-based packages under `ghcr.io/documentdb/documentdb-kubernetes-operator/`. The extension and gateway are now separate images with versioned tags (e.g., `:0.109.0`).
-- **PostgreSQL base image changed to Debian trixie**: The default `postgresImage` changed from `postgresql:18-minimal-bookworm` to `postgresql:18-minimal-trixie` (Debian 13) to satisfy the deb-based extension's GLIBC requirements. Existing clusters that don't explicitly set `postgresImage` will use the new base on upgrade.
+- **PostgreSQL base image changed to Debian trixie**: The default `image.postgres` changed from `postgresql:18-minimal-bookworm` to `postgresql:18-minimal-trixie` (Debian 13) to satisfy the deb-based extension's GLIBC requirements. Existing clusters that don't explicitly set `image.postgres` will use the new base on upgrade.
### Bug Fixes
- Gateway pods now restart when TLS secret name changes
diff --git a/docs/designs/image-management.md b/docs/designs/image-management.md
index c07cc050..79219b9f 100644
--- a/docs/designs/image-management.md
+++ b/docs/designs/image-management.md
@@ -88,7 +88,7 @@ The operator binary determines which database images to use through a priority c
```
Priority (highest → lowest):
-1. spec.documentDBImage ← CR field: full image URI override
+1. spec.image.documentDB ← CR field: full image URI override
2. spec.documentDBVersion ← CR field: used as tag with hardcoded repo
3. env DOCUMENTDB_VERSION ← from Helm chart (documentDbVersion in values.yaml)
4. ChangeStreams feature gate ← temporary override for changestream images
@@ -99,7 +99,7 @@ Priority (highest → lowest):
```
Priority (highest → lowest):
-1. spec.gatewayImage ← CR field: full image URI override
+1. spec.image.gateway ← CR field: full image URI override
2. spec.documentDBVersion ← CR field: used as tag with hardcoded repo
3. env DOCUMENTDB_VERSION ← from Helm chart (documentDbVersion in values.yaml)
4. ChangeStreams feature gate ← temporary override for changestream images
@@ -108,7 +108,7 @@ Priority (highest → lowest):
### PostgreSQL Image
-Set via `spec.postgresImage` in the DocumentDB CR. Defaults to `ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie` (hardcoded in the CRD schema).
+Set via `spec.image.postgres` in the DocumentDB CR. Defaults to `ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie` (hardcoded in the CRD schema).
### How Images Flow into Pods
diff --git a/docs/developer-guides/sidecar-injector-plugin-configuration.md b/docs/developer-guides/sidecar-injector-plugin-configuration.md
index 5f203930..2dc7b8de 100644
--- a/docs/developer-guides/sidecar-injector-plugin-configuration.md
+++ b/docs/developer-guides/sidecar-injector-plugin-configuration.md
@@ -42,7 +42,8 @@ spec:
storage:
pvcSize: "10Gi"
# Explicitly specify gateway image
- gatewayImage: "ghcr.io/microsoft/documentdb/documentdb-local:17"
+ image:
+ gateway: "ghcr.io/microsoft/documentdb/documentdb-local:17"
exposeViaService:
serviceType: "ClusterIP"
```
@@ -111,7 +112,8 @@ spec:
resource:
storage:
pvcSize: "10Gi"
- gatewayImage: "ghcr.io/microsoft/documentdb/documentdb-local:17"
+ image:
+ gateway: "ghcr.io/microsoft/documentdb/documentdb-local:17"
```
### Advanced Configuration (All Parameters)
@@ -127,8 +129,10 @@ spec:
resource:
storage:
pvcSize: "20Gi"
- gatewayImage: "ghcr.io/microsoft/documentdb/documentdb-local:17"
- sidecarInjectorPluginName: "cnpg-i-sidecar-injector.documentdb.io"
+ image:
+ gateway: "ghcr.io/microsoft/documentdb/documentdb-local:17"
+ plugins:
+ sidecarInjectorName: "cnpg-i-sidecar-injector.documentdb.io"
exposeViaService:
serviceType: "LoadBalancer"
```
diff --git a/docs/operator-public-documentation/preview/api-reference.md b/docs/operator-public-documentation/preview/api-reference.md
index d91cf3f0..bba6c1a1 100644
--- a/docs/operator-public-documentation/preview/api-reference.md
+++ b/docs/operator-public-documentation/preview/api-reference.md
@@ -153,14 +153,13 @@ _Appears in:_
| `nodeCount` _integer_ | NodeCount is the number of nodes in the DocumentDB cluster. Must be 1. | | Maximum: 1
Minimum: 1
|
| `instancesPerNode` _integer_ | InstancesPerNode is the number of DocumentDB instances per node. Range: 1-3. | | Maximum: 3
Minimum: 1
|
| `resource` _[Resource](#resource)_ | Resource specifies the storage resources for DocumentDB. | | |
-| `documentDBVersion` _string_ | DocumentDBVersion specifies the version for all DocumentDB components (engine, gateway).
When set, this overrides the default versions for documentDBImage and gatewayImage.
Individual image fields take precedence over this version. | | |
-| `documentDBImage` _string_ | DocumentDBImage is the container image to use for DocumentDB.
Changing this is not recommended for most users.
If not specified, defaults based on documentDBVersion or operator defaults. | | |
-| `gatewayImage` _string_ | GatewayImage is the container image to use for the DocumentDB Gateway sidecar.
Changing this is not recommended for most users.
If not specified, defaults to a version that matches the DocumentDB operator version. | | |
-| `postgresImage` _string_ | PostgresImage is the container image to use for the PostgreSQL server.
If not specified, defaults to the last stable PostgreSQL version compatible with DocumentDB.
Must use trixie (Debian 13) base to match the extension's GLIBC requirements. | ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie | Optional: \{\}
|
-| `documentDbCredentialSecret` _string_ | DocumentDbCredentialSecret is the name of the Kubernetes Secret containing credentials
for the DocumentDB gateway (expects keys `username` and `password`). If omitted,
a default secret name `documentdb-credentials` is used. | | |
+| `documentDBVersion` _string_ | DocumentDBVersion specifies the version for all DocumentDB components (engine, gateway).
When set, this overrides the default versions for image.documentDB and image.gateway.
Individual image fields under spec.image take precedence over this version. | | |
+| `image` _[ImageSpec](#imagespec)_ | Image groups container image settings for the DocumentDB stack
(extension image, gateway image, PostgreSQL image, and image mode).
All fields are optional; sensible defaults are applied when omitted. | | Optional: \{\}
|
+| `imagePullSecrets` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#localobjectreference-v1-core) array_ | ImagePullSecrets is an optional list of references to secrets in the same namespace
to use for pulling any of the images used by this cluster. Passed through to the
underlying CloudNative-PG cluster. | | Optional: \{\}
|
+| `documentDbCredentialSecret` _string_ | DocumentDbCredentialSecret is the name of the Kubernetes Secret containing credentials
for the DocumentDB gateway (expects keys `username` and `password`). If omitted,
a default secret name `documentdb-credentials` is used.
NOTE: Immutable today; will be relaxed in a future release to support credential rotation. | | |
| `clusterReplication` _[ClusterReplication](#clusterreplication)_ | ClusterReplication configures cross-cluster replication for DocumentDB. | | |
-| `sidecarInjectorPluginName` _string_ | SidecarInjectorPluginName is the name of the sidecar injector plugin to use. | | |
-| `walReplicaPluginName` _string_ | WalReplicaPluginName is the name of the wal replica plugin to use. | | |
+| `postgres` _[PostgresSpec](#postgresspec)_ | Postgres groups PostgreSQL process-level tuning (UID/GID, preload libraries,
custom post-init SQL). All fields are optional; defaults are preserved when omitted. | | Optional: \{\}
|
+| `plugins` _[PluginsSpec](#pluginsspec)_ | Plugins groups CNPG plugin configuration (sidecar injector name, WAL replica name).
All fields are optional; defaults are preserved when omitted. | | Optional: \{\}
|
| `exposeViaService` _[ExposeViaService](#exposeviaservice)_ | ExposeViaService configures how to expose DocumentDB via a Kubernetes service.
This can be a LoadBalancer or ClusterIP service. | | |
| `environment` _string_ | Environment specifies the cloud environment for deployment
This determines cloud-specific service annotations for LoadBalancer services | | Enum: [eks aks gke]
|
| `timeouts` _[Timeouts](#timeouts)_ | | | |
@@ -169,7 +168,26 @@ _Appears in:_
| `bootstrap` _[BootstrapConfiguration](#bootstrapconfiguration)_ | Bootstrap configures the initialization of the DocumentDB cluster. | | Optional: \{\}
|
| `backup` _[BackupConfiguration](#backupconfiguration)_ | Backup configures backup settings for DocumentDB. | | Optional: \{\}
|
| `featureGates` _object (keys:string, values:boolean)_ | FeatureGates enables or disables optional DocumentDB features.
Keys are PascalCase feature names following the Kubernetes feature gate convention.
Example: \{"ChangeStreams": true\}
IMPORTANT: When adding a new feature gate, update ALL of the following:
1. Add a new FeatureGate* constant in documentdb_types.go
2. Add the key name to the XValidation CEL rule's allowed list below
3. Add a default entry in the featureGateDefaults map in documentdb_types.go | | Optional: \{\}
|
+| `schemaVersion` _string_ | SchemaVersion controls the desired schema version for the DocumentDB extension.
The operator never changes your database schema unless you ask:
- Set schemaVersion → updates the database schema (irreversible)
- Set schemaVersion: "auto" → schema auto-updates with binary
Once the schema has been updated, the operator blocks image rollback below the
installed schema version to prevent running an untested binary/schema combination.
Values:
- "" (empty, default): Two-phase mode. Image upgrades happen automatically,
but ALTER EXTENSION UPDATE does NOT run. Users must explicitly set this
field to finalize the schema upgrade. This is the safest option for production
as it allows rollback by reverting the image before committing the schema change.
- "auto": Schema automatically updates to match the binary version whenever
the binary is upgraded. This is the simplest mode but provides no rollback
safety window. Only recommended for single-region clusters.
- "" (e.g. "0.112.0"): Schema updates to exactly this version.
Must be <= the binary version. | | Pattern: `^(auto\|[0-9]+\.[0-9]+\.[0-9]+)?$`
Optional: \{\}
|
| `affinity` _[AffinityConfiguration](https://pkg.go.dev/github.com/cloudnative-pg/cloudnative-pg/api/v1#AffinityConfiguration)_ | Affinity/Anti-affinity rules for Pods (cnpg passthrough) | | Optional: \{\}
|
+| `monitoring` _[MonitoringSpec](#monitoringspec)_ | Monitoring configures observability via an OTel Collector sidecar. | | Optional: \{\}
|
+
+
+#### ExporterSpec
+
+
+
+ExporterSpec configures metric export destinations.
+
+
+
+_Appears in:_
+- [MonitoringSpec](#monitoringspec)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `otlp` _[OTLPExporterSpec](#otlpexporterspec)_ | OTLP configures the OpenTelemetry Protocol exporter. | | Optional: \{\}
|
+| `prometheus` _[PrometheusExporterSpec](#prometheusexporterspec)_ | Prometheus configures a Prometheus scrape endpoint on the OTel Collector sidecar. | | Optional: \{\}
|
#### ExposeViaService
@@ -201,7 +219,7 @@ _Appears in:_
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
-| `mode` _string_ | Mode selects the TLS management strategy. Defaults to SelfSigned if not specified. | SelfSigned | Enum: [SelfSigned CertManager Provided]
|
+| `mode` _string_ | Mode selects the TLS management strategy.
Defaults to SelfSigned if not specified. | SelfSigned | Enum: [SelfSigned CertManager Provided]
|
| `certManager` _[CertManagerTLS](#certmanagertls)_ | CertManager config when Mode=CertManager. | | |
| `provided` _[ProvidedTLS](#providedtls)_ | Provided secret reference when Mode=Provided. | | |
@@ -219,6 +237,27 @@ _Appears in:_
+#### ImageSpec
+
+
+
+ImageSpec groups container image settings for the DocumentDB stack.
+All fields are optional; the operator falls back to documentDBVersion,
+environment variables, and built-in defaults in that order.
+
+
+
+_Appears in:_
+- [DocumentDBSpec](#documentdbspec)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `documentDB` _string_ | DocumentDB is the container image for the DocumentDB extension layer.
In layered mode this image is mounted into the PostgreSQL container via
CNPG's ImageVolumeSource so that the extension files are available
alongside an upstream PostgreSQL image.
In combined mode this field is ignored. | | Optional: \{\}
|
+| `gateway` _string_ | Gateway is the container image for the DocumentDB Gateway sidecar. | | Optional: \{\}
|
+| `postgres` _string_ | Postgres is the container image for the PostgreSQL server.
In layered mode (default) this is a vanilla CNPG-compatible PostgreSQL image.
In combined mode this image is expected to already bundle the DocumentDB
extension binaries; the operator will not inject an Extensions stanza.
Must use trixie (Debian 13) base to match the extension's GLIBC requirements
when running in layered mode. | ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie | Optional: \{\}
|
+| `mode` _string_ | Mode controls how the DocumentDB extension is provisioned into the
PostgreSQL container.
- layered (default): the operator mounts spec.image.documentDB as an
ImageVolumeSource via CNPG's Extensions stanza. Use this with
upstream-compatible CNPG PostgreSQL images.
- combined: the operator assumes spec.image.postgres already contains
the DocumentDB extension binaries. No Extensions stanza is emitted
and spec.postgres.preloadLibraries is used verbatim. | layered | Enum: [layered combined]
Optional: \{\}
|
+
+
#### IssuerRef
@@ -255,6 +294,39 @@ _Appears in:_
| `storageClass` _string_ | StorageClassOverride specifies the storage class for DocumentDB persistent volumes in this member cluster. | | |
+#### MonitoringSpec
+
+
+
+MonitoringSpec configures observability via an OTel Collector sidecar.
+
+
+
+_Appears in:_
+- [DocumentDBSpec](#documentdbspec)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `enabled` _boolean_ | Enabled turns on the OTel Collector sidecar for metrics collection. | | |
+| `exporter` _[ExporterSpec](#exporterspec)_ | Exporter configures where metrics are sent. | | Optional: \{\}
|
+
+
+#### OTLPExporterSpec
+
+
+
+OTLPExporterSpec configures the OTLP exporter.
+
+
+
+_Appears in:_
+- [ExporterSpec](#exporterspec)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `endpoint` _string_ | Endpoint is the OTLP gRPC endpoint (e.g., "otel-collector.monitoring:4317"). | | |
+
+
#### PVRecoveryConfiguration
@@ -271,6 +343,43 @@ _Appears in:_
| `name` _string_ | Name is the name of the PersistentVolume to recover from.
The PV must exist and be in Available or Released state. | | MinLength: 1
|
+#### PluginsSpec
+
+
+
+PluginsSpec groups CNPG plugin configuration.
+
+
+
+_Appears in:_
+- [DocumentDBSpec](#documentdbspec)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `sidecarInjectorName` _string_ | SidecarInjectorName is the name of the CNPG sidecar injector plugin
to use for the gateway and other sidecars. Immutable. | | Optional: \{\}
|
+| `walReplicaName` _string_ | WalReplicaName is the name of the WAL replica plugin to use for
cross-cluster replication. | | Optional: \{\}
|
+
+
+#### PostgresSpec
+
+
+
+PostgresSpec groups PostgreSQL process-level tuning.
+All fields are optional.
+
+
+
+_Appears in:_
+- [DocumentDBSpec](#documentdbspec)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `uid` _integer_ | UID is the numeric user ID under which the PostgreSQL server process runs.
When set, GID must also be set. | | Optional: \{\}
|
+| `gid` _integer_ | GID is the numeric group ID under which the PostgreSQL server process runs.
When set, UID must also be set. | | Optional: \{\}
|
+| `preloadLibraries` _string array_ | PreloadLibraries overrides the shared_preload_libraries list for the
PostgreSQL server. Only honored when spec.image.mode is "combined";
in layered mode the operator manages the preload libraries itself. | | Optional: \{\}
|
+| `postInitSQL` _string array_ | PostInitSQL is an ordered list of SQL statements executed after the
cluster is initialized. These statements run AFTER the operator's
mandatory bootstrap (CREATE EXTENSION documentdb, CREATE ROLE
documentdb, ALTER ROLE documentdb), so they can safely reference the
documentdb extension and role. | | Optional: \{\}
|
+
+
#### PostgresTLS
@@ -284,6 +393,22 @@ _Appears in:_
+#### PrometheusExporterSpec
+
+
+
+PrometheusExporterSpec configures the Prometheus scrape endpoint exporter.
+
+
+
+_Appears in:_
+- [ExporterSpec](#exporterspec)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `port` _integer_ | Port for the Prometheus scrape endpoint. Defaults to 8888. | 8888 | Maximum: 65535
Minimum: 1024
Optional: \{\}
|
+
+
#### ProvidedTLS
@@ -382,7 +507,7 @@ _Appears in:_
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
-| `pvcSize` _string_ | PvcSize is the size of the persistent volume claim for DocumentDB storage (e.g., "10Gi"). | | |
+| `pvcSize` _string_ | PvcSize is the size of the persistent volume claim for DocumentDB storage (e.g., "10Gi"). | | MinLength: 1
|
| `storageClass` _string_ | StorageClass specifies the storage class for DocumentDB persistent volumes.
If not specified, the cluster's default storage class will be used. | | |
| `persistentVolumeReclaimPolicy` _string_ | PersistentVolumeReclaimPolicy controls what happens to the PersistentVolume when
the DocumentDB cluster is deleted.
When a DocumentDB cluster is deleted, the following chain of deletions occurs:
DocumentDB deletion → CNPG Cluster deletion → PVC deletion → PV deletion (based on this policy)
Options:
- Retain (default): The PV is preserved after cluster deletion, allowing manual
data recovery or forensic analysis. Use for production workloads where data
safety is critical. Orphaned PVs must be manually deleted when no longer needed.
- Delete: The PV is automatically deleted when the PVC is deleted. Use for development,
testing, or ephemeral environments where data persistence is not required.
WARNING: Setting this to "Delete" means all data will be permanently lost when
the DocumentDB cluster is deleted. This cannot be undone. | Retain | Enum: [Retain Delete]
Optional: \{\}
|
diff --git a/docs/operator-public-documentation/preview/getting-started/deploy-on-eks.md b/docs/operator-public-documentation/preview/getting-started/deploy-on-eks.md
index 3b3a7186..df530432 100644
--- a/docs/operator-public-documentation/preview/getting-started/deploy-on-eks.md
+++ b/docs/operator-public-documentation/preview/getting-started/deploy-on-eks.md
@@ -354,7 +354,8 @@ spec:
storageClass: documentdb-storage
exposeViaService:
serviceType: LoadBalancer # (2)!
- sidecarInjectorPluginName: cnpg-i-sidecar-injector.documentdb.io
+ plugins:
+ sidecarInjectorName: cnpg-i-sidecar-injector.documentdb.io
```
1. Setting `environment: eks` automatically applies AWS-specific LoadBalancer annotations
diff --git a/docs/operator-public-documentation/preview/operations/upgrades.md b/docs/operator-public-documentation/preview/operations/upgrades.md
index bbbd3a60..f90c98c5 100644
--- a/docs/operator-public-documentation/preview/operations/upgrades.md
+++ b/docs/operator-public-documentation/preview/operations/upgrades.md
@@ -323,7 +323,8 @@ In most cases, use `spec.documentDBVersion` to upgrade both components together.
```yaml
spec:
- documentDBImage: "ghcr.io/documentdb/documentdb-kubernetes-operator/documentdb:"
+ image:
+ documentDB: "ghcr.io/documentdb/documentdb-kubernetes-operator/documentdb:"
```
This overrides only the database extension image while keeping the gateway at the version set by `documentDBVersion`.
@@ -332,7 +333,8 @@ In most cases, use `spec.documentDBVersion` to upgrade both components together.
```yaml
spec:
- gatewayImage: "ghcr.io/documentdb/documentdb-kubernetes-operator/gateway:"
+ image:
+ gateway: "ghcr.io/documentdb/documentdb-kubernetes-operator/gateway:"
```
This overrides only the gateway sidecar image while keeping the extension at the version set by `documentDBVersion`.
diff --git a/documentdb-playground/lightrag/documentdb.yaml b/documentdb-playground/lightrag/documentdb.yaml
index b21f7dd7..aa97052b 100644
--- a/documentdb-playground/lightrag/documentdb.yaml
+++ b/documentdb-playground/lightrag/documentdb.yaml
@@ -29,12 +29,14 @@ metadata:
spec:
nodeCount: 1
instancesPerNode: 1
- documentDBImage: ghcr.io/documentdb/documentdb-kubernetes-operator/documentdb:0.110.0
- gatewayImage: ghcr.io/documentdb/documentdb-kubernetes-operator/gateway:0.110.0
+ image:
+ documentDB: ghcr.io/documentdb/documentdb-kubernetes-operator/documentdb:0.110.0
+ gateway: ghcr.io/documentdb/documentdb-kubernetes-operator/gateway:0.110.0
documentDbCredentialSecret: docdb-credentials
resource:
storage:
pvcSize: "10Gi"
exposeViaService:
serviceType: ClusterIP
- sidecarInjectorPluginName: cnpg-i-sidecar-injector.documentdb.io
+ plugins:
+ sidecarInjectorName: cnpg-i-sidecar-injector.documentdb.io
diff --git a/operator/documentdb-helm-chart/crds/documentdb.io_dbs.yaml b/operator/documentdb-helm-chart/crds/documentdb.io_dbs.yaml
index 407336a8..db73bdb4 100644
--- a/operator/documentdb-helm-chart/crds/documentdb.io_dbs.yaml
+++ b/operator/documentdb-helm-chart/crds/documentdb.io_dbs.yaml
@@ -1084,8 +1084,8 @@ spec:
x-kubernetes-validations:
- message: cannot specify both backup and persistentVolume recovery
at the same time
- rule: '!(has(self.backup) && self.backup.name != '''' && has(self.persistentVolume)
- && self.persistentVolume.name != '''')'
+ rule: '!(has(self.backup) && self.backup.name != ” && has(self.persistentVolume)
+ && self.persistentVolume.name != ”)'
type: object
clusterReplication:
description: ClusterReplication configures cross-cluster replication
@@ -1135,17 +1135,11 @@ spec:
- clusterList
- primary
type: object
- documentDBImage:
- description: |-
- DocumentDBImage is the container image to use for DocumentDB.
- Changing this is not recommended for most users.
- If not specified, defaults based on documentDBVersion or operator defaults.
- type: string
documentDBVersion:
description: |-
DocumentDBVersion specifies the version for all DocumentDB components (engine, gateway).
- When set, this overrides the default versions for documentDBImage and gatewayImage.
- Individual image fields take precedence over this version.
+ When set, this overrides the default versions for image.documentDB and image.gateway.
+ Individual image fields under spec.image take precedence over this version.
type: string
documentDbCredentialSecret:
description: |-
@@ -1198,12 +1192,74 @@ spec:
x-kubernetes-validations:
- message: 'unsupported feature gate key; allowed keys: ChangeStreams'
rule: self.all(key, key in ['ChangeStreams'])
- gatewayImage:
+ image:
description: |-
- GatewayImage is the container image to use for the DocumentDB Gateway sidecar.
- Changing this is not recommended for most users.
- If not specified, defaults to a version that matches the DocumentDB operator version.
- type: string
+ Image groups container image settings for the DocumentDB stack
+ (extension image, gateway image, PostgreSQL image, and image mode).
+ All fields are optional; sensible defaults are applied when omitted.
+ properties:
+ documentDB:
+ description: |-
+ DocumentDB is the container image for the DocumentDB extension layer.
+ In layered mode this image is mounted into the PostgreSQL container via
+ CNPG's ImageVolumeSource so that the extension files are available
+ alongside an upstream PostgreSQL image.
+ In combined mode this field is ignored.
+ type: string
+ gateway:
+ description: Gateway is the container image for the DocumentDB
+ Gateway sidecar.
+ type: string
+ mode:
+ default: layered
+ description: |-
+ Mode controls how the DocumentDB extension is provisioned into the
+ PostgreSQL container.
+
+ - layered (default): the operator mounts spec.image.documentDB as an
+ ImageVolumeSource via CNPG's Extensions stanza. Use this with
+ upstream-compatible CNPG PostgreSQL images.
+
+ - combined: the operator assumes spec.image.postgres already contains
+ the DocumentDB extension binaries. No Extensions stanza is emitted
+ and spec.postgres.preloadLibraries is used verbatim.
+ enum:
+ - layered
+ - combined
+ type: string
+ postgres:
+ default: ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie
+ description: |-
+ Postgres is the container image for the PostgreSQL server.
+ In layered mode (default) this is a vanilla CNPG-compatible PostgreSQL image.
+ In combined mode this image is expected to already bundle the DocumentDB
+ extension binaries; the operator will not inject an Extensions stanza.
+ Must use trixie (Debian 13) base to match the extension's GLIBC requirements
+ when running in layered mode.
+ type: string
+ type: object
+ imagePullSecrets:
+ description: |-
+ ImagePullSecrets is an optional list of references to secrets in the same namespace
+ to use for pulling any of the images used by this cluster. Passed through to the
+ underlying CloudNative-PG cluster.
+ items:
+ description: |-
+ LocalObjectReference contains enough information to let you locate the
+ referenced object inside the same namespace.
+ properties:
+ name:
+ default: ""
+ description: |-
+ Name of the referent.
+ This field is effectively required, but due to backwards compatibility is
+ allowed to be empty. Instances of this type with an empty value here are
+ almost certainly wrong.
+ More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+ type: string
+ type: object
+ x-kubernetes-map-type: atomic
+ type: array
instancesPerNode:
description: 'InstancesPerNode is the number of DocumentDB instances
per node. Range: 1-3.'
@@ -1255,13 +1311,65 @@ spec:
maximum: 1
minimum: 1
type: integer
- postgresImage:
- default: ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie
+ plugins:
description: |-
- PostgresImage is the container image to use for the PostgreSQL server.
- If not specified, defaults to the last stable PostgreSQL version compatible with DocumentDB.
- Must use trixie (Debian 13) base to match the extension's GLIBC requirements.
- type: string
+ Plugins groups CNPG plugin configuration (sidecar injector name, WAL replica name).
+ All fields are optional; defaults are preserved when omitted.
+ properties:
+ sidecarInjectorName:
+ description: |-
+ SidecarInjectorName is the name of the CNPG sidecar injector plugin
+ to use for the gateway and other sidecars. Immutable.
+ type: string
+ x-kubernetes-validations:
+ - message: sidecar injector plugin name cannot be changed after
+ cluster creation
+ rule: self == oldSelf
+ walReplicaName:
+ description: |-
+ WalReplicaName is the name of the WAL replica plugin to use for
+ cross-cluster replication.
+ type: string
+ type: object
+ postgres:
+ description: |-
+ Postgres groups PostgreSQL process-level tuning (UID/GID, preload libraries,
+ custom post-init SQL). All fields are optional; defaults are preserved when omitted.
+ properties:
+ gid:
+ description: |-
+ GID is the numeric group ID under which the PostgreSQL server process runs.
+ When set, UID must also be set.
+ format: int64
+ type: integer
+ postInitSQL:
+ description: |-
+ PostInitSQL is an ordered list of SQL statements executed after the
+ cluster is initialized. These statements run AFTER the operator's
+ mandatory bootstrap (CREATE EXTENSION documentdb, CREATE ROLE
+ documentdb, ALTER ROLE documentdb), so they can safely reference the
+ documentdb extension and role.
+ items:
+ type: string
+ type: array
+ preloadLibraries:
+ description: |-
+ PreloadLibraries overrides the shared_preload_libraries list for the
+ PostgreSQL server. Only honored when spec.image.mode is "combined";
+ in layered mode the operator manages the preload libraries itself.
+ items:
+ type: string
+ type: array
+ uid:
+ description: |-
+ UID is the numeric user ID under which the PostgreSQL server process runs.
+ When set, GID must also be set.
+ format: int64
+ type: integer
+ type: object
+ x-kubernetes-validations:
+ - message: uid and gid must be set together
+ rule: has(self.uid) == has(self.gid)
resource:
description: Resource specifies the storage resources for DocumentDB.
properties:
@@ -1332,14 +1440,6 @@ spec:
Must be <= the binary version.
pattern: ^(auto|[0-9]+\.[0-9]+\.[0-9]+)?$
type: string
- sidecarInjectorPluginName:
- description: SidecarInjectorPluginName is the name of the sidecar
- injector plugin to use.
- type: string
- x-kubernetes-validations:
- - message: sidecar injector plugin name cannot be changed after cluster
- creation
- rule: self == oldSelf
timeouts:
properties:
stopDelay:
@@ -1416,10 +1516,6 @@ spec:
for future phases).
type: object
type: object
- walReplicaPluginName:
- description: WalReplicaPluginName is the name of the wal replica plugin
- to use.
- type: string
required:
- instancesPerNode
- nodeCount
diff --git a/operator/src/api/preview/documentdb_types.go b/operator/src/api/preview/documentdb_types.go
index 754287d5..6cdc4610 100644
--- a/operator/src/api/preview/documentdb_types.go
+++ b/operator/src/api/preview/documentdb_types.go
@@ -5,9 +5,25 @@ package preview
import (
cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
+ corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
+// Image mode constants for ImageSpec.Mode.
+const (
+ // ImageModeLayered (default) uses CNPG's Extensions configuration with
+ // pg_documentdb_core + pg_documentdb shipped via an ImageVolumeSource.
+ // Use this with images that contain only the extension layer.
+ ImageModeLayered = "layered"
+
+ // ImageModeCombined treats the PostgreSQL image itself as a fat image
+ // that already bundles the DocumentDB extension binaries.
+ // In this mode the operator does NOT inject an Extensions stanza or
+ // the default AdditionalLibraries list; callers must specify their
+ // own postgres.preloadLibraries.
+ ImageModeCombined = "combined"
+)
+
// Feature gate constants. PascalCase names following the Kubernetes feature gate convention.
const (
// FeatureGateChangeStreams enables change stream support by setting wal_level=logical.
@@ -30,26 +46,21 @@ type DocumentDBSpec struct {
Resource Resource `json:"resource"`
// DocumentDBVersion specifies the version for all DocumentDB components (engine, gateway).
- // When set, this overrides the default versions for documentDBImage and gatewayImage.
- // Individual image fields take precedence over this version.
+ // When set, this overrides the default versions for image.documentDB and image.gateway.
+ // Individual image fields under spec.image take precedence over this version.
DocumentDBVersion string `json:"documentDBVersion,omitempty"`
- // DocumentDBImage is the container image to use for DocumentDB.
- // Changing this is not recommended for most users.
- // If not specified, defaults based on documentDBVersion or operator defaults.
- DocumentDBImage string `json:"documentDBImage,omitempty"`
-
- // GatewayImage is the container image to use for the DocumentDB Gateway sidecar.
- // Changing this is not recommended for most users.
- // If not specified, defaults to a version that matches the DocumentDB operator version.
- GatewayImage string `json:"gatewayImage,omitempty"`
+ // Image groups container image settings for the DocumentDB stack
+ // (extension image, gateway image, PostgreSQL image, and image mode).
+ // All fields are optional; sensible defaults are applied when omitted.
+ // +optional
+ Image *ImageSpec `json:"image,omitempty"`
- // PostgresImage is the container image to use for the PostgreSQL server.
- // If not specified, defaults to the last stable PostgreSQL version compatible with DocumentDB.
- // Must use trixie (Debian 13) base to match the extension's GLIBC requirements.
- // +kubebuilder:default="ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie"
+ // ImagePullSecrets is an optional list of references to secrets in the same namespace
+ // to use for pulling any of the images used by this cluster. Passed through to the
+ // underlying CloudNative-PG cluster.
// +optional
- PostgresImage string `json:"postgresImage,omitempty"`
+ ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"`
// DocumentDbCredentialSecret is the name of the Kubernetes Secret containing credentials
// for the DocumentDB gateway (expects keys `username` and `password`). If omitted,
@@ -62,12 +73,15 @@ type DocumentDBSpec struct {
// ClusterReplication configures cross-cluster replication for DocumentDB.
ClusterReplication *ClusterReplication `json:"clusterReplication,omitempty"`
- // SidecarInjectorPluginName is the name of the sidecar injector plugin to use.
- // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="sidecar injector plugin name cannot be changed after cluster creation"
- SidecarInjectorPluginName string `json:"sidecarInjectorPluginName,omitempty"`
+ // Postgres groups PostgreSQL process-level tuning (UID/GID, preload libraries,
+ // custom post-init SQL). All fields are optional; defaults are preserved when omitted.
+ // +optional
+ Postgres *PostgresSpec `json:"postgres,omitempty"`
- // WalReplicaPluginName is the name of the wal replica plugin to use.
- WalReplicaPluginName string `json:"walReplicaPluginName,omitempty"`
+ // Plugins groups CNPG plugin configuration (sidecar injector name, WAL replica name).
+ // All fields are optional; defaults are preserved when omitted.
+ // +optional
+ Plugins *PluginsSpec `json:"plugins,omitempty"`
// ExposeViaService configures how to expose DocumentDB via a Kubernetes service.
// This can be a LoadBalancer or ClusterIP service.
@@ -140,6 +154,93 @@ type DocumentDBSpec struct {
Monitoring *MonitoringSpec `json:"monitoring,omitempty"`
}
+// ImageSpec groups container image settings for the DocumentDB stack.
+// All fields are optional; the operator falls back to documentDBVersion,
+// environment variables, and built-in defaults in that order.
+type ImageSpec struct {
+ // DocumentDB is the container image for the DocumentDB extension layer.
+ // In layered mode this image is mounted into the PostgreSQL container via
+ // CNPG's ImageVolumeSource so that the extension files are available
+ // alongside an upstream PostgreSQL image.
+ // In combined mode this field is ignored.
+ // +optional
+ DocumentDB string `json:"documentDB,omitempty"`
+
+ // Gateway is the container image for the DocumentDB Gateway sidecar.
+ // +optional
+ Gateway string `json:"gateway,omitempty"`
+
+ // Postgres is the container image for the PostgreSQL server.
+ // In layered mode (default) this is a vanilla CNPG-compatible PostgreSQL image.
+ // In combined mode this image is expected to already bundle the DocumentDB
+ // extension binaries; the operator will not inject an Extensions stanza.
+ // Must use trixie (Debian 13) base to match the extension's GLIBC requirements
+ // when running in layered mode.
+ // +kubebuilder:default="ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie"
+ // +optional
+ Postgres string `json:"postgres,omitempty"`
+
+ // Mode controls how the DocumentDB extension is provisioned into the
+ // PostgreSQL container.
+ //
+ // - layered (default): the operator mounts spec.image.documentDB as an
+ // ImageVolumeSource via CNPG's Extensions stanza. Use this with
+ // upstream-compatible CNPG PostgreSQL images.
+ //
+ // - combined: the operator assumes spec.image.postgres already contains
+ // the DocumentDB extension binaries. No Extensions stanza is emitted
+ // and spec.postgres.preloadLibraries is used verbatim.
+ //
+ // +kubebuilder:validation:Enum=layered;combined
+ // +kubebuilder:default=layered
+ // +optional
+ Mode string `json:"mode,omitempty"`
+}
+
+// PostgresSpec groups PostgreSQL process-level tuning.
+// All fields are optional.
+//
+// +kubebuilder:validation:XValidation:rule="has(self.uid) == has(self.gid)",message="uid and gid must be set together"
+type PostgresSpec struct {
+ // UID is the numeric user ID under which the PostgreSQL server process runs.
+ // When set, GID must also be set.
+ // +optional
+ UID *int64 `json:"uid,omitempty"`
+
+ // GID is the numeric group ID under which the PostgreSQL server process runs.
+ // When set, UID must also be set.
+ // +optional
+ GID *int64 `json:"gid,omitempty"`
+
+ // PreloadLibraries overrides the shared_preload_libraries list for the
+ // PostgreSQL server. Only honored when spec.image.mode is "combined";
+ // in layered mode the operator manages the preload libraries itself.
+ // +optional
+ PreloadLibraries []string `json:"preloadLibraries,omitempty"`
+
+ // PostInitSQL is an ordered list of SQL statements executed after the
+ // cluster is initialized. These statements run AFTER the operator's
+ // mandatory bootstrap (CREATE EXTENSION documentdb, CREATE ROLE
+ // documentdb, ALTER ROLE documentdb), so they can safely reference the
+ // documentdb extension and role.
+ // +optional
+ PostInitSQL []string `json:"postInitSQL,omitempty"`
+}
+
+// PluginsSpec groups CNPG plugin configuration.
+type PluginsSpec struct {
+ // SidecarInjectorName is the name of the CNPG sidecar injector plugin
+ // to use for the gateway and other sidecars. Immutable.
+ // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="sidecar injector plugin name cannot be changed after cluster creation"
+ // +optional
+ SidecarInjectorName string `json:"sidecarInjectorName,omitempty"`
+
+ // WalReplicaName is the name of the WAL replica plugin to use for
+ // cross-cluster replication.
+ // +optional
+ WalReplicaName string `json:"walReplicaName,omitempty"`
+}
+
// BootstrapConfiguration defines how to bootstrap a DocumentDB cluster.
type BootstrapConfiguration struct {
// Recovery configures recovery from a backup.
@@ -148,7 +249,7 @@ type BootstrapConfiguration struct {
}
// RecoveryConfiguration defines recovery settings for bootstrapping a DocumentDB cluster.
-// +kubebuilder:validation:XValidation:rule="!(has(self.backup) && self.backup.name != '' && has(self.persistentVolume) && self.persistentVolume.name != '')",message="cannot specify both backup and persistentVolume recovery at the same time"
+// +kubebuilder:validation:XValidation:rule="!(has(self.backup) && self.backup.name != ” && has(self.persistentVolume) && self.persistentVolume.name != ”)",message="cannot specify both backup and persistentVolume recovery at the same time"
type RecoveryConfiguration struct {
// Backup specifies the source backup to restore from.
// +optional
diff --git a/operator/src/api/preview/zz_generated.deepcopy.go b/operator/src/api/preview/zz_generated.deepcopy.go
index e53e0d62..3689a448 100644
--- a/operator/src/api/preview/zz_generated.deepcopy.go
+++ b/operator/src/api/preview/zz_generated.deepcopy.go
@@ -8,6 +8,7 @@
package preview
import (
+ "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
)
@@ -257,11 +258,31 @@ func (in *DocumentDBList) DeepCopyObject() runtime.Object {
func (in *DocumentDBSpec) DeepCopyInto(out *DocumentDBSpec) {
*out = *in
out.Resource = in.Resource
+ if in.Image != nil {
+ in, out := &in.Image, &out.Image
+ *out = new(ImageSpec)
+ **out = **in
+ }
+ if in.ImagePullSecrets != nil {
+ in, out := &in.ImagePullSecrets, &out.ImagePullSecrets
+ *out = make([]v1.LocalObjectReference, len(*in))
+ copy(*out, *in)
+ }
if in.ClusterReplication != nil {
in, out := &in.ClusterReplication, &out.ClusterReplication
*out = new(ClusterReplication)
(*in).DeepCopyInto(*out)
}
+ if in.Postgres != nil {
+ in, out := &in.Postgres, &out.Postgres
+ *out = new(PostgresSpec)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.Plugins != nil {
+ in, out := &in.Plugins, &out.Plugins
+ *out = new(PluginsSpec)
+ **out = **in
+ }
out.ExposeViaService = in.ExposeViaService
out.Timeouts = in.Timeouts
if in.TLS != nil {
@@ -404,6 +425,21 @@ func (in *GlobalEndpointsTLS) DeepCopy() *GlobalEndpointsTLS {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ImageSpec) DeepCopyInto(out *ImageSpec) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageSpec.
+func (in *ImageSpec) DeepCopy() *ImageSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(ImageSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *IssuerRef) DeepCopyInto(out *IssuerRef) {
*out = *in
@@ -470,31 +506,66 @@ func (in *OTLPExporterSpec) DeepCopy() *OTLPExporterSpec {
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *PrometheusExporterSpec) DeepCopyInto(out *PrometheusExporterSpec) {
+func (in *PVRecoveryConfiguration) DeepCopyInto(out *PVRecoveryConfiguration) {
*out = *in
}
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PrometheusExporterSpec.
-func (in *PrometheusExporterSpec) DeepCopy() *PrometheusExporterSpec {
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PVRecoveryConfiguration.
+func (in *PVRecoveryConfiguration) DeepCopy() *PVRecoveryConfiguration {
if in == nil {
return nil
}
- out := new(PrometheusExporterSpec)
+ out := new(PVRecoveryConfiguration)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *PVRecoveryConfiguration) DeepCopyInto(out *PVRecoveryConfiguration) {
+func (in *PluginsSpec) DeepCopyInto(out *PluginsSpec) {
*out = *in
}
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PVRecoveryConfiguration.
-func (in *PVRecoveryConfiguration) DeepCopy() *PVRecoveryConfiguration {
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PluginsSpec.
+func (in *PluginsSpec) DeepCopy() *PluginsSpec {
if in == nil {
return nil
}
- out := new(PVRecoveryConfiguration)
+ out := new(PluginsSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PostgresSpec) DeepCopyInto(out *PostgresSpec) {
+ *out = *in
+ if in.UID != nil {
+ in, out := &in.UID, &out.UID
+ *out = new(int64)
+ **out = **in
+ }
+ if in.GID != nil {
+ in, out := &in.GID, &out.GID
+ *out = new(int64)
+ **out = **in
+ }
+ if in.PreloadLibraries != nil {
+ in, out := &in.PreloadLibraries, &out.PreloadLibraries
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+ if in.PostInitSQL != nil {
+ in, out := &in.PostInitSQL, &out.PostInitSQL
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgresSpec.
+func (in *PostgresSpec) DeepCopy() *PostgresSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(PostgresSpec)
in.DeepCopyInto(out)
return out
}
@@ -514,6 +585,21 @@ func (in *PostgresTLS) DeepCopy() *PostgresTLS {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PrometheusExporterSpec) DeepCopyInto(out *PrometheusExporterSpec) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PrometheusExporterSpec.
+func (in *PrometheusExporterSpec) DeepCopy() *PrometheusExporterSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(PrometheusExporterSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProvidedTLS) DeepCopyInto(out *ProvidedTLS) {
*out = *in
diff --git a/operator/src/config/crd/bases/documentdb.io_dbs.yaml b/operator/src/config/crd/bases/documentdb.io_dbs.yaml
index 407336a8..db73bdb4 100644
--- a/operator/src/config/crd/bases/documentdb.io_dbs.yaml
+++ b/operator/src/config/crd/bases/documentdb.io_dbs.yaml
@@ -1084,8 +1084,8 @@ spec:
x-kubernetes-validations:
- message: cannot specify both backup and persistentVolume recovery
at the same time
- rule: '!(has(self.backup) && self.backup.name != '''' && has(self.persistentVolume)
- && self.persistentVolume.name != '''')'
+ rule: '!(has(self.backup) && self.backup.name != ” && has(self.persistentVolume)
+ && self.persistentVolume.name != ”)'
type: object
clusterReplication:
description: ClusterReplication configures cross-cluster replication
@@ -1135,17 +1135,11 @@ spec:
- clusterList
- primary
type: object
- documentDBImage:
- description: |-
- DocumentDBImage is the container image to use for DocumentDB.
- Changing this is not recommended for most users.
- If not specified, defaults based on documentDBVersion or operator defaults.
- type: string
documentDBVersion:
description: |-
DocumentDBVersion specifies the version for all DocumentDB components (engine, gateway).
- When set, this overrides the default versions for documentDBImage and gatewayImage.
- Individual image fields take precedence over this version.
+ When set, this overrides the default versions for image.documentDB and image.gateway.
+ Individual image fields under spec.image take precedence over this version.
type: string
documentDbCredentialSecret:
description: |-
@@ -1198,12 +1192,74 @@ spec:
x-kubernetes-validations:
- message: 'unsupported feature gate key; allowed keys: ChangeStreams'
rule: self.all(key, key in ['ChangeStreams'])
- gatewayImage:
+ image:
description: |-
- GatewayImage is the container image to use for the DocumentDB Gateway sidecar.
- Changing this is not recommended for most users.
- If not specified, defaults to a version that matches the DocumentDB operator version.
- type: string
+ Image groups container image settings for the DocumentDB stack
+ (extension image, gateway image, PostgreSQL image, and image mode).
+ All fields are optional; sensible defaults are applied when omitted.
+ properties:
+ documentDB:
+ description: |-
+ DocumentDB is the container image for the DocumentDB extension layer.
+ In layered mode this image is mounted into the PostgreSQL container via
+ CNPG's ImageVolumeSource so that the extension files are available
+ alongside an upstream PostgreSQL image.
+ In combined mode this field is ignored.
+ type: string
+ gateway:
+ description: Gateway is the container image for the DocumentDB
+ Gateway sidecar.
+ type: string
+ mode:
+ default: layered
+ description: |-
+ Mode controls how the DocumentDB extension is provisioned into the
+ PostgreSQL container.
+
+ - layered (default): the operator mounts spec.image.documentDB as an
+ ImageVolumeSource via CNPG's Extensions stanza. Use this with
+ upstream-compatible CNPG PostgreSQL images.
+
+ - combined: the operator assumes spec.image.postgres already contains
+ the DocumentDB extension binaries. No Extensions stanza is emitted
+ and spec.postgres.preloadLibraries is used verbatim.
+ enum:
+ - layered
+ - combined
+ type: string
+ postgres:
+ default: ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie
+ description: |-
+ Postgres is the container image for the PostgreSQL server.
+ In layered mode (default) this is a vanilla CNPG-compatible PostgreSQL image.
+ In combined mode this image is expected to already bundle the DocumentDB
+ extension binaries; the operator will not inject an Extensions stanza.
+ Must use trixie (Debian 13) base to match the extension's GLIBC requirements
+ when running in layered mode.
+ type: string
+ type: object
+ imagePullSecrets:
+ description: |-
+ ImagePullSecrets is an optional list of references to secrets in the same namespace
+ to use for pulling any of the images used by this cluster. Passed through to the
+ underlying CloudNative-PG cluster.
+ items:
+ description: |-
+ LocalObjectReference contains enough information to let you locate the
+ referenced object inside the same namespace.
+ properties:
+ name:
+ default: ""
+ description: |-
+ Name of the referent.
+ This field is effectively required, but due to backwards compatibility is
+ allowed to be empty. Instances of this type with an empty value here are
+ almost certainly wrong.
+ More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+ type: string
+ type: object
+ x-kubernetes-map-type: atomic
+ type: array
instancesPerNode:
description: 'InstancesPerNode is the number of DocumentDB instances
per node. Range: 1-3.'
@@ -1255,13 +1311,65 @@ spec:
maximum: 1
minimum: 1
type: integer
- postgresImage:
- default: ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie
+ plugins:
description: |-
- PostgresImage is the container image to use for the PostgreSQL server.
- If not specified, defaults to the last stable PostgreSQL version compatible with DocumentDB.
- Must use trixie (Debian 13) base to match the extension's GLIBC requirements.
- type: string
+ Plugins groups CNPG plugin configuration (sidecar injector name, WAL replica name).
+ All fields are optional; defaults are preserved when omitted.
+ properties:
+ sidecarInjectorName:
+ description: |-
+ SidecarInjectorName is the name of the CNPG sidecar injector plugin
+ to use for the gateway and other sidecars. Immutable.
+ type: string
+ x-kubernetes-validations:
+ - message: sidecar injector plugin name cannot be changed after
+ cluster creation
+ rule: self == oldSelf
+ walReplicaName:
+ description: |-
+ WalReplicaName is the name of the WAL replica plugin to use for
+ cross-cluster replication.
+ type: string
+ type: object
+ postgres:
+ description: |-
+ Postgres groups PostgreSQL process-level tuning (UID/GID, preload libraries,
+ custom post-init SQL). All fields are optional; defaults are preserved when omitted.
+ properties:
+ gid:
+ description: |-
+ GID is the numeric group ID under which the PostgreSQL server process runs.
+ When set, UID must also be set.
+ format: int64
+ type: integer
+ postInitSQL:
+ description: |-
+ PostInitSQL is an ordered list of SQL statements executed after the
+ cluster is initialized. These statements run AFTER the operator's
+ mandatory bootstrap (CREATE EXTENSION documentdb, CREATE ROLE
+ documentdb, ALTER ROLE documentdb), so they can safely reference the
+ documentdb extension and role.
+ items:
+ type: string
+ type: array
+ preloadLibraries:
+ description: |-
+ PreloadLibraries overrides the shared_preload_libraries list for the
+ PostgreSQL server. Only honored when spec.image.mode is "combined";
+ in layered mode the operator manages the preload libraries itself.
+ items:
+ type: string
+ type: array
+ uid:
+ description: |-
+ UID is the numeric user ID under which the PostgreSQL server process runs.
+ When set, GID must also be set.
+ format: int64
+ type: integer
+ type: object
+ x-kubernetes-validations:
+ - message: uid and gid must be set together
+ rule: has(self.uid) == has(self.gid)
resource:
description: Resource specifies the storage resources for DocumentDB.
properties:
@@ -1332,14 +1440,6 @@ spec:
Must be <= the binary version.
pattern: ^(auto|[0-9]+\.[0-9]+\.[0-9]+)?$
type: string
- sidecarInjectorPluginName:
- description: SidecarInjectorPluginName is the name of the sidecar
- injector plugin to use.
- type: string
- x-kubernetes-validations:
- - message: sidecar injector plugin name cannot be changed after cluster
- creation
- rule: self == oldSelf
timeouts:
properties:
stopDelay:
@@ -1416,10 +1516,6 @@ spec:
for future phases).
type: object
type: object
- walReplicaPluginName:
- description: WalReplicaPluginName is the name of the wal replica plugin
- to use.
- type: string
required:
- instancesPerNode
- nodeCount
diff --git a/operator/src/config/webhook/manifests.yaml b/operator/src/config/webhook/manifests.yaml
new file mode 100644
index 00000000..d4eb2b28
--- /dev/null
+++ b/operator/src/config/webhook/manifests.yaml
@@ -0,0 +1,26 @@
+---
+apiVersion: admissionregistration.k8s.io/v1
+kind: ValidatingWebhookConfiguration
+metadata:
+ name: validating-webhook-configuration
+webhooks:
+- admissionReviewVersions:
+ - v1
+ clientConfig:
+ service:
+ name: webhook-service
+ namespace: system
+ path: /validate-documentdb-io-preview-documentdb
+ failurePolicy: Fail
+ name: vdocumentdb.kb.io
+ rules:
+ - apiGroups:
+ - documentdb.io
+ apiVersions:
+ - preview
+ operations:
+ - CREATE
+ - UPDATE
+ resources:
+ - dbs
+ sideEffects: None
diff --git a/operator/src/internal/cnpg/cnpg_cluster.go b/operator/src/internal/cnpg/cnpg_cluster.go
index db6912a9..07bb360f 100644
--- a/operator/src/internal/cnpg/cnpg_cluster.go
+++ b/operator/src/internal/cnpg/cnpg_cluster.go
@@ -21,14 +21,14 @@ import (
)
func GetCnpgClusterSpec(req ctrl.Request, documentdb *dbpreview.DocumentDB, documentdbImage, serviceAccountName, storageClass string, isPrimaryRegion bool, log logr.Logger) *cnpgv1.Cluster {
- sidecarPluginName := documentdb.Spec.SidecarInjectorPluginName
+ sidecarPluginName := pluginsSidecarInjectorName(documentdb)
if sidecarPluginName == "" {
sidecarPluginName = util.DEFAULT_SIDECAR_INJECTOR_PLUGIN
}
// Get the gateway image for this DocumentDB instance
gatewayImage := util.GetGatewayImageForDocumentDB(documentdb)
- log.Info("Creating CNPG cluster with gateway image", "gatewayImage", gatewayImage, "documentdbName", documentdb.Name, "specGatewayImage", documentdb.Spec.GatewayImage)
+ log.Info("Creating CNPG cluster with gateway image", "gatewayImage", gatewayImage, "documentdbName", documentdb.Name, "specGatewayImage", imageGateway(documentdb))
credentialSecretName := documentdb.Spec.DocumentDbCredentialSecret
if credentialSecretName == "" {
@@ -68,7 +68,8 @@ func GetCnpgClusterSpec(req ctrl.Request, documentdb *dbpreview.DocumentDB, docu
Spec: func() cnpgv1.ClusterSpec {
spec := cnpgv1.ClusterSpec{
Instances: documentdb.Spec.InstancesPerNode,
- ImageName: documentdb.Spec.PostgresImage,
+ ImageName: imagePostgres(documentdb),
+ ImagePullSecrets: toCNPGImagePullSecrets(documentdb.Spec.ImagePullSecrets),
PrimaryUpdateMethod: cnpgv1.PrimaryUpdateMethodSwitchover,
StorageConfiguration: cnpgv1.StorageConfiguration{
StorageClass: storageClassPointer, // Use configured storage class or default
@@ -111,37 +112,9 @@ func GetCnpgClusterSpec(req ctrl.Request, documentdb *dbpreview.DocumentDB, docu
Parameters: params,
}}
}(),
- PostgresConfiguration: cnpgv1.PostgresConfiguration{
- Extensions: []cnpgv1.ExtensionConfiguration{
- {
- Name: "documentdb",
- ImageVolumeSource: extensionImageSource,
- DynamicLibraryPath: []string{"lib"},
- ExtensionControlPath: []string{"share"},
- LdLibraryPath: []string{"lib", "system"},
- },
- },
- AdditionalLibraries: []string{"pg_cron", "pg_documentdb_core", "pg_documentdb"},
- Parameters: func() map[string]string {
- params := map[string]string{
- "cron.database_name": "postgres",
- "max_replication_slots": "10",
- "max_wal_senders": "10",
- }
- // TODO: once DocumentDB supports change streams natively, additional GUC parameters may be needed here.
- if dbpreview.IsFeatureGateEnabled(documentdb, dbpreview.FeatureGateChangeStreams) {
- params["wal_level"] = "logical"
- }
- return params
- }(),
- PgHBA: []string{
- "host all all 0.0.0.0/0 trust",
- "host all all ::0/0 trust",
- "host replication all all trust",
- },
- },
- Bootstrap: getBootstrapConfiguration(documentdb, isPrimaryRegion, log),
- LogLevel: cmp.Or(documentdb.Spec.LogLevel, "info"),
+ PostgresConfiguration: buildPostgresConfiguration(documentdb, extensionImageSource),
+ Bootstrap: getBootstrapConfiguration(documentdb, isPrimaryRegion, log),
+ LogLevel: cmp.Or(documentdb.Spec.LogLevel, "info"),
Backup: &cnpgv1.BackupConfiguration{
VolumeSnapshot: &cnpgv1.VolumeSnapshotConfiguration{
SnapshotOwnerReference: "backup", // Set owner reference to 'backup' so that snapshots are deleted when Backup resource is deleted
@@ -151,6 +124,7 @@ func GetCnpgClusterSpec(req ctrl.Request, documentdb *dbpreview.DocumentDB, docu
Affinity: documentdb.Spec.Affinity,
}
spec.MaxStopDelay = getMaxStopDelayOrDefault(documentdb)
+ applyPostgresProcessIdentity(&spec, documentdb)
return spec
}(),
@@ -202,17 +176,21 @@ func getBootstrapConfiguration(documentdb *dbpreview.DocumentDB, isPrimaryRegion
}
}
- return getDefaultBootstrapConfiguration()
+ return getDefaultBootstrapConfiguration(documentdb)
}
-func getDefaultBootstrapConfiguration() *cnpgv1.BootstrapConfiguration {
+func getDefaultBootstrapConfiguration(documentdb *dbpreview.DocumentDB) *cnpgv1.BootstrapConfiguration {
+ postInitSQL := []string{
+ "CREATE EXTENSION documentdb CASCADE",
+ "CREATE ROLE documentdb WITH LOGIN PASSWORD 'Admin100'",
+ "ALTER ROLE documentdb WITH SUPERUSER CREATEDB CREATEROLE REPLICATION BYPASSRLS",
+ }
+ if documentdb != nil && documentdb.Spec.Postgres != nil && len(documentdb.Spec.Postgres.PostInitSQL) > 0 {
+ postInitSQL = append(postInitSQL, documentdb.Spec.Postgres.PostInitSQL...)
+ }
return &cnpgv1.BootstrapConfiguration{
InitDB: &cnpgv1.BootstrapInitDB{
- PostInitSQL: []string{
- "CREATE EXTENSION documentdb CASCADE",
- "CREATE ROLE documentdb WITH LOGIN PASSWORD 'Admin100'",
- "ALTER ROLE documentdb WITH SUPERUSER CREATEDB CREATEROLE REPLICATION BYPASSRLS",
- },
+ PostInitSQL: postInitSQL,
},
}
}
@@ -235,3 +213,132 @@ func parsePullPolicy(value string) corev1.PullPolicy {
return ""
}
}
+
+// isCombinedImageMode reports whether spec.image.mode is explicitly set to
+// "combined". In combined mode the operator assumes the PostgreSQL image
+// already bundles the DocumentDB extension, so it skips the CNPG Extensions
+// stanza and the default AdditionalLibraries list.
+func isCombinedImageMode(documentdb *dbpreview.DocumentDB) bool {
+ if documentdb == nil || documentdb.Spec.Image == nil {
+ return false
+ }
+ return documentdb.Spec.Image.Mode == dbpreview.ImageModeCombined
+}
+
+// imagePostgres returns spec.image.postgres or empty string when unset.
+// Nil-safe.
+func imagePostgres(documentdb *dbpreview.DocumentDB) string {
+ if documentdb == nil || documentdb.Spec.Image == nil {
+ return ""
+ }
+ return documentdb.Spec.Image.Postgres
+}
+
+// imageGateway returns spec.image.gateway or empty string when unset.
+// Nil-safe.
+func imageGateway(documentdb *dbpreview.DocumentDB) string {
+ if documentdb == nil || documentdb.Spec.Image == nil {
+ return ""
+ }
+ return documentdb.Spec.Image.Gateway
+}
+
+// pluginsSidecarInjectorName returns spec.plugins.sidecarInjectorName
+// or empty string when unset. Nil-safe.
+func pluginsSidecarInjectorName(documentdb *dbpreview.DocumentDB) string {
+ if documentdb == nil || documentdb.Spec.Plugins == nil {
+ return ""
+ }
+ return documentdb.Spec.Plugins.SidecarInjectorName
+}
+
+// toCNPGImagePullSecrets translates a list of corev1.LocalObjectReference
+// (the Kubernetes-native shape used on spec.imagePullSecrets) into the
+// CNPG-flavoured cnpgv1.LocalObjectReference shape that
+// cnpgv1.ClusterSpec.ImagePullSecrets expects.
+func toCNPGImagePullSecrets(secrets []corev1.LocalObjectReference) []cnpgv1.LocalObjectReference {
+ if len(secrets) == 0 {
+ return nil
+ }
+ out := make([]cnpgv1.LocalObjectReference, 0, len(secrets))
+ for _, s := range secrets {
+ if s.Name == "" {
+ continue
+ }
+ out = append(out, cnpgv1.LocalObjectReference{Name: s.Name})
+ }
+ return out
+}
+
+// applyPostgresProcessIdentity wires spec.postgres.uid / spec.postgres.gid
+// onto the CNPG ClusterSpec. CNPG validates that both are set together;
+// the CRD enforces the same invariant via XValidation on PostgresSpec.
+func applyPostgresProcessIdentity(spec *cnpgv1.ClusterSpec, documentdb *dbpreview.DocumentDB) {
+ if documentdb == nil || documentdb.Spec.Postgres == nil {
+ return
+ }
+ pg := documentdb.Spec.Postgres
+ if pg.UID != nil {
+ spec.PostgresUID = *pg.UID
+ }
+ if pg.GID != nil {
+ spec.PostgresGID = *pg.GID
+ }
+}
+
+// buildPostgresConfiguration returns the cnpgv1.PostgresConfiguration block
+// for the cluster, branching on spec.image.mode.
+//
+// In layered mode (default) the operator declares the DocumentDB extension
+// via CNPG's Extensions stanza, sets a fixed AdditionalLibraries list, and
+// applies a small set of operator-managed GUCs.
+//
+// In combined mode the operator emits no Extensions block, uses
+// spec.postgres.preloadLibraries verbatim (operator-managed GUC defaults
+// still apply for replication slots and pg_cron's database setting), and
+// leaves it to the caller to ship a fat PostgreSQL image that contains the
+// extension binaries.
+func buildPostgresConfiguration(documentdb *dbpreview.DocumentDB, extensionImageSource corev1.ImageVolumeSource) cnpgv1.PostgresConfiguration {
+ params := map[string]string{
+ "cron.database_name": "postgres",
+ "max_replication_slots": "10",
+ "max_wal_senders": "10",
+ }
+ // TODO: once DocumentDB supports change streams natively, additional GUC parameters may be needed here.
+ if dbpreview.IsFeatureGateEnabled(documentdb, dbpreview.FeatureGateChangeStreams) {
+ params["wal_level"] = "logical"
+ }
+
+ pgHBA := []string{
+ "host all all 0.0.0.0/0 trust",
+ "host all all ::0/0 trust",
+ "host replication all all trust",
+ }
+
+ if isCombinedImageMode(documentdb) {
+ var preload []string
+ if documentdb.Spec.Postgres != nil {
+ preload = documentdb.Spec.Postgres.PreloadLibraries
+ }
+ return cnpgv1.PostgresConfiguration{
+ AdditionalLibraries: preload,
+ Parameters: params,
+ PgHBA: pgHBA,
+ }
+ }
+
+ return cnpgv1.PostgresConfiguration{
+ Extensions: []cnpgv1.ExtensionConfiguration{
+ {
+ Name: "documentdb",
+ ImageVolumeSource: extensionImageSource,
+ DynamicLibraryPath: []string{"lib"},
+ ExtensionControlPath: []string{"share"},
+ LdLibraryPath: []string{"lib", "system"},
+ },
+ },
+ AdditionalLibraries: []string{"pg_cron", "pg_documentdb_core", "pg_documentdb"},
+ Parameters: params,
+ PgHBA: pgHBA,
+ }
+}
diff --git a/operator/src/internal/cnpg/cnpg_cluster_test.go b/operator/src/internal/cnpg/cnpg_cluster_test.go
index 67218331..169b32f6 100644
--- a/operator/src/internal/cnpg/cnpg_cluster_test.go
+++ b/operator/src/internal/cnpg/cnpg_cluster_test.go
@@ -160,19 +160,34 @@ var _ = Describe("getBootstrapConfiguration", func() {
var _ = Describe("getDefaultBootstrapConfiguration", func() {
It("returns a bootstrap configuration with InitDB", func() {
- result := getDefaultBootstrapConfiguration()
+ result := getDefaultBootstrapConfiguration(&dbpreview.DocumentDB{})
Expect(result).ToNot(BeNil())
Expect(result.InitDB).ToNot(BeNil())
Expect(result.Recovery).To(BeNil())
})
- It("includes required PostInitSQL statements", func() {
- result := getDefaultBootstrapConfiguration()
+ It("includes required PostInitSQL statements by default", func() {
+ result := getDefaultBootstrapConfiguration(&dbpreview.DocumentDB{})
Expect(result.InitDB.PostInitSQL).To(HaveLen(3))
Expect(result.InitDB.PostInitSQL).To(ContainElement("CREATE EXTENSION documentdb CASCADE"))
Expect(result.InitDB.PostInitSQL).To(ContainElement("CREATE ROLE documentdb WITH LOGIN PASSWORD 'Admin100'"))
Expect(result.InitDB.PostInitSQL).To(ContainElement("ALTER ROLE documentdb WITH SUPERUSER CREATEDB CREATEROLE REPLICATION BYPASSRLS"))
})
+
+ It("appends spec.postgres.postInitSQL after operator-required statements", func() {
+ db := &dbpreview.DocumentDB{
+ Spec: dbpreview.DocumentDBSpec{
+ Postgres: &dbpreview.PostgresSpec{
+ PostInitSQL: []string{"SELECT 1", "SELECT 2"},
+ },
+ },
+ }
+ result := getDefaultBootstrapConfiguration(db)
+ Expect(result.InitDB.PostInitSQL).To(HaveLen(5))
+ Expect(result.InitDB.PostInitSQL[0]).To(Equal("CREATE EXTENSION documentdb CASCADE"))
+ Expect(result.InitDB.PostInitSQL[3]).To(Equal("SELECT 1"))
+ Expect(result.InitDB.PostInitSQL[4]).To(Equal("SELECT 2"))
+ })
})
var _ = Describe("GetCnpgClusterSpec", func() {
@@ -186,7 +201,9 @@ var _ = Describe("GetCnpgClusterSpec", func() {
documentdb := &dbpreview.DocumentDB{
Spec: dbpreview.DocumentDBSpec{
InstancesPerNode: 3,
- PostgresImage: "ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie",
+ Image: &dbpreview.ImageSpec{
+ Postgres: "ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie",
+ },
Resource: dbpreview.Resource{
Storage: dbpreview.StorageConfiguration{
PvcSize: "10Gi",
@@ -618,6 +635,112 @@ var _ = Describe("GetCnpgClusterSpec", func() {
Expect(pluginParams).NotTo(HaveKey("otelCollectorImage"))
Expect(pluginParams).NotTo(HaveKey("otelConfigMapName"))
})
+
+ It("propagates spec.imagePullSecrets to the CNPG cluster spec", func() {
+ req := ctrl.Request{}
+ req.Name = "test-cluster"
+ req.Namespace = "default"
+ documentdb := &dbpreview.DocumentDB{
+ Spec: dbpreview.DocumentDBSpec{
+ InstancesPerNode: 1,
+ ImagePullSecrets: []corev1.LocalObjectReference{
+ {Name: "registry-creds"},
+ {Name: ""},
+ {Name: "private-pull"},
+ },
+ Image: &dbpreview.ImageSpec{
+ Postgres: "ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie",
+ },
+ Resource: dbpreview.Resource{
+ Storage: dbpreview.StorageConfiguration{PvcSize: "10Gi"},
+ },
+ },
+ }
+
+ cluster := GetCnpgClusterSpec(req, documentdb, "documentdb-oss:1.0", "test-sa", "", true, log)
+ Expect(cluster.Spec.ImagePullSecrets).To(HaveLen(2))
+ Expect(cluster.Spec.ImagePullSecrets[0].Name).To(Equal("registry-creds"))
+ Expect(cluster.Spec.ImagePullSecrets[1].Name).To(Equal("private-pull"))
+ })
+
+ It("propagates spec.postgres.uid and gid to PostgresUID/PostgresGID", func() {
+ req := ctrl.Request{}
+ req.Name = "test-cluster"
+ req.Namespace = "default"
+ documentdb := &dbpreview.DocumentDB{
+ Spec: dbpreview.DocumentDBSpec{
+ InstancesPerNode: 1,
+ Image: &dbpreview.ImageSpec{
+ Postgres: "ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie",
+ },
+ Postgres: &dbpreview.PostgresSpec{
+ UID: ptr.To(int64(1001)),
+ GID: ptr.To(int64(1002)),
+ },
+ Resource: dbpreview.Resource{
+ Storage: dbpreview.StorageConfiguration{PvcSize: "10Gi"},
+ },
+ },
+ }
+
+ cluster := GetCnpgClusterSpec(req, documentdb, "documentdb-oss:1.0", "test-sa", "", true, log)
+ Expect(cluster.Spec.PostgresUID).To(Equal(int64(1001)))
+ Expect(cluster.Spec.PostgresGID).To(Equal(int64(1002)))
+ })
+
+ Context("combined image mode", func() {
+ It("omits Extensions, honors preloadLibraries, and keeps operator-managed GUCs", func() {
+ req := ctrl.Request{}
+ req.Name = "test-cluster"
+ req.Namespace = "default"
+ documentdb := &dbpreview.DocumentDB{
+ Spec: dbpreview.DocumentDBSpec{
+ InstancesPerNode: 1,
+ Image: &dbpreview.ImageSpec{
+ Mode: dbpreview.ImageModeCombined,
+ Postgres: "registry.example/fat-postgres:18",
+ },
+ Postgres: &dbpreview.PostgresSpec{
+ PreloadLibraries: []string{"pg_cron", "pg_documentdb_core", "pg_documentdb", "auto_explain"},
+ },
+ Resource: dbpreview.Resource{
+ Storage: dbpreview.StorageConfiguration{PvcSize: "10Gi"},
+ },
+ },
+ }
+
+ cluster := GetCnpgClusterSpec(req, documentdb, "documentdb-oss:1.0", "test-sa", "", true, log)
+ Expect(cluster.Spec.ImageName).To(Equal("registry.example/fat-postgres:18"))
+ Expect(cluster.Spec.PostgresConfiguration.Extensions).To(BeEmpty())
+ Expect(cluster.Spec.PostgresConfiguration.AdditionalLibraries).To(Equal(
+ []string{"pg_cron", "pg_documentdb_core", "pg_documentdb", "auto_explain"}))
+ Expect(cluster.Spec.PostgresConfiguration.Parameters).To(HaveKeyWithValue("cron.database_name", "postgres"))
+ Expect(cluster.Spec.PostgresConfiguration.Parameters).To(HaveKeyWithValue("max_replication_slots", "10"))
+ Expect(cluster.Spec.PostgresConfiguration.PgHBA).To(HaveLen(3))
+ })
+
+ It("emits no AdditionalLibraries when spec.postgres is nil", func() {
+ req := ctrl.Request{}
+ req.Name = "test-cluster"
+ req.Namespace = "default"
+ documentdb := &dbpreview.DocumentDB{
+ Spec: dbpreview.DocumentDBSpec{
+ InstancesPerNode: 1,
+ Image: &dbpreview.ImageSpec{
+ Mode: dbpreview.ImageModeCombined,
+ Postgres: "registry.example/fat-postgres:18",
+ },
+ Resource: dbpreview.Resource{
+ Storage: dbpreview.StorageConfiguration{PvcSize: "10Gi"},
+ },
+ },
+ }
+
+ cluster := GetCnpgClusterSpec(req, documentdb, "documentdb-oss:1.0", "test-sa", "", true, log)
+ Expect(cluster.Spec.PostgresConfiguration.Extensions).To(BeEmpty())
+ Expect(cluster.Spec.PostgresConfiguration.AdditionalLibraries).To(BeEmpty())
+ })
+ })
})
// Standard Go tests for additional coverage
diff --git a/operator/src/internal/cnpg/cnpg_sync.go b/operator/src/internal/cnpg/cnpg_sync.go
index 5e227805..da0bf6e6 100644
--- a/operator/src/internal/cnpg/cnpg_sync.go
+++ b/operator/src/internal/cnpg/cnpg_sync.go
@@ -202,8 +202,8 @@ func SyncCnpgCluster(
// JSON Patch "add" requires the parent path to exist.
if current.Annotations == nil {
patchOps = append(patchOps, JSONPatch{
- Op: PatchOpAdd,
- Path: "/metadata/annotations",
+ Op: PatchOpAdd,
+ Path: "/metadata/annotations",
Value: map[string]string{
"kubectl.kubernetes.io/restartedAt": time.Now().Format(time.RFC3339Nano),
},
diff --git a/operator/src/internal/controller/certificate_controller_test.go b/operator/src/internal/controller/certificate_controller_test.go
index d4ba7f4e..a9b5f700 100644
--- a/operator/src/internal/controller/certificate_controller_test.go
+++ b/operator/src/internal/controller/certificate_controller_test.go
@@ -52,7 +52,7 @@ func baseDocumentDB(name, ns string) *dbpreview.DocumentDB {
NodeCount: 1,
InstancesPerNode: 1,
Resource: dbpreview.Resource{Storage: dbpreview.StorageConfiguration{PvcSize: "1Gi"}},
- DocumentDBImage: "test-image",
+ Image: &dbpreview.ImageSpec{DocumentDB: "test-image"},
ExposeViaService: dbpreview.ExposeViaService{ServiceType: "ClusterIP"},
},
}
diff --git a/operator/src/internal/controller/documentdb_controller.go b/operator/src/internal/controller/documentdb_controller.go
index 5c89fa8d..4a8b7dd4 100644
--- a/operator/src/internal/controller/documentdb_controller.go
+++ b/operator/src/internal/controller/documentdb_controller.go
@@ -1019,7 +1019,6 @@ func (r *DocumentDBReconciler) determineSchemaTarget(
}
}
-
// updateImageStatus reads the current extension and gateway images from the CNPG cluster
// and persists them into the DocumentDB status fields. This is a no-op if both fields
// are already up to date.
diff --git a/operator/src/internal/controller/physical_replication.go b/operator/src/internal/controller/physical_replication.go
index d5b0066e..e6adeca3 100644
--- a/operator/src/internal/controller/physical_replication.go
+++ b/operator/src/internal/controller/physical_replication.go
@@ -87,7 +87,10 @@ func (r *DocumentDBReconciler) AddClusterReplicationToClusterSpec(
}
/* TODO re-enable when we have a WAL replica image
- walReplicaPluginName := documentdb.Spec.WalReplicaPluginName
+ walReplicaPluginName := ""
+ if documentdb.Spec.Plugins != nil {
+ walReplicaPluginName = documentdb.Spec.Plugins.WalReplicaName
+ }
if walReplicaPluginName == "" {
walReplicaPluginName = util.DEFAULT_WAL_REPLICA_PLUGIN
}
diff --git a/operator/src/internal/utils/constants.go b/operator/src/internal/utils/constants.go
index 3186a91f..e47d6638 100644
--- a/operator/src/internal/utils/constants.go
+++ b/operator/src/internal/utils/constants.go
@@ -26,11 +26,11 @@ const (
MinK8sMinorVersion = 35
// DEFAULT_DOCUMENTDB_IMAGE is the extension image used in ImageVolume mode.
- DEFAULT_DOCUMENTDB_IMAGE = DOCUMENTDB_EXTENSION_IMAGE_REPO + ":0.110.0"
+ DEFAULT_DOCUMENTDB_IMAGE = DOCUMENTDB_EXTENSION_IMAGE_REPO + ":0.110.0"
// NOTE: Keep in sync with operator/cnpg-plugins/sidecar-injector/internal/config/config.go:applyDefaults()
DEFAULT_GATEWAY_IMAGE = GATEWAY_IMAGE_REPO + ":0.110.0"
DEFAULT_DOCUMENTDB_CREDENTIALS_SECRET = "documentdb-credentials"
- DEFAULT_OTEL_COLLECTOR_IMAGE = "otel/opentelemetry-collector-contrib:0.149.0"
+ DEFAULT_OTEL_COLLECTOR_IMAGE = "otel/opentelemetry-collector-contrib:0.149.0"
// TODO: remove these constants once change stream support is included in the official images.
CHANGESTREAM_DOCUMENTDB_IMAGE_REPOSITORY = "ghcr.io/wentingwu666666/documentdb-kubernetes-operator"
diff --git a/operator/src/internal/utils/pv_recovery.go b/operator/src/internal/utils/pv_recovery.go
index 6c09796b..16cd387e 100644
--- a/operator/src/internal/utils/pv_recovery.go
+++ b/operator/src/internal/utils/pv_recovery.go
@@ -13,10 +13,10 @@ import (
const (
// Label for identifying temporary PVCs created for PV recovery
LabelRecoveryTemp = "documentdb.io/recovery-temp"
-
+
// Label for identifying the DocumentDB cluster a PV/PVC belongs to
- LabelCluster = "documentdb.io/cluster"
- LabelNamespace = "documentdb.io/namespace"
+ LabelCluster = "documentdb.io/cluster"
+ LabelNamespace = "documentdb.io/namespace"
)
// TempPVCNameForPVRecovery generates the name for a temporary PVC used during PV recovery.
diff --git a/operator/src/internal/utils/util.go b/operator/src/internal/utils/util.go
index f43e1fe2..10511696 100644
--- a/operator/src/internal/utils/util.go
+++ b/operator/src/internal/utils/util.go
@@ -422,10 +422,10 @@ func GenerateConnectionString(documentdb *dbpreview.DocumentDB, serviceIp string
}
// GetGatewayImageForDocumentDB returns the gateway image for a DocumentDB instance.
-// Priority: spec.gatewayImage > spec.documentDBVersion > env.DOCUMENTDB_VERSION > default
+// Priority: spec.image.gateway > spec.documentDBVersion > env.DOCUMENTDB_VERSION > default
func GetGatewayImageForDocumentDB(documentdb *dbpreview.DocumentDB) string {
- if documentdb.Spec.GatewayImage != "" {
- return documentdb.Spec.GatewayImage
+ if documentdb.Spec.Image != nil && documentdb.Spec.Image.Gateway != "" {
+ return documentdb.Spec.Image.Gateway
}
// Use spec-level documentDBVersion if set
@@ -449,10 +449,10 @@ func GetGatewayImageForDocumentDB(documentdb *dbpreview.DocumentDB) string {
}
// GetDocumentDBImageForInstance returns the documentdb engine image.
-// Priority: spec.documentDBImage > spec.documentDBVersion > env.DOCUMENTDB_VERSION > default
+// Priority: spec.image.documentDB > spec.documentDBVersion > env.DOCUMENTDB_VERSION > default
func GetDocumentDBImageForInstance(documentdb *dbpreview.DocumentDB) string {
- if documentdb.Spec.DocumentDBImage != "" {
- return documentdb.Spec.DocumentDBImage
+ if documentdb.Spec.Image != nil && documentdb.Spec.Image.DocumentDB != "" {
+ return documentdb.Spec.Image.DocumentDB
}
// Use spec-level documentDBVersion if set
diff --git a/operator/src/internal/utils/util_test.go b/operator/src/internal/utils/util_test.go
index ab0da533..b0977add 100644
--- a/operator/src/internal/utils/util_test.go
+++ b/operator/src/internal/utils/util_test.go
@@ -388,12 +388,12 @@ func TestGetDocumentDBImageForInstance(t *testing.T) {
documentdb *dbpreview.DocumentDB
expected string
}{
- // Priority 1: spec.DocumentDBImage overrides everything
+ // Priority 1: spec.image.documentDB overrides everything
{
name: "custom image overrides feature gate",
documentdb: &dbpreview.DocumentDB{Spec: dbpreview.DocumentDBSpec{
- DocumentDBImage: "custom-registry/custom-image:v1",
- FeatureGates: map[string]bool{dbpreview.FeatureGateChangeStreams: true},
+ Image: &dbpreview.ImageSpec{DocumentDB: "custom-registry/custom-image:v1"},
+ FeatureGates: map[string]bool{dbpreview.FeatureGateChangeStreams: true},
}},
expected: "custom-registry/custom-image:v1",
},
@@ -409,7 +409,7 @@ func TestGetDocumentDBImageForInstance(t *testing.T) {
{
name: "custom image overrides documentDBVersion",
documentdb: &dbpreview.DocumentDB{Spec: dbpreview.DocumentDBSpec{
- DocumentDBImage: "custom-registry/custom-image:v1",
+ Image: &dbpreview.ImageSpec{DocumentDB: "custom-registry/custom-image:v1"},
DocumentDBVersion: "1.2.3",
}},
expected: "custom-registry/custom-image:v1",
@@ -516,7 +516,7 @@ func TestGetGatewayImageForDocumentDB(t *testing.T) {
{
name: "explicit image takes precedence over everything",
spec: dbpreview.DocumentDBSpec{
- GatewayImage: "custom-registry/custom-gateway:v1",
+ Image: &dbpreview.ImageSpec{Gateway: "custom-registry/custom-gateway:v1"},
FeatureGates: map[string]bool{dbpreview.FeatureGateChangeStreams: true},
},
expected: "custom-registry/custom-gateway:v1",
@@ -531,7 +531,7 @@ func TestGetGatewayImageForDocumentDB(t *testing.T) {
{
name: "explicit gatewayImage overrides documentDBVersion",
spec: dbpreview.DocumentDBSpec{
- GatewayImage: "custom-registry/custom-gateway:v1",
+ Image: &dbpreview.ImageSpec{Gateway: "custom-registry/custom-gateway:v1"},
DocumentDBVersion: "1.2.3",
},
expected: "custom-registry/custom-gateway:v1",
diff --git a/operator/src/internal/webhook/documentdb_webhook.go b/operator/src/internal/webhook/documentdb_webhook.go
index dfa9666c..7f13809c 100644
--- a/operator/src/internal/webhook/documentdb_webhook.go
+++ b/operator/src/internal/webhook/documentdb_webhook.go
@@ -181,7 +181,7 @@ func (v *DocumentDBValidator) validateImageRollback(newDB, oldDB *dbpreview.Docu
// where the image tag may not represent the extension version (e.g., CI tags
// like "0.2.0-test-12345" where 0.2.0 is the chart version, not the extension).
if newDB.Spec.DocumentDBVersion == oldDB.Spec.DocumentDBVersion &&
- newDB.Spec.DocumentDBImage == oldDB.Spec.DocumentDBImage {
+ specImageDocumentDB(newDB) == specImageDocumentDB(oldDB) {
return nil
}
@@ -292,12 +292,11 @@ func isBootstrapEqual(a, b *dbpreview.BootstrapConfiguration) bool {
// ---------------------------------------------------------------------------
// resolveBinaryVersion extracts the effective binary version from a DocumentDB spec.
-// Priority: documentDBImage tag > documentDBVersion > "" (unknown).
+// Priority: image.documentDB tag > documentDBVersion > "" (unknown).
// Digest-only references (e.g., "image@sha256:...") are not parseable as versions
// and return "".
func resolveBinaryVersion(db *dbpreview.DocumentDB) string {
- if db.Spec.DocumentDBImage != "" {
- ref := db.Spec.DocumentDBImage
+ if ref := specImageDocumentDB(db); ref != "" {
// Ignore digest-only references — they don't carry a version tag
if strings.Contains(ref, "@sha256:") {
return db.Spec.DocumentDBVersion
@@ -313,6 +312,14 @@ func resolveBinaryVersion(db *dbpreview.DocumentDB) string {
return db.Spec.DocumentDBVersion
}
+// specImageDocumentDB safely returns spec.image.documentDB or "" when unset.
+func specImageDocumentDB(db *dbpreview.DocumentDB) string {
+ if db == nil || db.Spec.Image == nil {
+ return ""
+ }
+ return db.Spec.Image.DocumentDB
+}
+
// extractSemver returns the leading "X.Y.Z" portion from a tag string,
// or "" if the tag doesn't start with a valid semver pattern.
func extractSemver(tag string) string {
diff --git a/operator/src/internal/webhook/documentdb_webhook_test.go b/operator/src/internal/webhook/documentdb_webhook_test.go
index a3dc45b8..a869aa54 100644
--- a/operator/src/internal/webhook/documentdb_webhook_test.go
+++ b/operator/src/internal/webhook/documentdb_webhook_test.go
@@ -35,7 +35,7 @@ func newTestDocumentDB(version, schemaVersion, image string) *dbpreview.Document
db.Spec.SchemaVersion = schemaVersion
}
if image != "" {
- db.Spec.DocumentDBImage = image
+ db.Spec.Image = &dbpreview.ImageSpec{DocumentDB: image}
}
return db
}
diff --git a/operator/src/scripts/deployment-examples/single-node-documentdb.yaml b/operator/src/scripts/deployment-examples/single-node-documentdb.yaml
index 2f1b805a..97e02ba9 100755
--- a/operator/src/scripts/deployment-examples/single-node-documentdb.yaml
+++ b/operator/src/scripts/deployment-examples/single-node-documentdb.yaml
@@ -19,4 +19,5 @@ spec:
exposeViaService:
serviceType: LoadBalancer
environment: eks
- sidecarInjectorPluginName: cnpg-i-sidecar-injector.documentdb.io
\ No newline at end of file
+ plugins:
+ sidecarInjectorName: cnpg-i-sidecar-injector.documentdb.io
\ No newline at end of file
diff --git a/test/e2e/manifests/backup/recovery_from_backup.yaml.template b/test/e2e/manifests/backup/recovery_from_backup.yaml.template
index 93db49e0..9517140b 100644
--- a/test/e2e/manifests/backup/recovery_from_backup.yaml.template
+++ b/test/e2e/manifests/backup/recovery_from_backup.yaml.template
@@ -6,8 +6,9 @@ metadata:
spec:
nodeCount: 1
instancesPerNode: ${INSTANCES}
- documentDBImage: ${DOCUMENTDB_IMAGE}
- gatewayImage: ${GATEWAY_IMAGE}
+ image:
+ documentDB: ${DOCUMENTDB_IMAGE}
+ gateway: ${GATEWAY_IMAGE}
documentDbCredentialSecret: ${CREDENTIAL_SECRET}
resource:
storage:
diff --git a/test/e2e/manifests/backup/recovery_from_pv.yaml.template b/test/e2e/manifests/backup/recovery_from_pv.yaml.template
index 7fa3a96e..2a383c49 100644
--- a/test/e2e/manifests/backup/recovery_from_pv.yaml.template
+++ b/test/e2e/manifests/backup/recovery_from_pv.yaml.template
@@ -6,8 +6,9 @@ metadata:
spec:
nodeCount: 1
instancesPerNode: ${INSTANCES}
- documentDBImage: ${DOCUMENTDB_IMAGE}
- gatewayImage: ${GATEWAY_IMAGE}
+ image:
+ documentDB: ${DOCUMENTDB_IMAGE}
+ gateway: ${GATEWAY_IMAGE}
documentDbCredentialSecret: ${CREDENTIAL_SECRET}
resource:
storage:
diff --git a/test/e2e/manifests/base/documentdb.yaml.template b/test/e2e/manifests/base/documentdb.yaml.template
index ed0bc512..a10a735a 100644
--- a/test/e2e/manifests/base/documentdb.yaml.template
+++ b/test/e2e/manifests/base/documentdb.yaml.template
@@ -6,8 +6,9 @@ metadata:
spec:
nodeCount: 1
instancesPerNode: ${INSTANCES}
- documentDBImage: ${DOCUMENTDB_IMAGE}
- gatewayImage: ${GATEWAY_IMAGE}
+ image:
+ documentDB: ${DOCUMENTDB_IMAGE}
+ gateway: ${GATEWAY_IMAGE}
documentDbCredentialSecret: ${CREDENTIAL_SECRET}
resource:
storage:
diff --git a/test/e2e/pkg/e2eutils/documentdb/documentdb.go b/test/e2e/pkg/e2eutils/documentdb/documentdb.go
index 47689c0b..c1159426 100644
--- a/test/e2e/pkg/e2eutils/documentdb/documentdb.go
+++ b/test/e2e/pkg/e2eutils/documentdb/documentdb.go
@@ -241,7 +241,7 @@ func RenderCR(baseName, name, ns string, mixins []string, vars map[string]string
// when merged[VAR] is an empty string. CNPG's envsubst treats empty
// values as missing, so this lets callers opt fields out of the
// rendered YAML by leaving the corresponding variable unset. Operator
-// defaults (documentDBImage, gatewayImage, ...) thus fall through to
+// defaults (image.documentDB, image.gateway, ...) thus fall through to
// server-side defaults instead of being forced to a pinned value.
func DropEmptyVarLines(data []byte, merged map[string]string) []byte {
return dropEmptyVarLines(data, merged)
@@ -250,7 +250,7 @@ func DropEmptyVarLines(data []byte, merged map[string]string) []byte {
// singleVarLineRe matches a line whose non-whitespace content is a
// single YAML scalar assignment to a single ${VAR} reference, e.g.:
//
-// documentDBImage: ${DOCUMENTDB_IMAGE}
+// documentDB: ${DOCUMENTDB_IMAGE}
//
// Leading whitespace is preserved, the captured group is the bare
// variable name. Lines with additional text around the reference do
@@ -261,8 +261,8 @@ var singleVarLineRe = regexp.MustCompile(`^\s*[A-Za-z0-9_.\-]+:\s*\$\{([A-Za-z_]
// `key: ${VAR}` when merged[VAR] is an empty string. CNPG's envsubst
// treats empty values as missing, so this lets callers opt fields out
// of the rendered CR by leaving the corresponding variable unset.
-// Fields the operator defaults server-side (e.g. documentDBImage,
-// gatewayImage) thus fall through to operator defaults.
+// Fields the operator defaults server-side (e.g. image.documentDB,
+// image.gateway) thus fall through to operator defaults.
func dropEmptyVarLines(data []byte, merged map[string]string) []byte {
if !bytes.Contains(data, []byte("${")) {
return data
diff --git a/test/e2e/tests/feature_gates/feature_gates_suite_test.go b/test/e2e/tests/feature_gates/feature_gates_suite_test.go
index 690b429c..5636d4f3 100644
--- a/test/e2e/tests/feature_gates/feature_gates_suite_test.go
+++ b/test/e2e/tests/feature_gates/feature_gates_suite_test.go
@@ -83,7 +83,7 @@ Expect(e2e.CheckOperatorUnchanged()).To(Succeed(),
// (single key: spec.featureGates.ChangeStreams: true).
// 2. Gate the spec behind a `needs-changestream-image` capability
// label (mirrors `needs-cert-manager`) and a preflight check that
-// skips when the current documentDBImage cannot handle it.
+// skips when the current spec.image.documentDB cannot handle it.
// 3. Layer a best-effort mongo-driver `Watch` smoke on top of the
// existing wal_level assertion so both the operator and extension
// contracts are covered.
diff --git a/test/e2e/tests/lifecycle/update_image_test.go b/test/e2e/tests/lifecycle/update_image_test.go
index 9a1f9fb6..a3c5abe7 100644
--- a/test/e2e/tests/lifecycle/update_image_test.go
+++ b/test/e2e/tests/lifecycle/update_image_test.go
@@ -21,12 +21,13 @@ import (
// The design doc calls the field `spec.documentDbVersion`; the CRD at
// operator/src/api/preview/documentdb_types.go names it DocumentDBVersion
-// (JSON `documentDBVersion`) and also exposes DocumentDBImage / GatewayImage
-// which take precedence when set. Because the base template provides
-// DocumentDBImage (not Version), we exercise the rollout via the image
-// field and assert against Status.DocumentDBImage — Phase 3 follow-up to
-// parameterise this once the Version-only path is wired into manifests.
-var _ = Describe("DocumentDB lifecycle — update documentDBImage",
+// (JSON `documentDBVersion`) and also exposes spec.image.documentDB /
+// spec.image.gateway which take precedence when set. Because the base
+// template provides spec.image.documentDB (not Version), we exercise the
+// rollout via the image field and assert against Status.DocumentDBImage —
+// Phase 3 follow-up to parameterise this once the Version-only path is
+// wired into manifests.
+var _ = Describe("DocumentDB lifecycle — update spec.image.documentDB",
Label(e2e.LifecycleLabel, e2e.DisruptiveLabel), e2e.MediumLevelLabel,
func() {
const name = "lifecycle-update-image"
@@ -81,7 +82,10 @@ var _ = Describe("DocumentDB lifecycle — update documentDBImage",
// Refetch for a fresh resourceVersion before patching.
fresh := getDD(ctx, ns, name)
Expect(documentdb.PatchSpec(ctx, c, fresh, func(s *previewv1.DocumentDBSpec) {
- s.DocumentDBImage = targetImage
+ if s.Image == nil {
+ s.Image = &previewv1.ImageSpec{}
+ }
+ s.Image.DocumentDB = targetImage
})).To(Succeed())
Eventually(func() string {
diff --git a/test/e2e/tests/upgrade/helpers_test.go b/test/e2e/tests/upgrade/helpers_test.go
index 1cda61ca..9f6792a2 100644
--- a/test/e2e/tests/upgrade/helpers_test.go
+++ b/test/e2e/tests/upgrade/helpers_test.go
@@ -30,8 +30,8 @@ const (
envNewDocumentDBImage = "E2E_UPGRADE_NEW_DOCUMENTDB_IMAGE"
// Optional gateway image overrides for the image-upgrade spec.
- // When unset the spec patches only spec.documentDBImage and leaves
- // spec.gatewayImage as-is (operator uses its default gateway). The
+ // When unset the spec patches only spec.image.documentDB and leaves
+ // spec.image.gateway as-is (operator uses its default gateway). The
// gateway image has an independent release cadence from the
// extension image; setting these to the same value as the
// documentdb env vars is INCORRECT under the layered-image
diff --git a/test/e2e/tests/upgrade/upgrade_images_test.go b/test/e2e/tests/upgrade/upgrade_images_test.go
index c71a58b8..c2bd1446 100644
--- a/test/e2e/tests/upgrade/upgrade_images_test.go
+++ b/test/e2e/tests/upgrade/upgrade_images_test.go
@@ -26,8 +26,8 @@ import (
)
// DocumentDB upgrade — images: with the operator already running at
-// the current version, patches the DocumentDB spec.documentDBImage
-// (and spec.gatewayImage) from an old image tag to a new one and
+// the current version, patches the DocumentDB spec.image.documentDB
+// (and spec.image.gateway) from an old image tag to a new one and
// verifies the rollout completes + the seeded dataset is retained.
// Unlike upgrade_control_plane_test.go this does not touch the Helm
// release; it only exercises the CR-driven data-plane image upgrade
@@ -128,13 +128,16 @@ var _ = Describe("DocumentDB upgrade — images",
Expect(inserted).To(Equal(seed.SmallDatasetSize))
Expect(handle.Close(ctx)).To(Succeed())
- By("patching spec.documentDBImage (and optionally gatewayImage) to the new image")
+ By("patching spec.image.documentDB (and optionally image.gateway) to the new image")
fresh, err := documentdb.Get(ctx, c, key)
Expect(err).NotTo(HaveOccurred(), "re-fetch DocumentDB before patch")
Expect(documentdb.PatchSpec(ctx, c, fresh, func(s *previewv1.DocumentDBSpec) {
- s.DocumentDBImage = newImage
+ if s.Image == nil {
+ s.Image = &previewv1.ImageSpec{}
+ }
+ s.Image.DocumentDB = newImage
if newGwImage != "" {
- s.GatewayImage = newGwImage
+ s.Image.Gateway = newGwImage
}
})).To(Succeed(), "patch DocumentDB image from %s to %s", oldImage, newImage)