Skip to content

Commit 8dc5348

Browse files
mattia-eleuteriMatthieu
andcommitted
feat(postgres): add a postgis image flavor
Tenants migrating spatial data need PostGIS, but the chart always runs the CloudNativePG PostgreSQL image, which does not ship it, and exposes no way to pick another one. CloudNativePG publishes a PostGIS operand image; a closed `flavor` enum selects it without handing tenants a free image field. The flavor pins the same PostgreSQL minor as the default one, on the standard-trixie variant: backups go through the barman-cloud plugin, so the deprecated system variant is not needed. update-versions.sh now derives the postgis map from the minors it picks, and a bats check fails if the two maps drift apart. The default images are built on bullseye and the postgis ones on trixie. Switching a live cluster between them would swap glibc under an initialised data directory and silently break every collatable index, and CNPG would roll it out as a plain image update. The chart therefore refuses a flavor change on an existing Cluster. Co-authored-by: Matthieu <[email protected]> Assisted-by: LLM Signed-off-by: Mattia Eleuteri <[email protected]>
1 parent 53829ea commit 8dc5348

12 files changed

Lines changed: 264 additions & 15 deletions

File tree

‎api/apps/v1alpha1/postgresql/types.go‎

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/usr/bin/env bats
2+
# Unit test: the postgis flavor of packages/apps/postgres must run the same
3+
# PostgreSQL minor as the default flavor. files/versions.yaml and
4+
# files/postgis-versions.yaml are two maps kept by hand or by
5+
# hack/update-versions.sh; if they drift, `flavor: postgis` silently deploys
6+
# an older or newer minor than the one the chart advertises. The postgis
7+
# image must also be the standard-trixie variant: the chart archives through
8+
# the barman-cloud plugin, and the system variant is deprecated upstream.
9+
10+
REPO_ROOT="$(cd "$(dirname "${BATS_TEST_FILENAME:-$0}")/.." && pwd)"
11+
PG_FILES="$REPO_ROOT/packages/apps/postgres/files"
12+
13+
@test "postgis flavor pins the same PostgreSQL minor as the default flavor" {
14+
checked=0
15+
while IFS=': ' read -r major tag; do
16+
major="${major//\"/}"
17+
tag="${tag//\"/}"
18+
[ -n "$major" ] || continue
19+
minor="$(grep "^\"${major}\":" "$PG_FILES/versions.yaml" | sed -E 's/.*"v([0-9.]+)"$/\1/')"
20+
[ -n "$minor" ] || { echo "$major is in postgis-versions.yaml but not in versions.yaml" >&2; exit 1; }
21+
case "$tag" in
22+
"${minor}"-*-standard-trixie) ;;
23+
*) echo "$major: postgis tag $tag does not match PostgreSQL $minor standard-trixie" >&2; exit 1 ;;
24+
esac
25+
checked=$((checked + 1))
26+
done < "$PG_FILES/postgis-versions.yaml"
27+
[ "$checked" -gt 0 ] || { echo "no entry read from postgis-versions.yaml" >&2; exit 1; }
28+
}

‎packages/apps/postgres/README.md‎

Lines changed: 12 additions & 11 deletions
Large diffs are not rendered by default.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"v18": "18.1-3.6.1-standard-trixie"
2+
"v17": "17.7-3.6.1-standard-trixie"
3+
"v16": "16.11-3.6.1-standard-trixie"
4+
"v15": "15.15-3.6.1-standard-trixie"
5+
"v14": "14.20-3.6.1-standard-trixie"
6+
"v13": "13.22-3.6.0-standard-trixie"

‎packages/apps/postgres/hack/update-versions.sh‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
88
POSTGRES_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
99
VALUES_FILE="${POSTGRES_DIR}/values.yaml"
1010
VERSIONS_FILE="${POSTGRES_DIR}/files/versions.yaml"
11+
POSTGIS_VERSIONS_FILE="${POSTGRES_DIR}/files/postgis-versions.yaml"
1112

1213
# Get supported major versions from GitHub README
1314
echo "Fetching supported major versions from GitHub..."
@@ -82,6 +83,27 @@ echo "Updating $VERSIONS_FILE..."
8283

8384
echo "Successfully updated $VERSIONS_FILE"
8485

