Repository: cloudnative-pg/cloudnative-pg

Issue:
[Docs]: Important Changes in Barman Cloud 3.16 and 3.16.1
### Is there an existing issue already for your request/idea?

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

### What problem in the existing documentation this issue aims to solve?

Starting with **Barman Cloud 3.16**, the default behaviour of most commands has changed, as described in the [release notes for 3.16.0](https://github.com/EnterpriseDB/barman/releases/tag/release%2F3.16.0):

> Cloud connectivity checks are now only performed when the `-t` / `--test` flag is used.
>
> Previously, Barman automatically tested cloud connectivity and verified bucket existence for most commands, raising errors if connectivity failed or the bucket did not exist.
>
> These checks are now executed **only** when the `-t` / `--test` flag is specified.
>
> The only exception is `barman-cloud-check-wal-archive`, which continues to check connectivity and bucket existence.
>
> Additionally, buckets are **no longer created automatically** — Barman now assumes that the target bucket already exists, making it the user’s responsibility to provision it in advance.

Version **3.16.1** partially restores this capability for the `barman-cloud-check-wal-archive` command, as detailed in the [3.16.1 release notes](https://github.com/EnterpriseDB/barman/releases/tag/release%2F3.16.1):

> The `barman-cloud-check-wal-archive` command now verifies whether the target bucket exists in the configured cloud storage (e.g. AWS S3) and automatically creates it if it does not.
>
> Previously, automatic bucket creation was handled by `barman-cloud-backup`. This change restores compatibility with **CloudNativePG (CNPG)** automation workflows that relied on this behaviour, which were affected by the 3.16.0 release.
>
> In collaboration with the CloudNativePG team, this functionality has been reintroduced specifically for `barman-cloud-check-wal-archive` to ensure seamless CNPG operations and internal testing, while preserving the simplified behaviour for regular Barman users.

However, this fix does **not** address cases where users update the operand image without upgrading the operator, then proceed to create new clusters. Although this affects a small subset of users, it should be documented clearly to prevent confusion and operational issues.


### Describe what additions need to be done to the documentation

The long-term direction of **Barman Cloud** is to require users to **pre-create and manage their own cloud storage buckets**. Although **CloudNativePG** executes `barman-cloud-check-wal-archive` to verify and, when possible, create the target bucket, some users may still be affected by this behavioural change.

Since **Barman Cloud** is the only point of contact between CloudNativePG and the object store, **ensuring that buckets are pre-provisioned** is the **recommended and future-proof approach** for maintaining reliable integration between CloudNativePG and Barman Cloud.

### Describe what pages need to change in the documentation, if any

1. https://cloudnative-pg.io/documentation/current/appendixes/backup_barmanobjectstore/
2. https://cloudnative-pg.io/documentation/current/recovery/#restoring-into-a-cluster-with-a-backup-section
3. Barman Cloud Plugin
4. Postgres Containers?

### Describe what pages need to be removed from the documentation, if any

_No response_

### Additional context

_No response_

### Backport?

Yes

### Code of Conduct

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

Hints from issue comments:
@gbartolini: Related #8897

Solution patch:
diff --git a/.github/renovate.json5 b/.github/renovate.json5
index bdc2e201..ef3934ad 100644
--- a/.github/renovate.json5
+++ b/.github/renovate.json5
@@ -50,6 +50,15 @@
         '# renovate: datasource=(?<datasource>[a-z-.]+?) depName=(?<depName>[^\\s]+?)(?: (?:lookupName|packageName)=(?<packageName>[^\\s]+?))?(?: versioning=(?<versioning>[^\\s]+?))?(?: extractVersion=(?<extractVersion>[^\\s]+?))?\\s+[A-Za-z0-9_]+?_VERSION\\s*: \\s*["\']?(?<currentValue>.+?)["\']?\\s',
       ],
     },
+    {
+      customType: 'regex',
+      managerFilePatterns: [
+        '/^docker-bake\\.hcl$/',
+      ],
+      matchStrings: [
+        '# renovate: datasource=(?<datasource>[a-z-.]+?)(?: (?:lookupName|packageName)=(?<packageName>[^\\s]+?))?(?: versioning=(?<versioning>[^\\s]+?))?(?: extractVersion=(?<extractVersion>[^\\s]+?))?\\s+[A-Za-z0-9_]+?\\s*=\\s*["\']?(?<depName>[^:@]+):(?<currentValue>[a-z0-9.-]+)(?:@(?<currentDigest>sha256:[a-f0-9]+))?["\']?,?\\s',
+      ],
+    },
     {
       customType: 'regex',
       managerFilePatterns: [
diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml
index 0771440c..34e3ba81 100644
--- a/.github/workflows/continuous-integration.yml
+++ b/.github/workflows/continuous-integration.yml
@@ -187,7 +187,7 @@ jobs:
         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
 
       - name: Validate Renovate JSON
-        run: npx --yes --package renovate@40.48.6 -- renovate-config-validator
+        run: npx --yes --package renovate@latest -- renovate-config-validator
 
   go-vulncheck:
     name: Run govulncheck
diff --git a/Makefile b/Makefile
index 36e46368..10d01cd9 100644
--- a/Makefile
+++ b/Makefile
@@ -57,7 +57,7 @@ KUSTOMIZE_VERSION ?= v5.6.0
 CONTROLLER_TOOLS_VERSION ?= v0.19.0
 GENREF_VERSION ?= 015aaac611407c4fe591bc8700d2c67b7521efca
 # renovate: datasource=go depName=github.com/goreleaser/goreleaser
-GORELEASER_VERSION ?= v2.12.5
+GORELEASER_VERSION ?= v2.12.6
 # renovate: datasource=docker depName=jonasbn/github-action-spellcheck versioning=docker
 SPELLCHECK_VERSION ?= 0.52.0
 # renovate: datasource=docker depName=getwoke/woke versioning=docker
diff --git a/api/v1/cluster_types.go b/api/v1/cluster_types.go
index 7026cc97..558f7c25 100644
--- a/api/v1/cluster_types.go
+++ b/api/v1/cluster_types.go
@@ -2363,8 +2363,9 @@ type PluginConfiguration struct {
 	// +optional
 	Enabled *bool `json:"enabled,omitempty"`
 
-	// Only one plugin can be declared as WALArchiver.
-	// Cannot be active if ".spec.backup.barmanObjectStore" configuration is present.
+	// Marks the plugin as the WAL archiver. At most one plugin can be
+	// designated as a WAL archiver. This cannot be enabled if the
+	// `.spec.backup.barmanObjectStore` configuration is present.
 	// +kubebuilder:default:=false
 	// +optional
 	IsWALArchiver *bool `json:"isWALArchiver,omitempty"`
diff --git a/config/crd/bases/postgresql.cnpg.io_clusters.yaml b/config/crd/bases/postgresql.cnpg.io_clusters.yaml
index 35928c2e..b1273531 100644
--- a/config/crd/bases/postgresql.cnpg.io_clusters.yaml
+++ b/config/crd/bases/postgresql.cnpg.io_clusters.yaml
@@ -3026,8 +3026,9 @@ spec:
                         isWALArchiver:
                           default: false
                           description: |-
-                            Only one plugin can be declared as WALArchiver.
-                            Cannot be active if ".spec.backup.barmanObjectStore" configuration is present.
+                            Marks the plugin as the WAL archiver. At most one plugin can be
+                            designated as a WAL archiver. This cannot be enabled if the
+                            `.spec.backup.barmanObjectStore` configuration is present.
                           type: boolean
                         name:
                           description: Name is the plugin name
@@ -4064,8 +4065,9 @@ spec:
                     isWALArchiver:
                       default: false
                       description: |-
-                        Only one plugin can be declared as WALArchiver.
-                        Cannot be active if ".spec.backup.barmanObjectStore" configuration is present.
+                        Marks the plugin as the WAL archiver. At most one plugin can be
+                        designated as a WAL archiver. This cannot be enabled if the
+                        `.spec.backup.barmanObjectStore` configuration is present.
                       type: boolean
                     name:
                       description: Name is the plugin name
diff --git a/docker-bake.hcl b/docker-bake.hcl
index c30d3f2d..076baf7e 100644
--- a/docker-bake.hcl
+++ b/docker-bake.hcl
@@ -61,11 +61,13 @@ now = timestamp()
 
 distros = {
   distroless = {
-    baseImage = "gcr.io/distroless/static-debian12:nonroot@sha256:6ec5aa99dc335666e79dc64e4a6c8b89c33a543a1967f20d360922a80dd21f02",
+    # renovate: datasource=docker
+    baseImage = "gcr.io/distroless/static-debian12:nonroot@sha256:e8a4044e0b4ae4257efa45fc026c0bc30ad320d43bd4c1a7d5271bd241e386d0",
     tag = ""
   }
   ubi = {
-    baseImage = "registry.access.redhat.com/ubi9/ubi-micro:latest@sha256:7e85855f6925e03f91b5c51f07886ff1c18c6ec69b5fc65491428a899da914a2",
+    # renovate: datasource=docker
+    baseImage = "registry.access.redhat.com/ubi9/ubi-micro:latest@sha256:aff810919642215e15c993b9bbc110dbcc446608730ad24499dafd9df7a8f8f4",
     tag = "-ubi9"
   }
 }
diff --git a/docs/src/appendixes/backup_barmanobjectstore.md b/docs/src/appendixes/backup_barmanobjectstore.md
index 85733155..76969785 100644
--- a/docs/src/appendixes/backup_barmanobjectstore.md
+++ b/docs/src/appendixes/backup_barmanobjectstore.md
@@ -35,6 +35,15 @@ as it is composed of a community PostgreSQL image and the latest
     in your system to take advantage of the improvements introduced in
     Barman cloud (as well as improve the security aspects of your cluster).
 
+!!! Warning "Changes in Barman Cloud 3.16+ and Bucket Creation"
+    Starting with Barman Cloud 3.16, most Barman Cloud commands no longer
+    automatically create the target bucket, assuming it already exists. Only the
+    `barman-cloud-check-wal-archive` command creates the bucket now. Whenever this
+    is not the first operation run on an empty bucket, CloudNativePG will throw an
+    error. As a result, to ensure reliable, future-proof operations and avoid
+    potential issues, we strongly recommend that you create and configure your
+    object store bucket *before* creating a `Cluster` resource that references it.
+
 A backup is performed from a primary or a designated primary instance in a
 `Cluster` (please refer to
 [replica clusters](../replica_cluster.md)
diff --git a/docs/src/cloudnative-pg.v1.md b/docs/src/cloudnative-pg.v1.md
index c68a8efe..fe06cf03 100644
--- a/docs/src/cloudnative-pg.v1.md
+++ b/docs/src/cloudnative-pg.v1.md
@@ -4326,8 +4326,9 @@ cluster to be reconciled</p>
 <i>bool</i>
 </td>
 <td>
-   <p>Only one plugin can be declared as WALArchiver.
-Cannot be active if &quot;.spec.backup.barmanObjectStore&quot; configuration is present.</p>
+   <p>Marks the plugin as the WAL archiver. At most one plugin can be
+designated as a WAL archiver. This cannot be enabled if the
+<code>.spec.backup.barmanObjectStore</code> configuration is present.</p>
 </td>
 </tr>
 <tr><td><code>parameters</code><br/>
diff --git a/internal/cmd/manager/instance/pgbasebackup/cmd.go b/internal/cmd/manager/instance/pgbasebackup/cmd.go
index 8196994f..349300dc 100644
--- a/internal/cmd/manager/instance/pgbasebackup/cmd.go
+++ b/internal/cmd/manager/instance/pgbasebackup/cmd.go
@@ -24,7 +24,9 @@ import (
 	"context"
 	"fmt"
 	"os"
+	"path/filepath"
 
+	"github.com/cloudnative-pg/machinery/pkg/fileutils"
 	"github.com/cloudnative-pg/machinery/pkg/log"
 	"github.com/spf13/cobra"
 	ctrl "sigs.k8s.io/controller-runtime/pkg/client"
@@ -35,6 +37,7 @@ import (
 	"github.com/cloudnative-pg/cloudnative-pg/pkg/management"
 	"github.com/cloudnative-pg/cloudnative-pg/pkg/management/external"
 	"github.com/cloudnative-pg/cloudnative-pg/pkg/management/postgres"
+	"github.com/cloudnative-pg/cloudnative-pg/pkg/management/postgres/constants"
 	"github.com/cloudnative-pg/cloudnative-pg/pkg/system"
 )
 
@@ -141,6 +144,13 @@ func (env *CloneInfo) bootstrapUsingPgbasebackup(ctx context.Context) error {
 		return fmt.Errorf("while cloning pgdata: %w", err)
 	}
 
+	filePath := filepath.Join(env.info.PgData, constants.CheckEmptyWalArchiveFile)
+	// We create the check empty wal archive file to tell that we should check if the
+	// destination path it is empty
+	if err := fileutils.CreateEmptyFile(filePath); err != nil {
+		return fmt.Errorf("could not create %v file: %w", filePath, err)
+	}
+
 	if cluster.IsReplica() {
 		// TODO: Using a replication slot on replica cluster is not supported (yet?)
 		_, err = postgres.UpdateReplicaConfiguration(env.info.PgData, connectionString, "")



Test patch:
diff --git a/tests/e2e/cluster_major_upgrade_test.go b/tests/e2e/cluster_major_upgrade_test.go
index b8750358..3eab51af 100644
--- a/tests/e2e/cluster_major_upgrade_test.go
+++ b/tests/e2e/cluster_major_upgrade_test.go
@@ -55,16 +55,17 @@ import (
 
 var _ = Describe("Postgres Major Upgrade", Label(tests.LabelPostgresMajorUpgrade), func() {
 	const (
-		level                     = tests.Medium
-		namespacePrefix           = "cluster-major-upgrade"
-		postgisEntry              = "postgis"
-		postgresqlEntry           = "postgresql"
-		postgresqlMinimalEntry    = "postgresql-minimal"
-		customImageRegistryEnvVar = "MAJOR_UPGRADE_IMAGE_REGISTRY"
+		level                  = tests.Medium
+		namespacePrefix        = "cluster-major-upgrade"
+		postgisEntry           = "postgis"
+		postgresqlEntry        = "postgresql"
+		postgresqlMinimalEntry = "postgresql-minimal"
+
+		// custom registry envs
+		customPostgresImageRegistryEnvVar = "POSTGRES_MAJOR_UPGRADE_IMAGE_REGISTRY"
+		customPostgisImageRegistryEnvVar  = "POSTGIS_MAJOR_UPGRADE_IMAGE_REGISTRY"
 	)
 
-	var namespace string
-
 	type scenario struct {
 		startingCluster *apiv1.Cluster
 		startingMajor   int
@@ -112,7 +113,7 @@ var _ = Describe("Postgres Major Upgrade", Label(tests.LabelPostgresMajorUpgrade
 
 	generatePostgreSQLCluster := func(namespace string, storageClass string, tagVersion string) *apiv1.Cluster {
 		cluster := generateBaseCluster(namespace, storageClass)
-		cluster.Spec.ImageName = fmt.Sprintf("ghcr.io/cloudnative-pg/postgresql:%s-standard-trixie", tagVersion)
+		cluster.Spec.ImageName = env.OfficialStandardImageName(tagVersion)
 		cluster.Spec.Bootstrap.InitDB.PostInitSQL = []string{
 			"CREATE EXTENSION IF NOT EXISTS pg_stat_statements;",
 			"CREATE EXTENSION IF NOT EXISTS pg_trgm;",
@@ -120,15 +121,16 @@ var _ = Describe("Postgres Major Upgrade", Label(tests.LabelPostgresMajorUpgrade
 		cluster.Spec.PostgresConfiguration.Parameters["pg_stat_statements.track"] = "top"
 		return cluster
 	}
+
 	generatePostgreSQLMinimalCluster := func(namespace string, storageClass string, tagVersion string) *apiv1.Cluster {
 		cluster := generatePostgreSQLCluster(namespace, storageClass, tagVersion)
-		cluster.Spec.ImageName = fmt.Sprintf("ghcr.io/cloudnative-pg/postgresql:%s-minimal-trixie", tagVersion)
+		cluster.Spec.ImageName = env.OfficialMinimalImageName(tagVersion)
 		return cluster
 	}
 
 	generatePostGISCluster := func(namespace string, storageClass string, tagVersion string) *apiv1.Cluster {
 		cluster := generateBaseCluster(namespace, storageClass)
-		cluster.Spec.ImageName = fmt.Sprintf("ghcr.io/cloudnative-pg/postgis:%s", tagVersion)
+		cluster.Spec.ImageName = env.OfficialPostGISImageName(tagVersion)
 		cluster.Spec.Bootstrap.InitDB.PostInitApplicationSQL = []string{
 			"CREATE EXTENSION postgis",
 			"CREATE EXTENSION postgis_raster",
@@ -157,10 +159,7 @@ var _ = Describe("Postgres Major Upgrade", Label(tests.LabelPostgresMajorUpgrade
 	}
 
 	determineVersionsForTesting := func() versionInfo {
-		currentImage := os.Getenv("POSTGRES_IMG")
-		Expect(currentImage).ToNot(BeEmpty())
-
-		currentVersion, err := version.FromTag(reference.New(currentImage).Tag)
+		currentVersion, err := version.FromTag(env.PostgresImageTag)
 		Expect(err).NotTo(HaveOccurred())
 		currentMajor := currentVersion.Major()
 		currentTag := strconv.FormatUint(currentMajor, 10)
@@ -183,7 +182,7 @@ var _ = Describe("Postgres Major Upgrade", Label(tests.LabelPostgresMajorUpgrade
 			// Beta images don't have a major version only tag yet, and
 			// are most likely in the following format: "18beta1", "18rc2"
 			// So, we split at the first `-` and use that prefix to build the target image.
-			targetTag = strings.Split(reference.New(currentImage).Tag, "-")[0]
+			targetTag = strings.Split(env.PostgresImageTag, "-")[0]
 			GinkgoWriter.Printf("Using %v as the current major and upgrading to %v.\n", currentMajor, targetMajor)
 		}
 
@@ -196,27 +195,22 @@ var _ = Describe("Postgres Major Upgrade", Label(tests.LabelPostgresMajorUpgrade
 	}
 
 	// generateTargetImages, given a targetMajor, generates a target image for each buildScenario.
-	// MAJOR_UPGRADE_IMAGE_REPO env allows to customize the target image repository.
+	// It allows overriding the target image's repositories via env variables.
 	generateTargetImages := func(targetTag string) map[string]string {
-		const (
-			// ImageRepository is the default repository for Postgres container images
-			ImageRepository = "ghcr.io/cloudnative-pg/postgresql"
-
-			// PostgisImageRepository is the default repository for Postgis container images
-			PostgisImageRepository = "ghcr.io/cloudnative-pg/postgis"
-		)
-
 		// Default target Images
 		targetImages := map[string]string{
-			postgisEntry:           fmt.Sprintf("%v:%v", PostgisImageRepository, targetTag),
-			postgresqlEntry:        fmt.Sprintf("%v:%v-standard-trixie", ImageRepository, targetTag),
-			postgresqlMinimalEntry: fmt.Sprintf("%v:%v-minimal-trixie", ImageRepository, targetTag),
+			postgisEntry:           env.PostGISImageName(targetTag),
+			postgresqlEntry:        env.StandardImageName(targetTag),
+			postgresqlMinimalEntry: env.MinimalImageName(targetTag),
 		}
-		// Set custom targets when detecting a given env variable
-		if envValue := os.Getenv(customImageRegistryEnvVar); envValue != "" {
+
+		// Set custom targets when detecting env variables
+		if envValue := os.Getenv(customPostgresImageRegistryEnvVar); envValue != "" {
+			targetImages[postgresqlEntry] = fmt.Sprintf("%v:%v-%s", envValue, targetTag, environment.StandardTrixieSuffix)
+			targetImages[postgresqlMinimalEntry] = fmt.Sprintf("%v:%v-%s", envValue, targetTag, environment.MinimalTrixieSuffix)
+		}
+		if envValue := os.Getenv(customPostgisImageRegistryEnvVar); envValue != "" {
 			targetImages[postgisEntry] = fmt.Sprintf("%v:%v-postgis-trixie", envValue, targetTag)
-			targetImages[postgresqlEntry] = fmt.Sprintf("%v:%v-standard-trixie", envValue, targetTag)
-			targetImages[postgresqlMinimalEntry] = fmt.Sprintf("%v:%v-minimal-trixie", envValue, targetTag)
 		}
 
 		return targetImages
@@ -319,8 +313,7 @@ var _ = Describe("Postgres Major Upgrade", Label(tests.LabelPostgresMajorUpgrade
 		}
 
 		versionInfo := determineVersionsForTesting()
-		var err error
-		namespace, err = env.CreateUniqueTestNamespace(env.Ctx, env.Client, namespacePrefix)
+		namespace, err := env.CreateUniqueTestNamespace(env.Ctx, env.Client, namespacePrefix)
 		Expect(err).ToNot(HaveOccurred())
 
 		storageClass := os.Getenv("E2E_DEFAULT_STORAGE_CLASS")
@@ -334,21 +327,7 @@ var _ = Describe("Postgres Major Upgrade", Label(tests.LabelPostgresMajorUpgrade
 
 	DescribeTable("can upgrade a Cluster to a newer major version", func(scenarioName string) {
 		By("Creating the starting cluster")
-		// Avoid running Postgis major upgrade tests when a custom registry is being specified, because our
-		// PostGIS images are still based on Debian bullseye which uses OpenSSL 1.1, thus making them incompatible
-		// with any other image that uses OpenSSL 3.0 or greater.
-		// TODO: remove once we have PostGIS trixie images
-		if scenarioName == postgisEntry && os.Getenv(customImageRegistryEnvVar) != "" {
-			Skip("Skipping PostGIS major upgrades when a custom registry is specified")
-		}
-
 		scenario := scenarios[scenarioName]
-
-		if scenarioName == postgisEntry && scenario.targetMajor > 17 {
-			// PostGIS images are not available for Postgres versions greater than 17
-			Skip("Skipping major upgrades on PostGIS images for Postgres versions greater than 17")
-		}
-
 		cluster := scenario.startingCluster
 		err := env.Client.Create(env.Ctx, cluster)
 		Expect(err).NotTo(HaveOccurred())
diff --git a/tests/e2e/configuration_update_test.go b/tests/e2e/configuration_update_test.go
index a983e316..dbea5a7c 100644
--- a/tests/e2e/configuration_update_test.go
+++ b/tests/e2e/configuration_update_test.go
@@ -26,7 +26,6 @@ import (
 	"strings"
 	"time"
 
-	"github.com/cloudnative-pg/machinery/pkg/image/reference"
 	cnpgTypes "github.com/cloudnative-pg/machinery/pkg/types"
 	corev1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -210,7 +209,7 @@ var _ = Describe("Configuration update", Label(tests.LabelClusterMetadata), func
 			var err error
 			cluster, err = clusterutils.Get(env.Ctx, env.Client, namespace, clusterName)
 			Expect(err).NotTo(HaveOccurred())
-			cluster.Spec.ImageName = fmt.Sprintf("ghcr.io/cloudnative-pg/postgresql:%s-standard-trixie", targetTag)
+			cluster.Spec.ImageName = env.StandardImageName(targetTag)
 			cluster.Spec.PostgresConfiguration.Parameters["pgaudit.log"] = "all, -misc"
 			cluster.Spec.PostgresConfiguration.Parameters["pgaudit.log_catalog"] = "off"
 			cluster.Spec.PostgresConfiguration.Parameters["pgaudit.log_parameter"] = "on"
@@ -248,9 +247,7 @@ var _ = Describe("Configuration update", Label(tests.LabelClusterMetadata), func
 
 		// TODO: remove this once all E2Es run on minimal images
 		// https://github.com/cloudnative-pg/cloudnative-pg/issues/8123
-		currentImage := os.Getenv("POSTGRES_IMG")
-		Expect(currentImage).ToNot(BeEmpty())
-		targetTag = strings.Split(reference.New(currentImage).Tag, "-")[0]
+		targetTag = strings.Split(env.PostgresImageTag, "-")[0]
 	})
 
 	Context("PrimaryUpdateMethod: switchover", Ordered, func() {
@@ -261,7 +258,7 @@ var _ = Describe("Configuration update", Label(tests.LabelClusterMetadata), func
 			Expect(err).ToNot(HaveOccurred())
 
 			cluster := generateBaseCluster(namespace)
-			cluster.Spec.ImageName = fmt.Sprintf("ghcr.io/cloudnative-pg/postgresql:%s-minimal-trixie", targetTag)
+			cluster.Spec.ImageName = env.MinimalImageName(targetTag)
 			cluster.Spec.PrimaryUpdateMethod = apiv1.PrimaryUpdateMethodSwitchover
 			err = env.Client.Create(env.Ctx, cluster)
 			Expect(err).NotTo(HaveOccurred())
@@ -456,7 +453,7 @@ var _ = Describe("Configuration update", Label(tests.LabelClusterMetadata), func
 			Expect(err).ToNot(HaveOccurred())
 
 			cluster := generateBaseCluster(namespace)
-			cluster.Spec.ImageName = fmt.Sprintf("ghcr.io/cloudnative-pg/postgresql:%s-minimal-trixie", targetTag)
+			cluster.Spec.ImageName = env.MinimalImageName(targetTag)
 			cluster.Spec.PrimaryUpdateMethod = apiv1.PrimaryUpdateMethodRestart
 			err = env.Client.Create(env.Ctx, cluster)
 			Expect(err).NotTo(HaveOccurred())
diff --git a/tests/utils/environment/environment.go b/tests/utils/environment/environment.go
index 82d722f1..ba6a1e85 100644
--- a/tests/utils/environment/environment.go
+++ b/tests/utils/environment/environment.go
@@ -58,19 +58,34 @@ import (
 const (
 	// RetryTimeout retry timeout (in seconds) when a client api call or kubectl cli request get failed
 	RetryTimeout = 60
+
+	// StandardTrixieSuffix is the suffix for standard Trixie images
+	StandardTrixieSuffix = "standard-trixie"
+
+	// MinimalTrixieSuffix is the suffix for minimal Trixie images
+	MinimalTrixieSuffix = "minimal-trixie"
+
+	// Official CloudNativePG image repositories
+	defaultPostgresImageRepository = "ghcr.io/cloudnative-pg/postgresql"
+	defaultPostGISImageRepository  = "ghcr.io/cloudnative-pg/postgis"
+	defaultPostGISVersion          = "3"
 )
 
 // TestingEnvironment struct for operator testing
 type TestingEnvironment struct {
-	RestClientConfig   *rest.Config
-	Client             client.Client
-	Interface          kubernetes.Interface
-	APIExtensionClient apiextensionsclientset.Interface
-	Ctx                context.Context
-	Scheme             *runtime.Scheme
-	Log                logr.Logger
-	PostgresVersion    uint64
-	createdNamespaces  *uniqueStringSlice
+	RestClientConfig        *rest.Config
+	Client                  client.Client
+	Interface               kubernetes.Interface
+	APIExtensionClient      apiextensionsclientset.Interface
+	Ctx                     context.Context
+	Scheme                  *runtime.Scheme
+	Log                     logr.Logger
+	PostgresImageName       string
+	PostgresImageTag        string
+	PostgresVersion         uint64
+	PostgresImageRepository string
+	PostGISImageRepository  string
+	createdNamespaces       *uniqueStringSlice
 }
 
 type uniqueStringSlice struct {
@@ -122,11 +137,26 @@ func NewTestingEnvironment() (*TestingEnvironment, error) {
 
 	postgresImage := versions.DefaultImageName
 
-	// Fetching postgres image version.
+	// Fetching postgres image.
 	if postgresImageFromUser, exist := os.LookupEnv("POSTGRES_IMG"); exist {
 		postgresImage = postgresImageFromUser
 	}
 	imageReference := reference.New(postgresImage)
+	env.PostgresImageName = imageReference.Name
+	env.PostgresImageTag = imageReference.Tag
+
+	// Set PostgreSQL image repository (can be overridden via env variable)
+	env.PostgresImageRepository = defaultPostgresImageRepository
+	if postgresRepoFromUser, exist := os.LookupEnv("POSTGRES_IMG_REPOSITORY"); exist {
+		env.PostgresImageRepository = postgresRepoFromUser
+	}
+
+	// Set PostGIS image repository (can be overridden via env variable)
+	env.PostGISImageRepository = defaultPostGISImageRepository
+	if postgisRepoFromUser, exist := os.LookupEnv("POSTGIS_IMG_REPOSITORY"); exist {
+		env.PostGISImageRepository = postgisRepoFromUser
+	}
+
 	postgresImageVersion, err := version.FromTag(imageReference.Tag)
 	if err != nil {
 		return nil, err
@@ -186,3 +216,54 @@ func (env TestingEnvironment) CreateUniqueTestNamespace(
 
 	return name, namespaces.CreateTestNamespace(ctx, crudClient, name, opts...)
 }
+
+// ImageNameWithSuffix constructs a full image name by appending a suffix to the tag.
+// It uses the PostgresImageName from the environment and formats it as: name:tag-suffix
+func (env *TestingEnvironment) ImageNameWithSuffix(tag, suffix string) string {
+	return fmt.Sprintf("%s:%s-%s", env.PostgresImageName, tag, suffix)
+}
+
+// StandardImageName returns the full image name for a standard Postgres image.
+// Example: ghcr.io/cloudnative-pg/postgresql:17-standard-trixie
+func (env *TestingEnvironment) StandardImageName(tag string) string {
+	return env.ImageNameWithSuffix(tag, StandardTrixieSuffix)
+}
+
+// MinimalImageName returns the full image name for a minimal Postgres image.
+// Example: ghcr.io/cloudnative-pg/postgresql:17-minimal-trixie
+func (env *TestingEnvironment) MinimalImageName(tag string) string {
+	return env.ImageNameWithSuffix(tag, MinimalTrixieSuffix)
+}
+
+// PostGISImageName returns the full image name for a PostGIS image.
+// Example: ghcr.io/cloudnative-pg/postgis:17-3-standard-trixie
+func (env *TestingEnvironment) PostGISImageName(tag string) string {
+	return fmt.Sprintf("%s:%s-%s-%s", env.PostGISImageRepository, tag, defaultPostGISVersion, StandardTrixieSuffix)
+}
+
+// OfficialPostgresImageName returns the full image name for the official CloudNativePG Postgres image.
+// Example: ghcr.io/cloudnative-pg/postgresql:17
+func (env *TestingEnvironment) OfficialPostgresImageName(tag string) string {
+	return fmt.Sprintf("%s:%s", defaultPostgresImageRepository, tag)
+}
+
+// OfficialStandardImageName returns the full image name for the official standard Postgres image.
+// This is used for major upgrade tests where source images must come from the official registry.
+// Example: ghcr.io/cloudnative-pg/postgresql:16-standard-trixie
+func (env *TestingEnvironment) OfficialStandardImageName(tag string) string {
+	return fmt.Sprintf("%s:%s-%s", defaultPostgresImageRepository, tag, StandardTrixieSuffix)
+}
+
+// OfficialMinimalImageName returns the full image name for the official minimal Postgres image.
+// This is used for major upgrade tests where source images must come from the official registry.
+// Example: ghcr.io/cloudnative-pg/postgresql:16-minimal-trixie
+func (env *TestingEnvironment) OfficialMinimalImageName(tag string) string {
+	return fmt.Sprintf("%s:%s-%s", defaultPostgresImageRepository, tag, MinimalTrixieSuffix)
+}
+
+// OfficialPostGISImageName returns the full image name for the official CloudNativePG PostGIS image.
+// This is used for major upgrade tests where source images must come from the official registry.
+// Example: ghcr.io/cloudnative-pg/postgis:16-3-standard-trixie
+func (env *TestingEnvironment) OfficialPostGISImageName(tag string) string {
+	return fmt.Sprintf("%s:%s-%s-%s", defaultPostGISImageRepository, tag, defaultPostGISVersion, StandardTrixieSuffix)
+}