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
10 changes: 6 additions & 4 deletions .github/workflows/release_documentdb_images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,14 @@
- name: Detect current default version
id: current
run: |
# Extract the assigned DEFAULT_DOCUMENTDB_IMAGE value from constants.go.
CURRENT=$(sed -nE 's|^[[:space:]]*DEFAULT_DOCUMENTDB_IMAGE[[:space:]]*=.*:([0-9]+\.[0-9]+\.[0-9]+)".*|\1|p' \
# Extract the assigned DEFAULT_DOCUMENTDB_TAG value from constants.go.
CURRENT=$(sed -nE 's|^[[:space:]]*DEFAULT_DOCUMENTDB_TAG[[:space:]]*=[[:space:]]*"([0-9]+\.[0-9]+\.[0-9]+)".*|\1|p' \
operator/src/internal/utils/constants.go | head -1)
if [[ -z "$CURRENT" ]]; then
echo "Failed to extract current default version from operator/src/internal/utils/constants.go" >&2
exit 1
fi
echo "current_version=$CURRENT" >> $GITHUB_OUTPUT

Check warning on line 84 in .github/workflows/release_documentdb_images.yml

View workflow job for this annotation

GitHub Actions / lint

[actionlint] reported by reviewdog 🐶 shellcheck reported issue in this script: SC2086:info:8:36: Double quote to prevent globbing and word splitting [shellcheck] Raw Output: i:.github/workflows/release_documentdb_images.yml:84:46: shellcheck reported issue in this script: SC2086:info:8:36: Double quote to prevent globbing and word splitting [shellcheck]
echo "Current default version: $CURRENT"

- name: Update version references
Expand All @@ -98,8 +98,10 @@

echo "Updating default versions: $OLD_VERSION → $NEW_VERSION"

# 1. Update operator constants.go
sed -i "s|:${OLD_VERSION}\"|:${NEW_VERSION}\"|g" \
# 1. Update operator constants.go. DEFAULT_DOCUMENTDB_TAG is the single
# version literal; the DEFAULT_DOCUMENTDB_IMAGE/DEFAULT_GATEWAY_IMAGE
# defaults are composed from it, so bumping the tag is sufficient.
sed -i "s|DEFAULT_DOCUMENTDB_TAG = \"${OLD_VERSION}\"|DEFAULT_DOCUMENTDB_TAG = \"${NEW_VERSION}\"|" \
operator/src/internal/utils/constants.go

# 2. Update sidecar plugin config.go
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
## [Unreleased]

### Major Features
- **Configurable image registry and repositories**: The Helm chart now exposes an `image.registry` prefix (default `ghcr.io`) plus relative per-component `repository` values, so every operator and data-plane image (operator, sidecar-injector, wal-replica, documentdb extension, gateway, and the optional OpenTelemetry collector / PostgreSQL operand) can be re-homed to a private or mirrored registry without rebuilding the operator. A `repository` that already includes a host (contains `.` or `:`, or is `localhost`) is used verbatim and ignores the prefix, letting you point a single image at a different registry. Repositories are host/path only — the tag is supplied via each component's `tag` field (or `documentDbVersion` for the data-plane images), and a tag or digest embedded in a `repository` is rejected at render time. For the optional OpenTelemetry collector and PostgreSQL operand images (which have no fallback tag source), the `repository` and `tag` must be set together — supplying only one is rejected at render time so a mistyped override can never leak a floating `:latest` or be silently dropped. Default rendering is unchanged (`ghcr.io/documentdb/...`), so existing deployments are unaffected.
- **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).
- **DocumentDB extension packages from PGDG**: The database image build now resolves `postgresql-18-documentdb` from the [PGDG APT repository](https://apt.postgresql.org/) (`trixie-pgdg`), pinning one version and SHA256 across both architectures and validating `default_version` before building. Replaces the upstream Debian 13 release assets that stopped being published after `0.115`.

### Breaking Changes
- **Image pull-policy Helm values moved under `image`**: The top-level `gatewayImagePullPolicy` and `documentDbImagePullPolicy` chart values have been removed and replaced by `image.gateway.pullPolicy` and `image.documentdb.pullPolicy` (both default `IfNotPresent`), aligning them with the other per-component image settings. Existing values files or `--set` overrides using the old keys no longer take effect; update them to the new nested keys. (Preview-only breaking change.)

## [0.3.0] - 2026-07-15

### Security
Expand Down
10 changes: 8 additions & 2 deletions docs/designs/image-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,10 +164,16 @@ image:

### Image Tag Resolution in Templates

**Operator-track images** use `Chart.AppVersion`:
**Operator-track images** are composed by the `documentdb.imageRef` helper, which
prepends `image.registry` to a relative repository (or uses the repository
verbatim when it already carries a host, i.e. its first segment contains `.` or
`:`, or equals `localhost`) and appends the tag, defaulting to `Chart.AppVersion`:
```yaml
image: "{{ .Values.image.documentdbk8soperator.repository }}:{{ .Values.image.documentdbk8soperator.tag | default .Chart.AppVersion }}"
image: "{{ include "documentdb.imageRef" (dict "name" "image.documentdbk8soperator.repository" "registry" .Values.image.registry "repo" .Values.image.documentdbk8soperator.repository "tag" (.Values.image.documentdbk8soperator.tag | default .Chart.AppVersion)) }}"
```
Repositories are host/path only: an empty registry/repository, or a tag or digest
embedded in the repository, fails rendering (naming the offending setting) rather
than producing an invalid reference.

**Database version** is passed as an environment variable:
```yaml
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ spec:
- --server-key=/server/tls.key
- --client-cert=/client/tls.crt
- --server-address=:9090
image: "{{ .Values.image.sidecarinjector.repository }}:{{ .Values.image.sidecarinjector.tag | default .Chart.AppVersion }}"
image: "{{ include "documentdb.imageRef" (dict "name" "image.sidecarinjector.repository" "registry" .Values.image.registry "repo" .Values.image.sidecarinjector.repository "tag" (.Values.image.sidecarinjector.tag | default .Chart.AppVersion)) }}"
imagePullPolicy: "{{ .Values.image.sidecarinjector.pullPolicy }}"
name: cnpg-i-sidecar-injector
{{- with .Values.sidecarInjector.containerSecurityContext }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ spec:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- image: "{{ .Values.image.walreplica.repository }}:{{ .Values.image.walreplica.tag | default .Chart.AppVersion }}"
- image: "{{ include "documentdb.imageRef" (dict "name" "image.walreplica.repository" "registry" .Values.image.registry "repo" .Values.image.walreplica.repository "tag" (.Values.image.walreplica.tag | default .Chart.AppVersion)) }}"
imagePullPolicy: "{{ .Values.image.walreplica.pullPolicy }}"
name: cnpg-i-wal-replica
{{- with .Values.walReplicaPlugin.containerSecurityContext }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ spec:
{{- end }}
containers:
- name: documentdb-operator
image: "{{ .Values.image.documentdbk8soperator.repository }}:{{ .Values.image.documentdbk8soperator.tag | default .Chart.AppVersion }}"
image: "{{ include "documentdb.imageRef" (dict "name" "image.documentdbk8soperator.repository" "registry" .Values.image.registry "repo" .Values.image.documentdbk8soperator.repository "tag" (.Values.image.documentdbk8soperator.tag | default .Chart.AppVersion)) }}"
imagePullPolicy: "{{ .Values.image.documentdbk8soperator.pullPolicy }}"
{{- with .Values.operator.containerSecurityContext }}
securityContext:
Expand Down Expand Up @@ -115,13 +115,39 @@ spec:
- name: DOCUMENTDB_VERSION
value: "{{ .Values.documentDbVersion }}"
{{- end }}
{{- if .Values.gatewayImagePullPolicy }}
# Data-plane image repositories (host+path). The operator composes the
# final extension/gateway image using DOCUMENTDB_VERSION as the tag.
{{- if .Values.image.documentdb.repository }}
- name: DOCUMENTDB_EXTENSION_IMAGE_REPO
value: "{{ include "documentdb.imageRef" (dict "name" "image.documentdb.repository" "registry" .Values.image.registry "repo" .Values.image.documentdb.repository "tag" "") }}"
{{- end }}
{{- if .Values.image.gateway.repository }}
- name: GATEWAY_IMAGE_REPO
value: "{{ include "documentdb.imageRef" (dict "name" "image.gateway.repository" "registry" .Values.image.registry "repo" .Values.image.gateway.repository "tag" "") }}"
{{- end }}
# Extra images: composed via the same registry-prefix rule. The
# repository and tag must be set together (both or neither); otherwise
# the operator uses its compiled-in default (otel) or defers to CNPG
# (postgres).
{{- $otelImage := include "documentdb.extraImageRef" (dict "registry" .Values.image.registry "repo" .Values.image.otelCollector.repository "tag" .Values.image.otelCollector.tag "repoName" "image.otelCollector.repository" "tagName" "image.otelCollector.tag") }}
{{- if $otelImage }}
- name: OTEL_COLLECTOR_IMAGE
value: "{{ $otelImage }}"
{{- end }}
{{- $postgresImage := include "documentdb.extraImageRef" (dict "registry" .Values.image.registry "repo" .Values.image.postgres.repository "tag" .Values.image.postgres.tag "repoName" "image.postgres.repository" "tagName" "image.postgres.tag") }}
{{- if $postgresImage }}
- name: POSTGRES_IMAGE
value: "{{ $postgresImage }}"
{{- end }}
{{- $gwPolicy := include "documentdb.pullPolicy" (dict "name" "image.gateway.pullPolicy" "policy" .Values.image.gateway.pullPolicy) }}
{{- if $gwPolicy }}
- name: GATEWAY_IMAGE_PULL_POLICY
value: "{{ .Values.gatewayImagePullPolicy }}"
value: "{{ $gwPolicy }}"
{{- end }}
{{- if .Values.documentDbImagePullPolicy }}
{{- $ddbPolicy := include "documentdb.pullPolicy" (dict "name" "image.documentdb.pullPolicy" "policy" .Values.image.documentdb.pullPolicy) }}
{{- if $ddbPolicy }}
- name: DOCUMENTDB_IMAGE_PULL_POLICY
value: "{{ .Values.documentDbImagePullPolicy }}"
value: "{{ $ddbPolicy }}"
{{- end }}
{{- if .Values.operator.ioUring.seccompProfile }}
- name: DOCUMENTDB_IOURING_SECCOMP_PROFILE
Expand Down
91 changes: 91 additions & 0 deletions operator/documentdb-helm-chart/templates/_helpers.tpl
Original file line number Diff line number Diff line change
@@ -1,3 +1,94 @@
{{- define "documentdb-chart.name" -}}
documentdb-operator
{{- end -}}

{{/*
documentdb.imageRef composes a container image reference from a registry prefix,
a repository, and an optional tag, following the canonical Docker/containerd
reference rule for deciding whether the repository already carries a registry
host: the first path segment is treated as a registry host when it contains a
"." or ":" (port), or equals "localhost". In that case the repository is used
verbatim and the registry prefix is ignored (this is also the per-component
per-registry override path). Otherwise the registry prefix is prepended.

The repository must be a non-empty host/path only. Rendering fails (naming the
offending setting via "name") when the repository is empty, when it already
carries a tag or digest (a ":" or "@" in the final path segment), or when the
registry prefix would be needed but is empty. The tag is always supplied via the
component's tag field (or documentDbVersion for the data-plane images) rather
than embedded in the repository.

Usage:
{{ include "documentdb.imageRef" (dict "name" "image.foo.repository" "registry" .Values.image.registry "repo" $repo "tag" $tag) }}
Pass tag "" to compose a bare repository (host+path, no tag).
*/}}
{{- define "documentdb.imageRef" -}}
{{- $name := .name | default "image repository" -}}
{{- if not .repo -}}
{{- fail (printf "%s is empty; set it to a host/path (with image.registry) or a full host reference" $name) -}}
{{- end -}}
{{- $lastSeg := (splitList "/" .repo) | last -}}
{{- if or (contains ":" $lastSeg) (contains "@" $lastSeg) -}}
{{- fail (printf "%s value %q must be a host/path without a tag or digest; set the tag via the component's tag field (or documentDbVersion for data-plane images)" $name .repo) -}}
{{- end -}}
{{- $first := (splitList "/" .repo) | first -}}
{{- $full := .repo -}}
{{- if not (or (contains "." $first) (contains ":" $first) (eq $first "localhost")) -}}
{{- if not .registry -}}
{{- fail (printf "image.registry is empty but %s (%q) is a relative path; set image.registry or use a full host reference in the repository" $name .repo) -}}
{{- end -}}
{{- $full = printf "%s/%s" .registry .repo -}}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An empty image.registry or operator repository renders an invalid image such as /documentdb/...:0.3.0 or ghcr.io/:0.3.0. Helm reports success and the failure appears later at pod startup. Can we fail rendering with the name of the empty setting?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ab28ab1. documentdb.imageRef now fails rendering when the repository is empty, or when image.registry is empty but a relative repository needs the prefix — instead of emitting /documentdb/... or ghcr.io/:tag. A required name argument makes the error name the exact setting, e.g. image.registry is empty but image.documentdbk8soperator.repository ("...") is a relative path and image.documentdbk8soperator.repository is empty. This covers all components (they share the helper). Added helm-unittest cases for empty registry and empty repository.

{{- end -}}
{{- if .tag -}}
{{- printf "%s:%s" $full .tag -}}
{{- else -}}
{{- $full -}}
{{- end -}}
{{- end -}}

{{/*
documentdb.extraImageRef composes an optional override image reference for the
"extra" images (otel collector, postgres) that carry no fallback tag source.
Unlike the first-party images (whose tag defaults to Chart.AppVersion or
documentDbVersion), these have no default tag, so a partial override is always a
mistake: a repository without a tag would leak a floating ":latest", and a tag
without a repository would be silently dropped. This helper therefore enforces
that the component's repository and tag are set together (both or neither):

- both empty -> returns "" (caller omits the env var; the operator uses its
compiled-in default for otel, or defers to CNPG for postgres)
- both set -> returns the composed "registry/repo:tag" reference
- exactly one -> rendering fails, naming both settings

Usage:
{{ include "documentdb.extraImageRef" (dict "registry" .Values.image.registry "repo" $repo "tag" $tag "repoName" "image.otelCollector.repository" "tagName" "image.otelCollector.tag") }}
*/}}
{{- define "documentdb.extraImageRef" -}}
{{- if and .repo (not .tag) -}}
{{- fail (printf "%s is set but %s is empty; pin a tag (%s and %s must be set together)" .repoName .tagName .repoName .tagName) -}}
{{- end -}}
{{- if and .tag (not .repo) -}}
{{- fail (printf "%s is set but %s is empty; set the repository (%s and %s must be set together)" .tagName .repoName .repoName .tagName) -}}
{{- end -}}
{{- if .repo -}}
{{- include "documentdb.imageRef" (dict "name" .repoName "registry" .registry "repo" .repo "tag" .tag) -}}
{{- end -}}
{{- end -}}

{{/*
documentdb.pullPolicy validates and echoes an image pull policy. An empty value
yields an empty string (the caller omits the setting and the consumer applies
its own default); a non-empty value must be one of Always, IfNotPresent, or
Never, otherwise rendering fails naming the offending setting via "name".

Usage:
{{ include "documentdb.pullPolicy" (dict "name" "image.gateway.pullPolicy" "policy" .Values.image.gateway.pullPolicy) }}
*/}}
{{- define "documentdb.pullPolicy" -}}
{{- if .policy -}}
{{- if not (has .policy (list "Always" "IfNotPresent" "Never")) -}}
{{- fail (printf "%s must be one of Always, IfNotPresent, Never (got %q)" .name .policy) -}}
{{- end -}}
{{- .policy -}}
{{- end -}}
{{- end -}}
Loading
Loading