Validate Helm Charts / Resource quantities (pull_request) Successful in 20s
Validate Helm Charts / Selector match (pull_request) Successful in 14s
Validate Helm Charts / Detect changed charts (pull_request) Successful in 1m20s
Validate Helm Charts / Build stalwart (pull_request) Successful in 21s
Validate Helm Charts / Trivy config stalwart (pull_request) Successful in 42s
Install-test / Install-test stalwart (pull_request) Successful in 4m31s
Validate Helm Charts / Install-test (pull_request) Successful in 4m55s
A two-replica node group was redundancy on paper: nothing kept both pods off one node, and no PodDisruptionBudget kept a drain from taking both at once. Both objects are now rendered per node group, gated on that group's COMPUTED replica count being >= 2. The distribution is a promise, not an effort: maxSkew 1 on kubernetes.io/hostname with whenUnsatisfiable DoNotSchedule, selecting only this group's own pods, so an "mx" and a "full" pod may share a node. The selector comes from the chart (stalwart.groupLabels), never from the operator - a spread constraint whose selector matches no pod is trivially satisfied, distributes nothing and reports nothing. nodeAffinityPolicy: Ignore is set explicitly, and that is the load-bearing detail. Its API default is Honor, under which a narrowing nodeSelector shrinks the set of counted domains until both replicas fit into one: skew 0, both pods co-located, no Pending, no event, and a selector that is correct - so no check can see it. With Ignore the same narrowing gives skew 2 and the second pod is Pending. A render abort was the alternative and would also have forbidden a nodeSelector spanning five nodes, which is legal. A user entry colliding with the chart's own on (topologyKey, whenUnsatisfiable) aborts the render naming both values, because the API rejects the duplicate with a message about a list index and an apply-based path merges the two over patchMergeKey: topologyKey silently, dropping one selector. Minor rather than major, proved with a diff rather than argued: with the shipped defaults (full: 1, mx: 0) and with ci/stalwart/test-values.yaml, helm template renders byte-for-byte what 1.0.1+upv0.16.24 rendered - both diffs empty. New in CI, over every chart on every PR: ci/run-selector-match-test.sh holds every rendered PodDisruptionBudget selector and every topologySpreadConstraints labelSelector against the labels of the pod templates in the same render. Helm itself is the YAML parser (ci/yaml2json), because the runners have neither pyyaml nor yq and a hand-written parser could read nothing and report nothing. podAffinity/podAntiAffinity are out of scope; matchExpressions-only selectors are reported as not checked rather than counted as passing. Two anti-vacuum guards: a chart with an overlay under ci/selector-match/ that renders no selector fails, and a run that checks no selector at all fails. Fixes #96
63 lines
3.3 KiB
YAML
63 lines
3.3 KiB
YAML
{{- /*
|
|
ONE PodDisruptionBudget PER NODE GROUP, and that is the whole difference from
|
|
the precedent. keycloak.pdb.yaml (Gitea #83, README guideline 11) set the
|
|
convention this file follows; what it did not have to deal with is a chart whose
|
|
replica count is a property of a GROUP rather than of the release. So the
|
|
condition is evaluated per group, and with full: 2 / mx: 1 exactly one object is
|
|
rendered.
|
|
|
|
WHY IT EXISTS: with two replicas in a group a node drain is otherwise free to
|
|
take both at once - and a drain is the most common reason a pod moves, because
|
|
it is what a node upgrade does. Without this object the second replica buys
|
|
nothing against the failure it was added for. The chart's topology spread
|
|
constraint and this budget are two halves of one answer: the constraint decides
|
|
that the two pods sit on different nodes, the budget decides that only one of
|
|
those nodes may be emptied at a time.
|
|
|
|
minAvailable rather than maxUnavailable, as in keycloak: the promise wanted is
|
|
about the service - at least one node of this group keeps answering throughout
|
|
the drain - and not about the size of the disruption.
|
|
|
|
AND WHY IT IS NOT RENDERED BELOW TWO EFFECTIVE REPLICAS. This is the trap that
|
|
makes a PDB worth a condition rather than an unconditional object, quoted from
|
|
the precedent because it has not changed: minAvailable: 1 with exactly ONE
|
|
replica forbids every voluntary eviction, so `kubectl drain` blocks forever on a
|
|
pod nothing is allowed to move - and the message then names the PDB and not the
|
|
replica count, which sends the reader to the wrong file. The same holds for
|
|
scaleDown: true, where the group has no pods at all and the object would only
|
|
describe a promise about nothing.
|
|
|
|
The count is therefore the COMPUTED one (stalwart.replicas), not the raw
|
|
nodes.<group>.replicas, which is what makes scaleDown: true remove the budget
|
|
rather than leave it behind. NOTES.txt prints, per group, which of the cases
|
|
applied, so an operator does not have to infer it from `kubectl get pdb`.
|
|
|
|
THE SELECTOR COMES FROM stalwart.groupLabels, the same definition the pod
|
|
template's labels come from. A PDB whose selector matches no pod does not fail:
|
|
it reports its arithmetic over zero pods, never blocks an eviction, and a drain
|
|
takes the whole group while the object sits there looking like protection.
|
|
ci/run-selector-match-test.sh holds this selector against the rendered pod
|
|
templates on every pull request for exactly that reason.
|
|
*/ -}}
|
|
{{- $root := . -}}
|
|
{{- range $group, $spec := fromYaml (include "stalwart.nodes" .) }}
|
|
{{- $name := printf "%s-%s" (include "stalwart.fullname" $root) $group }}
|
|
{{- $replicas := int (include "stalwart.replicas" (dict "scaleDown" $root.Values.scaleDown "replicas" $spec.replicas)) }}
|
|
{{- $settings := fromYaml (include "stalwart.groupSettings" (dict "group" $group "given" $spec)) }}
|
|
{{- if and $settings.podDisruptionBudget.enabled (ge $replicas 2) }}
|
|
---
|
|
apiVersion: policy/v1
|
|
kind: PodDisruptionBudget
|
|
metadata:
|
|
name: {{ $name }}-pdb
|
|
labels:
|
|
app: {{ $name }}-sfs
|
|
{{- include "stalwart.groupLabels" (dict "root" $root "group" $group) | nindent 4 }}
|
|
spec:
|
|
minAvailable: {{ $settings.podDisruptionBudget.minAvailable | int64 }}
|
|
selector:
|
|
matchLabels:
|
|
{{- include "stalwart.groupLabels" (dict "root" $root "group" $group) | nindent 6 }}
|
|
{{- end }}
|
|
{{- end }}
|