Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions .github/actions/setup-test-environment/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -151,12 +151,18 @@ runs:
echo " Operator: $OPERATOR_IMAGE"
echo " Sidecar: $SIDECAR_IMAGE"

if ! docker images --format "table {{.Repository}}:{{.Tag}}" | grep -q "$OPERATOR_IMAGE"; then
# Capture the list first rather than piping into grep. Under
# `set -o pipefail`, `grep -q` exits as soon as it matches, which
# SIGPIPEs docker and makes the pipeline report failure even though the
# image was found. Matching against a variable removes that race.
LOADED_IMAGES="$(docker images --format '{{.Repository}}:{{.Tag}}')"

if ! grep -qxF "$OPERATOR_IMAGE" <<< "$LOADED_IMAGES"; then
echo "❌ Required operator image not found: $OPERATOR_IMAGE"
exit 1
fi

if ! docker images --format "table {{.Repository}}:{{.Tag}}" | grep -q "$SIDECAR_IMAGE"; then
if ! grep -qxF "$SIDECAR_IMAGE" <<< "$LOADED_IMAGES"; then
echo "❌ Required sidecar image not found: $SIDECAR_IMAGE"
exit 1
fi
Expand Down Expand Up @@ -428,8 +434,11 @@ runs:
fi
done

# Check if cert-manager is already installed
if helm list -n ${{ inputs.cert-manager-namespace }} 2>/dev/null | grep -q cert-manager; then
# Check if cert-manager is already installed. Capture first: piping into
# `grep -q` under `set -o pipefail` can SIGPIPE helm and misreport a match
# as a failure.
HELM_RELEASES="$(helm list -n ${{ inputs.cert-manager-namespace }} 2>/dev/null || true)"
if grep -q cert-manager <<< "$HELM_RELEASES"; then
echo "cert-manager is already installed, skipping installation"
echo "CERT_MANAGER_READY=true" >> $GITHUB_ENV
else
Expand Down
8 changes: 5 additions & 3 deletions .github/workflows/test-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,15 @@ on:
type: string
default: ''
depth:
description: 'Test depth tier (mapped to TEST_DEPTH; accepts Low|Medium|High)'
description: 'Test depth tier (mapped to TEST_DEPTH). Lowest is the deepest/full sweep; Highest is the shallowest.'
required: false
type: choice
options:
- Low
- Medium
- Highest
- High
- Medium
- Low
- Lowest
default: Medium
keep_clusters:
description: 'Keep Kind clusters running after tests (for debugging)'
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## [Unreleased]

