Repository: cloudnative-pg/cloudnative-pg

Issue:
[Feature]: Allow the user to customize DefaultPgbouncerImage
### Is there an existing issue already for this feature request/idea?

- [x] I have searched for an existing issue, and could not find anything. I believe this is a new feature request to be evaluated.

### What problem is this feature going to solve? Why should it be added?

There is no way to customize DefaultPgbouncerImage.

### Describe the solution you'd like

Create an operator configuration to allow the user to customize DefaultPgbouncerImage

### Describe alternatives you've considered

Leave everything as it is now.

### Additional context

This is useful in case of airgap environments or company internal repositories.

### Backport?

Yes

### Are you willing to actively contribute to this feature?

Yes

### Code of Conduct

- [x] I agree to follow this project's Code of Conduct

Discussion:

Solution patch:
diff --git a/.github/renovate.json5 b/.github/renovate.json5
index 8f5df265..6ebf8c38 100644
--- a/.github/renovate.json5
+++ b/.github/renovate.json5
@@ -20,7 +20,6 @@
     'contribute/**',
     'licenses/**',
     'pkg/versions/**',
-    'pkg/specs/pgbouncer/',
   ],
   postUpdateOptions: [
     'gomodTidy',
@@ -90,7 +89,6 @@
       customType: 'regex',
       managerFilePatterns: [
         '/^pkg\\/versions\\/versions\\.go$/',
-        '/^pkg\\/specs\\/pgbouncer\\/deployments\\.go$/',
       ],
       matchStrings: [
         'DefaultImageName = "(?<depName>.+?):(?<currentValue>.*?)"\\n',
diff --git a/Makefile b/Makefile
index 1d1c2c41..431892d5 100644
--- a/Makefile
+++ b/Makefile
@@ -51,6 +51,7 @@ LOCALBIN ?= $(shell pwd)/bin
 
 BUILD_IMAGE ?= true
 POSTGRES_IMAGE_NAME ?= $(shell grep 'DefaultImageName.*=' "pkg/versions/versions.go" | cut -f 2 -d \")
+PGBOUNCER_IMAGE_NAME ?= $(shell grep 'DefaultPgbouncerImage.*=' "pkg/versions/versions.go" | cut -f 2 -d \")
 # renovate: datasource=github-releases depName=kubernetes-sigs/kustomize versioning=loose
 KUSTOMIZE_VERSION ?= v5.6.0
 # renovate: datasource=go depName=sigs.k8s.io/controller-tools
@@ -74,6 +75,7 @@ ARCH ?= amd64
 export CONTROLLER_IMG
 export BUILD_IMAGE
 export POSTGRES_IMAGE_NAME
+export PGBOUNCER_IMAGE_NAME
 export OPERATOR_MANIFEST_PATH
 # We don't need `trivialVersions=true` anymore, with `crd` it's ok for multi versions
 CRD_OPTIONS ?= "crd"
@@ -233,7 +235,8 @@ generate-manifest: manifests kustomize ## Generate manifest used for deployment.
 		$(KUSTOMIZE) edit set image controller="${CONTROLLER_IMG_WITH_DIGEST}" ;\
 		$(KUSTOMIZE) edit add patch --path env_override.yaml ;\
 		$(KUSTOMIZE) edit add configmap controller-manager-env \
-			--from-literal="POSTGRES_IMAGE_NAME=${POSTGRES_IMAGE_NAME}" ;\
+			--from-literal="POSTGRES_IMAGE_NAME=${POSTGRES_IMAGE_NAME}" \
+			--from-literal="PGBOUNCER_IMAGE_NAME=${PGBOUNCER_IMAGE_NAME}" ;\
 	} ;\
 	mkdir -p ${DIST_PATH} ;\
 	$(KUSTOMIZE) build $$CONFIG_TMP_DIR/default > ${OPERATOR_MANIFEST_PATH} ;\
diff --git a/docs/src/operator_conf.md b/docs/src/operator_conf.md
index 4a07addf..406d02c3 100644
--- a/docs/src/operator_conf.md
+++ b/docs/src/operator_conf.md
@@ -50,6 +50,7 @@ Name | Description
 `MONITORING_QUERIES_CONFIGMAP` | The name of a ConfigMap in the operator's namespace with a set of default queries (to be specified under the key `queries`) to be applied to all created Clusters
 `MONITORING_QUERIES_SECRET` | The name of a Secret in the operator's namespace with a set of default queries (to be specified under the key `queries`) to be applied to all created Clusters
 `OPERATOR_IMAGE_NAME` | The name of the operator image used to bootstrap Pods. Defaults to the image specified during installation.
+`PGBOUNCER_IMAGE_NAME` | The name of the PgBouncer image used by default for new poolers. Defaults to the version specified in the operator.
 `POSTGRES_IMAGE_NAME` | The name of the PostgreSQL image used by default for new clusters. Defaults to the version specified in the operator.
 `PULL_SECRET_NAME` | Name of an additional pull secret to be defined in the operator's namespace and to be used to download images
 `STANDBY_TCP_USER_TIMEOUT` | Defines the [`TCP_USER_TIMEOUT` socket option](https://www.postgresql.org/docs/current/runtime-config-connection.html#GUC-TCP-USER-TIMEOUT) for replication connections from standby instances to the primary. Default is 0 (system's default).
diff --git a/hack/e2e/run-e2e-kind.sh b/hack/e2e/run-e2e-kind.sh
index 95c7f5c5..0c69860d 100755
--- a/hack/e2e/run-e2e-kind.sh
+++ b/hack/e2e/run-e2e-kind.sh
@@ -42,6 +42,7 @@ export LOG_DIR=${LOG_DIR:-$ROOT_DIR/_logs/}
 export ENABLE_APISERVER_AUDIT=${ENABLE_APISERVER_AUDIT:-false}
 
 export POSTGRES_IMG=${POSTGRES_IMG:-$(grep 'DefaultImageName.*=' "${ROOT_DIR}/pkg/versions/versions.go" | cut -f 2 -d \")}
+export PGBOUNCER_IMG=${PGBOUNCER_IMG:-$(grep 'DefaultPgbouncerImage.*=' "${ROOT_DIR}/pkg/versions/versions.go" | cut -f 2 -d \")}
 export E2E_PRE_ROLLING_UPDATE_IMG=${E2E_PRE_ROLLING_UPDATE_IMG:-${POSTGRES_IMG%.*}}
 export E2E_DEFAULT_STORAGE_CLASS=${E2E_DEFAULT_STORAGE_CLASS:-standard}
 export E2E_CSI_STORAGE_CLASS=${E2E_CSI_STORAGE_CLASS:-csi-hostpath-sc}
diff --git a/hack/e2e/run-e2e-local.sh b/hack/e2e/run-e2e-local.sh
index dafd4a9d..520d086f 100755
--- a/hack/e2e/run-e2e-local.sh
+++ b/hack/e2e/run-e2e-local.sh
@@ -54,10 +54,15 @@ function get_postgres_image() {
   grep 'DefaultImageName.*=' "${ROOT_DIR}/pkg/versions/versions.go" | cut -f 2 -d \"
 }
 
+function get_pgbouncer_image() {
+  grep 'DefaultPgbouncerImage.*=' "${ROOT_DIR}/pkg/versions/versions.go" | cut -f 2 -d \"
+}
+
 export E2E_DEFAULT_STORAGE_CLASS=${E2E_DEFAULT_STORAGE_CLASS:-$(get_default_storage_class)}
 export E2E_CSI_STORAGE_CLASS=${E2E_CSI_STORAGE_CLASS:-csi-hostpath-sc}
 export E2E_DEFAULT_VOLUMESNAPSHOT_CLASS=${E2E_DEFAULT_VOLUMESNAPSHOT_CLASS:-$(get_default_snapshot_class "$E2E_CSI_STORAGE_CLASS")}
 export POSTGRES_IMG=${POSTGRES_IMG:-$(get_postgres_image)}
+export PGBOUNCER_IMG=${PGBOUNCER_IMG:-$(get_pgbouncer_image)}
 export E2E_PRE_ROLLING_UPDATE_IMG=${E2E_PRE_ROLLING_UPDATE_IMG:-${POSTGRES_IMG%.*}}
 
 # Ensure GOBIN is in path, we'll use this to install and execute ginkgo
diff --git a/hack/e2e/run-e2e-ocp.sh b/hack/e2e/run-e2e-ocp.sh
index fde266dd..40998456 100755
--- a/hack/e2e/run-e2e-ocp.sh
+++ b/hack/e2e/run-e2e-ocp.sh
@@ -76,6 +76,7 @@ function retry {
 ROOT_DIR=$(realpath "$(dirname "$0")/../../")
 # we need to export ENVs defined in the workflow and used in run-e2e.sh script
 export POSTGRES_IMG=${POSTGRES_IMG:-$(grep 'DefaultImageName.*=' "${ROOT_DIR}/pkg/versions/versions.go" | cut -f 2 -d \")}
+export PGBOUNCER_IMG=${PGBOUNCER_IMG:-$(grep 'DefaultPgbouncerImage.*=' "${ROOT_DIR}/pkg/versions/versions.go" | cut -f 2 -d \")}
 export E2E_PRE_ROLLING_UPDATE_IMG=${E2E_PRE_ROLLING_UPDATE_IMG:-${POSTGRES_IMG%.*}}
 export E2E_DEFAULT_STORAGE_CLASS=${E2E_DEFAULT_STORAGE_CLASS:-standard}
 export E2E_CSI_STORAGE_CLASS=${E2E_CSI_STORAGE_CLASS:-}
diff --git a/hack/e2e/run-e2e.sh b/hack/e2e/run-e2e.sh
index 3e7a5bbb..ca0b2b50 100755
--- a/hack/e2e/run-e2e.sh
+++ b/hack/e2e/run-e2e.sh
@@ -32,6 +32,8 @@ CONTROLLER_IMG_DIGEST=${CONTROLLER_IMG_DIGEST:-""}
 CONTROLLER_IMG_PRIME_DIGEST=${CONTROLLER_IMG_PRIME_DIGEST:-""}
 TEST_UPGRADE_TO_V1=${TEST_UPGRADE_TO_V1:-true}
 POSTGRES_IMG=${POSTGRES_IMG:-$(grep 'DefaultImageName.*=' "${ROOT_DIR}/pkg/versions/versions.go" | cut -f 2 -d \")}
+PGBOUNCER_IMG=${PGBOUNCER_IMG:-$(grep 'DefaultPgbouncerImage.*=' "${ROOT_DIR}/pkg/versions/versions.go" | cut -f 2 -d \")}
+
 # variable need export otherwise be invisible in e2e test case
 export DOCKER_SERVER=${DOCKER_SERVER:-${REGISTRY:-}}
 export DOCKER_USERNAME=${DOCKER_USERNAME:-${REGISTRY_USER:-}}
@@ -89,6 +91,7 @@ if [[ "${TEST_UPGRADE_TO_V1}" != "false" ]] && [[ "${TEST_CLOUD_VENDOR}" != "ocp
   #   - built and pushed to nodes or the local registry (by setup-cluster.sh)
   #   - built by the `buildx` step in continuous delivery and pushed to the test registry
   make CONTROLLER_IMG="${CONTROLLER_IMG}" POSTGRES_IMG="${POSTGRES_IMG}" \
+   PGBOUNCER_IMG="${PGBOUNCER_IMG}" \
    CONTROLLER_IMG_DIGEST="${CONTROLLER_IMG_DIGEST}" \
    OPERATOR_MANIFEST_PATH="${ROOT_DIR}/tests/e2e/fixtures/upgrade/current-manifest.yaml" \
    generate-manifest
@@ -104,6 +107,7 @@ if [[ "${TEST_UPGRADE_TO_V1}" != "false" ]] && [[ "${TEST_CLOUD_VENDOR}" != "ocp
   # added to the tag by convention, which assumes the image is in place.
   # This manifest is used to upgrade into in the upgrade_test E2E.
   make CONTROLLER_IMG="${CONTROLLER_IMG}-prime" POSTGRES_IMG="${POSTGRES_IMG}" \
+   PGBOUNCER_IMG="${PGBOUNCER_IMG}" \
    CONTROLLER_IMG_DIGEST="${CONTROLLER_IMG_PRIME_DIGEST}" \
    OPERATOR_MANIFEST_PATH="${ROOT_DIR}/tests/e2e/fixtures/upgrade/current-manifest-prime.yaml" \
    generate-manifest
@@ -130,6 +134,7 @@ if [[ "${TEST_CLOUD_VENDOR}" != "ocp" ]]; then
 
   CONTROLLER_IMG="${CONTROLLER_IMG}" \
     POSTGRES_IMAGE_NAME="${POSTGRES_IMG}" \
+    PGBOUNCER_IMAGE_NAME="${PGBOUNCER_IMG}" \
     make -C "${ROOT_DIR}" deploy
   kubectl wait --for=condition=Available --timeout=2m \
     -n cnpg-system deployments \
diff --git a/hack/setup-cluster.sh b/hack/setup-cluster.sh
index 2245f8ac..c2d65f8c 100755
--- a/hack/setup-cluster.sh
+++ b/hack/setup-cluster.sh
@@ -89,7 +89,7 @@ builder_name=cnpg-builder
 # #########################################################################
 POSTGRES_IMG=${POSTGRES_IMG:-$(grep 'DefaultImageName.*=' "${ROOT_DIR}/pkg/versions/versions.go" | cut -f 2 -d \")}
 E2E_PRE_ROLLING_UPDATE_IMG=${E2E_PRE_ROLLING_UPDATE_IMG:-${POSTGRES_IMG%.*}}
-PGBOUNCER_IMG=${PGBOUNCER_IMG:-$(grep 'DefaultPgbouncerImage.*=' "${ROOT_DIR}/pkg/specs/pgbouncer/deployments.go" | cut -f 2 -d \")}
+PGBOUNCER_IMG=${PGBOUNCER_IMG:-$(grep 'DefaultPgbouncerImage.*=' "${ROOT_DIR}/pkg/versions/versions.go" | cut -f 2 -d \")}
 MINIO_IMG=${MINIO_IMG:-$(grep 'minioImage.*=' "${ROOT_DIR}/tests/utils/minio/minio.go"  | cut -f 2 -d \")}
 APACHE_IMG=${APACHE_IMG:-"httpd"}
 
diff --git a/internal/configuration/configuration.go b/internal/configuration/configuration.go
index 94d003b4..66fe83df 100644
--- a/internal/configuration/configuration.go
+++ b/internal/configuration/configuration.go
@@ -108,6 +108,10 @@ type Data struct {
 	// used by default for new clusters
 	PostgresImageName string `json:"postgresImageName" env:"POSTGRES_IMAGE_NAME"`
 
+	// PgbouncerImageName is the name of the image of PgBouncer that is
+	// used by default for new poolers
+	PgbouncerImageName string `json:"pgbouncerImageName" env:"PGBOUNCER_IMAGE_NAME"`
+
 	// InheritedAnnotations is a list of annotations that every resource could inherit from
 	// the owning Cluster
 	InheritedAnnotations []string `json:"inheritedAnnotations" env:"INHERITED_ANNOTATIONS"`
@@ -180,6 +184,7 @@ func newDefaultConfig() *Data {
 		OperatorPullSecretName:  DefaultOperatorPullSecretName,
 		OperatorImageName:       versions.DefaultOperatorImageName,
 		PostgresImageName:       versions.DefaultImageName,
+		PgbouncerImageName:      versions.DefaultPgbouncerImage,
 		PluginSocketDir:         DefaultPluginSocketDir,
 		CreateAnyService:        false,
 		CertificateDuration:     CertificateDuration,
diff --git a/pkg/specs/pgbouncer/deployments.go b/pkg/specs/pgbouncer/deployments.go
index 233e38b3..b174cb50 100644
--- a/pkg/specs/pgbouncer/deployments.go
+++ b/pkg/specs/pgbouncer/deployments.go
@@ -40,11 +40,6 @@ import (
 	"github.com/cloudnative-pg/cloudnative-pg/pkg/utils/hash"
 )
 
-const (
-	// DefaultPgbouncerImage is the name of the pgbouncer image used by default
-	DefaultPgbouncerImage = "ghcr.io/cloudnative-pg/pgbouncer:1.24.1"
-)
-
 // Deployment create the deployment of pgbouncer, given
 // the configurations we have in the pooler specifications
 func Deployment(pooler *apiv1.Pooler, cluster *apiv1.Cluster) (*appsv1.Deployment, error) {
@@ -80,7 +75,7 @@ func Deployment(pooler *apiv1.Pooler, cluster *apiv1.Cluster) (*appsv1.Deploymen
 			},
 		}).
 		WithSecurityContext(createPodSecurityContext(cluster.GetSeccompProfile(), 998, 996), true).
-		WithContainerImage("pgbouncer", DefaultPgbouncerImage, false).
+		WithContainerImage("pgbouncer", config.Current.PgbouncerImageName, false).
 		WithContainerCommand("pgbouncer", []string{
 			"/controller/manager",
 			"pgbouncer",
diff --git a/pkg/versions/versions.go b/pkg/versions/versions.go
index 3c4c43a7..5906cff2 100644
--- a/pkg/versions/versions.go
+++ b/pkg/versions/versions.go
@@ -30,6 +30,9 @@ const (
 
 	// DefaultOperatorImageName is the default operator image used by the controller in the pods running PostgreSQL
 	DefaultOperatorImageName = "ghcr.io/cloudnative-pg/cloudnative-pg:1.28.0-rc1"
+
+	// DefaultPgbouncerImage is the name of the pgbouncer image used by default
+	DefaultPgbouncerImage = "ghcr.io/cloudnative-pg/pgbouncer:1.24.1"
 )
 
 // BuildInfo is a struct containing all the info about the build



Test patch:
diff --git a/pkg/specs/pgbouncer/deployments_test.go b/pkg/specs/pgbouncer/deployments_test.go
index ac98913f..62d10d5b 100644
--- a/pkg/specs/pgbouncer/deployments_test.go
+++ b/pkg/specs/pgbouncer/deployments_test.go
@@ -120,7 +120,7 @@ var _ = Describe("Deployment", func() {
 		// Check the containers
 		Expect(podTemplate.Spec.Containers).ToNot(BeEmpty())
 		Expect(podTemplate.Spec.Containers[0].Name).To(Equal("pgbouncer"))
-		Expect(podTemplate.Spec.Containers[0].Image).To(Equal(DefaultPgbouncerImage))
+		Expect(podTemplate.Spec.Containers[0].Image).To(Equal(config.Current.PgbouncerImageName))
 	})
 
 	It("sets the correct number of replicas", func() {