From 33398644dadcdb4ca65aa6f055063a35a8ebfa6a Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Wed, 19 Aug 2026 16:05:53 +0200 Subject: [PATCH 1/2] feat: add support for `TopologySpreadConstraints` --- api/v1/postgres_types.go | 29 +++++++++++++++++++++++++++++ api/v1/postgres_types_test.go | 3 ++- controllers/postgres_controller.go | 7 +++++-- main.go | 16 ++++++++++++++++ 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/api/v1/postgres_types.go b/api/v1/postgres_types.go index cac10437..e012f5f7 100644 --- a/api/v1/postgres_types.go +++ b/api/v1/postgres_types.go @@ -829,6 +829,35 @@ func (p *Postgres) ToUnstructuredZalandoPostgresql(z *zalando.Postgresql, c *cor z.Spec.TLS = nil } + if tscEnable { + tsc := corev1.TopologySpreadConstraint{ + MaxSkew: tscMaxSkew, + WhenUnsatisfiable: corev1.ScheduleAnyway, + LabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "application": "spilo", + "cluster-name": z.Name, + NameLabelName: p.Name, + PartitionIDLabelName: p.Spec.PartitionID, + ProjectIDLabelName: p.Spec.ProjectID, + TenantLabelName: p.Spec.Tenant, + UIDLabelName: string(p.UID), + "team": p.generateTeamID(), + }, + }, + TopologyKey: tscKey, + } + + // if defined, set the minDomains (and corresponding whenUnsatisfied) field as well + if tscMinDomains > 0 { + tsc.MinDomains = &tscMinDomains + tsc.WhenUnsatisfiable = corev1.DoNotSchedule + } + + // override topology spread constraints + z.Spec.TopologySpreadConstraints = []corev1.TopologySpreadConstraint{tsc} + } + jsonZ, err := runtime.DefaultUnstructuredConverter.ToUnstructured(z) if err != nil { return nil, fmt.Errorf("failed to convert to unstructured zalando postgresql: %w", err) diff --git a/api/v1/postgres_types_test.go b/api/v1/postgres_types_test.go index 005e1c79..7186756a 100644 --- a/api/v1/postgres_types_test.go +++ b/api/v1/postgres_types_test.go @@ -379,7 +379,8 @@ func TestPostgresRestoreTimestamp_ToUnstructuredZalandoPostgresql(t *testing.T) p := &Postgres{ Spec: tt.spec, } - got, _ := p.ToUnstructuredZalandoPostgresql(nil, tt.c, tt.sc, tt.pgParamBlockList, tt.rbs, tt.srcDB, 130, 10, 60, false, false, "dockerImage", 66) + got, _ := p.ToUnstructuredZalandoPostgresql(nil, tt.c, tt.sc, tt.pgParamBlockList, tt.rbs, tt.srcDB, + 130, 10, 60, false, false, "dockerImage", 66, false, "", 0, 0) jsonZ, err := runtime.DefaultUnstructuredConverter.ToUnstructured(got) if err != nil { diff --git a/controllers/postgres_controller.go b/controllers/postgres_controller.go index 70408def..a79758ae 100644 --- a/controllers/postgres_controller.go +++ b/controllers/postgres_controller.go @@ -114,6 +114,9 @@ type PostgresReconciler struct { WalGExporterCPULimit string WalGExporterMemoryLimit string SpiloCpuRequestsPercentage int + EnableTopologySpreadConstraints bool + TscMinDomains, TscMaxSkew int32 + TscKey string } type PatroniStandbyCluster struct { @@ -480,7 +483,7 @@ func (r *PostgresReconciler) createOrUpdateZalandoPostgresql(ctx context.Context return fmt.Errorf("failed to fetch zalando postgresql: %w", err) } - u, err := instance.ToUnstructuredZalandoPostgresql(nil, sidecarsCM, r.StorageClass, r.PgParamBlockList, restoreBackupConfig, restoreSourceInstance, patroniTTL, patroniLoopWait, patroniRetryTimeout, r.EnableSuperUserForDBO, r.EnableCustomTLSCert, r.PostgresImage, r.SpiloCpuRequestsPercentage) + u, err := instance.ToUnstructuredZalandoPostgresql(nil, sidecarsCM, r.StorageClass, r.PgParamBlockList, restoreBackupConfig, restoreSourceInstance, patroniTTL, patroniLoopWait, patroniRetryTimeout, r.EnableSuperUserForDBO, r.EnableCustomTLSCert, r.PostgresImage, r.SpiloCpuRequestsPercentage, r.EnableTopologySpreadConstraints, r.TscKey, r.TscMaxSkew, r.TscMinDomains) if err != nil { return fmt.Errorf("failed to convert to unstructured zalando postgresql: %w", err) } @@ -496,7 +499,7 @@ func (r *PostgresReconciler) createOrUpdateZalandoPostgresql(ctx context.Context // Update zalando postgresql mergeFrom := client.MergeFrom(rawZ.DeepCopy()) - u, err := instance.ToUnstructuredZalandoPostgresql(rawZ, sidecarsCM, r.StorageClass, r.PgParamBlockList, restoreBackupConfig, restoreSourceInstance, patroniTTL, patroniLoopWait, patroniRetryTimeout, r.EnableSuperUserForDBO, r.EnableCustomTLSCert, r.PostgresImage, r.SpiloCpuRequestsPercentage) + u, err := instance.ToUnstructuredZalandoPostgresql(rawZ, sidecarsCM, r.StorageClass, r.PgParamBlockList, restoreBackupConfig, restoreSourceInstance, patroniTTL, patroniLoopWait, patroniRetryTimeout, r.EnableSuperUserForDBO, r.EnableCustomTLSCert, r.PostgresImage, r.SpiloCpuRequestsPercentage, r.EnableTopologySpreadConstraints, r.TscKey, r.TscMaxSkew, r.TscMinDomains) if err != nil { return fmt.Errorf("failed to convert to unstructured zalando postgresql: %w", err) } diff --git a/main.go b/main.go index d8c41ac9..a54e1a19 100644 --- a/main.go +++ b/main.go @@ -98,6 +98,7 @@ const ( podAntiaffinityPreferredDuringSchedulingFlg = "pod-antiaffinity-preferred-during-scheduling" podAntiaffinityTopologyKeyFlg = "pod-antiaffinity-topology-key" enablePodTopologySpreadConstraintWebhookFlg = "enable-pod-topology-spread-constraint-webhook" + enablePodTopologySpreadConstraintFlg = "enable-pod-topology-spread-constraint" podTopologySpreadConstraintTopologyKeyFlg = "pod-topology-spread-constraint-topology-key" podTopologySpreadConstraintMaxSkewFlg = "pod-topology-spread-constraint-max-skew" podTopologySpreadConstraintMinDomainsFlg = "pod-topology-spread-constraint-min-domains" @@ -173,6 +174,7 @@ func main() { enableWalGExporter bool podAntiaffinityPreferredDuringScheduling bool enablePodTopologySpreadConstraintWebhook bool + enablePodTopologySpreadConstraint bool enableSpiloReadinessProbe bool enableKubernetesUseConfigMaps bool @@ -369,6 +371,15 @@ func main() { viper.SetDefault(enablePodTopologySpreadConstraintWebhookFlg, false) enablePodTopologySpreadConstraintWebhook = viper.GetBool(enablePodTopologySpreadConstraintWebhookFlg) + + viper.SetDefault(enablePodTopologySpreadConstraintFlg, false) + enablePodTopologySpreadConstraint = viper.GetBool(enablePodTopologySpreadConstraintFlg) + + if !strings.Contains(operatorImage, `:v2`) && enablePodTopologySpreadConstraint { + setupLog.Error(nil, fmt.Sprintf("Flag %s not supported with postgres operator v1, use %s instead, exiting.", enablePodTopologySpreadConstraintFlg, enablePodTopologySpreadConstraintWebhookFlg)) + os.Exit(1) + } + viper.SetDefault(podTopologySpreadConstraintTopologyKeyFlg, "machine.metal-stack.io/rack") podTopologySpreadConstraintTopologyKey = viper.GetString(podTopologySpreadConstraintTopologyKeyFlg) viper.SetDefault(podTopologySpreadConstraintMaxSkewFlg, 1) @@ -437,6 +448,7 @@ func main() { walGExporterCPULimitFlg, walGExporterCPULimit, walGExporterMemoryLimitFlg, walGExporterMemoryLimit, enablePodTopologySpreadConstraintWebhookFlg, enablePodTopologySpreadConstraintWebhook, + enablePodTopologySpreadConstraintFlg, enablePodTopologySpreadConstraint, podTopologySpreadConstraintTopologyKeyFlg, podTopologySpreadConstraintTopologyKey, podTopologySpreadConstraintMaxSkewFlg, podTopologySpreadConstraintMaxSkew, podTopologySpreadConstraintMinDomainsFlg, podTopologySpreadConstraintMinDomains, @@ -570,6 +582,10 @@ func main() { WalGExporterCPULimit: walGExporterCPULimit, WalGExporterMemoryLimit: walGExporterMemoryLimit, SpiloCpuRequestsPercentage: spiloCpuRequestsPercentage, + EnableTopologySpreadConstraints: enablePodTopologySpreadConstraint, + TscKey: podTopologySpreadConstraintTopologyKey, + TscMaxSkew: podTopologySpreadConstraintMaxSkew, + TscMinDomains: podTopologySpreadConstraintMinDomains, }).SetupWithManager(ctrlPlaneClusterMgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Postgres") os.Exit(1) From 312a533dfc5747225fcf3c4127ce74a2ac344502 Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Mon, 24 Aug 2026 08:19:00 +0200 Subject: [PATCH 2/2] fix ToUnstructuredZalandoPostgresql signature --- api/v1/postgres_types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/v1/postgres_types.go b/api/v1/postgres_types.go index e012f5f7..2c64ef6b 100644 --- a/api/v1/postgres_types.go +++ b/api/v1/postgres_types.go @@ -680,7 +680,7 @@ func (p *Postgres) ToPeripheralResourceLookupKey() types.NamespacedName { } } -func (p *Postgres) ToUnstructuredZalandoPostgresql(z *zalando.Postgresql, c *corev1.ConfigMap, sc string, pgParamBlockList map[string]bool, rbs *BackupConfig, srcDB *Postgres, patroniTTL, patroniLoopWait, patroniRetryTimeout uint32, dboIsSuperuser bool, enableTlsCert bool, image string, cpuRequestsPercentage int) (*unstructured.Unstructured, error) { +func (p *Postgres) ToUnstructuredZalandoPostgresql(z *zalando.Postgresql, c *corev1.ConfigMap, sc string, pgParamBlockList map[string]bool, rbs *BackupConfig, srcDB *Postgres, patroniTTL, patroniLoopWait, patroniRetryTimeout uint32, dboIsSuperuser bool, enableTlsCert bool, image string, cpuRequestsPercentage int, tscEnable bool, tscKey string, tscMaxSkew, tscMinDomains int32) (*unstructured.Unstructured, error) { if z == nil { z = &zalando.Postgresql{} }