### Major Features
- **Fail-fast preflight for extension update paths**: Before running `ALTER EXTENSION documentdb UPDATE`, the operator now queries `pg_extension_update_paths` to confirm PostgreSQL can actually resolve a migration path from the installed schema to the requested `spec.schemaVersion`. When no path exists — for example when `schemaVersion` names a version that was never released — the operator **skips the migration entirely** rather than firing an `ALTER EXTENSION` that fails with a raw PostgreSQL error on every reconcile. It surfaces a `SchemaUpgradeBlocked` status condition (reason `NoUpdatePath`) naming both versions, plus a matching warning event, and stops cleanly; correcting `spec.schemaVersion` clears the condition and completes the migration. Multi-minor jumps (e.g. `0.110.0` → `0.113.0`) remain a fully supported single-step path — the preflight only blocks paths PostgreSQL genuinely cannot resolve. `DocumentDB.status` gains a standard `conditions` array to carry this. See the [upgrade guide](docs/operator-public-documentation/preview/operations/upgrades.md#skipping-versions).
- **Fail-fast ImageVolume capability check**: The operator now depends on the Kubernetes [ImageVolume](https://kubernetes.io/docs/concepts/storage/volumes/#image) feature to mount the DocumentDB extension into PostgreSQL pods. Instead of gating on a Kubernetes version number, the validating webhook performs a capability probe (a server-side dry-run) when a `DocumentDB` is created and **rejects the resource with an actionable error if ImageVolume is unavailable**, so you find out immediately instead of waiting for pods that never become ready. ImageVolume is GA (on by default) in Kubernetes **1.35+**; on **1.33/1.34** it is beta and must be enabled via the `ImageVolume` feature gate on a containerd/CRI-O runtime. The Helm chart's `kubeVersion` floor is relaxed to `>= 1.33.0-0` accordingly. See [Before you start](docs/operator-public-documentation/preview/getting-started/before-you-start.md).

## [0.3.0] - 2026-07-15
Expand Down
31 changes: 28 additions & 3 deletions docs/operator-public-documentation/preview/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,28 @@ _Appears in:_
| `primary` _string_ | Primary is the name of the primary cluster for replication. | | |
| `clusterList` _[MemberCluster](#membercluster) array_ | ClusterList is the list of clusters participating in replication. | | |
| `highAvailability` _boolean_ | Whether or not to have replicas on the primary cluster. | | |
| `disableTLS` _boolean_ | Disables TLS for replication traffic between clusters.<br />Only for use when an existing mesh is already providing TLS. | false | |


#### ComponentResources



ComponentResources overrides the CPU and/or memory allocated to an individual
container in the DocumentDB pod (PostgreSQL, the gateway, or the OTel
collector). Each field is a Kubernetes quantity string; when set it is applied
as both the request and the limit for that container (Guaranteed-class) and
overrides the automatic carve-out derived from spec.resource.memory.



_Appears in:_
- [Resource](#resource)

| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `memory` _string_ | Memory is the memory request=limit for the container (e.g. "512Mi", "2Gi"). | | Pattern: `^([0-9]+(\.[0-9]+)?(m\|Ki\|Mi\|Gi\|Ti\|Pi\|Ei\|k\|M\|G\|T\|P\|E)?)?$` <br />Optional: \{\} <br /> |
| `cpu` _string_ | CPU is the CPU request=limit for the container (e.g. "500m", "2"). | | Pattern: `^([0-9]+(\.[0-9]+)?(m\|Ki\|Mi\|Gi\|Ti\|Pi\|Ei\|k\|M\|G\|T\|P\|E)?)?$` <br />Optional: \{\} <br /> |


#### DocumentDB
Expand Down Expand Up @@ -254,7 +276,7 @@ _Appears in:_
| --- | --- | --- | --- |
| `documentDB` _string_ | DocumentDB is the container image for the DocumentDB extension layer.<br />This image is mounted into the PostgreSQL container via CNPG's<br />ImageVolumeSource so that the extension files are available alongside<br />an upstream PostgreSQL image. | | Optional: \{\} <br /> |
| `gateway` _string_ | Gateway is the container image for the DocumentDB Gateway sidecar. | | Optional: \{\} <br /> |
| `postgres` _string_ | Postgres is the container image for the PostgreSQL server.<br />Must be an upstream CNPG-compatible PostgreSQL image (the operator<br />adds the DocumentDB extension via an ImageVolume mount), and must<br />use trixie (Debian 13) base to match the extension's GLIBC<br />requirements. | ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie | Optional: \{\} <br /> |
| `postgres` _string_ | Postgres is the container image for the PostgreSQL server.<br />Must be an upstream CNPG-compatible PostgreSQL image (the operator<br />adds the DocumentDB extension via an ImageVolume mount), and must<br />use trixie (Debian 13) base to match the extension's GLIBC<br />requirements.<br />Pinned to the 18.4 minor tag instead of the floating<br />"18-minimal-trixie" tag, which rolled 18.4 -> 18.6 on 2026-08-13 and<br />crashed the DocumentDB 0.113.0 extension on insert. Staying on 18.4<br />avoids that regression while still receiving CNPG's Debian/PGDG<br />security rebuilds; revert to the floating "18-minimal-trixie" tag once<br />a DocumentDB release carrying the PG 18.6 fix ships. | ghcr.io/cloudnative-pg/postgresql:18.4-minimal-trixie | Optional: \{\} <br /> |


#### IssuerRef
Expand Down Expand Up @@ -443,7 +465,10 @@ _Appears in:_
| --- | --- | --- | --- |
| `storage` _[StorageConfiguration](#storageconfiguration)_ | Storage configuration for DocumentDB persistent volumes. | | |
| `memory` _string_ | Memory specifies the memory limit for each DocumentDB instance pod.<br />This value is passed to the CNPG Cluster's spec.resources.limits.memory<br />and spec.resources.requests.memory (Guaranteed QoS).<br />Memory-aware PostgreSQL parameters (shared_buffers, effective_cache_size, etc.)<br />are auto-computed from this value.<br />If not specified or set to "0", no memory limit is applied and static<br />defaults are used for memory-aware parameters.<br />Examples: "2Gi", "4Gi", "8Gi" | | Optional: \{\} <br /> |
| `cpu` _string_ | CPU specifies the CPU limit for each DocumentDB instance pod.<br />This value is passed to the CNPG Cluster's spec.resources.limits.cpu<br />and spec.resources.requests.cpu (Guaranteed QoS).<br />If not specified or set to "0", no CPU limit is applied.<br />Examples: "2", "4", "500m" | | Optional: \{\} <br /> |
| `cpu` _string_ | CPU specifies the total CPU envelope for each DocumentDB instance pod.<br />The operator divides this envelope across PostgreSQL, the documentdb-gateway<br />sidecar, and, when monitoring is enabled, the OTel collector sidecar.<br />PostgreSQL receives the remainder after gateway and OTel CPU reservations;<br />an explicit per-container CPU override wins over the automatic carve-out.<br />If not specified or set to "0", no CPU envelope is applied.<br />Examples: "2", "4", "500m" | | Optional: \{\} <br /> |
| `gateway` _[ComponentResources](#componentresources)_ | Gateway optionally overrides the resources allocated to the<br />documentdb-gateway sidecar container. When unset, the operator derives the<br />gateway's memory as min(gatewayMemoryFraction × memory, gatewayMemoryCap)<br />and carves it out of the pod memory envelope. The value is applied as both<br />the request and the limit (Guaranteed-class) so a gateway leak is<br />OOM-isolated and cannot crowd out PostgreSQL. | | Optional: \{\} <br /> |
| `database` _[ComponentResources](#componentresources)_ | Database optionally overrides the resources allocated to the PostgreSQL<br />container. When unset, PostgreSQL receives the pod memory and CPU envelopes<br />minus the gateway and (when monitoring is enabled) OTel collector carve-outs. | | Optional: \{\} <br /> |
| `otel` _[ComponentResources](#componentresources)_ | OTel optionally overrides the resources allocated to the otel-collector<br />sidecar container (only present when spec.monitoring.enabled is true).<br />When unset, the operator applies built-in defaults: memory request 48Mi /<br />limit 128Mi and CPU request 50m / limit 200m (Burstable — the requests are<br />the reserved floor and the limits cap a telemetry burst). Setting otel.cpu<br />or otel.memory pins that dimension to request == limit (Guaranteed). | | Optional: \{\} <br /> |


#### ScheduledBackup
Expand Down Expand Up @@ -514,7 +539,7 @@ _Appears in:_
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `gateway` _[GatewayTLS](#gatewaytls)_ | Gateway configures TLS for the gateway sidecar (Phase 1: certificate provisioning only). | | |
| `postgres` _[CertificatesConfiguration](https://pkg.go.dev/github.com/cloudnative-pg/cloudnative-pg/api/v1#CertificatesConfiguration)_ | Postgres configures TLS for the Postgres server. | | |
| `postgres` _[CertificatesConfiguration](https://pkg.go.dev/github.com/cloudnative-pg/cloudnative-pg/api/v1#CertificatesConfiguration)_ | Postgres configures TLS for the Postgres server.<br />If server side certs are provided alone, the operator will use sslMode=require for cross-regional replication connections.<br />If replication certs are also provided, the operator will use verify-full, which requires the hostname to be correctly set.<br />See the multi-region-deployment docs for how to do that. | | |
| `globalEndpoints` _[GlobalEndpointsTLS](#globalendpointstls)_ | GlobalEndpoints configures TLS for global endpoints (placeholder for future phases). | | |


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,22 @@ Choose the approach that matches your use case:
!!! warning
With `schemaVersion: "auto"`, the schema migration is irreversible once applied. You cannot roll back to the previous version — only restore from backup.

### Skipping Versions

You do **not** have to upgrade one minor at a time. DocumentDB ships a continuous chain of extension migration scripts, so a single upgrade can span several minors — for example `0.110.0` → `0.113.0`. PostgreSQL resolves the intermediate steps internally and the operator applies them in one `ALTER EXTENSION UPDATE`.

Before running the migration the operator **preflights** the update path by querying `pg_extension_update_paths`. If no path exists between the installed schema and the version you requested, the operator does **not** run `ALTER EXTENSION` at all. Instead it stops cleanly and reports:

- a `SchemaUpgradeBlocked` condition with status `True` and reason `NoUpdatePath` on `status.conditions`, and
- a `SchemaUpgradeBlocked` warning event on the DocumentDB resource.

```bash
kubectl get documentdb my-cluster -n default \
-o jsonpath='{.status.conditions[?(@.type=="SchemaUpgradeBlocked")]}'
```

The most common cause is requesting a `schemaVersion` that was never released (for example a patch version that does not exist). Set `spec.schemaVersion` to a real released version — or to `"auto"` to target whatever the binary provides — and the condition clears on the next reconcile. Your data is untouched while the upgrade is blocked: the migration never started.

### Monitoring the Upgrade

```bash
Expand All @@ -264,6 +280,10 @@ kubectl get documentdb my-cluster -n default

# Check the current schema version
kubectl get documentdb my-cluster -n default -o jsonpath='{.status.schemaVersion}'

# Check whether a schema migration was blocked
kubectl get documentdb my-cluster -n default \
-o jsonpath='{.status.conditions[?(@.type=="SchemaUpgradeBlocked")].reason}'
```

### Rollback and Recovery
Expand Down
61 changes: 61 additions & 0 deletions operator/documentdb-helm-chart/crds/documentdb.io_dbs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1666,6 +1666,67 @@ spec:
status:
description: DocumentDBStatus defines the observed state of DocumentDB.
properties:
conditions:
description: Conditions represent the latest available observations
of the DocumentDB state.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
connectionString:
type: string
documentDBImage:
Expand Down
36 changes: 36 additions & 0 deletions operator/src/api/preview/documentdb_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -535,8 +535,44 @@ type DocumentDBStatus struct {

// TLS reports gateway TLS provisioning status (Phase 1).
TLS *TLSStatus `json:"tls,omitempty"`

// Conditions represent the latest available observations of the DocumentDB state.
// +optional
// +listType=map
// +listMapKey=type
// +patchStrategy=merge
// +patchMergeKey=type
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
}

const (
// ConditionSchemaUpgradeBlocked is set when the operator has determined that the
// requested extension schema migration cannot be performed and has deliberately
// not run ALTER EXTENSION UPDATE. Status "True" means the upgrade is blocked.
ConditionSchemaUpgradeBlocked = "SchemaUpgradeBlocked"

// ReasonNoUpdatePath indicates PostgreSQL exposes no chain of extension update
// scripts between the installed schema version and the requested target, so
// ALTER EXTENSION UPDATE would fail. Used with ConditionSchemaUpgradeBlocked=True.
ReasonNoUpdatePath = "NoUpdatePath"

// ReasonUpdatePathAvailable indicates a resolvable update path exists between the
// installed schema version and the requested target. Used with
// ConditionSchemaUpgradeBlocked=False.
ReasonUpdatePathAvailable = "UpdatePathAvailable"

// ReasonSchemaUpToDate indicates no schema migration is pending. Used with
// ConditionSchemaUpgradeBlocked=False.
ReasonSchemaUpToDate = "SchemaUpToDate"

// ReasonNoMigrationPlanned indicates the operator is not attempting a schema
// migration on this reconcile — for example two-phase mode where the user has
// not set spec.schemaVersion, or a detected extension rollback. Used with
// ConditionSchemaUpgradeBlocked=False so a previously blocked upgrade does not
// leave a stale True condition after the user changes the spec.
ReasonNoMigrationPlanned = "NoMigrationPlanned"
)

// TLSStatus captures readiness and secret information.
type TLSStatus struct {
Ready bool `json:"ready,omitempty"`
Expand Down
8 changes: 8 additions & 0 deletions operator/src/api/preview/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading