Repository: cloudnative-pg/cloudnative-pg

Issue:
[Bug]: "old and new pg_controldata WAL segment sizes are invalid or do not match"
### Is there an existing issue already for this bug?

- [x] I have searched for an existing issue, and could not find anything. I believe this is a new bug.

### I have read the troubleshooting guide

- [x] I have read the troubleshooting guide and I think this is a new bug.

### I am running a supported version of CloudNativePG

- [x] I have read the troubleshooting guide and I think this is a new bug.

### Contact Details

_No response_

### Version

trunk (main)

### What version of Kubernetes are you using?

1.32

### What is your Kubernetes environment?

Self-managed: kind (evaluation)

### How did you install the operator?

YAML manifest

### What happened?

I have created a Postgres 13 cluster and requested data checksums and a different segment size. The major upgrade process fails with the following error:

```
old and new pg_controldata WAL segment sizes are invalid or do not match
```

To replicate it, use the RC1 version, create the cluster with version 13 then once it is up change 13 with 17.

### Cluster resource

```shell
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: pg
spec:
  imageName: ghcr.io/cloudnative-pg/postgresql:13-minimal-bullseye
  instances: 3

  storage:
    size: 1Gi
  walStorage:
    size: 1Gi
  bootstrap:
    initdb:
      dataChecksums: true
      walSegmentSize: 64
```

### Relevant log output

```shell
pg-1-major-upgrade-6g5cg major-upgrade {"level":"info","ts":"2025-04-01T14:40:52.696911223Z","logger":"pg_upgrade","msg":"Checking cluster versions                                     ok","pipe":"stdout","logging_pod":"pg-1-major-upgrade"}
pg-1-major-upgrade-6g5cg major-upgrade {"level":"info","ts":"2025-04-01T14:40:52.702945202Z","logger":"pg_upgrade","msg":"old and new pg_controldata WAL segment sizes are invalid or do not match","pipe":"stdout","logging_pod":"pg-1-major-upgrade"}
pg-1-major-upgrade-6g5cg major-upgrade {"level":"info","ts":"2025-04-01T14:40:52.702964575Z","logger":"pg_upgrade","msg":"Failure, exiting","pipe":"stdout","logging_pod":"pg-1-major-upgrade"}
pg-1-major-upgrade-6g5cg major-upgrade Error: error while running pg_upgrade: error while running "/usr/lib/postgresql/17/bin/pg_upgrade --link --username postgres --old-bindir /controller/old/usr/lib/postgresql/16/bin --old-datadir /var/lib/postgresql/data/pgdata --new-datadir /var/lib/postgresql/data/pgdata-new": exit status 1
```

### Code of Conduct

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

Discussion:

