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: