Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions helm/kagent/templates/NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,24 @@ DOCUMENTATION:

Ensure migrations are applied out-of-band before installing or upgrading.
{{- end }}
{{- range $component := (list "controller" "ui") }}
{{- $cfg := index $.Values $component }}
{{- $pdb := $cfg.pdb | default dict }}
{{- if and $pdb.enabled (include "kagent.pdb.isSet" $pdb.minAvailable) (le (int $cfg.replicas) (int $pdb.minAvailable)) }}
################################################################################
{{ printf "# WARNING: %s PODDISRUPTIONBUDGET WILL BLOCK NODE DRAINS" (upper $component) | printf "%-79s" }}#
################################################################################
{{ $component }}.pdb.minAvailable is {{ $pdb.minAvailable }} but {{ $component }}.replicas is {{ $cfg.replicas }}.
The budget can never be satisfied while a pod is evicted, so every voluntary
eviction is refused and node drains and cluster upgrades will hang.

Either raise the replica count:
{{ $component }}.replicas={{ add (int $pdb.minAvailable) 1 }}
or switch to maxUnavailable:
{{ $component }}.pdb.minAvailable=null {{ $component }}.pdb.maxUnavailable=1

{{ end }}
{{- end }}
{{- if .Values.substrate.enabled }}
################################################################################
# WARNING: SUBSTRATE IS EXPERIMENTAL, USE AT OWN RISK #
Expand Down
38 changes: 38 additions & 0 deletions helm/kagent/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,44 @@ Guards on the rbac block
{{- end -}}
{{- end -}}

{{/*
Returns "1" when a PodDisruptionBudget threshold is explicitly set, empty otherwise.

Uses `kindIs "invalid"` rather than `default ""` so that an explicit `0` counts as
set: Helm's `default` treats 0 as empty, which would silently drop a
`maxUnavailable: 0` budget and render a manifest the user never asked for.
An empty string is also treated as unset, so `minAvailable: ""` disables the field.
*/}}
{{- define "kagent.pdb.isSet" -}}
{{- if not (kindIs "invalid" .) -}}
{{- if ne (toString .) "" -}}1{{- end -}}
{{- end -}}
{{- end -}}

{{/*
Guards on a component `pdb` block.

Kubernetes rejects a PodDisruptionBudget that sets both `minAvailable` and
`maxUnavailable`, and a budget that sets neither is meaningless, so both cases
fail at template time with a message naming the offending values path rather
than surfacing later as an opaque API server error.

Call with a dict: (dict "pdb" .Values.controller.pdb "path" "controller.pdb")
*/}}
{{- define "kagent.pdb.validate" -}}
{{- $pdb := .pdb | default dict -}}
{{- if $pdb.enabled -}}
{{- $hasMin := include "kagent.pdb.isSet" $pdb.minAvailable -}}
{{- $hasMax := include "kagent.pdb.isSet" $pdb.maxUnavailable -}}
{{- if and $hasMin $hasMax -}}
{{- fail (printf "%s: minAvailable and maxUnavailable are mutually exclusive. Set exactly one (to use minAvailable, set %s.maxUnavailable=null)." .path .path) -}}
{{- end -}}
{{- if not (or $hasMin $hasMax) -}}
{{- fail (printf "%s is enabled but neither minAvailable nor maxUnavailable is set. Set exactly one." .path) -}}
{{- end -}}
{{- end -}}
{{- end -}}

{{/*
UI selector labels
*/}}
Expand Down
32 changes: 32 additions & 0 deletions helm/kagent/templates/controller-pdb.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{{- $pdb := .Values.controller.pdb | default dict }}
{{- include "kagent.pdb.validate" (dict "pdb" $pdb "path" "controller.pdb") }}
{{- if $pdb.enabled }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ include "kagent.fullname" . }}-controller
namespace: {{ include "kagent.namespace" . }}
{{- with $pdb.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
labels:
{{- include "kagent.controller.labels" . | nindent 4 }}
{{- with $pdb.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if include "kagent.pdb.isSet" $pdb.minAvailable }}
minAvailable: {{ $pdb.minAvailable }}
{{- else }}
maxUnavailable: {{ $pdb.maxUnavailable }}
{{- end }}
{{- with $pdb.unhealthyPodEvictionPolicy }}
unhealthyPodEvictionPolicy: {{ . }}
{{- end }}
selector:
matchLabels:
{{- /* Same helper the controller Deployment uses for spec.selector, so the
budget can never drift from the pods it is meant to protect. */}}
{{- include "kagent.controller.selectorLabels" . | nindent 6 }}
{{- end }}
32 changes: 32 additions & 0 deletions helm/kagent/templates/ui-pdb.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{{- $pdb := .Values.ui.pdb | default dict }}
{{- include "kagent.pdb.validate" (dict "pdb" $pdb "path" "ui.pdb") }}
{{- if $pdb.enabled }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ include "kagent.fullname" . }}-ui
namespace: {{ include "kagent.namespace" . }}
{{- with $pdb.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
labels:
{{- include "kagent.ui.labels" . | nindent 4 }}
{{- with $pdb.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if include "kagent.pdb.isSet" $pdb.minAvailable }}
minAvailable: {{ $pdb.minAvailable }}
{{- else }}
maxUnavailable: {{ $pdb.maxUnavailable }}
{{- end }}
{{- with $pdb.unhealthyPodEvictionPolicy }}
unhealthyPodEvictionPolicy: {{ . }}
{{- end }}
selector:
matchLabels:
{{- /* Same helper the UI Deployment uses for spec.selector, so the budget
can never drift from the pods it is meant to protect. */}}
{{- include "kagent.ui.selectorLabels" . | nindent 6 }}
{{- end }}
218 changes: 218 additions & 0 deletions helm/kagent/tests/pdb_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
suite: test controller and UI PodDisruptionBudgets
templates:
- controller-pdb.yaml
- ui-pdb.yaml
tests:
- it: should not render a controller PDB by default
template: controller-pdb.yaml
asserts:
- hasDocuments:
count: 0

- it: should not render a UI PDB by default
template: ui-pdb.yaml
asserts:
- hasDocuments:
count: 0

- it: should render the controller PDB with maxUnavailable when enabled
template: controller-pdb.yaml
set:
controller:
pdb:
enabled: true
asserts:
- hasDocuments:
count: 1
- isKind:
of: PodDisruptionBudget
- isAPIVersion:
of: policy/v1
- equal:
path: metadata.name
value: RELEASE-NAME-controller
- equal:
path: spec.maxUnavailable
value: 1
- notExists:
path: spec.minAvailable
- notExists:
path: spec.unhealthyPodEvictionPolicy

- it: should render the UI PDB with maxUnavailable when enabled
template: ui-pdb.yaml
set:
ui:
pdb:
enabled: true
asserts:
- hasDocuments:
count: 1
- equal:
path: metadata.name
value: RELEASE-NAME-ui
- equal:
path: spec.maxUnavailable
value: 1

- it: should render minAvailable when maxUnavailable is cleared
template: controller-pdb.yaml
set:
controller:
replicas: 3
pdb:
enabled: true
minAvailable: 2
maxUnavailable: null
asserts:
- equal:
path: spec.minAvailable
value: 2
- notExists:
path: spec.maxUnavailable

# Helm's `default` treats 0 as empty, so an explicit 0 must still be honoured
# rather than silently falling through to the other threshold.
- it: should honour an explicit maxUnavailable of 0
template: controller-pdb.yaml
set:
controller:
pdb:
enabled: true
maxUnavailable: 0
asserts:
- equal:
path: spec.maxUnavailable
value: 0
- notExists:
path: spec.minAvailable

- it: should honour an explicit minAvailable of 0
template: controller-pdb.yaml
set:
controller:
pdb:
enabled: true
minAvailable: 0
maxUnavailable: null
asserts:
- equal:
path: spec.minAvailable
value: 0
- notExists:
path: spec.maxUnavailable

- it: should support percentage thresholds
template: ui-pdb.yaml
set:
ui:
pdb:
enabled: true
minAvailable: 50%
maxUnavailable: null
asserts:
- equal:
path: spec.minAvailable
value: 50%

- it: should render unhealthyPodEvictionPolicy when set
template: controller-pdb.yaml
set:
controller:
pdb:
enabled: true
unhealthyPodEvictionPolicy: AlwaysAllow
asserts:
- equal:
path: spec.unhealthyPodEvictionPolicy
value: AlwaysAllow

# The budget selector must match the Deployment selector exactly, otherwise it
# protects nothing. Both are rendered from kagent.<component>.selectorLabels.
- it: should select the controller pods via the component selector labels
template: controller-pdb.yaml
set:
controller:
pdb:
enabled: true
asserts:
- equal:
path: spec.selector.matchLabels
value:
app.kubernetes.io/name: kagent
app.kubernetes.io/instance: RELEASE-NAME
app.kubernetes.io/component: controller

- it: should select the UI pods via the component selector labels
template: ui-pdb.yaml
set:
ui:
pdb:
enabled: true
asserts:
- equal:
path: spec.selector.matchLabels
value:
app.kubernetes.io/name: kagent
app.kubernetes.io/instance: RELEASE-NAME
app.kubernetes.io/component: ui

- it: should merge extra labels and annotations into the controller PDB
template: controller-pdb.yaml
set:
controller:
pdb:
enabled: true
labels:
team: platform
annotations:
owner: sre
asserts:
- equal:
path: metadata.labels.team
value: platform
- equal:
path: metadata.annotations.owner
value: sre
# chart labels survive the merge
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: controller

- it: should fail when both minAvailable and maxUnavailable are set
template: controller-pdb.yaml
set:
controller:
pdb:
enabled: true
minAvailable: 1
maxUnavailable: 1
asserts:
- failedTemplate:
errorMessage: "controller.pdb: minAvailable and maxUnavailable are mutually exclusive. Set exactly one (to use minAvailable, set controller.pdb.maxUnavailable=null)."

- it: should fail when neither minAvailable nor maxUnavailable is set
template: ui-pdb.yaml
set:
ui:
pdb:
enabled: true
minAvailable: null
maxUnavailable: null
asserts:
- failedTemplate:
errorMessage: "ui.pdb is enabled but neither minAvailable nor maxUnavailable is set. Set exactly one."

# A disabled block is never validated, so a half-configured pdb block that is
# switched off must not break rendering.
- it: should not validate a disabled pdb block
template: controller-pdb.yaml
set:
controller:
pdb:
enabled: false
minAvailable: 1
maxUnavailable: 1
asserts:
- hasDocuments:
count: 0
Loading
Loading