Solution patch:
diff --git a/internal/cmd/manager/instance/upgrade/execute/cmd.go b/internal/cmd/manager/instance/upgrade/execute/cmd.go
index b1c4260b..2e326303 100644
--- a/internal/cmd/manager/instance/upgrade/execute/cmd.go
+++ b/internal/cmd/manager/instance/upgrade/execute/cmd.go
@@ -27,6 +27,7 @@ import (
 	"os/exec"
 	"path"
 	"path/filepath"
+	"strconv"
 	"strings"
 	"time"
 
@@ -36,6 +37,7 @@ import (
 	"github.com/cloudnative-pg/machinery/pkg/fileutils"
 	"github.com/cloudnative-pg/machinery/pkg/fileutils/compatibility"
 	"github.com/cloudnative-pg/machinery/pkg/log"
+	"github.com/cloudnative-pg/machinery/pkg/postgres/version"
 	"github.com/spf13/cobra"
 	"k8s.io/utils/ptr"
 	ctrl "sigs.k8s.io/controller-runtime/pkg/client"
@@ -48,9 +50,10 @@ import (
 	"github.com/cloudnative-pg/cloudnative-pg/pkg/management"
 	"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/management/postgres/utils"
+	postgresutils "github.com/cloudnative-pg/cloudnative-pg/pkg/management/postgres/utils"
 	"github.com/cloudnative-pg/cloudnative-pg/pkg/management/postgres/webserver/metricserver"
 	"github.com/cloudnative-pg/cloudnative-pg/pkg/specs"
+	"github.com/cloudnative-pg/cloudnative-pg/pkg/utils"
 )
 
 // NewCmd creates the cobra command
@@ -187,8 +190,19 @@ func upgradeSubCommand(
 		}
 	}
 
+	// Extract controldata information from the old data directory
+	controlData, err := getControlData(oldBinDir, pgData)
+	if err != nil {
+		return fmt.Errorf("error while getting old data directory control data: %w", err)
+	}
+
+	targetVersion, err := cluster.GetPostgresqlVersion()
+	if err != nil {
+		return fmt.Errorf("error while getting the target version from the cluster object: %w", err)
+	}
+
 	contextLogger.Info("Creating data directory", "directory", newDataDir)
-	if err := runInitDB(newDataDir, newWalDir); err != nil {
+	if err := runInitDB(newDataDir, newWalDir, controlData, targetVersion); err != nil {
 		return fmt.Errorf("error while creating the data directory: %w", err)
 	}
 
@@ -199,12 +213,12 @@ func upgradeSubCommand(
 
 	contextLogger.Info("Checking if we have anything to update")
 	// Read pg_version from both the old and new data directories
-	oldVersion, err := utils.GetPgdataVersion(pgData)
+	oldVersion, err := postgresutils.GetPgdataVersion(pgData)
 	if err != nil {
 		return fmt.Errorf("error while reading the old version: %w", err)
 	}
 
-	newVersion, err := utils.GetPgdataVersion(newDataDir)
+	newVersion, err := postgresutils.GetPgdataVersion(newDataDir)
 	if err != nil {
 		return fmt.Errorf("error while reading the new version: %w", err)
 	}
@@ -262,7 +276,19 @@ func upgradeSubCommand(
 	return nil
 }
 
-func runInitDB(destDir string, walDir *string) error {
+func getControlData(binDir, pgData string) (map[string]string, error) {
+	pgControlDataCmd := exec.Command(path.Join(binDir, "pg_controldata")) // #nosec
+	pgControlDataCmd.Env = append(os.Environ(), "PGDATA="+pgData)
+
+	out, err := pgControlDataCmd.CombinedOutput()
+	if err != nil {
+		return nil, fmt.Errorf("while executing pg_controldata: %w", err)
+	}
+
+	return utils.ParsePgControldataOutput(string(out)), nil
+}
+
+func runInitDB(destDir string, walDir *string, pgControlData map[string]string, targetVersion version.Data) error {
 	// Invoke initdb to generate a data directory
 	options := []string{
 		"--username",
@@ -275,6 +301,17 @@ func runInitDB(destDir string, walDir *string) error {
 		options = append(options, "--waldir", *walDir)
 	}
 
+	// Extract the WAL segment size from the pg_controldata output
+	options, err := tryAddWalSegmentSize(pgControlData, options)
+	if err != nil {
+		return err
+	}
+
+	options, err = tryAddDataChecksums(pgControlData, targetVersion, options)
+	if err != nil {
+		return err
+	}
+
 	// Certain CSI drivers may add setgid permissions on newly created folders.
 	// A default umask is set to attempt to avoid this, by revoking group/other
 	// permission bits on the PGDATA
@@ -288,6 +325,46 @@ func runInitDB(destDir string, walDir *string) error {
 	return nil
 }
 
+// TODO: refactor it should be a method of pgControlData
+func tryAddDataChecksums(
+	pgControlData map[string]string,
+	targetVersion version.Data,
+	options []string,
+) ([]string, error) {
+	dataPageChecksumVersion, ok := pgControlData[utils.PgControlDataDataPageChecksumVersion]
+	if !ok {
+		return nil, fmt.Errorf("no '%s' section into pg_controldata output", utils.PgControlDataDataPageChecksumVersion)
+	}
+
+	if dataPageChecksumVersion != "1" {
+		// In postgres 18 we will have to set "--no-data-checksums" if checksums are disabled (they are enabled by default)
+		if targetVersion.Major() >= 18 {
+			return append(options, "--no-data-checksums"), nil
+		}
+		return options, nil
+	}
+
+	return append(options, "--data-checksums"), nil
+}
+
+// TODO: refactor it should be a method of pgControlData
+func tryAddWalSegmentSize(pgControlData map[string]string, options []string) ([]string, error) {
+	walSegmentSizeString, ok := pgControlData[utils.PgControlDataBytesPerWALSegment]
+	if !ok {
+		return nil, fmt.Errorf("no '%s' section into pg_controldata output", utils.PgControlDataBytesPerWALSegment)
+	}
+
+	walSegmentSize, err := strconv.Atoi(walSegmentSizeString)
+	if err != nil {
+		return nil, fmt.Errorf(
+			"wrong '%s' pg_controldata value (not an integer): '%s' %w",
+			utils.PgControlDataBytesPerWALSegment, walSegmentSizeString, err)
+	}
+
+	param := "--wal-segsize=" + strconv.Itoa(walSegmentSize/(1024*1024))
+	return append(options, param), nil
+}
+
 func prepareConfigurationFiles(ctx context.Context, cluster apiv1.Cluster, destDir string) error {
 	// Always read the custom and override configuration files created by the operator
 	_, err := configfile.EnsureIncludes(path.Join(destDir, "postgresql.conf"),
@@ -298,8 +375,20 @@ func prepareConfigurationFiles(ctx context.Context, cluster apiv1.Cluster, destD
 		return fmt.Errorf("appending inclusion directives to postgresql.conf file resulted in an error: %w", err)
 	}
 
+	// Set `max_slot_wal_keep_size` to the default value because any other value it is not supported in pg_upgrade
+	tmpCluster := cluster.DeepCopy()
+	tmpCluster.Spec.PostgresConfiguration.Parameters["max_slot_wal_keep_size"] = "-1"
+
+	pgVersion, err := postgresutils.GetPgdataVersion(destDir)
+	if err != nil {
+		return fmt.Errorf("error while reading the new data directory version: %w", err)
+	}
+	if pgVersion.Major >= 18 {
+		tmpCluster.Spec.PostgresConfiguration.Parameters["idle_replication_slot_timeout"] = "0"
+	}
+
 	newInstance := postgres.Instance{PgData: destDir}
-	if _, err := newInstance.RefreshConfigurationFilesFromCluster(ctx, &cluster, false); err != nil {
+	if _, err := newInstance.RefreshConfigurationFilesFromCluster(ctx, tmpCluster, false); err != nil {
 		return fmt.Errorf("error while creating the configuration files for new datadir %q: %w", destDir, err)
 	}
 
diff --git a/pkg/management/postgres/instance.go b/pkg/management/postgres/instance.go
index 9076de82..0987c498 100644
--- a/pkg/management/postgres/instance.go
+++ b/pkg/management/postgres/instance.go
@@ -271,7 +271,7 @@ func (instance *Instance) CheckHasDiskSpaceForWAL(ctx context.Context) (bool, er
 	}
 
 	pgControlData := utils.ParsePgControldataOutput(pgControlDataString)
-	walSegmentSizeString, ok := pgControlData["Bytes per WAL segment"]
+	walSegmentSizeString, ok := pgControlData[utils.PgControlDataBytesPerWALSegment]
 	if !ok {
 		return false, fmt.Errorf("no 'Bytes per WAL segment' section into pg_controldata output")
 	}
diff --git a/pkg/utils/parser.go b/pkg/utils/parser.go
index a889c2cb..f039ebcb 100644
--- a/pkg/utils/parser.go
+++ b/pkg/utils/parser.go
@@ -57,6 +57,12 @@ const (
 	// PgControlDataDatabaseClusterStateKey is the status
 	// of the latest primary that run on this data directory.
 	PgControlDataDatabaseClusterStateKey pgControlDataKey = "Database cluster state"
+
+	// PgControlDataDataPageChecksumVersion reports whether the checksums are enabled in the cluster
+	PgControlDataDataPageChecksumVersion pgControlDataKey = "Data page checksum version"
+
+	// PgControlDataBytesPerWALSegment reports the size of the WAL segments
+	PgControlDataBytesPerWALSegment pgControlDataKey = "Bytes per WAL segment"
 )
 
 // PgDataState represents the "Database cluster state" field of pg_controldata
@@ -82,7 +88,7 @@ func (state PgDataState) IsShutdown(ctx context.Context) bool {
 }
 
 // ParsePgControldataOutput parses a pg_controldata output into a map of key-value pairs
-func ParsePgControldataOutput(data string) map[string]string {
+func ParsePgControldataOutput(data string) map[pgControlDataKey]string {
 	pairs := make(map[string]string)
 	lines := strings.Split(data, "\n")
 	for _, line := range lines {



Test patch:
diff --git a/tests/e2e/cluster_major_upgrade_test.go b/tests/e2e/cluster_major_upgrade_test.go
index 86704741..d79eb474 100644
--- a/tests/e2e/cluster_major_upgrade_test.go
+++ b/tests/e2e/cluster_major_upgrade_test.go
@@ -32,6 +32,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/util/rand"
+	"k8s.io/utils/ptr"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	v1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
@@ -78,6 +79,12 @@ var _ = Describe("Postgres Major Upgrade", Label(tests.LabelPostgresMajorUpgrade
 			},
 			Spec: v1.ClusterSpec{
 				Instances: 3,
+				Bootstrap: &v1.BootstrapConfiguration{
+					InitDB: &v1.BootstrapInitDB{
+						DataChecksums:  ptr.To(true),
+						WalSegmentSize: 32,
+					},
+				},
 				StorageConfiguration: v1.StorageConfiguration{
 					StorageClass: &storageClass,
 					Size:         "1Gi",
@@ -95,6 +102,7 @@ var _ = Describe("Postgres Major Upgrade", Label(tests.LabelPostgresMajorUpgrade
 						"log_temp_files":              "1024",
 						"log_autovacuum_min_duration": "1000",
 						"log_replication_commands":    "on",
+						"max_slot_wal_keep_size":      "1GB",
 					},
 				},
 			},