Add topologySpreadConstraints configuration to pod spec. (#2530)

* Add topologySpreadConstraints configuration to pod spec.
* Run update-codegen.sh to add deepcopy for new field to the api.
* Reuse configured TopologySpreadConstraints for logical backup.
* Remove x-kubernetes-preserve-unknown-fields and XPreserveUnknownFields.
* Add topologySpreadConstraint example in the complete manifest.
* Add support for helm chart.
* Add documentation for topologySpreadConstraint.
* Update e2e test to patch topologySpreadConstraints into the postgresqls manifest.
* For e2e test, updated the PVC retention policy to remove redundant PVCs.
* Fix e2e test, expected PVC count in end-to-end test after config changes.
This commit is contained in:
laiminhtrung1997 2026-05-29 22:07:47 +07:00 committed by GitHub
parent 4d40270890
commit e871a167ed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 245 additions and 25 deletions

View File

@ -4055,6 +4055,23 @@ spec:
type: string type: string
type: object type: object
type: array type: array
topologySpreadConstraints:
type: array
nullable: true
items:
type: object
properties:
maxskew:
type: integer
format: int32
minimum: 1
topologyKey:
type: string
whenUnsatisfiable:
type: string
enum:
- DoNotSchedule
- ScheduleAnyway
useLoadBalancer: useLoadBalancer:
description: |- description: |-
deprecated load balancer settings maintained for backward compatibility deprecated load balancer settings maintained for backward compatibility

View File

@ -714,7 +714,7 @@ but Kubernetes will not spin up the pod if the requested HugePages cannot be all
For more information on HugePages in Kubernetes, see also For more information on HugePages in Kubernetes, see also
[https://kubernetes.io/docs/tasks/manage-hugepages/scheduling-hugepages/](https://kubernetes.io/docs/tasks/manage-hugepages/scheduling-hugepages/) [https://kubernetes.io/docs/tasks/manage-hugepages/scheduling-hugepages/](https://kubernetes.io/docs/tasks/manage-hugepages/scheduling-hugepages/)
## Use taints, tolerations and node affinity for dedicated PostgreSQL nodes ## Use taints, tolerations, node affinity and topology spread constraint for dedicated PostgreSQL nodes
To ensure Postgres pods are running on nodes without any other application pods, To ensure Postgres pods are running on nodes without any other application pods,
you can use [taints and tolerations](https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/) you can use [taints and tolerations](https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/)
@ -755,6 +755,23 @@ spec:
If you need to define a `nodeAffinity` for all your Postgres clusters use the If you need to define a `nodeAffinity` for all your Postgres clusters use the
`node_readiness_label` [configuration](administrator.md#node-readiness-labels). `node_readiness_label` [configuration](administrator.md#node-readiness-labels).
If you need PostgreSQL Pods to run on separate nodes, you can use the
[topologySpreadConstraints](https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/) to control how they are distributed across your cluster.
This ensures they are spread among failure domains such as
regions, zones, nodes, or other user-defined topology domains.
```yaml
apiVersion: "acid.zalan.do/v1"
kind: postgresql
metadata:
name: acid-minimal-cluster
spec:
topologySpreadConstraints:
- maxskew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
```
## In-place major version upgrade ## In-place major version upgrade
Starting with Spilo 14, operator supports in-place major version upgrade to a Starting with Spilo 14, operator supports in-place major version upgrade to a
@ -1064,7 +1081,7 @@ spec:
- all - all
volumeSource: volumeSource:
emptyDir: {} emptyDir: {}
sidecars: sidecars:
- name: "container-name" - name: "container-name"
image: "company/image:tag" image: "company/image:tag"
volumeMounts: volumeMounts:

View File

@ -561,7 +561,7 @@ class EndToEndTestCase(unittest.TestCase):
pg_patch_config["spec"]["patroni"]["slots"][slot_to_change]["database"] = "bar" pg_patch_config["spec"]["patroni"]["slots"][slot_to_change]["database"] = "bar"
del pg_patch_config["spec"]["patroni"]["slots"][slot_to_remove] del pg_patch_config["spec"]["patroni"]["slots"][slot_to_remove]
k8s.api.custom_objects_api.patch_namespaced_custom_object( k8s.api.custom_objects_api.patch_namespaced_custom_object(
"acid.zalan.do", "v1", "default", "postgresqls", "acid-minimal-cluster", pg_delete_slot_patch) "acid.zalan.do", "v1", "default", "postgresqls", "acid-minimal-cluster", pg_delete_slot_patch)
@ -578,7 +578,7 @@ class EndToEndTestCase(unittest.TestCase):
self.eventuallyEqual(lambda: self.query_database(leader.metadata.name, "postgres", get_slot_query%("database", slot_to_change))[0], "bar", self.eventuallyEqual(lambda: self.query_database(leader.metadata.name, "postgres", get_slot_query%("database", slot_to_change))[0], "bar",
"The replication slot cannot be updated", 10, 5) "The replication slot cannot be updated", 10, 5)
# make sure slot from Patroni didn't get deleted # make sure slot from Patroni didn't get deleted
self.eventuallyEqual(lambda: len(self.query_database(leader.metadata.name, "postgres", get_slot_query%("slot_name", patroni_slot))), 1, self.eventuallyEqual(lambda: len(self.query_database(leader.metadata.name, "postgres", get_slot_query%("slot_name", patroni_slot))), 1,
"The replication slot from Patroni gets deleted", 10, 5) "The replication slot from Patroni gets deleted", 10, 5)
@ -932,7 +932,7 @@ class EndToEndTestCase(unittest.TestCase):
}, },
} }
} }
old_sts_creation_timestamp = sts.metadata.creation_timestamp old_sts_creation_timestamp = sts.metadata.creation_timestamp
k8s.api.apps_v1.patch_namespaced_stateful_set(sts.metadata.name, sts.metadata.namespace, annotation_patch) k8s.api.apps_v1.patch_namespaced_stateful_set(sts.metadata.name, sts.metadata.namespace, annotation_patch)
old_svc_creation_timestamp = svc.metadata.creation_timestamp old_svc_creation_timestamp = svc.metadata.creation_timestamp
@ -1370,7 +1370,7 @@ class EndToEndTestCase(unittest.TestCase):
} }
k8s.update_config(patch_scaled_policy_retain) k8s.update_config(patch_scaled_policy_retain)
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync") self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
# decrease the number of instances # decrease the number of instances
k8s.api.custom_objects_api.patch_namespaced_custom_object( k8s.api.custom_objects_api.patch_namespaced_custom_object(
'acid.zalan.do', 'v1', 'default', 'postgresqls', 'acid-minimal-cluster', pg_patch_scale_down_instances) 'acid.zalan.do', 'v1', 'default', 'postgresqls', 'acid-minimal-cluster', pg_patch_scale_down_instances)
@ -1647,7 +1647,6 @@ class EndToEndTestCase(unittest.TestCase):
# toggle pod anti affinity to move replica away from master node # toggle pod anti affinity to move replica away from master node
self.assert_distributed_pods(master_nodes) self.assert_distributed_pods(master_nodes)
@timeout_decorator.timeout(TEST_TIMEOUT_SEC) @timeout_decorator.timeout(TEST_TIMEOUT_SEC)
def test_overwrite_pooler_deployment(self): def test_overwrite_pooler_deployment(self):
pooler_name = 'acid-minimal-cluster-pooler' pooler_name = 'acid-minimal-cluster-pooler'
@ -1800,7 +1799,7 @@ class EndToEndTestCase(unittest.TestCase):
}, },
} }
k8s.api.core_v1.patch_namespaced_secret( k8s.api.core_v1.patch_namespaced_secret(
name="foo-user.acid-minimal-cluster.credentials.postgresql.acid.zalan.do", name="foo-user.acid-minimal-cluster.credentials.postgresql.acid.zalan.do",
namespace="default", namespace="default",
body=secret_fake_rotation) body=secret_fake_rotation)
@ -1817,7 +1816,7 @@ class EndToEndTestCase(unittest.TestCase):
"enable_password_rotation": "true", "enable_password_rotation": "true",
"inherited_annotations": "environment", "inherited_annotations": "environment",
"password_rotation_interval": "30", "password_rotation_interval": "30",
"password_rotation_user_retention": "30", # should be set to 60 "password_rotation_user_retention": "30", # should be set to 60
}, },
} }
k8s.update_config(enable_password_rotation) k8s.update_config(enable_password_rotation)
@ -1886,7 +1885,7 @@ class EndToEndTestCase(unittest.TestCase):
self.assertTrue("environment" in db_user_secret.metadata.annotations, "Added annotation was not propagated to secret") self.assertTrue("environment" in db_user_secret.metadata.annotations, "Added annotation was not propagated to secret")
# disable password rotation for all other users (foo_user) # disable password rotation for all other users (foo_user)
# and pick smaller intervals to see if the third fake rotation user is dropped # and pick smaller intervals to see if the third fake rotation user is dropped
enable_password_rotation = { enable_password_rotation = {
"data": { "data": {
"enable_password_rotation": "false", "enable_password_rotation": "false",
@ -2386,6 +2385,90 @@ class EndToEndTestCase(unittest.TestCase):
# toggle pod anti affinity to move replica away from master node # toggle pod anti affinity to move replica away from master node
self.assert_distributed_pods(master_nodes) self.assert_distributed_pods(master_nodes)
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
def test_topology_spread_constraints(self):
'''
Enable topologySpreadConstraints for pods
'''
k8s = self.k8s
cluster_labels = "application=spilo,cluster-name=acid-minimal-cluster"
# Verify we are in good state from potential previous tests
self.eventuallyEqual(lambda: k8s.count_running_pods(), 2, "No 2 pods running")
# patch the pvc retention policy to enable delete when scale down
patch_scaled_policy_delete = {
"data": {
"persistent_volume_claim_retention_policy": "when_deleted:retain,when_scaled:delete"
}
}
k8s.update_config(patch_scaled_policy_delete)
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
master_nodes, replica_nodes = k8s.get_cluster_nodes()
self.assertNotEqual(master_nodes, [])
self.assertNotEqual(replica_nodes, [])
# Patch label to nodes for topologySpreadConstraints
patch_node_label = {
"metadata": {
"labels": {
"topology.kubernetes.io/zone": "zalando"
}
}
}
k8s.api.core_v1.patch_node(master_nodes[0], patch_node_label)
k8s.api.core_v1.patch_node(replica_nodes[0], patch_node_label)
# Patch topologySpreadConstraint and scale-out postgresql pods to postgresqls manifest.
patch_topologySpreadConstraint_config = {
"spec": {
"numberOfInstances": 6,
"topologySpreadConstraint": [
{
"maxskew": 1,
"topologyKey": "topology.kubernetes.io/zone",
"whenUnsatisfiable": "DoNotSchedule"
}
]
}
}
k8s.api.custom_objects_api.patch_namespaced_custom_object(
"acid.zalan.do", "v1", "default",
"postgresqls", "acid-minimal-cluster",
patch_topologySpreadConstraint_config)
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
self.eventuallyEqual(lambda: k8s.count_pods_with_label(cluster_labels), 6, "Postgresql StatefulSet are scale to 6")
self.eventuallyEqual(lambda: k8s.count_running_pods(), 6, "All pods are running")
worker_node_1 = 0
worker_node_2 = 0
pods = k8s.api.core_v1.list_namespaced_pod('default', label_selector=cluster_labels)
for pod in pods.items:
if pod.spec.node_name == 'postgres-operator-e2e-tests-worker':
worker_node_1 += 1
elif pod.spec.node_name == 'postgres-operator-e2e-tests-worker2':
worker_node_2 += 1
self.assertEqual(worker_node_1, worker_node_2)
self.assertEqual(worker_node_1, 3)
self.assertEqual(worker_node_2, 3)
# Reset configurations
patch_topologySpreadConstraint_config = {
"spec": {
"numberOfInstances": 2,
"topologySpreadConstraint": []
}
}
k8s.api.custom_objects_api.patch_namespaced_custom_object(
"acid.zalan.do", "v1", "default",
"postgresqls", "acid-minimal-cluster",
patch_topologySpreadConstraint_config)
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
self.eventuallyEqual(lambda: k8s.count_pods_with_label(cluster_labels), 2, "Postgresql StatefulSet are scale to 2")
self.eventuallyEqual(lambda: k8s.count_running_pods(), 2, "All pods are running")
@timeout_decorator.timeout(TEST_TIMEOUT_SEC) @timeout_decorator.timeout(TEST_TIMEOUT_SEC)
def test_zz_cluster_deletion(self): def test_zz_cluster_deletion(self):
''' '''
@ -2461,7 +2544,7 @@ class EndToEndTestCase(unittest.TestCase):
self.eventuallyEqual(lambda: k8s.count_deployments_with_label(cluster_label), 0, "Deployments not deleted") self.eventuallyEqual(lambda: k8s.count_deployments_with_label(cluster_label), 0, "Deployments not deleted")
self.eventuallyEqual(lambda: k8s.count_pdbs_with_label(cluster_label), 0, "Pod disruption budget not deleted") self.eventuallyEqual(lambda: k8s.count_pdbs_with_label(cluster_label), 0, "Pod disruption budget not deleted")
self.eventuallyEqual(lambda: k8s.count_secrets_with_label(cluster_label), 8, "Secrets were deleted although disabled in config") self.eventuallyEqual(lambda: k8s.count_secrets_with_label(cluster_label), 8, "Secrets were deleted although disabled in config")
self.eventuallyEqual(lambda: k8s.count_pvcs_with_label(cluster_label), 3, "PVCs were deleted although disabled in config") self.eventuallyEqual(lambda: k8s.count_pvcs_with_label(cluster_label), 2, "PVCs were deleted although disabled in config")
except timeout_decorator.TimeoutError: except timeout_decorator.TimeoutError:
print('Operator log: {}'.format(k8s.get_operator_log())) print('Operator log: {}'.format(k8s.get_operator_log()))
@ -2503,7 +2586,7 @@ class EndToEndTestCase(unittest.TestCase):
# if nodes are different we can quit here # if nodes are different we can quit here
if master_nodes[0] not in replica_nodes: if master_nodes[0] not in replica_nodes:
return True return True
# enable pod anti affintiy in config map which should trigger movement of replica # enable pod anti affintiy in config map which should trigger movement of replica
patch_enable_antiaffinity = { patch_enable_antiaffinity = {
@ -2527,7 +2610,7 @@ class EndToEndTestCase(unittest.TestCase):
} }
k8s.update_config(patch_disable_antiaffinity, "disable antiaffinity") k8s.update_config(patch_disable_antiaffinity, "disable antiaffinity")
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync") self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
k8s.wait_for_pod_start('spilo-role=replica,' + cluster_labels) k8s.wait_for_pod_start('spilo-role=replica,' + cluster_labels)
k8s.wait_for_running_pods(cluster_labels, 2) k8s.wait_for_running_pods(cluster_labels, 2)
@ -2538,7 +2621,7 @@ class EndToEndTestCase(unittest.TestCase):
# if nodes are different we can quit here # if nodes are different we can quit here
for target_node in target_nodes: for target_node in target_nodes:
if (target_node not in master_nodes or target_node not in replica_nodes) and master_nodes[0] in replica_nodes: if (target_node not in master_nodes or target_node not in replica_nodes) and master_nodes[0] in replica_nodes:
print('Pods run on the same node') print('Pods run on the same node')
return False return False
except timeout_decorator.TimeoutError: except timeout_decorator.TimeoutError:

View File

@ -232,6 +232,12 @@ spec:
# values: # values:
# - enabled # - enabled
# Add topology spread constraint to distribute PostgreSQL pods across all nodes labeled with "topology.kubernetes.io/zone".
# topologySpreadConstraint:
# - maxSkew: 1
# topologyKey: topology.kubernetes.io/zone
# whenUnsatisfiable: DoNotSchedule
# Enables change data capture streams for defined database tables # Enables change data capture streams for defined database tables
# streams: # streams:
# - applicationId: test-app # - applicationId: test-app

View File

@ -4056,6 +4056,22 @@ spec:
type: string type: string
type: object type: object
type: array type: array
topologySpreadConstraints:
type: array
nullable: true
items:
type: object
properties:
maxSkew:
type: integer
format: int32
topologyKey:
type: string
whenUnsatisfiable:
type: string
enum:
- DoNotSchedule
- ScheduleAnyway
useLoadBalancer: useLoadBalancer:
description: |- description: |-
deprecated load balancer settings maintained for backward compatibility deprecated load balancer settings maintained for backward compatibility

View File

@ -56,6 +56,7 @@ var OperatorConfigCRDResourceColumns = []apiextv1.CustomResourceColumnDefinition
} }
var min1 = 1.0 var min1 = 1.0
var minLength1 int64 = 1
var minDisable = -1.0 var minDisable = -1.0
// OperatorConfigCRDResourceValidation to check applied manifest parameters // OperatorConfigCRDResourceValidation to check applied manifest parameters

View File

@ -92,17 +92,18 @@ type PostgresSpec struct {
Clone *CloneDescription `json:"clone,omitempty"` Clone *CloneDescription `json:"clone,omitempty"`
// Note: usernames specified here as database owners must be declared // Note: usernames specified here as database owners must be declared
// in the users key of the spec key. // in the users key of the spec key.
Databases map[string]string `json:"databases,omitempty"` Databases map[string]string `json:"databases,omitempty"`
PreparedDatabases map[string]PreparedDatabase `json:"preparedDatabases,omitempty"` PreparedDatabases map[string]PreparedDatabase `json:"preparedDatabases,omitempty"`
SchedulerName *string `json:"schedulerName,omitempty"` SchedulerName *string `json:"schedulerName,omitempty"`
NodeAffinity *v1.NodeAffinity `json:"nodeAffinity,omitempty"` NodeAffinity *v1.NodeAffinity `json:"nodeAffinity,omitempty"`
Tolerations []v1.Toleration `json:"tolerations,omitempty"` TopologySpreadConstraints []v1.TopologySpreadConstraint `json:"topologySpreadConstraints,omitempty"`
Sidecars []Sidecar `json:"sidecars,omitempty"` Tolerations []v1.Toleration `json:"tolerations,omitempty"`
InitContainers []v1.Container `json:"initContainers,omitempty"` Sidecars []Sidecar `json:"sidecars,omitempty"`
PodPriorityClassName string `json:"podPriorityClassName,omitempty"` InitContainers []v1.Container `json:"initContainers,omitempty"`
ShmVolume *bool `json:"enableShmVolume,omitempty"` PodPriorityClassName string `json:"podPriorityClassName,omitempty"`
EnableLogicalBackup bool `json:"enableLogicalBackup,omitempty"` ShmVolume *bool `json:"enableShmVolume,omitempty"`
LogicalBackupRetention string `json:"logicalBackupRetention,omitempty"` EnableLogicalBackup bool `json:"enableLogicalBackup,omitempty"`
LogicalBackupRetention string `json:"logicalBackupRetention,omitempty"`
// +kubebuilder:validation:Pattern=`^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$` // +kubebuilder:validation:Pattern=`^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$`
LogicalBackupSchedule string `json:"logicalBackupSchedule,omitempty"` LogicalBackupSchedule string `json:"logicalBackupSchedule,omitempty"`
StandbyCluster *StandbyDescription `json:"standby,omitempty"` StandbyCluster *StandbyDescription `json:"standby,omitempty"`

View File

@ -801,6 +801,13 @@ func (in *PostgresSpec) DeepCopyInto(out *PostgresSpec) {
*out = new(corev1.NodeAffinity) *out = new(corev1.NodeAffinity)
(*in).DeepCopyInto(*out) (*in).DeepCopyInto(*out)
} }
if in.TopologySpreadConstraints != nil {
in, out := &in.TopologySpreadConstraints, &out.TopologySpreadConstraints
*out = make([]corev1.TopologySpreadConstraint, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.Tolerations != nil { if in.Tolerations != nil {
in, out := &in.Tolerations, &out.Tolerations in, out := &in.Tolerations, &out.Tolerations
*out = make([]corev1.Toleration, len(*in)) *out = make([]corev1.Toleration, len(*in))

View File

@ -504,6 +504,11 @@ func (c *Cluster) compareStatefulSetWith(statefulSet *appsv1.StatefulSet) *compa
needsRollUpdate = true needsRollUpdate = true
reasons = append(reasons, "new statefulset's pod affinity does not match the current one") reasons = append(reasons, "new statefulset's pod affinity does not match the current one")
} }
if !reflect.DeepEqual(c.Statefulset.Spec.Template.Spec.TopologySpreadConstraints, statefulSet.Spec.Template.Spec.TopologySpreadConstraints) {
needsReplace = true
needsRollUpdate = true
reasons = append(reasons, "new statefulset's pod topologySpreadConstraints does not match the current one")
}
if len(c.Statefulset.Spec.Template.Spec.Tolerations) != len(statefulSet.Spec.Template.Spec.Tolerations) { if len(c.Statefulset.Spec.Template.Spec.Tolerations) != len(statefulSet.Spec.Template.Spec.Tolerations) {
needsReplace = true needsReplace = true
needsRollUpdate = true needsRollUpdate = true

View File

@ -612,6 +612,13 @@ func generatePodAntiAffinity(podAffinityTerm v1.PodAffinityTerm, preferredDuring
return podAntiAffinity return podAntiAffinity
} }
func generateTopologySpreadConstraints(labels labels.Set, topologySpreadConstraints []v1.TopologySpreadConstraint) []v1.TopologySpreadConstraint {
for _, topologySpreadConstraint := range topologySpreadConstraints {
topologySpreadConstraint.LabelSelector = &metav1.LabelSelector{MatchLabels: labels}
}
return topologySpreadConstraints
}
func tolerations(tolerationsSpec *[]v1.Toleration, podToleration map[string]string) []v1.Toleration { func tolerations(tolerationsSpec *[]v1.Toleration, podToleration map[string]string) []v1.Toleration {
// allow to override tolerations by postgresql manifest // allow to override tolerations by postgresql manifest
if len(*tolerationsSpec) > 0 { if len(*tolerationsSpec) > 0 {
@ -817,6 +824,7 @@ func (c *Cluster) generatePodTemplate(
initContainers []v1.Container, initContainers []v1.Container,
sidecarContainers []v1.Container, sidecarContainers []v1.Container,
sharePgSocketWithSidecars *bool, sharePgSocketWithSidecars *bool,
topologySpreadConstraintsSpec []v1.TopologySpreadConstraint,
tolerationsSpec *[]v1.Toleration, tolerationsSpec *[]v1.Toleration,
nodeAffinity *v1.Affinity, nodeAffinity *v1.Affinity,
schedulerName *string, schedulerName *string,
@ -887,6 +895,8 @@ func (c *Cluster) generatePodTemplate(
podSpec.PriorityClassName = priorityClassName podSpec.PriorityClassName = priorityClassName
} }
podSpec.TopologySpreadConstraints = generateTopologySpreadConstraints(labels, topologySpreadConstraintsSpec)
if sharePgSocketWithSidecars != nil && *sharePgSocketWithSidecars { if sharePgSocketWithSidecars != nil && *sharePgSocketWithSidecars {
addVarRunVolume(&podSpec) addVarRunVolume(&podSpec)
} }
@ -1469,6 +1479,7 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
initContainers, initContainers,
sidecarContainers, sidecarContainers,
c.OpConfig.SharePgSocketWithSidecars, c.OpConfig.SharePgSocketWithSidecars,
spec.TopologySpreadConstraints,
&tolerationSpec, &tolerationSpec,
c.nodeAffinity(c.OpConfig.NodeReadinessLabel, spec.NodeAffinity), c.nodeAffinity(c.OpConfig.NodeReadinessLabel, spec.NodeAffinity),
spec.SchedulerName, spec.SchedulerName,
@ -2346,6 +2357,8 @@ func (c *Cluster) generateLogicalBackupJob() (*batchv1.CronJob, error) {
tolerationsSpec := tolerations(&spec.Tolerations, c.OpConfig.PodToleration) tolerationsSpec := tolerations(&spec.Tolerations, c.OpConfig.PodToleration)
topologySpreadConstraintsSpec := generateTopologySpreadConstraints(labels, spec.TopologySpreadConstraints)
// re-use the method that generates DB pod templates // re-use the method that generates DB pod templates
if podTemplate, err = c.generatePodTemplate( if podTemplate, err = c.generatePodTemplate(
c.Namespace, c.Namespace,
@ -2355,6 +2368,7 @@ func (c *Cluster) generateLogicalBackupJob() (*batchv1.CronJob, error) {
[]v1.Container{}, []v1.Container{},
[]v1.Container{}, []v1.Container{},
util.False(), util.False(),
topologySpreadConstraintsSpec,
&tolerationsSpec, &tolerationsSpec,
c.nodeAffinity(c.OpConfig.NodeReadinessLabel, nil), c.nodeAffinity(c.OpConfig.NodeReadinessLabel, nil),
nil, nil,

View File

@ -4272,3 +4272,56 @@ func TestGenerateCapabilities(t *testing.T) {
} }
} }
} }
func TestTopologySpreadConstraints(t *testing.T) {
clusterName := "acid-test-cluster"
namespace := "default"
labelSelector := &metav1.LabelSelector{
MatchLabels: cluster.labelsSet(true),
}
pg := acidv1.Postgresql{
ObjectMeta: metav1.ObjectMeta{
Name: clusterName,
Namespace: namespace,
},
Spec: acidv1.PostgresSpec{
NumberOfInstances: 1,
Resources: &acidv1.Resources{
ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("10")},
ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("10")},
},
Volume: acidv1.Volume{
Size: "1G",
},
TopologySpreadConstraints: []v1.TopologySpreadConstraint{
{
MaxSkew: 1,
TopologyKey: "topology.kubernetes.io/zone",
WhenUnsatisfiable: v1.DoNotSchedule,
LabelSelector: labelSelector,
},
},
},
}
cluster := New(
Config{
OpConfig: config.Config{
PodManagementPolicy: "ordered_ready",
},
}, k8sutil.KubernetesClient{}, acidv1.Postgresql{}, logger, eventRecorder)
cluster.Name = clusterName
cluster.Namespace = namespace
cluster.labelsSet(true)
s, err := cluster.generateStatefulSet(&pg.Spec)
assert.NoError(t, err)
assert.Contains(t, s.Spec.Template.Spec.TopologySpreadConstraints, v1.TopologySpreadConstraint{
MaxSkew: int32(1),
TopologyKey: "topology.kubernetes.io/zone",
WhenUnsatisfiable: v1.DoNotSchedule,
LabelSelector: labelSelector,
},
)
}