This is an automated email from the ASF dual-hosted git repository.

villebro pushed a commit to branch main
in repository 
https://gitbox.apache.org/repos/asf/superset-kubernetes-operator.git


The following commit(s) were added to refs/heads/main by this push:
     new b86c836  feat(helm): add podDisruptionBudget value for operator 
Deployment (#386)
b86c836 is described below

commit b86c8364879b27663209cc92dc0811772eb6896d
Author: Younsung Lee <[email protected]>
AuthorDate: Sat Sep 26 00:48:53 2026 +0900

    feat(helm): add podDisruptionBudget value for operator Deployment (#386)
    
    * feat(helm): add podDisruptionBudget value for operator Deployment
    
    Render an optional PodDisruptionBudget for the manager pod, disabled by
    default. When enabled without a bound it defaults to maxUnavailable: 1 so
    single-replica installs never block node drains. minAvailable and
    maxUnavailable are mutually exclusive and fail rendering when both are set.
    unhealthyPodEvictionPolicy is exposed as its own knob.
    
    The full-options extraManifests object example switches from a PDB to a
    NetworkPolicy, since two PDBs selecting the same pods make the Eviction API
    reject evictions.
    
    Signed-off-by: younsl <[email protected]>
    
    * docs: add podDisruptionBudget changelog entry
    
    Signed-off-by: younsl <[email protected]>
    
    * feat(helm): reject PDB bounds that block node drains, document HA
    
    Address review feedback:
    
https://github.com/apache/superset-kubernetes-operator/pull/386#pullrequestreview-5308811823
    
    Reject budgets that allow zero voluntary evictions. With replicas: 1, an
    explicit minAvailable: 1 or maxUnavailable: 0 left disruptionsAllowed at
    0 and hung kubectl drain, so the "never blocks node drains" claim only
    held for the unset fallback. Rendering now fails for:
    
    - integer minAvailable >= replicas (minAvailable: 0 stays allowed)
    - maxUnavailable of 0 or 0%
    - percentage minAvailable that rounds up to every replica
      (ceil(p * replicas / 100) >= replicas)
    
    The operator chart has no HPA, so replicas is static, and the disruption
    controller rounds both bounds up against it. That makes percentage bounds
    exactly checkable at render time as well. The rounding model was confirmed
    against status.disruptionsAllowed on a kind cluster.
    
    Document the HA model in a new "High availability" section of the
    installation guide: replicas are active/standby standbys for failover,
    leader election must stay enabled, failover takes about one 15s lease
    duration because LeaderElectionReleaseOnCancel is not enabled, replicas
    should be spread across nodes, and the PDB is only useful with
    replicas >= 2. The podDisruptionBudget.enabled value description notes
    the replicas >= 2 requirement, and the changelog entry is corrected.
    
    The replicas > 1 without leader election guard is split into #390.
    
    Signed-off-by: younsl <[email protected]>
    
    ---------
    
    Signed-off-by: younsl <[email protected]>
---
 charts/superset-operator/README.md                 |   4 +
 .../templates/poddisruptionbudget.yaml             |  64 ++++++++++
 .../tests/__snapshot__/full_options_test.yaml.snap |  40 ++++--
 .../tests/optout_and_helpers_test.yaml             | 142 +++++++++++++++++++++
 .../tests/values/full-options.yaml                 |  18 ++-
 charts/superset-operator/values.schema.json        |  26 ++++
 charts/superset-operator/values.yaml               |  10 ++
 docs/reference/releases.md                         |   1 +
 docs/user-guide/installation.md                    |  24 ++++
 9 files changed, 315 insertions(+), 14 deletions(-)

diff --git a/charts/superset-operator/README.md 
b/charts/superset-operator/README.md
index 1cd38a2..6416365 100644
--- a/charts/superset-operator/README.md
+++ b/charts/superset-operator/README.md
@@ -73,6 +73,10 @@ The chart values schema intentionally allows undeclared 
top-level keys so shared
 | metrics.serviceMonitor.tlsConfig | object | `{"insecureSkipVerify":true}` | 
TLS configuration for the ServiceMonitor scrape. The default trusts any 
certificate, matching the operator's built-in self-signed flow. Override to 
scrape over verified TLS. |
 | nodeSelector | object | `{}` | Node selector for the manager pod. |
 | podAnnotations | object | `{}` | Extra annotations to add to the manager 
pod. |
+| podDisruptionBudget.enabled | bool | `false` | Create a PodDisruptionBudget 
for the manager pod. Only useful with `replicas >= 2`. |
+| podDisruptionBudget.maxUnavailable | intOrString | `nil` | Maximum 
unavailable pods (integer or percentage). Defaults to `1` when neither is set. |
+| podDisruptionBudget.minAvailable | intOrString | `nil` | Minimum available 
pods (integer or percentage). Mutually exclusive with `maxUnavailable`. |
+| podDisruptionBudget.unhealthyPodEvictionPolicy | string | `""` | Unhealthy 
pod eviction policy (`IfHealthyBudget` or `AlwaysAllow`). Empty uses the 
Kubernetes default. |
 | podLabels | object | `{}` | Extra labels to add to the manager pod. |
 | podSecurityContext | object | 
`{"runAsNonRoot":true,"seccompProfile":{"type":"RuntimeDefault"}}` | Pod-level 
security context for the manager pod. See 
[corev1.PodSecurityContext](https://pkg.go.dev/k8s.io/api/core/v1#PodSecurityContext).
 |
 | replicas | int | `1` | Number of operator manager replicas. |
diff --git a/charts/superset-operator/templates/poddisruptionbudget.yaml 
b/charts/superset-operator/templates/poddisruptionbudget.yaml
new file mode 100644
index 0000000..84fa442
--- /dev/null
+++ b/charts/superset-operator/templates/poddisruptionbudget.yaml
@@ -0,0 +1,64 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+{{- with .Values.podDisruptionBudget }}
+{{- if .enabled }}
+{{- $hasMin := not (kindIs "invalid" .minAvailable) }}
+{{- $hasMax := not (kindIs "invalid" .maxUnavailable) }}
+{{- if and $hasMin $hasMax }}
+{{- fail "podDisruptionBudget: set at most one of minAvailable or 
maxUnavailable" }}
+{{- end }}
+{{- /*
+Reject budgets that allow zero voluntary evictions, which would hang node 
drains.
+replicas is static (no HPA), and the disruption controller rounds percentages 
up
+against it, so both integer and percentage bounds can be checked exactly here.
+*/}}
+{{- $replicas := int $.Values.replicas }}
+{{- if $hasMin }}
+{{- if kindIs "string" .minAvailable }}
+{{- $pct := int (trimSuffix "%" .minAvailable) }}
+{{- if gt (mul $pct $replicas) (mul 100 (sub $replicas 1)) }}
+{{- fail (printf "podDisruptionBudget.minAvailable (%s) leaves no allowed 
disruptions at replicas=%d, so node drains would be blocked" .minAvailable 
$replicas) }}
+{{- end }}
+{{- else if and (gt (int .minAvailable) 0) (ge (int .minAvailable) $replicas) 
}}
+{{- fail (printf "podDisruptionBudget.minAvailable (%d) must be less than 
replicas (%d), otherwise node drains are blocked" (int .minAvailable) 
$replicas) }}
+{{- end }}
+{{- end }}
+{{- if and $hasMax (eq (int (trimSuffix "%" (toString .maxUnavailable))) 0) }}
+{{- fail (printf "podDisruptionBudget.maxUnavailable (%v) blocks all node 
drains" .maxUnavailable) }}
+{{- end }}
+apiVersion: policy/v1
+kind: PodDisruptionBudget
+metadata:
+  name: {{ include "superset-operator.fullname" $ }}
+  namespace: {{ $.Release.Namespace }}
+  labels:
+    {{- include "superset-operator.labels" $ | nindent 4 }}
+spec:
+  {{- if $hasMin }}
+  minAvailable: {{ .minAvailable }}
+  {{- else if $hasMax }}
+  maxUnavailable: {{ .maxUnavailable }}
+  {{- else }}
+  maxUnavailable: 1
+  {{- end }}
+  {{- with .unhealthyPodEvictionPolicy }}
+  unhealthyPodEvictionPolicy: {{ . }}
+  {{- end }}
+  selector:
+    matchLabels:
+      {{- include "superset-operator.selectorLabels" $ | nindent 6 }}
+{{- end }}
+{{- end }}
diff --git 
a/charts/superset-operator/tests/__snapshot__/full_options_test.yaml.snap 
b/charts/superset-operator/tests/__snapshot__/full_options_test.yaml.snap
index a391e22..d81735c 100644
--- a/charts/superset-operator/tests/__snapshot__/full_options_test.yaml.snap
+++ b/charts/superset-operator/tests/__snapshot__/full_options_test.yaml.snap
@@ -134,15 +134,16 @@ renders the full install manifests:
       name: superset-operator-extra
       namespace: my-namespace
   3: |
-    apiVersion: policy/v1
-    kind: PodDisruptionBudget
+    apiVersion: networking.k8s.io/v1
+    kind: NetworkPolicy
     metadata:
-      name: superset-operator-extra-pdb
+      name: superset-operator-extra-netpol
     spec:
-      minAvailable: 1
-      selector:
+      podSelector:
         matchLabels:
           app.kubernetes.io/name: superset-operator
+      policyTypes:
+        - Ingress
   4: |
     apiVersion: rbac.authorization.k8s.io/v1
     kind: Role
@@ -312,6 +313,27 @@ renders the full install manifests:
           app.kubernetes.io/name: superset-operator
           control-plane: controller-manager
   11: |
+    apiVersion: policy/v1
+    kind: PodDisruptionBudget
+    metadata:
+      labels:
+        app.kubernetes.io/instance: my-release
+        app.kubernetes.io/managed-by: Helm
+        app.kubernetes.io/name: superset-operator
+        app.kubernetes.io/version: 1.0.0
+        control-plane: controller-manager
+        helm.sh/chart: superset-operator-1.0.0
+      name: my-release-superset-operator
+      namespace: my-namespace
+    spec:
+      minAvailable: 1
+      selector:
+        matchLabels:
+          app.kubernetes.io/instance: my-release
+          app.kubernetes.io/name: superset-operator
+          control-plane: controller-manager
+      unhealthyPodEvictionPolicy: AlwaysAllow
+  12: |
     apiVersion: rbac.authorization.k8s.io/v1
     kind: Role
     metadata:
@@ -463,7 +485,7 @@ renders the full install manifests:
           - list
           - patch
           - watch
-  12: |
+  13: |
     apiVersion: rbac.authorization.k8s.io/v1
     kind: Role
     metadata:
@@ -615,7 +637,7 @@ renders the full install manifests:
           - list
           - patch
           - watch
-  13: |
+  14: |
     apiVersion: rbac.authorization.k8s.io/v1
     kind: RoleBinding
     metadata:
@@ -636,7 +658,7 @@ renders the full install manifests:
       - kind: ServiceAccount
         name: superset-operator-custom-sa
         namespace: my-namespace
-  14: |
+  15: |
     apiVersion: rbac.authorization.k8s.io/v1
     kind: RoleBinding
     metadata:
@@ -657,7 +679,7 @@ renders the full install manifests:
       - kind: ServiceAccount
         name: superset-operator-custom-sa
         namespace: my-namespace
-  15: |
+  16: |
     apiVersion: v1
     kind: ServiceAccount
     metadata:
diff --git a/charts/superset-operator/tests/optout_and_helpers_test.yaml 
b/charts/superset-operator/tests/optout_and_helpers_test.yaml
index 38cc060..042a26d 100644
--- a/charts/superset-operator/tests/optout_and_helpers_test.yaml
+++ b/charts/superset-operator/tests/optout_and_helpers_test.yaml
@@ -124,3 +124,145 @@ tests:
       - contains:
           path: spec.template.spec.containers[0].args
           content: --leader-elect=false
+
+  # --- podDisruptionBudget ---
+  - it: renders no PDB by default
+    template: templates/poddisruptionbudget.yaml
+    asserts:
+      - hasDocuments:
+          count: 0
+
+  - it: defaults the PDB to maxUnavailable 1 when neither bound is set
+    set:
+      podDisruptionBudget:
+        enabled: true
+    template: templates/poddisruptionbudget.yaml
+    asserts:
+      - equal:
+          path: spec.maxUnavailable
+          value: 1
+      - notExists:
+          path: spec.minAvailable
+      - notExists:
+          path: spec.unhealthyPodEvictionPolicy
+      - equal:
+          path: spec.selector.matchLabels
+          value:
+            app.kubernetes.io/name: superset-operator
+            app.kubernetes.io/instance: my-release
+            control-plane: controller-manager
+
+  - it: renders a percentage maxUnavailable and the eviction policy
+    set:
+      podDisruptionBudget:
+        enabled: true
+        maxUnavailable: 50%
+        unhealthyPodEvictionPolicy: IfHealthyBudget
+    template: templates/poddisruptionbudget.yaml
+    asserts:
+      - equal:
+          path: spec.maxUnavailable
+          value: 50%
+      - equal:
+          path: spec.unhealthyPodEvictionPolicy
+          value: IfHealthyBudget
+
+  - it: fails when both minAvailable and maxUnavailable are set
+    set:
+      podDisruptionBudget:
+        enabled: true
+        minAvailable: 1
+        maxUnavailable: 1
+    template: templates/poddisruptionbudget.yaml
+    asserts:
+      - failedTemplate:
+          errorMessage: "podDisruptionBudget: set at most one of minAvailable 
or maxUnavailable"
+
+  - it: rejects an unknown unhealthyPodEvictionPolicy
+    set:
+      podDisruptionBudget:
+        enabled: true
+        unhealthyPodEvictionPolicy: Never
+    template: templates/poddisruptionbudget.yaml
+    asserts:
+      - failedTemplate:
+          errorPattern: "unhealthyPodEvictionPolicy"
+
+  # --- podDisruptionBudget: reject budgets that block every voluntary 
eviction ---
+  - it: fails when minAvailable equals the single default replica
+    set:
+      podDisruptionBudget:
+        enabled: true
+        minAvailable: 1
+    template: templates/poddisruptionbudget.yaml
+    asserts:
+      - failedTemplate:
+          errorMessage: "podDisruptionBudget.minAvailable (1) must be less 
than replicas (1), otherwise node drains are blocked"
+
+  - it: fails when maxUnavailable is 0
+    set:
+      replicas: 3
+      podDisruptionBudget:
+        enabled: true
+        maxUnavailable: 0
+    template: templates/poddisruptionbudget.yaml
+    asserts:
+      - failedTemplate:
+          errorMessage: "podDisruptionBudget.maxUnavailable (0) blocks all 
node drains"
+
+  - it: fails when maxUnavailable is 0%
+    set:
+      replicas: 3
+      podDisruptionBudget:
+        enabled: true
+        maxUnavailable: 0%
+    template: templates/poddisruptionbudget.yaml
+    asserts:
+      - failedTemplate:
+          errorMessage: "podDisruptionBudget.maxUnavailable (0%) blocks all 
node drains"
+
+  - it: fails when a minAvailable percentage rounds up to every replica
+    set:
+      replicas: 2
+      podDisruptionBudget:
+        enabled: true
+        minAvailable: 51%
+    template: templates/poddisruptionbudget.yaml
+    asserts:
+      - failedTemplate:
+          errorMessage: "podDisruptionBudget.minAvailable (51%) leaves no 
allowed disruptions at replicas=2, so node drains would be blocked"
+
+  - it: renders minAvailable 1 with two replicas
+    set:
+      replicas: 2
+      podDisruptionBudget:
+        enabled: true
+        minAvailable: 1
+    template: templates/poddisruptionbudget.yaml
+    asserts:
+      - equal:
+          path: spec.minAvailable
+          value: 1
+
+  - it: renders a minAvailable percentage that still allows one disruption
+    set:
+      replicas: 2
+      podDisruptionBudget:
+        enabled: true
+        minAvailable: 50%
+    template: templates/poddisruptionbudget.yaml
+    asserts:
+      - equal:
+          path: spec.minAvailable
+          value: 50%
+
+  - it: renders minAvailable 0 on a single replica
+    set:
+      podDisruptionBudget:
+        enabled: true
+        minAvailable: 0
+    template: templates/poddisruptionbudget.yaml
+    asserts:
+      - equal:
+          path: spec.minAvailable
+          value: 0
diff --git a/charts/superset-operator/tests/values/full-options.yaml 
b/charts/superset-operator/tests/values/full-options.yaml
index a9e7334..0fc5e29 100644
--- a/charts/superset-operator/tests/values/full-options.yaml
+++ b/charts/superset-operator/tests/values/full-options.yaml
@@ -124,6 +124,13 @@ topologySpreadConstraints:
       matchLabels:
         control-plane: controller-manager
 
+podDisruptionBudget:
+  enabled: true
+  minAvailable: 1
+  # Mutually exclusive with minAvailable; listed as null so the key is covered.
+  maxUnavailable: ~
+  unhealthyPodEvictionPolicy: AlwaysAllow
+
 podAnnotations:
   prometheus.io/scrape: "false"
 
@@ -147,12 +154,13 @@ extraManifests:
     data:
       hello: world
   # Object form.
-  - apiVersion: policy/v1
-    kind: PodDisruptionBudget
+  - apiVersion: networking.k8s.io/v1
+    kind: NetworkPolicy
     metadata:
-      name: superset-operator-extra-pdb
+      name: superset-operator-extra-netpol
     spec:
-      minAvailable: 1
-      selector:
+      podSelector:
         matchLabels:
           app.kubernetes.io/name: superset-operator
+      policyTypes:
+        - Ingress
diff --git a/charts/superset-operator/values.schema.json 
b/charts/superset-operator/values.schema.json
index 6eebeae..a4c4256 100644
--- a/charts/superset-operator/values.schema.json
+++ b/charts/superset-operator/values.schema.json
@@ -140,6 +140,32 @@
       "description": "Topology spread constraints for the manager pod. See 
https://pkg.go.dev/k8s.io/api/core/v1#TopologySpreadConstraint";,
       "type": "array"
     },
+    "podDisruptionBudget": {
+      "description": "PodDisruptionBudget for the manager pod. See 
https://pkg.go.dev/k8s.io/api/policy/v1#PodDisruptionBudgetSpec";,
+      "type": "object",
+      "additionalProperties": false,
+      "properties": {
+        "enabled": { "type": "boolean" },
+        "minAvailable": {
+          "oneOf": [
+            { "type": "null" },
+            { "type": "integer", "minimum": 0 },
+            { "type": "string", "pattern": "^[0-9]+%$" }
+          ]
+        },
+        "maxUnavailable": {
+          "oneOf": [
+            { "type": "null" },
+            { "type": "integer", "minimum": 0 },
+            { "type": "string", "pattern": "^[0-9]+%$" }
+          ]
+        },
+        "unhealthyPodEvictionPolicy": {
+          "type": "string",
+          "enum": ["", "IfHealthyBudget", "AlwaysAllow"]
+        }
+      }
+    },
     "podAnnotations": { "type": "object" },
     "podLabels": { "type": "object" },
     "watch": {
diff --git a/charts/superset-operator/values.yaml 
b/charts/superset-operator/values.yaml
index 233ca2b..f7f9f2c 100644
--- a/charts/superset-operator/values.yaml
+++ b/charts/superset-operator/values.yaml
@@ -119,6 +119,16 @@ affinity: {}
 # See 
[corev1.TopologySpreadConstraint](https://pkg.go.dev/k8s.io/api/core/v1#TopologySpreadConstraint).
 topologySpreadConstraints: []
 
+podDisruptionBudget:
+  # -- Create a PodDisruptionBudget for the manager pod. Only useful with 
`replicas >= 2`.
+  enabled: false
+  # -- (intOrString) Minimum available pods (integer or percentage). Mutually 
exclusive with `maxUnavailable`.
+  minAvailable: ~
+  # -- (intOrString) Maximum unavailable pods (integer or percentage). 
Defaults to `1` when neither is set.
+  maxUnavailable: ~
+  # -- Unhealthy pod eviction policy (`IfHealthyBudget` or `AlwaysAllow`). 
Empty uses the Kubernetes default.
+  unhealthyPodEvictionPolicy: ""
+
 # -- Extra annotations to add to the manager pod.
 podAnnotations: {}
 # -- Extra labels to add to the manager pod.
diff --git a/docs/reference/releases.md b/docs/reference/releases.md
index 4045e97..7179eaa 100644
--- a/docs/reference/releases.md
+++ b/docs/reference/releases.md
@@ -26,6 +26,7 @@ This page tracks notable changes in Apache Superset 
Kubernetes Operator releases
 ### Added
 
 - Structured metastore, Valkey, and lifecycle seed-source connection fields 
can now be sourced individually from Kubernetes Secret keys, allowing 
`Superset` resources to consume provisioner-owned endpoints and credentials 
without copying discovered values or assembling a separate connection URI 
([#369](https://github.com/apache/superset-kubernetes-operator/pull/369)).
+- **Helm `podDisruptionBudget`.** The Helm chart now exposes a 
`podDisruptionBudget` value that renders a 
[PodDisruptionBudget](https://kubernetes.io/docs/concepts/workloads/pods/disruptions/#pod-disruption-budgets)
 for the operator manager pods. It is disabled by default and falls back to 
`maxUnavailable: 1` when no bound is set. Rendering fails for integer or 
percentage bounds that would leave zero allowed disruptions at the configured 
`replicas` (for example `minAvailable: 1` with one [...]
 
 ### Changed
 
diff --git a/docs/user-guide/installation.md b/docs/user-guide/installation.md
index 66a0779..bcb4a03 100644
--- a/docs/user-guide/installation.md
+++ b/docs/user-guide/installation.md
@@ -101,6 +101,30 @@ Kustomize users can opt in via the bundled 
`watch-namespace` component — uncom
 
 See [Install Scope](../reference/security.md#install-scope) in the security 
reference for the trust-model context.
 
+### High availability
+
+Running more than one manager replica gives you failover, not horizontal 
scaling. Replicas are active/standby: only the replica holding the [leader 
election 
Lease](https://kubernetes.io/docs/concepts/architecture/leases/#leader-election)
 reconciles, and the others wait to take over. Keep `leaderElection.enabled: 
true` whenever `replicas` is greater than 1; without it, every replica 
reconciles the same `Superset` resources and they race on Jobs, drains, and 
status.
+
+- **Failover time.** A standby takes over roughly one lease duration (about 15 
seconds) after the leader stops renewing, because the manager does not release 
the Lease on shutdown.
+- **Spread replicas across nodes** with 
[`topologySpreadConstraints`](https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/)
 or 
[`affinity`](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity),
 so a single node failure or drain does not take out every replica.
+- **Enable `podDisruptionBudget` only with `replicas >= 2`.** It renders a 
[PodDisruptionBudget](https://kubernetes.io/docs/concepts/workloads/pods/disruptions/#pod-disruption-budgets)
 for the manager pods. The chart rejects bounds that would leave zero allowed 
disruptions at the configured replica count (for example `minAvailable: 1` with 
one replica, or `maxUnavailable: 0`), since those would hang [`kubectl 
drain`](https://kubernetes.io/docs/tasks/administer-cluster/safely-drain-node/) 
[...]
+
+```yaml
+replicas: 2
+leaderElection:
+  enabled: true
+topologySpreadConstraints:
+  - maxSkew: 1
+    topologyKey: kubernetes.io/hostname
+    whenUnsatisfiable: ScheduleAnyway
+    labelSelector:
+      matchLabels:
+        app.kubernetes.io/name: superset-operator
+podDisruptionBudget:
+  enabled: true
+  maxUnavailable: 1
+```
+
 ## 2. Create secrets
 
 Superset requires a secret key for session signing. In production, mount it as 
an environment variable:

Reply via email to