Repository: cloudnative-pg/cloudnative-pg

Issue:
[Feature]: Support beta versions in major upgrade E2E tests
### 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?

Major upgrade E2E tests currently do not support running with beta or rc versions, which is required given https://github.com/cloudnative-pg/cloudnative-pg/issues/7992.
Improve the test logic to recognize and allow upgrading to a BETA version.

This would also standardize and remove the necessity for a workaround that's currently being done in https://github.com/cloudnative-pg/postgres-trunk-containers/blob/main/.github/workflows/reusable-e2e.yml#L151-L152 to override the latest `DefaultImageName` of the codebase.

### Describe the solution you'd like

Beta and RC images do not (yet) have a major only tag (e.g `18` or `18-minimal-bookworm`) given that they are not stable images. We could improve the logic to detect BETA versions and then build the current image and the target image accordingly.

### Describe alternatives you've considered

.

### Additional context

_No response_

### 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/pg_versions.json b/.github/pg_versions.json
index 828288a6..85a0a03c 100644
--- a/.github/pg_versions.json
+++ b/.github/pg_versions.json
@@ -1,4 +1,8 @@
 {
+  "18": [
+    "18beta1",
+    "18beta1-7"
+  ],
   "17": [
     "17.5",
     "17.4"



Test patch:
diff --git a/tests/e2e/cluster_major_upgrade_test.go b/tests/e2e/cluster_major_upgrade_test.go
index 17b94df2..178dd7e0 100644
--- a/tests/e2e/cluster_major_upgrade_test.go
+++ b/tests/e2e/cluster_major_upgrade_test.go
@@ -24,6 +24,7 @@ import (
 	"fmt"
 	"os"
 	"strconv"
+	"strings"
 	"time"
 
 	"github.com/cloudnative-pg/machinery/pkg/image/reference"
@@ -109,9 +110,9 @@ var _ = Describe("Postgres Major Upgrade", Label(tests.LabelPostgresMajorUpgrade
 		}
 	}
 
-	generatePostgreSQLCluster := func(namespace string, storageClass string, majorVersion int) *v1.Cluster {
+	generatePostgreSQLCluster := func(namespace string, storageClass string, tagVersion string) *v1.Cluster {
 		cluster := generateBaseCluster(namespace, storageClass)
-		cluster.Spec.ImageName = fmt.Sprintf("ghcr.io/cloudnative-pg/postgresql:%d-standard-bookworm", majorVersion)
+		cluster.Spec.ImageName = fmt.Sprintf("ghcr.io/cloudnative-pg/postgresql:%s-standard-bookworm", tagVersion)
 		cluster.Spec.Bootstrap.InitDB.PostInitSQL = []string{
 			"CREATE EXTENSION IF NOT EXISTS pg_stat_statements;",
 			"CREATE EXTENSION IF NOT EXISTS pg_trgm;",
@@ -119,15 +120,15 @@ 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, majorVersion int) *v1.Cluster {
-		cluster := generatePostgreSQLCluster(namespace, storageClass, majorVersion)
-		cluster.Spec.ImageName = fmt.Sprintf("ghcr.io/cloudnative-pg/postgresql:%d-minimal-bookworm", majorVersion)
+	generatePostgreSQLMinimalCluster := func(namespace string, storageClass string, tagVersion string) *v1.Cluster {
+		cluster := generatePostgreSQLCluster(namespace, storageClass, tagVersion)
+		cluster.Spec.ImageName = fmt.Sprintf("ghcr.io/cloudnative-pg/postgresql:%s-minimal-bookworm", tagVersion)
 		return cluster
 	}
 
-	generatePostGISCluster := func(namespace string, storageClass string, majorVersion int) *v1.Cluster {
+	generatePostGISCluster := func(namespace string, storageClass string, tagVersion string) *v1.Cluster {
 		cluster := generateBaseCluster(namespace, storageClass)
-		cluster.Spec.ImageName = "ghcr.io/cloudnative-pg/postgis:" + strconv.Itoa(majorVersion)
+		cluster.Spec.ImageName = fmt.Sprintf("ghcr.io/cloudnative-pg/postgis:%s", tagVersion)
 		cluster.Spec.Bootstrap.InitDB.PostInitApplicationSQL = []string{
 			"CREATE EXTENSION postgis",
 			"CREATE EXTENSION postgis_raster",
@@ -148,17 +149,26 @@ var _ = Describe("Postgres Major Upgrade", Label(tests.LabelPostgresMajorUpgrade
 		return cluster
 	}
 
-	determineVersionsForTesting := func() (uint64, uint64) {
+	type versionInfo struct {
+		currentMajor uint64
+		currentTag   string
+		targetMajor  uint64
+		targetTag    string
+	}
+
+	determineVersionsForTesting := func() versionInfo {
 		currentImage := os.Getenv("POSTGRES_IMG")
 		Expect(currentImage).ToNot(BeEmpty())
 
 		currentVersion, err := version.FromTag(reference.New(currentImage).Tag)
 		Expect(err).NotTo(HaveOccurred())
 		currentMajor := currentVersion.Major()
+		currentTag := strconv.FormatUint(currentMajor, 10)
 
 		targetVersion, err := version.FromTag(reference.New(versions.DefaultImageName).Tag)
 		Expect(err).ToNot(HaveOccurred())
 		targetMajor := targetVersion.Major()
+		targetTag := strconv.FormatUint(targetMajor, 10)
 
 		// If same version, choose a previous one for testing
 		if currentMajor == targetMajor {
@@ -166,12 +176,28 @@ var _ = Describe("Postgres Major Upgrade", Label(tests.LabelPostgresMajorUpgrade
 			GinkgoWriter.Printf("Using %v as the current major version instead.\n", currentMajor)
 		}
 
-		return currentMajor, targetMajor
+		// This means we are on a beta version, so we can just invert the versions
+		if currentMajor > targetMajor {
+			currentMajor, targetMajor = targetMajor, currentMajor
+			currentTag = targetTag
+			// 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]
+			GinkgoWriter.Printf("Using %v as the current major and upgrading to %v.\n", currentMajor, targetMajor)
+		}
+
+		return versionInfo{
+			currentMajor: currentMajor,
+			currentTag:   currentTag,
+			targetMajor:  targetMajor,
+			targetTag:    targetTag,
+		}
 	}
 
 	// generateTargetImages, given a targetMajor, generates a target image for each buildScenario.
 	// MAJOR_UPGRADE_IMAGE_REPO env allows to customize the target image repository.
-	generateTargetImages := func(targetMajor uint64) map[string]string {
+	generateTargetImages := func(targetTag string) map[string]string {
 		const (
 			// ImageRepository is the default repository for Postgres container images
 			ImageRepository = "ghcr.io/cloudnative-pg/postgresql"
@@ -182,43 +208,45 @@ var _ = Describe("Postgres Major Upgrade", Label(tests.LabelPostgresMajorUpgrade
 
 		// Default target Images
 		targetImages := map[string]string{
-			postgisEntry:           fmt.Sprintf("%v:%v", PostgisImageRepository, targetMajor),
-			postgresqlEntry:        fmt.Sprintf("%v:%v-standard-bookworm", ImageRepository, targetMajor),
-			postgresqlMinimalEntry: fmt.Sprintf("%v:%v-minimal-bookworm", ImageRepository, targetMajor),
+			postgisEntry:           fmt.Sprintf("%v:%v", PostgisImageRepository, targetTag),
+			postgresqlEntry:        fmt.Sprintf("%v:%v-standard-bookworm", ImageRepository, targetTag),
+			postgresqlMinimalEntry: fmt.Sprintf("%v:%v-minimal-bookworm", ImageRepository, targetTag),
 		}
 		// Set custom targets when detecting a given env variable
 		if envValue := os.Getenv(customImageRegistryEnvVar); envValue != "" {
-			targetImages[postgisEntry] = fmt.Sprintf("%v:%v-postgis-bookworm", envValue, targetMajor)
-			targetImages[postgresqlEntry] = fmt.Sprintf("%v:%v-standard-bookworm", envValue, targetMajor)
-			targetImages[postgresqlMinimalEntry] = fmt.Sprintf("%v:%v-minimal-bookworm", envValue, targetMajor)
+			targetImages[postgisEntry] = fmt.Sprintf("%v:%v-postgis-bookworm", envValue, targetTag)
+			targetImages[postgresqlEntry] = fmt.Sprintf("%v:%v-standard-bookworm", envValue, targetTag)
+			targetImages[postgresqlMinimalEntry] = fmt.Sprintf("%v:%v-minimal-bookworm", envValue, targetTag)
 		}
 
 		return targetImages
 	}
 
 	buildScenarios := func(
-		namespace string, storageClass string, currentMajor, targetMajor uint64,
+		namespace string, storageClass string, info versionInfo,
 	) map[string]*scenario {
-		targetImages := generateTargetImages(targetMajor)
+		targetImages := generateTargetImages(info.targetTag)
 
 		return map[string]*scenario{
 			postgisEntry: {
-				startingCluster: generatePostGISCluster(namespace, storageClass, int(currentMajor)),
-				startingMajor:   int(currentMajor),
+				startingCluster: generatePostGISCluster(namespace, storageClass, strconv.FormatUint(info.currentMajor, 10)),
+				startingMajor:   int(info.currentMajor),
 				targetImage:     targetImages[postgisEntry],
-				targetMajor:     int(targetMajor),
+				targetMajor:     int(info.targetMajor),
 			},
 			postgresqlEntry: {
-				startingCluster: generatePostgreSQLCluster(namespace, storageClass, int(currentMajor)),
-				startingMajor:   int(currentMajor),
-				targetImage:     targetImages[postgresqlEntry],
-				targetMajor:     int(targetMajor),
+				startingCluster: generatePostgreSQLCluster(namespace, storageClass,
+					strconv.FormatUint(info.currentMajor, 10)),
+				startingMajor: int(info.currentMajor),
+				targetImage:   targetImages[postgresqlEntry],
+				targetMajor:   int(info.targetMajor),
 			},
 			postgresqlMinimalEntry: {
-				startingCluster: generatePostgreSQLMinimalCluster(namespace, storageClass, int(currentMajor)),
-				startingMajor:   int(currentMajor),
-				targetImage:     targetImages[postgresqlMinimalEntry],
-				targetMajor:     int(targetMajor),
+				startingCluster: generatePostgreSQLMinimalCluster(namespace, storageClass,
+					strconv.FormatUint(info.currentMajor, 10)),
+				startingMajor: int(info.currentMajor),
+				targetImage:   targetImages[postgresqlMinimalEntry],
+				targetMajor:   int(info.targetMajor),
 			},
 		}
 	}
@@ -290,7 +318,7 @@ var _ = Describe("Postgres Major Upgrade", Label(tests.LabelPostgresMajorUpgrade
 			Skip("Test depth is lower than the amount requested for this test")
 		}
 
-		currentMajor, targetMajor := determineVersionsForTesting()
+		versionInfo := determineVersionsForTesting()
 		var err error
 		namespace, err = env.CreateUniqueTestNamespace(env.Ctx, env.Client, namespacePrefix)
 		Expect(err).ToNot(HaveOccurred())
@@ -301,7 +329,7 @@ var _ = Describe("Postgres Major Upgrade", Label(tests.LabelPostgresMajorUpgrade
 		// We cannot use generated entries in the DescribeTable, so we use the scenario key as a constant, but
 		// define the actual content here.
 		// See https://onsi.github.io/ginkgo/#mental-model-table-specs-are-just-syntactic-sugar
-		scenarios = buildScenarios(namespace, storageClass, currentMajor, targetMajor)
+		scenarios = buildScenarios(namespace, storageClass, versionInfo)
 	})
 
 	DescribeTable("can upgrade a Cluster to a newer major version", func(scenarioName string) {
@@ -315,6 +343,12 @@ var _ = Describe("Postgres Major Upgrade", Label(tests.LabelPostgresMajorUpgrade
 		}
 
 		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())