From 845da608b932d1272aa664aa22767ab756100ffd Mon Sep 17 00:00:00 2001 From: Gabriele Bartolini Date: Sun, 12 Jul 2026 10:27:08 +1000 Subject: [PATCH 1/2] feat(demo): deploy the image catalog with common extensions by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply the cloudnative-pg/artifacts ClusterImageCatalog (catalog-minimal-trixie.yaml) in every region alongside the CNPG operator, providing common extensions (pgvector, PostGIS, TimescaleDB, pgaudit, wal2json, pg-crash) out of the box. Closes #89. Add `IMAGE_CATALOG_URL`/`IMAGE_CATALOG_NAME` to select the catalog, and `POSTGRESQL_VERSION` to pick the PostgreSQL major from it. In plugin mode, the Cluster now selects its image via `spec.imageCatalogRef` (rendered from the new `demo/templates/image-catalog.yaml` fragment) instead of a fixed `POSTGRESQL_IMAGE`. Legacy mode is unaffected — the catalog only ships minimal images, so `demo/templates/legacy/image-legacy.yaml` keeps a direct `imageName: ${POSTGRESQL_LEGACY_IMAGE}` reference. `demo/teardown.sh` removes the catalog alongside the other requirements. Closes #89 Assisted-by: Claude Signed-off-by: Gabriele Bartolini --- demo/README.md | 18 +++++++++----- demo/funcs_requirements.sh | 20 ++++++++++----- demo/setup.sh | 33 +++++++++++++++++-------- demo/teardown.sh | 4 +++ demo/templates/cluster.yaml | 1 - demo/templates/image-catalog.yaml | 7 ++++++ demo/templates/legacy/image-legacy.yaml | 2 ++ scripts/common.sh | 7 ++++++ 8 files changed, 69 insertions(+), 23 deletions(-) create mode 100644 demo/templates/image-catalog.yaml create mode 100644 demo/templates/legacy/image-legacy.yaml diff --git a/demo/README.md b/demo/README.md index 983b2d0..fab2c9a 100644 --- a/demo/README.md +++ b/demo/README.md @@ -91,9 +91,11 @@ regions, pass them as arguments: > command line. This process takes a few minutes to complete. -It installs the latest version of CloudNativePG, cert-manager, and the -[Barman Cloud plugin](https://cloudnative-pg.io/plugin-barman-cloud/), -followed by the deployment of the PostgreSQL clusters. +It installs the latest version of CloudNativePG, cert-manager, the +[Barman Cloud plugin](https://cloudnative-pg.io/plugin-barman-cloud/), and a +[`ClusterImageCatalog`](https://github.com/cloudnative-pg/artifacts/tree/main/image-catalogs-extensions) +providing common extensions (pgvector, PostGIS, TimescaleDB, pgaudit, +wal2json, pg-crash), followed by the deployment of the PostgreSQL clusters. ### Options @@ -101,12 +103,14 @@ followed by the deployment of the PostgreSQL clusters. |----------|---------|-------------| | `LEGACY=true` | `false` | Use the legacy in-tree Barman Cloud code instead of the Barman Cloud Plugin | | `TRUNK=true` | `false` | Deploy from the `main` branch of both CloudNativePG and the Barman Cloud Plugin | -| `REQUIREMENTS_ONLY=true` | `false` | Deploy only CloudNativePG, cert-manager, and the Barman Cloud Plugin; skip ObjectStores/Clusters. A later plain run automatically detects and skips already-installed requirements | +| `REQUIREMENTS_ONLY=true` | `false` | Deploy only CloudNativePG, cert-manager, the Barman Cloud Plugin, and the `ClusterImageCatalog`; skip ObjectStores/Clusters. A later plain run automatically detects and skips already-installed requirements | +| `IMAGE_CATALOG_URL=` | [`catalog-minimal-trixie.yaml`](https://github.com/cloudnative-pg/artifacts/blob/main/image-catalogs-extensions/catalog-minimal-trixie.yaml) | `ClusterImageCatalog` manifest applied in every region | +| `IMAGE_CATALOG_NAME=` | `postgresql-minimal-trixie` | Must match `metadata.name` in `IMAGE_CATALOG_URL`; referenced by the Cluster's `imageCatalogRef` (plugin mode) | +| `POSTGRESQL_VERSION=` | `18` | PostgreSQL major version selected from the catalog, in plugin mode | | `DRY_RUN=true` | `false` | Print the generated YAML to stdout without applying it | | `OUTPUT_DIR=` | _(unset)_ | Save the generated YAML to `/.yaml` (one file per region) and apply it | | `DRY_RUN=true OUTPUT_DIR=` | | Save the generated YAML to files only, without applying | -| `POSTGRESQL_IMAGE=` | `ghcr.io/cloudnative-pg/postgresql:18-standard-trixie` | PostgreSQL image used in plugin mode | -| `POSTGRESQL_LEGACY_IMAGE=` | `ghcr.io/cloudnative-pg/postgresql:18-system-trixie` | PostgreSQL image used in legacy mode | +| `POSTGRESQL_LEGACY_IMAGE=` | `ghcr.io/cloudnative-pg/postgresql:18-system-trixie` | PostgreSQL image used directly (`imageName`) in legacy mode; the catalog only ships minimal images, so legacy mode can't use `imageCatalogRef` | | `K8S_CONTEXT_PREFIX` | `kind-` | Prefix of kubectl context names; override when targeting non-Kind clusters | | `K8S_NAME` | `k8s-` | Base name of clusters in kubectl context names | | `DEBUG=true` | `false` | Enable shell trace output (`set -x`) for debugging | @@ -129,6 +133,7 @@ fragments without modifying the repository. | `CLUSTER_TEMPLATE=` | Override `cluster.yaml` only | | `BOOTSTRAP_INITDB_TEMPLATE=` | Override `bootstrap-initdb.yaml` | | `BOOTSTRAP_RECOVERY_TEMPLATE=` | Override `bootstrap-recovery.yaml` | +| `IMAGE_CATALOG_TEMPLATE=` | Override `image-catalog.yaml` (plugin mode's `imageCatalogRef`) | | `CLUSTER_PLUGIN_PARAMS_TEMPLATE=` | Override `cluster-plugin-params.yaml` | | `REPLICA_SECTION_TEMPLATE=` | Override `replica-section.yaml` | | `EXTERNAL_CLUSTER_PLUGIN_TEMPLATE=` | Override `external-cluster-plugin.yaml` | @@ -141,6 +146,7 @@ Legacy-mode equivalents (used with `LEGACY=true`): | Variable | Description | |----------|-------------| | `CLUSTER_LEGACY_PARAMS_TEMPLATE=` | Override `legacy/cluster-legacy-params.yaml` | +| `IMAGE_LEGACY_TEMPLATE=` | Override `legacy/image-legacy.yaml` (legacy mode's `imageName`) | | `EXTERNAL_CLUSTER_LEGACY_TEMPLATE=` | Override `legacy/external-cluster-legacy.yaml` | | `SCHEDULEDBACKUP_LEGACY_TEMPLATE=` | Override `legacy/scheduledbackup-legacy.yaml` | diff --git a/demo/funcs_requirements.sh b/demo/funcs_requirements.sh index 211ca3c..0adfe21 100755 --- a/demo/funcs_requirements.sh +++ b/demo/funcs_requirements.sh @@ -20,7 +20,8 @@ # # Deployment of the CloudNativePG demo requirements: the CNPG operator, -# cert-manager, and the Barman Cloud Plugin. Sourced by demo/setup.sh. +# cert-manager, the Barman Cloud Plugin, and a ClusterImageCatalog (common +# extensions). Sourced by demo/setup.sh. # # Check whether a CRD exists in the given cluster context @@ -30,18 +31,19 @@ check_crd_existence() { kubectl --context "${context}" get crd "${crd}" &>/dev/null } -# Deploy CloudNativePG, cert-manager, and the Barman Cloud Plugin into a -# single region, unless they are already installed there. +# Deploy CloudNativePG, cert-manager, the Barman Cloud Plugin, and a +# ClusterImageCatalog into a single region, unless they are already +# installed there. # Globals used: trunk, CERT_MANAGER_VERSION, CNPG_RELEASE_BRANCH, -# CNPG_VERSION_BARE, BARMAN_CLOUD_PLUGIN_VERSION (set by scripts/common.sh -# and demo/setup.sh). +# CNPG_VERSION_BARE, BARMAN_CLOUD_PLUGIN_VERSION, IMAGE_CATALOG_URL (set by +# scripts/common.sh and demo/setup.sh). deploy_cnpg_requirements() { local region="$1" local context="$2" if check_crd_existence "${context}" clusters.postgresql.cnpg.io; then echo "â„šī¸ CloudNativePG requirements already installed in region '${region}' (context: ${context});" \ - "skipping operator/cert-manager/Barman Cloud Plugin installation." + "skipping operator/cert-manager/Barman Cloud Plugin/ClusterImageCatalog installation." return fi @@ -71,6 +73,12 @@ deploy_cnpg_requirements() { echo "đŸ“Ļ CloudNativePG: $(kubectl --context "${context}" get deployment cnpg-controller-manager \ -n cnpg-system -o jsonpath='{.spec.template.spec.containers[0].image}')" + # Deploy the ClusterImageCatalog with common extensions (requires the + # postgresql.cnpg.io CRDs installed by the operator above) + kubectl apply --context "${context}" -f "${IMAGE_CATALOG_URL}" + echo "đŸ“Ļ ClusterImageCatalog: $(kubectl --context "${context}" get clusterimagecatalog \ + -o jsonpath='{.items[*].metadata.name}')" + # Deploy cert-manager kubectl apply --context "${context}" -f \ "https://github.com/cert-manager/cert-manager/releases/download/${CERT_MANAGER_VERSION}/cert-manager.yaml" diff --git a/demo/setup.sh b/demo/setup.sh index 43a5419..fcaf41b 100755 --- a/demo/setup.sh +++ b/demo/setup.sh @@ -48,14 +48,17 @@ kube_config_path="${KUBE_CONFIG_PATH}" templates_dir="${TEMPLATES_DIR:-${REPO_ROOT}/demo/templates}" legacy_templates_dir="${templates_dir}/legacy" -# Default PostgreSQL image (overridable via env var) -POSTGRESQL_IMAGE="${POSTGRESQL_IMAGE:-ghcr.io/cloudnative-pg/postgresql:18-standard-trixie}" +# Default PostgreSQL major version for plugin mode (selects the entry in +# IMAGE_CATALOG_NAME's ClusterImageCatalog). Legacy mode still selects a +# full image name directly, since the catalog only ships minimal images. +POSTGRESQL_VERSION="${POSTGRESQL_VERSION:-18}" POSTGRESQL_LEGACY_IMAGE="${POSTGRESQL_LEGACY_IMAGE:-ghcr.io/cloudnative-pg/postgresql:18-system-trixie}" # Template file overrides — set any of these to replace the corresponding built-in fragment tmpl_cluster="${CLUSTER_TEMPLATE:-${templates_dir}/cluster.yaml}" tmpl_bootstrap_initdb="${BOOTSTRAP_INITDB_TEMPLATE:-${templates_dir}/bootstrap-initdb.yaml}" tmpl_bootstrap_recovery="${BOOTSTRAP_RECOVERY_TEMPLATE:-${templates_dir}/bootstrap-recovery.yaml}" +tmpl_image_catalog="${IMAGE_CATALOG_TEMPLATE:-${templates_dir}/image-catalog.yaml}" tmpl_cluster_plugin_params="${CLUSTER_PLUGIN_PARAMS_TEMPLATE:-${templates_dir}/cluster-plugin-params.yaml}" tmpl_replica_section="${REPLICA_SECTION_TEMPLATE:-${templates_dir}/replica-section.yaml}" tmpl_external_cluster_plugin="${EXTERNAL_CLUSTER_PLUGIN_TEMPLATE:-${templates_dir}/external-cluster-plugin.yaml}" @@ -63,6 +66,7 @@ tmpl_scheduledbackup_plugin="${SCHEDULEDBACKUP_PLUGIN_TEMPLATE:-${templates_dir} tmpl_objectstore="${OBJECTSTORE_TEMPLATE:-${templates_dir}/objectstore.yaml}" tmpl_podmonitor="${PODMONITOR_TEMPLATE:-${templates_dir}/podmonitor.yaml}" tmpl_cluster_legacy_params="${CLUSTER_LEGACY_PARAMS_TEMPLATE:-${legacy_templates_dir}/cluster-legacy-params.yaml}" +tmpl_image_legacy="${IMAGE_LEGACY_TEMPLATE:-${legacy_templates_dir}/image-legacy.yaml}" tmpl_external_cluster_legacy="${EXTERNAL_CLUSTER_LEGACY_TEMPLATE:-${legacy_templates_dir}/external-cluster-legacy.yaml}" tmpl_scheduledbackup_legacy="${SCHEDULEDBACKUP_LEGACY_TEMPLATE:-${legacy_templates_dir}/scheduledbackup-legacy.yaml}" @@ -156,9 +160,10 @@ kubectl_apply() { # --------------------------------------------------------------------------- # YAML generators — each function writes a complete YAML stream to stdout. # The caller is responsible for piping to kubectl. -# Template files use ${REGION}, ${PRIMARY_REGION}, ${SOURCE_REGION}, and -# ${POSTGRESQL_IMAGE} as placeholders; envsubst substitutes only those -# variables (explicit list prevents accidental expansion of env vars). +# Template files use ${REGION}, ${PRIMARY_REGION}, ${SOURCE_REGION}, +# ${IMAGE_CATALOG_NAME}, ${POSTGRESQL_VERSION}, and ${POSTGRESQL_LEGACY_IMAGE} +# as placeholders; envsubst substitutes only those variables (explicit list +# prevents accidental expansion of env vars). # --------------------------------------------------------------------------- generate_objectstore_yaml() { @@ -179,9 +184,13 @@ generate_cluster_yaml_plugin() { local source_region source_region=$(get_source_region "${region}") - # Cluster header: apiVersion through affinity - REGION="${region}" POSTGRESQL_IMAGE="${POSTGRESQL_IMAGE}" \ - envsubst '${REGION} ${POSTGRESQL_IMAGE}' < "${tmpl_cluster}" + # Cluster header: apiVersion through postgresql parameters + REGION="${region}" \ + envsubst '${REGION}' < "${tmpl_cluster}" + + # ClusterImageCatalog reference (see demo/funcs_requirements.sh for the catalog itself) + IMAGE_CATALOG_NAME="${IMAGE_CATALOG_NAME}" POSTGRESQL_VERSION="${POSTGRESQL_VERSION}" \ + envsubst '${IMAGE_CATALOG_NAME} ${POSTGRESQL_VERSION}' < "${tmpl_image_catalog}" # Bootstrap: initdb for the primary (or single-region); recovery for replicas if [ "${region}" = "${primary_region}" ] || [ "${num_regions}" -eq 1 ]; then @@ -219,8 +228,12 @@ generate_cluster_yaml_legacy() { local source_region source_region=$(get_source_region "${region}") - REGION="${region}" POSTGRESQL_IMAGE="${POSTGRESQL_LEGACY_IMAGE}" \ - envsubst '${REGION} ${POSTGRESQL_IMAGE}' < "${tmpl_cluster}" + REGION="${region}" \ + envsubst '${REGION}' < "${tmpl_cluster}" + + # Direct image reference — no catalog available for legacy/system images + POSTGRESQL_LEGACY_IMAGE="${POSTGRESQL_LEGACY_IMAGE}" \ + envsubst '${POSTGRESQL_LEGACY_IMAGE}' < "${tmpl_image_legacy}" if [ "${region}" = "${primary_region}" ] || [ "${num_regions}" -eq 1 ]; then cat "${tmpl_bootstrap_initdb}" diff --git a/demo/teardown.sh b/demo/teardown.sh index c313907..f8ebe6c 100755 --- a/demo/teardown.sh +++ b/demo/teardown.sh @@ -67,6 +67,10 @@ for region in "${REGIONS[@]}"; do kubectl delete --context "${CONTEXT_NAME}" --ignore-not-found=true -f \ "https://github.com/cert-manager/cert-manager/releases/download/${CERT_MANAGER_VERSION}/cert-manager.yaml" + # Delete the ClusterImageCatalog + kubectl delete --context "${CONTEXT_NAME}" --ignore-not-found=true -f \ + "${IMAGE_CATALOG_URL}" + # Delete CNPG operator kubectl delete --context "${CONTEXT_NAME}" --ignore-not-found=true -f \ "https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-${CNPG_RELEASE_BRANCH}/releases/cnpg-${CNPG_VERSION_BARE}.yaml" diff --git a/demo/templates/cluster.yaml b/demo/templates/cluster.yaml index 1c69b1e..82c3ee7 100644 --- a/demo/templates/cluster.yaml +++ b/demo/templates/cluster.yaml @@ -4,7 +4,6 @@ metadata: name: pg-${REGION} spec: instances: 3 - imageName: ${POSTGRESQL_IMAGE} storage: size: 1Gi diff --git a/demo/templates/image-catalog.yaml b/demo/templates/image-catalog.yaml new file mode 100644 index 0000000..f207960 --- /dev/null +++ b/demo/templates/image-catalog.yaml @@ -0,0 +1,7 @@ + # See https://cloudnative-pg.io/documentation/current/image_catalog/ + imageCatalogRef: + apiGroup: postgresql.cnpg.io + kind: ClusterImageCatalog + name: ${IMAGE_CATALOG_NAME} + major: ${POSTGRESQL_VERSION} + diff --git a/demo/templates/legacy/image-legacy.yaml b/demo/templates/legacy/image-legacy.yaml new file mode 100644 index 0000000..0d04768 --- /dev/null +++ b/demo/templates/legacy/image-legacy.yaml @@ -0,0 +1,2 @@ + imageName: ${POSTGRESQL_LEGACY_IMAGE} + diff --git a/scripts/common.sh b/scripts/common.sh index e56fdeb..d29506a 100644 --- a/scripts/common.sh +++ b/scripts/common.sh @@ -79,5 +79,12 @@ CNPG_RELEASE_BRANCH="${CNPG_VERSION_BARE%.*}" BARMAN_CLOUD_PLUGIN_VERSION="${BARMAN_CLOUD_PLUGIN_VERSION:-v0.13.0}" # renovate: datasource=github-releases depName=grafana/grafana-operator GRAFANA_OPERATOR_VERSION="${GRAFANA_OPERATOR_VERSION:-v5.24.0}" +# ClusterImageCatalog with common extensions (pg-crash, pgaudit, pgvector, +# postgis, timescaledb-oss, wal2json); images are pinned by digest within the +# file, so tracking the `main` branch is safe. See +# https://github.com/cloudnative-pg/artifacts/tree/main/image-catalogs-extensions +IMAGE_CATALOG_URL="${IMAGE_CATALOG_URL:-https://raw.githubusercontent.com/cloudnative-pg/artifacts/refs/heads/main/image-catalogs-extensions/catalog-minimal-trixie.yaml}" +# Must match metadata.name in IMAGE_CATALOG_URL; referenced by Cluster.spec.imageCatalogRef +IMAGE_CATALOG_NAME="${IMAGE_CATALOG_NAME:-postgresql-minimal-trixie}" source "${REPO_ROOT}/scripts/funcs_regions.sh" From b6d1834e4828e19f9d3b58cc00870af7441db5d3 Mon Sep 17 00:00:00 2001 From: Gabriele Bartolini Date: Sun, 12 Jul 2026 10:28:05 +1000 Subject: [PATCH 2/2] refactor(demo): extract spec.storage into its own template fragment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split spec.storage/spec.walStorage out of demo/templates/cluster.yaml into a dedicated demo/templates/storage.yaml, overridable via STORAGE_TEMPLATE. It's shared unchanged by both plugin and legacy modes — unlike the image reference, storage sizing doesn't diverge between them — but PVC sizing is one of the most commonly tweaked settings, so it earns its own fragment following the same one-concern-per-file convention as the other templates. Assisted-by: Claude Signed-off-by: Gabriele Bartolini --- demo/README.md | 1 + demo/setup.sh | 7 +++++++ demo/templates/cluster.yaml | 7 ------- demo/templates/storage.yaml | 7 +++++++ 4 files changed, 15 insertions(+), 7 deletions(-) create mode 100644 demo/templates/storage.yaml diff --git a/demo/README.md b/demo/README.md index fab2c9a..1bd9ffc 100644 --- a/demo/README.md +++ b/demo/README.md @@ -131,6 +131,7 @@ fragments without modifying the repository. |----------|-------------| | `TEMPLATES_DIR=` | Replace the whole templates directory with your own | | `CLUSTER_TEMPLATE=` | Override `cluster.yaml` only | +| `STORAGE_TEMPLATE=` | Override `storage.yaml` (shared by both modes) | | `BOOTSTRAP_INITDB_TEMPLATE=` | Override `bootstrap-initdb.yaml` | | `BOOTSTRAP_RECOVERY_TEMPLATE=` | Override `bootstrap-recovery.yaml` | | `IMAGE_CATALOG_TEMPLATE=` | Override `image-catalog.yaml` (plugin mode's `imageCatalogRef`) | diff --git a/demo/setup.sh b/demo/setup.sh index fcaf41b..0bb6d28 100755 --- a/demo/setup.sh +++ b/demo/setup.sh @@ -56,6 +56,7 @@ POSTGRESQL_LEGACY_IMAGE="${POSTGRESQL_LEGACY_IMAGE:-ghcr.io/cloudnative-pg/postg # Template file overrides — set any of these to replace the corresponding built-in fragment tmpl_cluster="${CLUSTER_TEMPLATE:-${templates_dir}/cluster.yaml}" +tmpl_storage="${STORAGE_TEMPLATE:-${templates_dir}/storage.yaml}" tmpl_bootstrap_initdb="${BOOTSTRAP_INITDB_TEMPLATE:-${templates_dir}/bootstrap-initdb.yaml}" tmpl_bootstrap_recovery="${BOOTSTRAP_RECOVERY_TEMPLATE:-${templates_dir}/bootstrap-recovery.yaml}" tmpl_image_catalog="${IMAGE_CATALOG_TEMPLATE:-${templates_dir}/image-catalog.yaml}" @@ -188,6 +189,9 @@ generate_cluster_yaml_plugin() { REGION="${region}" \ envsubst '${REGION}' < "${tmpl_cluster}" + # Storage (data + WAL volumes) + cat "${tmpl_storage}" + # ClusterImageCatalog reference (see demo/funcs_requirements.sh for the catalog itself) IMAGE_CATALOG_NAME="${IMAGE_CATALOG_NAME}" POSTGRESQL_VERSION="${POSTGRESQL_VERSION}" \ envsubst '${IMAGE_CATALOG_NAME} ${POSTGRESQL_VERSION}' < "${tmpl_image_catalog}" @@ -231,6 +235,9 @@ generate_cluster_yaml_legacy() { REGION="${region}" \ envsubst '${REGION}' < "${tmpl_cluster}" + # Storage (data + WAL volumes) + cat "${tmpl_storage}" + # Direct image reference — no catalog available for legacy/system images POSTGRESQL_LEGACY_IMAGE="${POSTGRESQL_LEGACY_IMAGE}" \ envsubst '${POSTGRESQL_LEGACY_IMAGE}' < "${tmpl_image_legacy}" diff --git a/demo/templates/cluster.yaml b/demo/templates/cluster.yaml index 82c3ee7..0460c43 100644 --- a/demo/templates/cluster.yaml +++ b/demo/templates/cluster.yaml @@ -5,13 +5,6 @@ metadata: spec: instances: 3 - storage: - size: 1Gi - - # See https://cloudnative-pg.io/documentation/current/storage/#volume-for-wal - walStorage: - size: 1Gi - # See https://cloudnative-pg.io/documentation/current/scheduling/#isolating-postgresql-workloads affinity: nodeSelector: diff --git a/demo/templates/storage.yaml b/demo/templates/storage.yaml new file mode 100644 index 0000000..67873cb --- /dev/null +++ b/demo/templates/storage.yaml @@ -0,0 +1,7 @@ + storage: + size: 1Gi + + # See https://cloudnative-pg.io/documentation/current/storage/#volume-for-wal + walStorage: + size: 1Gi +