86+
# The postgis flavor must stay on the same PostgreSQL minor as the default
87+
# flavor, so each major is pinned to the newest postgis build of the minor
88+
# chosen above. standard-trixie: backups go through the barman-cloud plugin,
89+
# so the operand does not need the deprecated system variant.
90+
echo "Fetching available postgis image tags from registry..."
91+
POSTGIS_TAGS=$(skopeo list-tags docker://ghcr.io/cloudnative-pg/postgis | jq -r '.Tags[] | select(test("^[0-9]+\\.[0-9]+-[0-9]+\\.[0-9]+\\.[0-9]+-standard-trixie$"))' | sort -V)
92+
93+
{
94+
for major_ver in "${MAJOR_VERSIONS[@]}"; do
95+
minor="${VERSION_MAP[$major_ver]#v}"
96+
postgis_tag=$(echo "$POSTGIS_TAGS" | grep "^${minor//./\\.}-" | tail -n1 || true)
97+
if [ -z "$postgis_tag" ]; then
98+
echo "Warning: no postgis image for PostgreSQL ${minor}, ${major_ver} left out of the postgis flavor" >&2
99+
continue
100+
fi
101+
echo "\"${major_ver}\": \"${postgis_tag}\""
102+
done
103+
} > "$POSTGIS_VERSIONS_FILE"
104+
105+
echo "Successfully updated $POSTGIS_VERSIONS_FILE"
106+
85107
# Update values.yaml - enum with major versions only
86108
TEMP_FILE=$(mktemp)
87109
trap "rm -f $TEMP_FILE" EXIT

‎packages/apps/postgres/templates/_versions.tpl‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,43 @@
66
{{- index $versionMap .Values.version }}
77
{{- end }}
88

9+
{{- define "postgres.postgisVersionMap" }}
10+
{{- $versionMap := .Files.Get "files/postgis-versions.yaml" | fromYaml }}
11+
{{- if not (hasKey $versionMap .Values.version) }}
12+
{{- printf `PostgreSQL version %s is not supported by the postgis flavor, allowed versions are %s` $.Values.version (keys $versionMap | sortAlpha) | fail }}
13+
{{- end }}
14+
{{- index $versionMap .Values.version }}
15+
{{- end }}
16+
17+
{{/*
18+
Single source for the operand image: the init Job runs psql against the
19+
Cluster, so both must resolve the same image.
20+
*/}}
21+
{{- define "postgres.imageName" -}}
22+
{{- if eq (default "postgresql" .Values.flavor) "postgis" -}}
23+
ghcr.io/cloudnative-pg/postgis:{{ include "postgres.postgisVersionMap" . | trim }}
24+
{{- else -}}
25+
ghcr.io/cloudnative-pg/postgresql:{{ include "postgres.versionMap" . | trim | trimPrefix "v" }}
26+
{{- end -}}
27+
{{- end -}}
28+
29+
{{/*
30+
The flavor is fixed at bootstrap. The two image families are built on
31+
different Debian releases, so moving a live cluster from one to the other
32+
swaps glibc under an initialised data directory and silently invalidates
33+
every index on collatable text; the postgis-to-postgresql direction also
34+
drops the shared libraries behind every PostGIS object. CNPG would roll
35+
either change out as an ordinary image update, so refuse it here, before
36+
the Cluster is patched. Only the image name is compared: a minor bump or
37+
a registry mirror rewriting the host must keep passing.
38+
*/}}
39+
{{- define "postgres.flavorGuard" -}}
40+
{{- $live := lookup "postgresql.cnpg.io/v1" "Cluster" .Release.Namespace .Release.Name -}}
41+
{{- if and $live $live.spec $live.spec.imageName -}}
42+
{{- $liveName := regexReplaceAll "[:@].*$" (last (splitList "/" $live.spec.imageName)) "" -}}
43+
{{- $wantName := regexReplaceAll "[:@].*$" (last (splitList "/" (include "postgres.imageName" .))) "" -}}
44+
{{- if ne $liveName $wantName -}}
45+
{{- fail (printf "postgres: flavor cannot change on an existing cluster (running %s, requested %s). The image families use different glibc versions, which would corrupt text indexes. Create a new Postgres with the wanted flavor and migrate the data, or restore a backup into it with bootstrap.enabled." $liveName $wantName) -}}
46+
{{- end -}}
47+
{{- end -}}
48+
{{- end -}}

‎packages/apps/postgres/templates/db.yaml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,8 @@ spec:
248248

249249
resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.resourcesPreset .Values.resources $) | nindent 4 }}
250250

251-
imageName: ghcr.io/cloudnative-pg/postgresql:{{ include "postgres.versionMap" $ | trimPrefix "v" }}
251+
{{- include "postgres.flavorGuard" $ }}
252+
imageName: {{ include "postgres.imageName" $ }}
252253
enableSuperuserAccess: true
253254
{{- if .Values._cluster.scheduling }}
254255
{{- $rawConstraints := get .Values._cluster.scheduling "globalAppTopologySpreadConstraints" }}

