From 6d68d448943e88fc9d61ba3e49f0244c24729741 Mon Sep 17 00:00:00 2001 From: Markus Pesch Date: Sun, 4 Oct 2026 00:38:34 +0200 Subject: [PATCH] feat(cloudnativePG): support topologySpreadConstraints A HA `Cluster` is only actually highly available if its instances end up in different failure domains. Pod anti-affinity alone cannot express that: it only keeps the instances off the same node, not off the same zone, and `cloudnativePG.affinity` has no way to encode a skew budget. Without `topologySpreadConstraints` a three instance cluster can legitimately be scheduled into a single availability zone, where a zone outage takes down the primary and both replicas at once. Both `affinity` and `topologySpreadConstraints` are rendered only when they are set. The CloudNativePG API declares them as `omitempty`, so an empty `{}` or `[]` in the manifest would show up as a permanent diff against the state the operator writes back. Tolerations deliberately do not get a dedicated value. CloudNativePG has no `spec.tolerations`, it nests them below `spec.affinity.tolerations`, so they are configured through `cloudnativePG.affinity` like `nodeSelector` and the anti-affinity settings. A separate top-level value would only have to be folded back into the same place and would silently conflict with a tolerations list set via `cloudnativePG.affinity`. Co-authored-by: Copilot --- README.md | 5 +-- templates/cluster.yaml | 8 ++++- unittests/helm/cluster/basics.yaml | 51 ++++++++++++++++++++++++++++++ values.yaml | 13 +++++--- 4 files changed, 69 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 4f83951..ebf7658 100644 --- a/README.md +++ b/README.md @@ -1387,10 +1387,11 @@ To comply with the Gitea helm chart definition of the digest parameter, a "custo | `cloudnativePG.walStorage.enabled` | Store the write-ahead log on a dedicated volume. | `false` | | `cloudnativePG.walStorage.size` | Size of the write-ahead log volume. | `2Gi` | | `cloudnativePG.walStorage.storageClass` | Storage class of the write-ahead log volume. Defaults to the cluster's default storage class. | `""` | -| `cloudnativePG.monitoring.enablePodMonitor` | Create a `PodMonitor` for the CloudNativePG `Cluster`. | `false` | -| `cloudnativePG.affinity` | Affinity for the PostgreSQL pods. | `{}` | +| `cloudnativePG.affinity` | Affinity for the PostgreSQL pods. CloudNativePG nests `nodeSelector` and `tolerations` below this key as well. | `{}` | | `cloudnativePG.priorityClassName` | Priority class name for the PostgreSQL pods. | `""` | +| `cloudnativePG.monitoring.enablePodMonitor` | Create a `PodMonitor` for the CloudNativePG `Cluster`. | `false` | | `cloudnativePG.resources` | Kubernetes resources for the PostgreSQL containers. | `{}` | +| `cloudnativePG.topologySpreadConstraints` | Topology spread constraints for the PostgreSQL pods. | `[]` | ### valkey diff --git a/templates/cluster.yaml b/templates/cluster.yaml index 6cea386..335f90e 100644 --- a/templates/cluster.yaml +++ b/templates/cluster.yaml @@ -12,8 +12,10 @@ metadata: name: {{ include "gitea.cluster.cloudnativePG.name" . }} namespace: {{ .Values.namespace | default .Release.Namespace }} spec: + {{- with .Values.cloudnativePG.affinity }} affinity: - {{- toYaml .Values.cloudnativePG.affinity | nindent 4 }} + {{- toYaml . | nindent 4 }} + {{- end }} bootstrap: initdb: database: {{ .Values.cloudnativePG.credentials.database | quote }} @@ -35,6 +37,10 @@ spec: {{- with .Values.cloudnativePG.storage.storageClass }} storageClass: {{ . | quote }} {{- end }} + {{- with .Values.cloudnativePG.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 4 }} + {{- end }} {{- if .Values.cloudnativePG.walStorage.enabled }} walStorage: size: {{ .Values.cloudnativePG.walStorage.size | quote }} diff --git a/unittests/helm/cluster/basics.yaml b/unittests/helm/cluster/basics.yaml index 574c5cf..22e35e9 100644 --- a/unittests/helm/cluster/basics.yaml +++ b/unittests/helm/cluster/basics.yaml @@ -45,6 +45,10 @@ tests: path: spec.storage.storageClass - notExists: path: spec.walStorage + - notExists: + path: spec.affinity + - notExists: + path: spec.topologySpreadConstraints - it: "Render the credential Secret with the default values" template: templates/secret_cloudnativePG.yaml @@ -129,3 +133,50 @@ tests: value: size: "42Mi" storageClass: "my-wal-storage-class" + + - it: "Apply the scheduling customizations" + template: templates/cluster.yaml + set: + cloudnativePG: + enabled: true + affinity: + nodeSelector: + disktype: ssd + podAntiAffinityType: required + tolerations: + - key: my-key + operator: Equal + value: my-value + effect: NoSchedule + priorityClassName: my-priority-class + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: topology.kubernetes.io/zone + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app.kubernetes.io/name: gitea + asserts: + - equal: + path: spec.affinity + value: + nodeSelector: + disktype: ssd + podAntiAffinityType: required + tolerations: + - key: my-key + operator: Equal + value: my-value + effect: NoSchedule + - equal: + path: spec.priorityClassName + value: my-priority-class + - equal: + path: spec.topologySpreadConstraints + value: + - maxSkew: 1 + topologyKey: topology.kubernetes.io/zone + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app.kubernetes.io/name: gitea diff --git a/values.yaml b/values.yaml index 48b1765..8fcf312 100644 --- a/values.yaml +++ b/values.yaml @@ -1178,19 +1178,22 @@ cloudnativePG: size: 2Gi storageClass: "" - ## @param cloudnativePG.monitoring.enablePodMonitor Create a `PodMonitor` for the CloudNativePG `Cluster`. - monitoring: - enablePodMonitor: false - - ## @param cloudnativePG.affinity Affinity for the PostgreSQL pods. + ## @param cloudnativePG.affinity Affinity for the PostgreSQL pods. CloudNativePG nests `nodeSelector` and `tolerations` below this key as well. affinity: {} ## @param cloudnativePG.priorityClassName Priority class name for the PostgreSQL pods. priorityClassName: "" + ## @param cloudnativePG.monitoring.enablePodMonitor Create a `PodMonitor` for the CloudNativePG `Cluster`. + monitoring: + enablePodMonitor: false + ## @param cloudnativePG.resources Kubernetes resources for the PostgreSQL containers. resources: {} + ## @param cloudnativePG.topologySpreadConstraints Topology spread constraints for the PostgreSQL pods. + topologySpreadConstraints: [] + ## @section valkey valkey: