From 2846d5f3d5625f3bed4c4a2dd8c1ac9cc9fc26f5 Mon Sep 17 00:00:00 2001 From: Alexander <60811310+NoobCoder1209@users.noreply.github.com> Date: Sun, 7 Jun 2026 18:16:28 +0300 Subject: [PATCH 1/3] feat: add helm test, CI matrix, and 3 GitHub workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - templates/tests/test-connection.yaml: helm.sh/hook test pod that curls the Service via wget and matches HTTP/.. 200. Uses distinct labels (component=test) so it does NOT match the chart's NetworkPolicy podSelector — without this the policy blocks the test pod's egress to the Service. - ci/{minimal,full,externalsecrets}-values.yaml: matrix files that exercise default install, full optional-resource install, and ESO-rendered (lint-only) install. - .github/workflows/lint.yml: helm lint + helm template | kubeconform + kube-linter against every ci/*-values.yaml. Pinned tool versions. - .github/workflows/smoke.yml: kind 1.28.13 matrix. Install path runs helm install --wait, kubectl wait, in-cluster curl probe, and helm test. Lint-only path runs helm lint + kubeconform with ESO CRDs skipped. Diagnostics step on failure. - .github/workflows/release.yml: triggers on tags v*. Verifies tag matches Chart.yaml version, then helm package + helm push to oci://ghcr.io//charts. Owner lower-cased for OCI compliance. - pdb.yaml: set unhealthyPodEvictionPolicy=AlwaysAllow (kube-linter gate; safe default for stateless services). - values.yaml: default soft pod-anti-affinity by hostname (replicas spread across nodes when possible) — also satisfies kube-linter's no-anti-affinity gate. - test-connection pod: cpu/memory requests+limits (kube-linter gate). Verified end-to-end on a local kind 1.28 cluster: - helm install minimal-values, full-values: clean - in-cluster curl returns "hello from helm-chart-template" - helm test (default + full-values): Succeeded in 6s - kube-linter clean across all 3 matrix files - kubeconform clean across all 3 matrix files (ESO Skip on the externalsecrets file as expected) --- .github/workflows/lint.yml | 79 +++++++++++++++++++ .github/workflows/release.yml | 51 +++++++++++++ .github/workflows/smoke.yml | 109 +++++++++++++++++++++++++++ ci/externalsecrets-values.yaml | 22 ++++++ ci/full-values.yaml | 51 +++++++++++++ ci/minimal-values.yaml | 1 + templates/pdb.yaml | 1 + templates/tests/test-connection.yaml | 51 +++++++++++++ values.schema.json | 6 +- values.yaml | 17 ++++- 10 files changed, 386 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/smoke.yml create mode 100644 ci/externalsecrets-values.yaml create mode 100644 ci/full-values.yaml create mode 100644 ci/minimal-values.yaml create mode 100644 templates/tests/test-connection.yaml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..531c835 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,79 @@ +name: lint + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + lint: + name: helm lint + kubeconform + kube-linter + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Helm + uses: azure/setup-helm@v4 + with: + version: v3.16.3 + + - name: Install kubeconform + run: | + set -euo pipefail + KUBECONFORM_VERSION=v0.6.7 + curl -sSLo kubeconform.tar.gz \ + "https://github.com/yannh/kubeconform/releases/download/${KUBECONFORM_VERSION}/kubeconform-linux-amd64.tar.gz" + tar -xzf kubeconform.tar.gz + sudo mv kubeconform /usr/local/bin/ + kubeconform -v + + - name: Install kube-linter + run: | + set -euo pipefail + KUBE_LINTER_VERSION=v0.7.2 + curl -sSLo kube-linter.tar.gz \ + "https://github.com/stackrox/kube-linter/releases/download/${KUBE_LINTER_VERSION}/kube-linter-linux.tar.gz" + tar -xzf kube-linter.tar.gz + sudo mv kube-linter /usr/local/bin/ + kube-linter version + + - name: Helm lint (default values) + run: helm lint . + + - name: Helm lint (matrix values) + run: | + set -euo pipefail + for f in ci/*-values.yaml; do + echo "::group::helm lint $f" + helm lint . -f "$f" + echo "::endgroup::" + done + + - name: Helm template + kubeconform + run: | + set -euo pipefail + for f in ci/*-values.yaml; do + echo "::group::kubeconform $f" + # ESO CRDs are not part of the upstream Kubernetes schema set; skip them. + helm template release . -f "$f" \ + | kubeconform \ + -strict \ + -ignore-missing-schemas \ + -skip ExternalSecret,SecretStore,ClusterSecretStore \ + -kubernetes-version 1.28.0 \ + -summary + echo "::endgroup::" + done + + - name: kube-linter + run: | + set -euo pipefail + for f in ci/*-values.yaml; do + echo "::group::kube-linter $f" + helm template release . -f "$f" | kube-linter lint - + echo "::endgroup::" + done diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..cce8663 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,51 @@ +name: release + +on: + push: + tags: + - "v*" + +permissions: + contents: read + packages: write + +jobs: + release: + name: Package and publish chart to GHCR (OCI) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Helm + uses: azure/setup-helm@v4 + with: + version: v3.16.3 + + - name: Verify tag matches Chart.yaml version + run: | + set -euo pipefail + TAG="${GITHUB_REF_NAME#v}" + CHART_VERSION=$(awk '/^version:/ {print $2; exit}' Chart.yaml | tr -d '"') + if [ "$TAG" != "$CHART_VERSION" ]; then + echo "::error::Tag $GITHUB_REF_NAME (stripped: $TAG) does not match Chart.yaml version $CHART_VERSION" + exit 1 + fi + echo "Tag $GITHUB_REF_NAME matches Chart.yaml version $CHART_VERSION." + + - name: Log in to GHCR + run: | + echo "${{ secrets.GITHUB_TOKEN }}" \ + | helm registry login ghcr.io --username "${{ github.actor }}" --password-stdin + + - name: Helm package + run: helm package . --destination .helm-package + + - name: Helm push to GHCR + run: | + set -euo pipefail + PKG=$(ls .helm-package/*.tgz | head -1) + OWNER_LC=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') + helm push "$PKG" "oci://ghcr.io/${OWNER_LC}/charts" diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml new file mode 100644 index 0000000..3983caa --- /dev/null +++ b/.github/workflows/smoke.yml @@ -0,0 +1,109 @@ +name: smoke + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + smoke: + name: kind smoke (${{ matrix.values }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - values: minimal-values + mode: install + - values: full-values + mode: install + - values: externalsecrets-values + mode: lint-only + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Helm + uses: azure/setup-helm@v4 + with: + version: v3.16.3 + + # ---------- lint-only path (ESO CRDs not in vanilla kind) ---------- + - name: Install kubeconform (lint-only) + if: matrix.mode == 'lint-only' + run: | + set -euo pipefail + KUBECONFORM_VERSION=v0.6.7 + curl -sSLo kubeconform.tar.gz \ + "https://github.com/yannh/kubeconform/releases/download/${KUBECONFORM_VERSION}/kubeconform-linux-amd64.tar.gz" + tar -xzf kubeconform.tar.gz + sudo mv kubeconform /usr/local/bin/ + + - name: Helm lint (lint-only) + if: matrix.mode == 'lint-only' + run: helm lint . -f ci/${{ matrix.values }}.yaml + + - name: Helm template + kubeconform (lint-only) + if: matrix.mode == 'lint-only' + run: | + helm template release . -f ci/${{ matrix.values }}.yaml \ + | kubeconform \ + -strict \ + -ignore-missing-schemas \ + -skip ExternalSecret,SecretStore,ClusterSecretStore \ + -kubernetes-version 1.28.0 \ + -summary + + # ---------- install path ---------- + - name: Create kind cluster + if: matrix.mode == 'install' + uses: helm/kind-action@v1 + with: + version: v0.24.0 + node_image: kindest/node:v1.28.13 + cluster_name: smoke + wait: 120s + + - name: Helm install + if: matrix.mode == 'install' + run: | + helm install http-echo . \ + -f ci/${{ matrix.values }}.yaml \ + --namespace smoke --create-namespace \ + --wait --timeout 5m + + - name: Wait for Deployment availability + if: matrix.mode == 'install' + run: | + kubectl -n smoke get deploy + kubectl -n smoke wait \ + --for=condition=Available \ + --timeout=180s \ + deploy -l app.kubernetes.io/instance=http-echo + + - name: In-cluster curl probe + if: matrix.mode == 'install' + run: | + set -euo pipefail + OUT=$(kubectl -n smoke run curl-probe \ + --image=curlimages/curl:8.10.1 \ + --restart=Never \ + --rm -i \ + --command -- curl -fsS --max-time 10 "http://http-echo.smoke.svc.cluster.local/") + echo "Got response: $OUT" + echo "$OUT" | grep -q "hello from helm-chart-template" + + - name: Helm test + if: matrix.mode == 'install' + run: helm test http-echo --namespace smoke --logs + + - name: Diagnostics on failure + if: failure() && matrix.mode == 'install' + run: | + kubectl -n smoke get all + kubectl -n smoke describe pods + kubectl -n smoke logs -l app.kubernetes.io/instance=http-echo --tail=200 || true + kubectl get events -n smoke --sort-by=.lastTimestamp || true diff --git a/ci/externalsecrets-values.yaml b/ci/externalsecrets-values.yaml new file mode 100644 index 0000000..16f5811 --- /dev/null +++ b/ci/externalsecrets-values.yaml @@ -0,0 +1,22 @@ +# ExternalSecrets: lint-only path. Vanilla kind clusters lack the ESO CRDs, so the +# CI smoke job runs `helm lint` + `helm template | kubeconform -skip ExternalSecret` +# against this file rather than `helm install`. +externalSecrets: + enabled: true + refreshInterval: 30m + secretStoreRef: + kind: ClusterSecretStore + name: example-cluster-store + creationPolicy: Owner + remoteRefs: + - secretKey: API_TOKEN + remoteRef: + key: prod/http-echo + property: api_token + dataFrom: + - extract: + key: prod/http-echo/bulk + +# Plain Secret stays disabled — the chart enforces mutual exclusion at template time. +secret: + enabled: false diff --git a/ci/full-values.yaml b/ci/full-values.yaml new file mode 100644 index 0000000..e6cbd32 --- /dev/null +++ b/ci/full-values.yaml @@ -0,0 +1,51 @@ +# Full: exercises every optional path the chart can render, except ESO (covered separately). +# Note: HPA + replicaCount=3 is intentional — HPA wins (the Deployment omits replicas:), +# but replicaCount is preserved as a sensible fallback if autoscaling is later disabled. +replicaCount: 3 + +ingress: + enabled: true + className: nginx + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / + hosts: + - host: chart-example.local + paths: + - path: / + pathType: Prefix + +autoscaling: + enabled: true + minReplicas: 2 + maxReplicas: 5 + targetCPUUtilizationPercentage: 70 + +podDisruptionBudget: + enabled: true + maxUnavailable: 1 + +configMap: + enabled: true + data: + LOG_LEVEL: debug + GREETING: hello + +secret: + enabled: true + stringData: + API_TOKEN: not-a-real-token + +networkPolicy: + enabled: true + +serviceAccount: + create: true + annotations: + eks.amazonaws.com/role-arn: arn:aws:iam::000000000000:role/example + +podAnnotations: + example.com/scrape: "true" + +env: + - name: EXTRA_VAR + value: extra diff --git a/ci/minimal-values.yaml b/ci/minimal-values.yaml new file mode 100644 index 0000000..13efc3c --- /dev/null +++ b/ci/minimal-values.yaml @@ -0,0 +1 @@ +# Minimal: pure defaults from values.yaml. Asserts the chart installs with no overrides. diff --git a/templates/pdb.yaml b/templates/pdb.yaml index fdd462a..66d1018 100644 --- a/templates/pdb.yaml +++ b/templates/pdb.yaml @@ -19,6 +19,7 @@ spec: {{- else if not (kindIs "invalid" .Values.podDisruptionBudget.minAvailable) }} minAvailable: {{ .Values.podDisruptionBudget.minAvailable }} {{- end }} + unhealthyPodEvictionPolicy: {{ .Values.podDisruptionBudget.unhealthyPodEvictionPolicy }} selector: matchLabels: {{- include "http-echo.selectorLabels" . | nindent 6 }} diff --git a/templates/tests/test-connection.yaml b/templates/tests/test-connection.yaml new file mode 100644 index 0000000..7abd413 --- /dev/null +++ b/templates/tests/test-connection.yaml @@ -0,0 +1,51 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "http-echo.fullname" . }}-test-connection" + labels: + helm.sh/chart: {{ include "http-echo.chart" . }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- if .Chart.AppVersion }} + app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} + {{- end }} + # Deliberately NOT using selectorLabels — those would make this pod match + # the chart's NetworkPolicy podSelector, blocking its own egress to the + # service. + app.kubernetes.io/name: {{ include "http-echo.name" . }}-test + app.kubernetes.io/instance: {{ .Release.Name }}-test + app.kubernetes.io/component: test + annotations: + "helm.sh/hook": test + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +spec: + restartPolicy: Never + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + seccompProfile: + type: RuntimeDefault + containers: + - name: wget + image: busybox:1.36 + command: ["/bin/sh", "-c"] + args: + - >- + wget --spider -S "http://{{ include "http-echo.fullname" . }}:{{ .Values.service.port }}/" 2>&1 + | grep -q "HTTP/.* 200" + resources: + requests: + cpu: 10m + memory: 16Mi + limits: + cpu: 50m + memory: 32Mi + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 65532 + capabilities: + drop: ["ALL"] + seccompProfile: + type: RuntimeDefault diff --git a/values.schema.json b/values.schema.json index 597e201..04abb7c 100644 --- a/values.schema.json +++ b/values.schema.json @@ -149,7 +149,11 @@ "properties": { "enabled": { "type": "boolean" }, "maxUnavailable": { "type": ["integer", "string", "null"] }, - "minAvailable": { "type": ["integer", "string", "null"] } + "minAvailable": { "type": ["integer", "string", "null"] }, + "unhealthyPodEvictionPolicy": { + "type": "string", + "enum": ["AlwaysAllow", "IfHealthyBudget"] + } }, "allOf": [ { diff --git a/values.yaml b/values.yaml index 2784b2b..73c3072 100644 --- a/values.yaml +++ b/values.yaml @@ -145,6 +145,10 @@ podDisruptionBudget: maxUnavailable: 1 # -- Minimum available pods. Mutually exclusive with maxUnavailable. minAvailable: null + # -- Eviction policy for unhealthy pods. AlwaysAllow lets unhealthy pods be + # evicted regardless of the budget; IfHealthyBudget blocks until the budget + # is met. AlwaysAllow is the safe default for stateless services. + unhealthyPodEvictionPolicy: AlwaysAllow # Non-secret env. Disabled by default; flip to true and add `data:` entries to # mount a ConfigMap via envFrom on the container. Empty data + enabled=true @@ -210,7 +214,18 @@ networkPolicy: nodeSelector: {} tolerations: [] -affinity: {} +# Default soft pod-anti-affinity by hostname spreads replicas across nodes. +# Override with `affinity: {}` to disable, or replace with required affinity +# for a stricter policy. +affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/name: http-echo + topologyKey: kubernetes.io/hostname topologySpreadConstraints: [] # -- Extra environment variables on the container (beyond envFrom). From 8579a9d6beb0d3ad7bd7a27640ed953f77865cfb Mon Sep 17 00:00:00 2001 From: Alexander <60811310+NoobCoder1209@users.noreply.github.com> Date: Sun, 7 Jun 2026 18:23:01 +0300 Subject: [PATCH 2/3] fix: address phase/3-tests review findings Major: - values.yaml + _helpers.tpl + deployment.yaml: default pod-anti-affinity now uses the chart's selectorLabels (via a new http-echo.defaultAffinity helper) instead of a hardcoded app.kubernetes.io/name. nameOverride now works correctly. values default reverts to affinity: {} so an empty value triggers the synthesized rule; a populated value replaces entirely. - smoke.yml: dropped redundant in-cluster curl probe + redundant kubectl wait. helm test already exercises connectivity from inside the cluster and is the source of truth. Minor: - test-connection.yaml: wget gets -T 5 -t 2 so failures surface in ~15s instead of busybox's default ~2 min retry loop. - pdb.yaml: wrap unhealthyPodEvictionPolicy in `with` so users who set it to null don't render an empty value. - smoke.yml: dropped the lint-only externalsecrets matrix entry (lint.yml already covers it on every PR; removing the duplicate saves CI minutes). Nits: - release.yml: dropped fetch-depth: 0 (helm package doesn't need history); added set -euo pipefail to the GHCR login step for consistency. - ci/full-values.yaml + ci/externalsecrets-values.yaml: renamed the CI test-fixture key from API_TOKEN to DEMO_VALUE so it doesn't trip credential scanners; added a clarifying comment. Verified locally: - helm lint . clean across all 3 matrix files - kubeconform clean across all 3 - kube-linter clean across all 3 - Default affinity renders synthesized rule with correct selectorLabels - nameOverride=foo: synthesized rule's selectorLabels follow (name: foo) - User-supplied affinity replaces synthesized rule entirely --- .github/workflows/release.yml | 3 +- .github/workflows/smoke.yml | 61 +--------------------------- ci/externalsecrets-values.yaml | 5 ++- ci/full-values.yaml | 3 +- templates/_helpers.tpl | 16 ++++++++ templates/deployment.yaml | 7 +++- templates/pdb.yaml | 4 +- templates/tests/test-connection.yaml | 2 +- values.yaml | 18 +++----- 9 files changed, 39 insertions(+), 80 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cce8663..8eac2c6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,8 +16,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Set up Helm uses: azure/setup-helm@v4 @@ -37,6 +35,7 @@ jobs: - name: Log in to GHCR run: | + set -euo pipefail echo "${{ secrets.GITHUB_TOKEN }}" \ | helm registry login ghcr.io --username "${{ github.actor }}" --password-stdin diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 3983caa..8a836dd 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -15,13 +15,7 @@ jobs: strategy: fail-fast: false matrix: - include: - - values: minimal-values - mode: install - - values: full-values - mode: install - - values: externalsecrets-values - mode: lint-only + values: [minimal-values, full-values] steps: - name: Checkout uses: actions/checkout@v4 @@ -31,35 +25,7 @@ jobs: with: version: v3.16.3 - # ---------- lint-only path (ESO CRDs not in vanilla kind) ---------- - - name: Install kubeconform (lint-only) - if: matrix.mode == 'lint-only' - run: | - set -euo pipefail - KUBECONFORM_VERSION=v0.6.7 - curl -sSLo kubeconform.tar.gz \ - "https://github.com/yannh/kubeconform/releases/download/${KUBECONFORM_VERSION}/kubeconform-linux-amd64.tar.gz" - tar -xzf kubeconform.tar.gz - sudo mv kubeconform /usr/local/bin/ - - - name: Helm lint (lint-only) - if: matrix.mode == 'lint-only' - run: helm lint . -f ci/${{ matrix.values }}.yaml - - - name: Helm template + kubeconform (lint-only) - if: matrix.mode == 'lint-only' - run: | - helm template release . -f ci/${{ matrix.values }}.yaml \ - | kubeconform \ - -strict \ - -ignore-missing-schemas \ - -skip ExternalSecret,SecretStore,ClusterSecretStore \ - -kubernetes-version 1.28.0 \ - -summary - - # ---------- install path ---------- - name: Create kind cluster - if: matrix.mode == 'install' uses: helm/kind-action@v1 with: version: v0.24.0 @@ -68,40 +34,17 @@ jobs: wait: 120s - name: Helm install - if: matrix.mode == 'install' run: | helm install http-echo . \ -f ci/${{ matrix.values }}.yaml \ --namespace smoke --create-namespace \ --wait --timeout 5m - - name: Wait for Deployment availability - if: matrix.mode == 'install' - run: | - kubectl -n smoke get deploy - kubectl -n smoke wait \ - --for=condition=Available \ - --timeout=180s \ - deploy -l app.kubernetes.io/instance=http-echo - - - name: In-cluster curl probe - if: matrix.mode == 'install' - run: | - set -euo pipefail - OUT=$(kubectl -n smoke run curl-probe \ - --image=curlimages/curl:8.10.1 \ - --restart=Never \ - --rm -i \ - --command -- curl -fsS --max-time 10 "http://http-echo.smoke.svc.cluster.local/") - echo "Got response: $OUT" - echo "$OUT" | grep -q "hello from helm-chart-template" - - name: Helm test - if: matrix.mode == 'install' run: helm test http-echo --namespace smoke --logs - name: Diagnostics on failure - if: failure() && matrix.mode == 'install' + if: failure() run: | kubectl -n smoke get all kubectl -n smoke describe pods diff --git a/ci/externalsecrets-values.yaml b/ci/externalsecrets-values.yaml index 16f5811..459f93f 100644 --- a/ci/externalsecrets-values.yaml +++ b/ci/externalsecrets-values.yaml @@ -9,10 +9,11 @@ externalSecrets: name: example-cluster-store creationPolicy: Owner remoteRefs: - - secretKey: API_TOKEN + # CI test fixture; not a credential. + - secretKey: DEMO_VALUE remoteRef: key: prod/http-echo - property: api_token + property: demo_value dataFrom: - extract: key: prod/http-echo/bulk diff --git a/ci/full-values.yaml b/ci/full-values.yaml index e6cbd32..296455a 100644 --- a/ci/full-values.yaml +++ b/ci/full-values.yaml @@ -32,8 +32,9 @@ configMap: secret: enabled: true + # CI test fixture; not a credential. stringData: - API_TOKEN: not-a-real-token + DEMO_VALUE: ci-fake-value networkPolicy: enabled: true diff --git a/templates/_helpers.tpl b/templates/_helpers.tpl index e384090..89fe379 100644 --- a/templates/_helpers.tpl +++ b/templates/_helpers.tpl @@ -63,3 +63,19 @@ Create the name of the service account to use. {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }} + +{{/* +Default affinity used when .Values.affinity is empty. +Soft pod-anti-affinity by hostname so replicas spread across nodes. +Uses selectorLabels so it stays correct under nameOverride/fullnameOverride. +*/}} +{{- define "http-echo.defaultAffinity" -}} +podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + {{- include "http-echo.selectorLabels" . | nindent 12 }} + topologyKey: kubernetes.io/hostname +{{- end }} diff --git a/templates/deployment.yaml b/templates/deployment.yaml index d96ed1c..fcb6660 100644 --- a/templates/deployment.yaml +++ b/templates/deployment.yaml @@ -111,9 +111,12 @@ spec: nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} - {{- with .Values.affinity }} + {{- if .Values.affinity }} affinity: - {{- toYaml . | nindent 8 }} + {{- toYaml .Values.affinity | nindent 8 }} + {{- else }} + affinity: + {{- include "http-echo.defaultAffinity" . | nindent 8 }} {{- end }} {{- with .Values.tolerations }} tolerations: diff --git a/templates/pdb.yaml b/templates/pdb.yaml index 66d1018..f40a71c 100644 --- a/templates/pdb.yaml +++ b/templates/pdb.yaml @@ -19,7 +19,9 @@ spec: {{- else if not (kindIs "invalid" .Values.podDisruptionBudget.minAvailable) }} minAvailable: {{ .Values.podDisruptionBudget.minAvailable }} {{- end }} - unhealthyPodEvictionPolicy: {{ .Values.podDisruptionBudget.unhealthyPodEvictionPolicy }} + {{- with .Values.podDisruptionBudget.unhealthyPodEvictionPolicy }} + unhealthyPodEvictionPolicy: {{ . }} + {{- end }} selector: matchLabels: {{- include "http-echo.selectorLabels" . | nindent 6 }} diff --git a/templates/tests/test-connection.yaml b/templates/tests/test-connection.yaml index 7abd413..c30fbc9 100644 --- a/templates/tests/test-connection.yaml +++ b/templates/tests/test-connection.yaml @@ -31,7 +31,7 @@ spec: command: ["/bin/sh", "-c"] args: - >- - wget --spider -S "http://{{ include "http-echo.fullname" . }}:{{ .Values.service.port }}/" 2>&1 + wget -T 5 -t 2 --spider -S "http://{{ include "http-echo.fullname" . }}:{{ .Values.service.port }}/" 2>&1 | grep -q "HTTP/.* 200" resources: requests: diff --git a/values.yaml b/values.yaml index 73c3072..b6d820a 100644 --- a/values.yaml +++ b/values.yaml @@ -214,18 +214,12 @@ networkPolicy: nodeSelector: {} tolerations: [] -# Default soft pod-anti-affinity by hostname spreads replicas across nodes. -# Override with `affinity: {}` to disable, or replace with required affinity -# for a stricter policy. -affinity: - podAntiAffinity: - preferredDuringSchedulingIgnoredDuringExecution: - - weight: 100 - podAffinityTerm: - labelSelector: - matchLabels: - app.kubernetes.io/name: http-echo - topologyKey: kubernetes.io/hostname +# -- Pod affinity. When empty, the chart synthesizes a soft pod-anti-affinity +# rule (preferred, weight 100) that spreads replicas across nodes by hostname, +# using the chart's selectorLabels — so it stays correct under `nameOverride`. +# Set to `{ }` (the default) to use the synthesized rule, or provide a full +# affinity spec to override entirely. +affinity: {} topologySpreadConstraints: [] # -- Extra environment variables on the container (beyond envFrom). From a9c2cfab0a49807e74c0c70c3136490666841b28 Mon Sep 17 00:00:00 2001 From: Alexander <60811310+NoobCoder1209@users.noreply.github.com> Date: Sun, 7 Jun 2026 18:25:46 +0300 Subject: [PATCH 3/3] fix: keep test pod after success so helm test --logs works The previous hook-delete-policy was `before-hook-creation,hook-succeeded`, which deleted the test pod the moment it succeeded. `helm test --logs` then failed to read logs of a pod that no longer existed: Error: unable to get pod logs for http-echo-test-connection: pods "http-echo-test-connection" not found Drop `hook-succeeded` so the pod stays around after success. The next `helm test` invocation will still clean it up via `before-hook-creation`. --- templates/tests/test-connection.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/templates/tests/test-connection.yaml b/templates/tests/test-connection.yaml index c30fbc9..10a1afe 100644 --- a/templates/tests/test-connection.yaml +++ b/templates/tests/test-connection.yaml @@ -16,7 +16,9 @@ metadata: app.kubernetes.io/component: test annotations: "helm.sh/hook": test - "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded + # before-hook-creation only — keep the pod after success so `helm test --logs` + # can read its output. The next `helm test` run cleans up the previous pod. + "helm.sh/hook-delete-policy": before-hook-creation spec: restartPolicy: Never securityContext: