diff --git a/helm/kagent-tools/templates/NOTES.txt b/helm/kagent-tools/templates/NOTES.txt index 2876aa1..1c14345 100644 --- a/helm/kagent-tools/templates/NOTES.txt +++ b/helm/kagent-tools/templates/NOTES.txt @@ -7,4 +7,8 @@ # # 2. Then visit http://127.0.0.1:{{.Values.service.ports.tools.port}}/mcp to use MCP # -############################################################################################### \ No newline at end of file +###############################################################################################{{- if and (.Values.podDisruptionBudget).enabled (not (include "kagent-tools.pdb.shouldRender" .)) }} + +WARNING: podDisruptionBudget.enabled is true but replicaCount is {{ .Values.replicaCount }}. +The PodDisruptionBudget was not created; set replicaCount > 1 to enable it. +{{- end }} diff --git a/helm/kagent-tools/templates/_helpers.tpl b/helm/kagent-tools/templates/_helpers.tpl index f98bd5a..cb46072 100644 --- a/helm/kagent-tools/templates/_helpers.tpl +++ b/helm/kagent-tools/templates/_helpers.tpl @@ -120,6 +120,45 @@ over it would brick an install the value was never about. {{- end -}} {{- end -}} +{{/* +Prints "1" when a PDB budget value is set. null and "" mean unset; 0 and +percentage strings are valid budgets and must be kept. +*/}} +{{- define "kagent-tools.pdb.isSet" -}} +{{- if not (kindIs "invalid" .) -}} +{{- if ne (toString .) "" -}}1{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Kubernetes rejects a PodDisruptionBudget that sets both minAvailable and +maxUnavailable, and one that sets neither is meaningless, so both fail at +template time. Runs whenever the PDB is enabled, even if the replica guard +skips rendering, so misconfiguration surfaces before replicas are scaled up. +*/}} +{{- define "kagent-tools.pdb.validate" -}} +{{- $pdb := .Values.podDisruptionBudget | default dict -}} +{{- if $pdb.enabled -}} +{{- $hasMinAvailable := include "kagent-tools.pdb.isSet" $pdb.minAvailable -}} +{{- $hasMaxUnavailable := include "kagent-tools.pdb.isSet" $pdb.maxUnavailable -}} +{{- if and $hasMinAvailable $hasMaxUnavailable -}} +{{- fail "podDisruptionBudget: minAvailable and maxUnavailable are mutually exclusive. Set exactly one (to use minAvailable, set podDisruptionBudget.maxUnavailable=null)." -}} +{{- end -}} +{{- if not (or $hasMinAvailable $hasMaxUnavailable) -}} +{{- fail "podDisruptionBudget is enabled but neither minAvailable nor maxUnavailable is set. Set exactly one." -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +A PodDisruptionBudget is only rendered for more than one replica: with a single +replica it either protects nothing or blocks node drains entirely. +*/}} +{{- define "kagent-tools.pdb.shouldRender" -}} +{{- $pdb := .Values.podDisruptionBudget | default dict -}} +{{- if and $pdb.enabled (gt (int .Values.replicaCount) 1) -}}1{{- end -}} +{{- end -}} + {{/* Pull secrets for the pod: the chart's own list merged (union) with global.imagePullSecrets. Renders nothing when both are empty. diff --git a/helm/kagent-tools/templates/deployment.yaml b/helm/kagent-tools/templates/deployment.yaml index 5a81ca9..715acab 100644 --- a/helm/kagent-tools/templates/deployment.yaml +++ b/helm/kagent-tools/templates/deployment.yaml @@ -5,6 +5,10 @@ metadata: namespace: {{ include "kagent-tools.namespace" . }} labels: {{- include "kagent-tools.labels" . | nindent 4 }} + {{- with .Values.deploymentAnnotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} spec: replicas: {{ .Values.replicaCount }} selector: diff --git a/helm/kagent-tools/templates/pdb.yaml b/helm/kagent-tools/templates/pdb.yaml new file mode 100644 index 0000000..0ff5484 --- /dev/null +++ b/helm/kagent-tools/templates/pdb.yaml @@ -0,0 +1,31 @@ +{{- include "kagent-tools.pdb.validate" . }} +{{- if include "kagent-tools.pdb.shouldRender" . }} +{{- $pdb := .Values.podDisruptionBudget }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "kagent-tools.fullname" . }} + namespace: {{ include "kagent-tools.namespace" . }} + labels: + {{- include "kagent-tools.labels" . | nindent 4 }} + {{- with $pdb.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with $pdb.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if include "kagent-tools.pdb.isSet" $pdb.minAvailable }} + minAvailable: {{ $pdb.minAvailable }} + {{- else }} + maxUnavailable: {{ $pdb.maxUnavailable }} + {{- end }} + {{- with $pdb.unhealthyPodEvictionPolicy }} + unhealthyPodEvictionPolicy: {{ . }} + {{- end }} + selector: + matchLabels: + {{- /* Same helper as the Deployment's spec.selector, so the budget cannot drift from its pods. */}} + {{- include "kagent-tools.selectorLabels" . | nindent 6 }} +{{- end }} diff --git a/helm/kagent-tools/tests/deployment_test.yaml b/helm/kagent-tools/tests/deployment_test.yaml index 84bdaae..5f3060f 100644 --- a/helm/kagent-tools/tests/deployment_test.yaml +++ b/helm/kagent-tools/tests/deployment_test.yaml @@ -173,3 +173,23 @@ tests: value: app.kubernetes.io/name: RELEASE-NAME app.kubernetes.io/instance: RELEASE-NAME + + - it: should not have deployment annotations by default + template: deployment.yaml + asserts: + - isNull: + path: metadata.annotations + + - it: should render custom deployment annotations + template: deployment.yaml + set: + deploymentAnnotations: + keel.sh/policy: "force" + example.com/annotation: "test-value" + asserts: + - equal: + path: metadata.annotations["keel.sh/policy"] + value: "force" + - equal: + path: metadata.annotations["example.com/annotation"] + value: "test-value" diff --git a/helm/kagent-tools/tests/pdb_test.yaml b/helm/kagent-tools/tests/pdb_test.yaml new file mode 100644 index 0000000..b2d8885 --- /dev/null +++ b/helm/kagent-tools/tests/pdb_test.yaml @@ -0,0 +1,151 @@ +suite: test pod disruption budget +templates: + - pdb.yaml +tests: + - it: should not render pdb by default + asserts: + - hasDocuments: + count: 0 + + - it: should render default maxUnavailable of 1 when enabled with multiple replicas + set: + replicaCount: 2 + podDisruptionBudget: + enabled: true + asserts: + - isKind: + of: PodDisruptionBudget + - equal: + path: metadata.name + value: RELEASE-NAME + - equal: + path: spec.maxUnavailable + value: 1 + - notExists: + path: spec.minAvailable + - notExists: + path: spec.unhealthyPodEvictionPolicy + - equal: + path: spec.selector.matchLabels + value: + app.kubernetes.io/name: RELEASE-NAME + app.kubernetes.io/instance: RELEASE-NAME + + - it: should not render pdb with a single replica + set: + replicaCount: 1 + podDisruptionBudget: + enabled: true + asserts: + - hasDocuments: + count: 0 + + - it: should still validate pdb settings with a single replica + set: + replicaCount: 1 + podDisruptionBudget: + enabled: true + minAvailable: 1 + asserts: + - failedTemplate: + errorMessage: "podDisruptionBudget: minAvailable and maxUnavailable are mutually exclusive. Set exactly one (to use minAvailable, set podDisruptionBudget.maxUnavailable=null)." + + - it: should render minAvailable when maxUnavailable is nulled + set: + replicaCount: 3 + podDisruptionBudget: + enabled: true + minAvailable: "50%" + maxUnavailable: null + asserts: + - equal: + path: spec.minAvailable + value: "50%" + - notExists: + path: spec.maxUnavailable + + - it: should render minAvailable of zero + set: + replicaCount: 2 + podDisruptionBudget: + enabled: true + minAvailable: 0 + maxUnavailable: null + asserts: + - equal: + path: spec.minAvailable + value: 0 + - notExists: + path: spec.maxUnavailable + + - it: should render maxUnavailable of zero + set: + replicaCount: 2 + podDisruptionBudget: + enabled: true + maxUnavailable: 0 + asserts: + - equal: + path: spec.maxUnavailable + value: 0 + - notExists: + path: spec.minAvailable + + - it: should treat empty string budgets as unset + set: + replicaCount: 2 + podDisruptionBudget: + enabled: true + minAvailable: "" + maxUnavailable: "25%" + asserts: + - equal: + path: spec.maxUnavailable + value: "25%" + - notExists: + path: spec.minAvailable + + - it: should render eviction policy, labels and annotations + set: + replicaCount: 2 + podDisruptionBudget: + enabled: true + unhealthyPodEvictionPolicy: AlwaysAllow + labels: + custom-label: pdb-label + annotations: + custom-annotation: pdb-annotation + asserts: + - equal: + path: spec.unhealthyPodEvictionPolicy + value: AlwaysAllow + - equal: + path: metadata.labels["custom-label"] + value: pdb-label + - equal: + path: metadata.labels["app.kubernetes.io/name"] + value: RELEASE-NAME + - equal: + path: metadata.annotations["custom-annotation"] + value: pdb-annotation + + - it: should fail when both minAvailable and maxUnavailable are set + set: + replicaCount: 2 + podDisruptionBudget: + enabled: true + minAvailable: 1 + maxUnavailable: 1 + asserts: + - failedTemplate: + errorMessage: "podDisruptionBudget: minAvailable and maxUnavailable are mutually exclusive. Set exactly one (to use minAvailable, set podDisruptionBudget.maxUnavailable=null)." + + - it: should fail when neither minAvailable nor maxUnavailable is set + set: + replicaCount: 2 + podDisruptionBudget: + enabled: true + maxUnavailable: null + asserts: + - failedTemplate: + errorMessage: "podDisruptionBudget is enabled but neither minAvailable nor maxUnavailable is set. Set exactly one." diff --git a/helm/kagent-tools/values.yaml b/helm/kagent-tools/values.yaml index 45755f3..7b519e9 100644 --- a/helm/kagent-tools/values.yaml +++ b/helm/kagent-tools/values.yaml @@ -78,6 +78,9 @@ imagePullSecrets: [] nameOverride: "" fullnameOverride: "" +# Annotations to add to the deployment +deploymentAnnotations: {} + podAnnotations: {} podSecurityContext: {} @@ -90,7 +93,28 @@ securityContext: {} # readOnlyRootFilesystem: true # runAsNonRoot: true # runAsUser: 1000 - + +# PodDisruptionBudget for the tools Deployment. +# Only rendered when replicaCount > 1: with a single replica a PDB either +# protects nothing or blocks node drains entirely. +podDisruptionBudget: + # -- Set to true to create the PodDisruptionBudget. + enabled: false + # -- Minimum number of pods that must remain available. Int or percentage + # string (e.g. `1` or `"50%"`). Mutually exclusive with `maxUnavailable`; + # to use it, also set `maxUnavailable: null`. + minAvailable: null + # -- Maximum number of pods that may be unavailable. Int or percentage + # string (e.g. `1` or `"50%"`). Mutually exclusive with `minAvailable`. + maxUnavailable: 1 + # -- `spec.unhealthyPodEvictionPolicy`: `IfHealthyBudget` (Kubernetes + # default) or `AlwaysAllow`. Unset leaves the Kubernetes default. + unhealthyPodEvictionPolicy: "" + # -- Extra labels for the PodDisruptionBudget. + labels: {} + # -- Annotations for the PodDisruptionBudget. + annotations: {} + # Node taints which will be tolerated for Pod. tolerations: []