‎packages/apps/postgres/templates/init-job.yaml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ spec:
5656
restartPolicy: Never
5757
containers:
5858
- name: postgres
59-
image: ghcr.io/cloudnative-pg/postgresql:{{ include "postgres.versionMap" $ | trimPrefix "v" }}
59+
image: {{ include "postgres.imageName" $ }}
6060
command:
6161
- bash
6262
- /scripts/init.sh
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
suite: postgres image flavor
2+
# The Cluster and the init Job must resolve the same image, and a live
3+
# cluster must never be moved to the other flavor: the two image families
4+
# ship different glibc versions, so CNPG would roll an image swap that
5+
# corrupts collatable indexes. Tags are matched by pattern so that
6+
# hack/update-versions.sh can bump minors without touching this suite.
7+
release:
8+
name: pg-test
9+
namespace: tenant-test
10+
set:
11+
_cluster:
12+
scheduling: {}
13+
tests:
14+
- it: defaults to the CloudNativePG PostgreSQL image
15+
templates:
16+
- templates/db.yaml
17+
documentSelector:
18+
path: kind
19+
value: Cluster
20+
asserts:
21+
- matchRegex:
22+
path: spec.imageName
23+
pattern: ^ghcr\.io/cloudnative-pg/postgresql:18\.[0-9]+$
24+
25+
- it: postgis flavor switches the Cluster to the standard-trixie PostGIS image
26+
set:
27+
flavor: postgis
28+
templates:
29+
- templates/db.yaml
30+
documentSelector:
31+
path: kind
32+
value: Cluster
33+
asserts:
34+
- matchRegex:
35+
path: spec.imageName
36+
pattern: ^ghcr\.io/cloudnative-pg/postgis:18\.[0-9]+-[0-9.]+-standard-trixie$
37+
38+
- it: postgis flavor runs the init Job from the same image as the Cluster
39+
set:
40+
flavor: postgis
41+
version: v16
42+
templates:
43+
- templates/init-job.yaml
44+
documentSelector:
45+
path: kind
46+
value: Job
47+
asserts:
48+
- matchRegex:
49+
path: spec.template.spec.containers[0].image
50+
pattern: ^ghcr\.io/cloudnative-pg/postgis:16\.[0-9]+-[0-9.]+-standard-trixie$
51+
52+
- it: refuses to move a live postgresql cluster to postgis
53+
set:
54+
flavor: postgis
55+
kubernetesProvider:
56+
scheme:
57+
"postgresql.cnpg.io/v1/Cluster":
58+
gvr:
59+
group: postgresql.cnpg.io
60+
version: v1
61+
resource: clusters
62+
namespaced: true
63+
objects:
64+
- apiVersion: postgresql.cnpg.io/v1
65+
kind: Cluster
66+
metadata:
67+
name: pg-test
68+
namespace: tenant-test
69+
spec:
70+
imageName: ghcr.io/cloudnative-pg/postgresql:18.1
71+
templates:
72+
- templates/db.yaml
73+
asserts:
74+
- failedTemplate:
75+
errorPattern: "flavor cannot change on an existing cluster \\(running postgresql, requested postgis\\)"
76+
77+
- it: refuses to move a live postgis cluster back to postgresql
78+
kubernetesProvider:
79+
scheme:
80+
"postgresql.cnpg.io/v1/Cluster":
81+
gvr:
82+
group: postgresql.cnpg.io
83+
version: v1
84+
resource: clusters
85+
namespaced: true
86+
objects:
87+
- apiVersion: postgresql.cnpg.io/v1
88+
kind: Cluster
89+
metadata:
90+
name: pg-test
91+
namespace: tenant-test
92+
spec:
93+
imageName: ghcr.io/cloudnative-pg/postgis:18.1-3.6.1-standard-trixie
94+
templates:
95+
- templates/db.yaml
96+
asserts:
97+
- failedTemplate:
98+
errorPattern: "flavor cannot change on an existing cluster \\(running postgis, requested postgresql\\)"
99+
100+
- it: lets a live postgis cluster take a new minor from a mirrored, digest-pinned image
101+
set:
102+
flavor: postgis
103+
kubernetesProvider:
104+
scheme:
105+
"postgresql.cnpg.io/v1/Cluster":
106+
gvr:
107+
group: postgresql.cnpg.io
108+
version: v1
109+
resource: clusters
110+
namespaced: true
111+
objects:
112+
- apiVersion: postgresql.cnpg.io/v1
113+
kind: Cluster
114+
metadata:
115+
name: pg-test
116+
namespace: tenant-test
117+
spec:
118+
imageName: registry.example:5000/cloudnative-pg/postgis:18.0-3.6.0-standard-trixie@sha256:5a6b6d9016f6de30cd389c8a921b8acb64de0421f125a0ed3e95ce1b978bf169
119+
templates:
120+
- templates/db.yaml
121+
documentSelector:
122+
path: kind
123+
value: Cluster
124+
asserts:
125+
- matchRegex:
126+
path: spec.imageName
127+
pattern: ^ghcr\.io/cloudnative-pg/postgis:18\.
128+

‎packages/apps/postgres/values.schema.json‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,15 @@
140140
"v13"
141141
]
142142
},
143+
"flavor": {
144+
"description": "PostgreSQL image flavor, fixed when the cluster is created. `postgresql` uses the CloudNativePG PostgreSQL image. `postgis` uses the CloudNativePG PostGIS image, which adds postgis, postgis_raster, postgis_topology, postgis_sfcgal, postgis_tiger_geocoder and address_standardizer; enable them per database in `databases.\u003cname\u003e.extensions`. Changing the flavor of an existing cluster is refused: create a new cluster and migrate the data.",
145+
"type": "string",
146+
"default": "postgresql",
147+
"enum": [
148+
"postgresql",
149+
"postgis"
150+
]
151+
},
143152
"tls": {
144153
"description": "TLS configuration for server connections.",
145154
"type": "object",

0 commit comments

Comments
 (0)