From e4346bd701c9cd5dfd00c601d399b8b790b17268 Mon Sep 17 00:00:00 2001 From: stheppi Date: Wed, 7 Oct 2026 22:37:42 +0100 Subject: [PATCH 01/19] test(chart): add render test harness and CI helm-unittest covers ConfigMap shape and render failures. tests/render/run.sh renders each case under tests/render/cases, checks config.json with jq, and with EDGE_SRC set also loads it with the Edge's own config loader (built via go build -overlay, so the Edge checkout is never written to). tests/ and ci/ are excluded from the packaged chart. Signed-off-by: stheppi --- .github/workflows/ci.yaml | 22 ++++++++ .helmignore | 3 ++ tests/configmap_test.yaml | 13 +++++ tests/render/cases/defaults/expect.jq | 3 ++ tests/render/cases/defaults/values.yaml | 1 + tests/render/edgecheck/main.go | 19 +++++++ tests/render/run.sh | 71 +++++++++++++++++++++++++ 7 files changed, 132 insertions(+) create mode 100644 .github/workflows/ci.yaml create mode 100644 tests/configmap_test.yaml create mode 100644 tests/render/cases/defaults/expect.jq create mode 100644 tests/render/cases/defaults/values.yaml create mode 100644 tests/render/edgecheck/main.go create mode 100755 tests/render/run.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..76ed0c3 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,22 @@ +name: CI + +on: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: azure/setup-helm@v4 + with: + version: v3.18.0 + - name: Install helm-unittest + run: helm plugin install https://github.com/helm-unittest/helm-unittest --version v1.2.1 + - name: Install yq + run: | + sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/v4.44.3/yq_linux_amd64 + sudo chmod +x /usr/local/bin/yq + - run: helm lint . + - run: helm unittest . + - run: tests/render/run.sh diff --git a/.helmignore b/.helmignore index 836ebb0..a3812d0 100644 --- a/.helmignore +++ b/.helmignore @@ -31,3 +31,6 @@ docs/ .idea/ *.tmproj .vscode/ +# Tests and CI helpers are not chart content +tests/ +ci/ diff --git a/tests/configmap_test.yaml b/tests/configmap_test.yaml new file mode 100644 index 0000000..8b0b14e --- /dev/null +++ b/tests/configmap_test.yaml @@ -0,0 +1,13 @@ +suite: configmap +templates: + - templates/configmap.yaml +tests: + - it: renders a config.json ConfigMap with defaults + asserts: + - isKind: + of: ConfigMap + - matchRegex: + path: metadata.name + pattern: -config$ + - exists: + path: data["config.json"] diff --git a/tests/render/cases/defaults/expect.jq b/tests/render/cases/defaults/expect.jq new file mode 100644 index 0000000..e23e03e --- /dev/null +++ b/tests/render/cases/defaults/expect.jq @@ -0,0 +1,3 @@ +(.kube | has("namespaceFilter") | not) +and (.netobs | has("edgeExistenceTtl") | not) +and (.netobs | has("excludeNamespaces") | not) diff --git a/tests/render/cases/defaults/values.yaml b/tests/render/cases/defaults/values.yaml new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/tests/render/cases/defaults/values.yaml @@ -0,0 +1 @@ +{} diff --git a/tests/render/edgecheck/main.go b/tests/render/edgecheck/main.go new file mode 100644 index 0000000..6c0047b --- /dev/null +++ b/tests/render/edgecheck/main.go @@ -0,0 +1,19 @@ +// edgecheck reads an Edge config.json on stdin and loads it with the Edge's own +// loader, so the chart's rendered config is checked against the real validation. +// It is built by run.sh via `go build -overlay`, which injects this file into +// the Edge module without writing to the Edge checkout. +package main + +import ( + "fmt" + "os" + + "github.com/NOFireAI/agent/internal/edge/config" +) + +func main() { + if _, err := config.LoadConfigFromReader(os.Stdin); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/tests/render/run.sh b/tests/render/run.sh new file mode 100755 index 0000000..4c4e13e --- /dev/null +++ b/tests/render/run.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# Renders templates/configmap.yaml for every case in tests/render/cases/ and +# checks the result. Usage: tests/render/run.sh [case-name-filter] +# +# A case dir holds values.yaml plus either: +# expect.jq a jq program run against config.json; must print true +# expect-fail a regex the `helm template` stderr must match (render must fail) +# With EDGE_SRC set to an Edge checkout, every rendered config.json must also be +# accepted by the Edge's own config loader (see edgecheck/). +set -euo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +chart="$(cd "$here/../.." && pwd)" +filter="${1:-}" +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT + +edgecheck="" +if [[ -n "${EDGE_SRC:-}" ]]; then + cat > "$tmp/overlay.json" <&1)"; then + report FAIL "$name" "render succeeded, expected failure" + elif grep -Eq "$(cat "$dir/expect-fail")" <<<"$out"; then + report PASS "$name" + else + report FAIL "$name" "error did not match $(cat "$dir/expect-fail"): $out" + fi + continue + fi + + if ! out="$(helm template t "$chart" -f "$dir/values.yaml" -s templates/configmap.yaml 2>&1)"; then + report FAIL "$name" "render failed: $out" + continue + fi + cfg="$tmp/$name.json" + yq -r '.data["config.json"]' <<<"$out" > "$cfg" + if ! jq -e . "$cfg" >/dev/null 2>"$tmp/err"; then + report FAIL "$name" "config.json is not valid JSON: $(cat "$tmp/err")" + continue + fi + if [[ "$(jq -f "$dir/expect.jq" "$cfg" 2>&1)" != true ]]; then + report FAIL "$name" "expect.jq: $(jq -f "$dir/expect.jq" "$cfg" 2>&1 | head -5 | tr '\n' ' ')" + continue + fi + if [[ -n "$edgecheck" ]] && ! err="$("$edgecheck" < "$cfg" 2>&1)"; then + report FAIL "$name" "Edge rejected config: $err" + continue + fi + report PASS "$name" +done + +echo "$pass passed, $fail failed" +[[ "$fail" -eq 0 ]] From 7ac44a53e61f70f0e906f34a3ce0f87f2d42f252 Mon Sep 17 00:00:00 2001 From: stheppi Date: Wed, 7 Oct 2026 22:38:50 +0100 Subject: [PATCH 02/19] fix(chart): render kube.namespaceFilter into config.json The template dropped config.kube.namespaceFilter, so namespace scoping set in values never reached the Edge. Write it whenever the map is non-empty. Before: cases ns-filter-allow/deny/none FAIL: .kube.namespaceFilter missing Signed-off-by: stheppi --- templates/configmap.yaml | 4 ++++ tests/render/cases/ns-filter-allow/expect.jq | 1 + tests/render/cases/ns-filter-allow/values.yaml | 5 +++++ tests/render/cases/ns-filter-deny/expect.jq | 1 + tests/render/cases/ns-filter-deny/values.yaml | 5 +++++ tests/render/cases/ns-filter-none/expect.jq | 1 + tests/render/cases/ns-filter-none/values.yaml | 4 ++++ 7 files changed, 21 insertions(+) create mode 100644 tests/render/cases/ns-filter-allow/expect.jq create mode 100644 tests/render/cases/ns-filter-allow/values.yaml create mode 100644 tests/render/cases/ns-filter-deny/expect.jq create mode 100644 tests/render/cases/ns-filter-deny/values.yaml create mode 100644 tests/render/cases/ns-filter-none/expect.jq create mode 100644 tests/render/cases/ns-filter-none/values.yaml diff --git a/templates/configmap.yaml b/templates/configmap.yaml index be2e70f..e09b56b 100644 --- a/templates/configmap.yaml +++ b/templates/configmap.yaml @@ -24,6 +24,10 @@ data: {{ $resource | quote }} {{- end }} ] + {{- $nf := pick (.Values.config.kube.namespaceFilter | default dict) "mode" "namespaces" }} + {{- if $nf }}, + "namespaceFilter": {{ $nf | toJson }} + {{- end }} }, "enableCausalAnalysis": {{ .Values.config.enableCausalAnalysis }}, "causal": { diff --git a/tests/render/cases/ns-filter-allow/expect.jq b/tests/render/cases/ns-filter-allow/expect.jq new file mode 100644 index 0000000..1d43a44 --- /dev/null +++ b/tests/render/cases/ns-filter-allow/expect.jq @@ -0,0 +1 @@ +.kube.namespaceFilter == {"mode": "allow", "namespaces": ["a", "b"]} diff --git a/tests/render/cases/ns-filter-allow/values.yaml b/tests/render/cases/ns-filter-allow/values.yaml new file mode 100644 index 0000000..1b7b44f --- /dev/null +++ b/tests/render/cases/ns-filter-allow/values.yaml @@ -0,0 +1,5 @@ +config: + kube: + namespaceFilter: + mode: allow + namespaces: [a, b] diff --git a/tests/render/cases/ns-filter-deny/expect.jq b/tests/render/cases/ns-filter-deny/expect.jq new file mode 100644 index 0000000..6df668a --- /dev/null +++ b/tests/render/cases/ns-filter-deny/expect.jq @@ -0,0 +1 @@ +.kube.namespaceFilter == {"mode": "deny", "namespaces": ["kube-system"]} diff --git a/tests/render/cases/ns-filter-deny/values.yaml b/tests/render/cases/ns-filter-deny/values.yaml new file mode 100644 index 0000000..299781a --- /dev/null +++ b/tests/render/cases/ns-filter-deny/values.yaml @@ -0,0 +1,5 @@ +config: + kube: + namespaceFilter: + mode: deny + namespaces: [kube-system] diff --git a/tests/render/cases/ns-filter-none/expect.jq b/tests/render/cases/ns-filter-none/expect.jq new file mode 100644 index 0000000..e6675ce --- /dev/null +++ b/tests/render/cases/ns-filter-none/expect.jq @@ -0,0 +1 @@ +.kube.namespaceFilter == {"mode": "none"} diff --git a/tests/render/cases/ns-filter-none/values.yaml b/tests/render/cases/ns-filter-none/values.yaml new file mode 100644 index 0000000..baf41d9 --- /dev/null +++ b/tests/render/cases/ns-filter-none/values.yaml @@ -0,0 +1,4 @@ +config: + kube: + namespaceFilter: + mode: none From ddbc37f2402534876ac554563bb2eb857dda4f47 Mon Sep 17 00:00:00 2001 From: stheppi Date: Wed, 7 Oct 2026 22:39:05 +0100 Subject: [PATCH 03/19] fix(chart): fail render on an invalid kube.namespaceFilter Mirror the Edge's validation (config.go:708-725) so a bad filter fails at helm install time instead of crashlooping the pod. Before: cases ns-filter-fail-* FAIL: render succeeded, expected failure Signed-off-by: stheppi --- templates/configmap.yaml | 19 +++++++++++++++- tests/configmap_test.yaml | 22 +++++++++++++++++++ .../ns-filter-fail-allow-empty/expect-fail | 1 + .../ns-filter-fail-allow-empty/values.yaml | 5 +++++ .../ns-filter-fail-deny-missing/expect-fail | 1 + .../ns-filter-fail-deny-missing/values.yaml | 4 ++++ .../expect-fail | 1 + .../values.yaml | 4 ++++ .../expect-fail | 1 + .../values.yaml | 5 +++++ .../ns-filter-fail-unknown-mode/expect-fail | 1 + .../ns-filter-fail-unknown-mode/values.yaml | 5 +++++ 12 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 tests/render/cases/ns-filter-fail-allow-empty/expect-fail create mode 100644 tests/render/cases/ns-filter-fail-allow-empty/values.yaml create mode 100644 tests/render/cases/ns-filter-fail-deny-missing/expect-fail create mode 100644 tests/render/cases/ns-filter-fail-deny-missing/values.yaml create mode 100644 tests/render/cases/ns-filter-fail-namespaces-no-mode/expect-fail create mode 100644 tests/render/cases/ns-filter-fail-namespaces-no-mode/values.yaml create mode 100644 tests/render/cases/ns-filter-fail-none-with-namespaces/expect-fail create mode 100644 tests/render/cases/ns-filter-fail-none-with-namespaces/values.yaml create mode 100644 tests/render/cases/ns-filter-fail-unknown-mode/expect-fail create mode 100644 tests/render/cases/ns-filter-fail-unknown-mode/values.yaml diff --git a/templates/configmap.yaml b/templates/configmap.yaml index e09b56b..fd2cc02 100644 --- a/templates/configmap.yaml +++ b/templates/configmap.yaml @@ -25,7 +25,24 @@ data: {{- end }} ] {{- $nf := pick (.Values.config.kube.namespaceFilter | default dict) "mode" "namespaces" }} - {{- if $nf }}, + {{- if $nf }} + {{- /* Mirror the Edge's validation so a bad filter fails at install, not as a crashloop. */}} + {{- $mode := printf "%v" (get $nf "mode" | default "") }} + {{- $namespaces := get $nf "namespaces" | default list }} + {{- if not (kindIs "slice" $namespaces) }} + {{- fail "config.kube.namespaceFilter.namespaces must be a list" }} + {{- end }} + {{- if has $mode (list "" "none") }} + {{- if $namespaces }} + {{- fail (printf "config.kube.namespaceFilter.namespaces is set but mode %q disables filtering; set config.kube.namespaceFilter.mode to 'allow' or 'deny'" $mode) }} + {{- end }} + {{- else if has $mode (list "allow" "deny") }} + {{- if not $namespaces }} + {{- fail (printf "config.kube.namespaceFilter.mode is %q but config.kube.namespaceFilter.namespaces is empty" $mode) }} + {{- end }} + {{- else }} + {{- fail (printf "config.kube.namespaceFilter.mode must be one of '', 'none', 'allow', 'deny' (got %q)" $mode) }} + {{- end }}, "namespaceFilter": {{ $nf | toJson }} {{- end }} }, diff --git a/tests/configmap_test.yaml b/tests/configmap_test.yaml index 8b0b14e..69e1669 100644 --- a/tests/configmap_test.yaml +++ b/tests/configmap_test.yaml @@ -11,3 +11,25 @@ tests: pattern: -config$ - exists: path: data["config.json"] + + - it: fails when namespaces are set without a mode + set: + config.kube.namespaceFilter.namespaces: [a] + asserts: + - failedTemplate: + errorPattern: config\.kube\.namespaceFilter\.mode + + - it: fails when mode is allow with no namespaces + set: + config.kube.namespaceFilter.mode: allow + asserts: + - failedTemplate: + errorPattern: config\.kube\.namespaceFilter\.namespaces + + - it: fails on an unknown mode + set: + config.kube.namespaceFilter.mode: bogus + config.kube.namespaceFilter.namespaces: [a] + asserts: + - failedTemplate: + errorPattern: config\.kube\.namespaceFilter\.mode diff --git a/tests/render/cases/ns-filter-fail-allow-empty/expect-fail b/tests/render/cases/ns-filter-fail-allow-empty/expect-fail new file mode 100644 index 0000000..4a0503c --- /dev/null +++ b/tests/render/cases/ns-filter-fail-allow-empty/expect-fail @@ -0,0 +1 @@ +config\.kube\.namespaceFilter\.namespaces diff --git a/tests/render/cases/ns-filter-fail-allow-empty/values.yaml b/tests/render/cases/ns-filter-fail-allow-empty/values.yaml new file mode 100644 index 0000000..0d3417d --- /dev/null +++ b/tests/render/cases/ns-filter-fail-allow-empty/values.yaml @@ -0,0 +1,5 @@ +config: + kube: + namespaceFilter: + mode: allow + namespaces: [] diff --git a/tests/render/cases/ns-filter-fail-deny-missing/expect-fail b/tests/render/cases/ns-filter-fail-deny-missing/expect-fail new file mode 100644 index 0000000..4a0503c --- /dev/null +++ b/tests/render/cases/ns-filter-fail-deny-missing/expect-fail @@ -0,0 +1 @@ +config\.kube\.namespaceFilter\.namespaces diff --git a/tests/render/cases/ns-filter-fail-deny-missing/values.yaml b/tests/render/cases/ns-filter-fail-deny-missing/values.yaml new file mode 100644 index 0000000..e8a6b22 --- /dev/null +++ b/tests/render/cases/ns-filter-fail-deny-missing/values.yaml @@ -0,0 +1,4 @@ +config: + kube: + namespaceFilter: + mode: deny diff --git a/tests/render/cases/ns-filter-fail-namespaces-no-mode/expect-fail b/tests/render/cases/ns-filter-fail-namespaces-no-mode/expect-fail new file mode 100644 index 0000000..2bc2cc0 --- /dev/null +++ b/tests/render/cases/ns-filter-fail-namespaces-no-mode/expect-fail @@ -0,0 +1 @@ +config\.kube\.namespaceFilter\.mode diff --git a/tests/render/cases/ns-filter-fail-namespaces-no-mode/values.yaml b/tests/render/cases/ns-filter-fail-namespaces-no-mode/values.yaml new file mode 100644 index 0000000..f06e72b --- /dev/null +++ b/tests/render/cases/ns-filter-fail-namespaces-no-mode/values.yaml @@ -0,0 +1,4 @@ +config: + kube: + namespaceFilter: + namespaces: [a] diff --git a/tests/render/cases/ns-filter-fail-none-with-namespaces/expect-fail b/tests/render/cases/ns-filter-fail-none-with-namespaces/expect-fail new file mode 100644 index 0000000..2bc2cc0 --- /dev/null +++ b/tests/render/cases/ns-filter-fail-none-with-namespaces/expect-fail @@ -0,0 +1 @@ +config\.kube\.namespaceFilter\.mode diff --git a/tests/render/cases/ns-filter-fail-none-with-namespaces/values.yaml b/tests/render/cases/ns-filter-fail-none-with-namespaces/values.yaml new file mode 100644 index 0000000..7fd390d --- /dev/null +++ b/tests/render/cases/ns-filter-fail-none-with-namespaces/values.yaml @@ -0,0 +1,5 @@ +config: + kube: + namespaceFilter: + mode: none + namespaces: [a] diff --git a/tests/render/cases/ns-filter-fail-unknown-mode/expect-fail b/tests/render/cases/ns-filter-fail-unknown-mode/expect-fail new file mode 100644 index 0000000..e052513 --- /dev/null +++ b/tests/render/cases/ns-filter-fail-unknown-mode/expect-fail @@ -0,0 +1 @@ +config\.kube\.namespaceFilter\.mode.*bogus diff --git a/tests/render/cases/ns-filter-fail-unknown-mode/values.yaml b/tests/render/cases/ns-filter-fail-unknown-mode/values.yaml new file mode 100644 index 0000000..019f238 --- /dev/null +++ b/tests/render/cases/ns-filter-fail-unknown-mode/values.yaml @@ -0,0 +1,5 @@ +config: + kube: + namespaceFilter: + mode: bogus + namespaces: [a] From 9351817baba679f4b354e0d9cf8dc4c60ca1a82c Mon Sep 17 00:00:00 2001 From: stheppi Date: Wed, 7 Oct 2026 22:39:22 +0100 Subject: [PATCH 04/19] fix(chart): render netobs.edgeExistenceTtl into config.json Written whenever set, including "0s"; an explicit null counts as unset (Helm keeps the nil for keys not in the chart defaults, so it is checked). Before: cases ttl-1h, ttl-zero FAIL: .netobs.edgeExistenceTtl missing Signed-off-by: stheppi --- templates/configmap.yaml | 3 +++ tests/render/cases/ttl-1h/expect.jq | 1 + tests/render/cases/ttl-1h/values.yaml | 1 + tests/render/cases/ttl-null/expect.jq | 1 + tests/render/cases/ttl-null/values.yaml | 1 + tests/render/cases/ttl-zero/expect.jq | 1 + tests/render/cases/ttl-zero/values.yaml | 1 + 7 files changed, 9 insertions(+) create mode 100644 tests/render/cases/ttl-1h/expect.jq create mode 100644 tests/render/cases/ttl-1h/values.yaml create mode 100644 tests/render/cases/ttl-null/expect.jq create mode 100644 tests/render/cases/ttl-null/values.yaml create mode 100644 tests/render/cases/ttl-zero/expect.jq create mode 100644 tests/render/cases/ttl-zero/values.yaml diff --git a/templates/configmap.yaml b/templates/configmap.yaml index fd2cc02..de25105 100644 --- a/templates/configmap.yaml +++ b/templates/configmap.yaml @@ -113,6 +113,9 @@ data: "prometheusUrl": {{ .Values.config.netobs.prometheusUrl | default "" | quote }}, "source": {{ .Values.config.netobs.source | default "" | quote }}, "prometheusTimeout": {{ .Values.config.netobs.prometheusTimeout | default "10s" | quote }} + {{- if and (hasKey .Values.config.netobs "edgeExistenceTtl") (not (kindIs "invalid" .Values.config.netobs.edgeExistenceTtl)) }}, + "edgeExistenceTtl": {{ .Values.config.netobs.edgeExistenceTtl | quote }} + {{- end }} }, "configMapCapture": { "clearText": {{ .Values.config.configMapCapture.clearText | default false }}, diff --git a/tests/render/cases/ttl-1h/expect.jq b/tests/render/cases/ttl-1h/expect.jq new file mode 100644 index 0000000..9c72bf6 --- /dev/null +++ b/tests/render/cases/ttl-1h/expect.jq @@ -0,0 +1 @@ +.netobs.edgeExistenceTtl == "1h" diff --git a/tests/render/cases/ttl-1h/values.yaml b/tests/render/cases/ttl-1h/values.yaml new file mode 100644 index 0000000..c4a42ec --- /dev/null +++ b/tests/render/cases/ttl-1h/values.yaml @@ -0,0 +1 @@ +config: {netobs: {edgeExistenceTtl: 1h}} diff --git a/tests/render/cases/ttl-null/expect.jq b/tests/render/cases/ttl-null/expect.jq new file mode 100644 index 0000000..a6ed465 --- /dev/null +++ b/tests/render/cases/ttl-null/expect.jq @@ -0,0 +1 @@ +.netobs | has("edgeExistenceTtl") | not diff --git a/tests/render/cases/ttl-null/values.yaml b/tests/render/cases/ttl-null/values.yaml new file mode 100644 index 0000000..53f23e2 --- /dev/null +++ b/tests/render/cases/ttl-null/values.yaml @@ -0,0 +1 @@ +config: {netobs: {edgeExistenceTtl: null}} diff --git a/tests/render/cases/ttl-zero/expect.jq b/tests/render/cases/ttl-zero/expect.jq new file mode 100644 index 0000000..1bd662b --- /dev/null +++ b/tests/render/cases/ttl-zero/expect.jq @@ -0,0 +1 @@ +.netobs.edgeExistenceTtl == "0s" diff --git a/tests/render/cases/ttl-zero/values.yaml b/tests/render/cases/ttl-zero/values.yaml new file mode 100644 index 0000000..fd66deb --- /dev/null +++ b/tests/render/cases/ttl-zero/values.yaml @@ -0,0 +1 @@ +config: {netobs: {edgeExistenceTtl: 0s}} From 744e5191f6bc0dff7db0ab898b02620cfc0b704f Mon Sep 17 00:00:00 2001 From: stheppi Date: Wed, 7 Oct 2026 22:39:35 +0100 Subject: [PATCH 05/19] fix(chart): render netobs.excludeNamespaces into config.json Written when set, including [] (which replaces the Edge default list); an explicit null counts as unset. Before: cases exclude-ns, exclude-ns-empty FAIL: .netobs.excludeNamespaces missing Signed-off-by: stheppi --- templates/configmap.yaml | 3 +++ tests/render/cases/exclude-ns-empty/expect.jq | 1 + tests/render/cases/exclude-ns-empty/values.yaml | 1 + tests/render/cases/exclude-ns-null/expect.jq | 1 + tests/render/cases/exclude-ns-null/values.yaml | 1 + tests/render/cases/exclude-ns/expect.jq | 1 + tests/render/cases/exclude-ns/values.yaml | 1 + 7 files changed, 9 insertions(+) create mode 100644 tests/render/cases/exclude-ns-empty/expect.jq create mode 100644 tests/render/cases/exclude-ns-empty/values.yaml create mode 100644 tests/render/cases/exclude-ns-null/expect.jq create mode 100644 tests/render/cases/exclude-ns-null/values.yaml create mode 100644 tests/render/cases/exclude-ns/expect.jq create mode 100644 tests/render/cases/exclude-ns/values.yaml diff --git a/templates/configmap.yaml b/templates/configmap.yaml index de25105..5951c8e 100644 --- a/templates/configmap.yaml +++ b/templates/configmap.yaml @@ -116,6 +116,9 @@ data: {{- if and (hasKey .Values.config.netobs "edgeExistenceTtl") (not (kindIs "invalid" .Values.config.netobs.edgeExistenceTtl)) }}, "edgeExistenceTtl": {{ .Values.config.netobs.edgeExistenceTtl | quote }} {{- end }} + {{- if and (hasKey .Values.config.netobs "excludeNamespaces") (not (kindIs "invalid" .Values.config.netobs.excludeNamespaces)) }}, + "excludeNamespaces": {{ .Values.config.netobs.excludeNamespaces | toJson }} + {{- end }} }, "configMapCapture": { "clearText": {{ .Values.config.configMapCapture.clearText | default false }}, diff --git a/tests/render/cases/exclude-ns-empty/expect.jq b/tests/render/cases/exclude-ns-empty/expect.jq new file mode 100644 index 0000000..373ca11 --- /dev/null +++ b/tests/render/cases/exclude-ns-empty/expect.jq @@ -0,0 +1 @@ +.netobs.excludeNamespaces == [] diff --git a/tests/render/cases/exclude-ns-empty/values.yaml b/tests/render/cases/exclude-ns-empty/values.yaml new file mode 100644 index 0000000..85d5f75 --- /dev/null +++ b/tests/render/cases/exclude-ns-empty/values.yaml @@ -0,0 +1 @@ +config: {netobs: {excludeNamespaces: []}} diff --git a/tests/render/cases/exclude-ns-null/expect.jq b/tests/render/cases/exclude-ns-null/expect.jq new file mode 100644 index 0000000..61909a4 --- /dev/null +++ b/tests/render/cases/exclude-ns-null/expect.jq @@ -0,0 +1 @@ +.netobs | has("excludeNamespaces") | not diff --git a/tests/render/cases/exclude-ns-null/values.yaml b/tests/render/cases/exclude-ns-null/values.yaml new file mode 100644 index 0000000..b581f46 --- /dev/null +++ b/tests/render/cases/exclude-ns-null/values.yaml @@ -0,0 +1 @@ +config: {netobs: {excludeNamespaces: null}} diff --git a/tests/render/cases/exclude-ns/expect.jq b/tests/render/cases/exclude-ns/expect.jq new file mode 100644 index 0000000..6931805 --- /dev/null +++ b/tests/render/cases/exclude-ns/expect.jq @@ -0,0 +1 @@ +.netobs.excludeNamespaces == ["x"] diff --git a/tests/render/cases/exclude-ns/values.yaml b/tests/render/cases/exclude-ns/values.yaml new file mode 100644 index 0000000..5aec140 --- /dev/null +++ b/tests/render/cases/exclude-ns/values.yaml @@ -0,0 +1 @@ +config: {netobs: {excludeNamespaces: [x]}} From 02c8b6610c7b3ec3cc367bd4cdf2c58df4930844 Mon Sep 17 00:00:00 2001 From: stheppi Date: Wed, 7 Oct 2026 22:39:53 +0100 Subject: [PATCH 06/19] fix(chart): render services.address without the full services block services.address alone failed helm template (nil pointer on the missing tls/compression/handshake maps) and the old fallbacks (empty durations, unset maxConns) would have been rejected by the Edge anyway. Scalars are now emitted only when set; services-full guards the previously supported full config. Before: case services-address-only FAIL: render failed: nil pointer evaluating interface {}.enabled Signed-off-by: stheppi --- templates/configmap.yaml | 72 ++++++++++++------- .../cases/services-address-only/expect.jq | 1 + .../cases/services-address-only/values.yaml | 1 + tests/render/cases/services-full/expect.jq | 8 +++ tests/render/cases/services-full/values.yaml | 22 ++++++ 5 files changed, 79 insertions(+), 25 deletions(-) create mode 100644 tests/render/cases/services-address-only/expect.jq create mode 100644 tests/render/cases/services-address-only/values.yaml create mode 100644 tests/render/cases/services-full/expect.jq create mode 100644 tests/render/cases/services-full/values.yaml diff --git a/templates/configmap.yaml b/templates/configmap.yaml index 5951c8e..da2dbcf 100644 --- a/templates/configmap.yaml +++ b/templates/configmap.yaml @@ -1,3 +1,17 @@ +{{- /* +nofire-edge.pickSet: JSON object of the given keys that are set on src. A key is +kept when present, even if falsy (false, 0, "", []); an explicit null counts as unset. +*/ -}} +{{- define "nofire-edge.pickSet" -}} +{{- $out := dict -}} +{{- $src := .src | default dict -}} +{{- range $k := .keys -}} +{{- if and (hasKey $src $k) (not (kindIs "invalid" (index $src $k))) -}} +{{- $_ := set $out $k (index $src $k) -}} +{{- end -}} +{{- end -}} +{{- $out | toJson -}} +{{- end -}} apiVersion: v1 kind: ConfigMap metadata: @@ -60,35 +74,43 @@ data: "tracesSampleRate": {{ .Values.config.sentry.tracesSampleRate | default 0.0 }} }, "services": { - "workers": {{ .Values.config.services.workers }} - {{- if .Values.config.services.address }}, - "address": {{ .Values.config.services.address | quote }}, - "maxConns": {{ .Values.config.services.maxConns }}, - "readTimeout": {{ .Values.config.services.readTimeout | quote }}, - "writeTimeout": {{ .Values.config.services.writeTimeout | quote }}, + {{- $s := .Values.config.services | default dict }} + {{- $scalars := include "nofire-edge.pickSet" (dict "src" $s "keys" (list "workers" "address" "maxConns" "readTimeout" "writeTimeout")) | fromJson }} + {{- $first := true }} + {{- range $k, $v := $scalars }} + {{- if not $first }},{{ end }} + {{ $k | quote }}: {{ $v | toJson }} + {{- $first = false }} + {{- end }} + {{- if kindIs "map" $s.tls }} + {{- if not $first }},{{ end }} "tls": { - "enabled": {{ .Values.config.services.tls.enabled | default false }}, - "certFile": {{ .Values.config.services.tls.certFile | default "" | quote }}, - "keyFile": {{ .Values.config.services.tls.keyFile | default "" | quote }}, - "caFile": {{ .Values.config.services.tls.caFile | default "" | quote }}, - "requireClientCert": {{ .Values.config.services.tls.requireClientCert | default false }}, - "minVersion": {{ .Values.config.services.tls.minVersion | default "" | quote }}, - "cipherSuites": [ - {{- range $index, $suite := .Values.config.services.tls.cipherSuites }} - {{- if $index }},{{ end }} - {{ $suite | quote }} - {{- end }} - ] - }, + "enabled": {{ $s.tls.enabled | default false }}, + "certFile": {{ $s.tls.certFile | default "" | quote }}, + "keyFile": {{ $s.tls.keyFile | default "" | quote }}, + "caFile": {{ $s.tls.caFile | default "" | quote }}, + "requireClientCert": {{ $s.tls.requireClientCert | default false }}, + "minVersion": {{ $s.tls.minVersion | default "" | quote }}, + "cipherSuites": {{ $s.tls.cipherSuites | default list | toJson }} + } + {{- $first = false }} + {{- end }} + {{- if kindIs "map" $s.compression }} + {{- if not $first }},{{ end }} "compression": { - "enabled": {{ .Values.config.services.compression.enabled | default false }}, - "type": {{ .Values.config.services.compression.type | default "" | quote }}, - "level": {{ .Values.config.services.compression.level | default -1 }} - }, + "enabled": {{ $s.compression.enabled | default false }}, + "type": {{ $s.compression.type | default "" | quote }}, + "level": {{ $s.compression.level | default -1 }} + } + {{- $first = false }} + {{- end }} + {{- if kindIs "map" $s.handshake }} + {{- if not $first }},{{ end }} "handshake": { - "timeout": {{ .Values.config.services.handshake.timeout | default "" | quote }}, - "contentType": {{ .Values.config.services.handshake.contentType | default "" | quote }} + "timeout": {{ $s.handshake.timeout | default "" | quote }}, + "contentType": {{ $s.handshake.contentType | default "" | quote }} } + {{- $first = false }} {{- end }} }, "enablePublishing": {{ .Values.config.enablePublishing }}, diff --git a/tests/render/cases/services-address-only/expect.jq b/tests/render/cases/services-address-only/expect.jq new file mode 100644 index 0000000..d18c8fa --- /dev/null +++ b/tests/render/cases/services-address-only/expect.jq @@ -0,0 +1 @@ +.services == {"workers": 4, "address": ":6000"} diff --git a/tests/render/cases/services-address-only/values.yaml b/tests/render/cases/services-address-only/values.yaml new file mode 100644 index 0000000..a55b4f8 --- /dev/null +++ b/tests/render/cases/services-address-only/values.yaml @@ -0,0 +1 @@ +config: {services: {address: ":6000"}} diff --git a/tests/render/cases/services-full/expect.jq b/tests/render/cases/services-full/expect.jq new file mode 100644 index 0000000..79b2a57 --- /dev/null +++ b/tests/render/cases/services-full/expect.jq @@ -0,0 +1,8 @@ +.services == { + "workers": 8, "address": "127.0.0.1:6000", "maxConns": 10, + "readTimeout": "30s", "writeTimeout": "20s", + "tls": {"enabled": false, "certFile": "", "keyFile": "", "caFile": "", + "requireClientCert": false, "minVersion": "1.2", "cipherSuites": []}, + "compression": {"enabled": false, "type": "none", "level": -1}, + "handshake": {"timeout": "5s", "contentType": "protobuf:dnstap.Dnstap"} +} diff --git a/tests/render/cases/services-full/values.yaml b/tests/render/cases/services-full/values.yaml new file mode 100644 index 0000000..74ff976 --- /dev/null +++ b/tests/render/cases/services-full/values.yaml @@ -0,0 +1,22 @@ +config: + services: + workers: 8 + address: "127.0.0.1:6000" + maxConns: 10 + readTimeout: "30s" + writeTimeout: "20s" + tls: + enabled: false + certFile: "" + keyFile: "" + caFile: "" + requireClientCert: false + minVersion: "1.2" + cipherSuites: [] + compression: + enabled: false + type: "none" + level: -1 + handshake: + timeout: "5s" + contentType: "protobuf:dnstap.Dnstap" From 68e39ad00cbffb728448fc81f3c9439098d8f794 Mon Sep 17 00:00:00 2001 From: stheppi Date: Wed, 7 Oct 2026 22:40:38 +0100 Subject: [PATCH 07/19] fix(chart): render partial services tls/compression/handshake objects The objects were rebuilt with ""/false/-1 fallbacks for every field, so a partial config overrode Edge defaults with invalid zero values. Now only the keys the user set are written, and an object is omitted when none are set (never null); the Edge merges the partial object into its defaults. Before: cases services-tls-enabled, services-handshake-timeout, services-empty-objects FAIL: .services.tls == {"enabled":true} was false (all fields filled) Signed-off-by: stheppi --- templates/configmap.yaml | 30 +++---------------- .../cases/services-empty-objects/expect.jq | 1 + .../cases/services-empty-objects/values.yaml | 1 + .../services-handshake-timeout/expect.jq | 1 + .../services-handshake-timeout/values.yaml | 1 + .../cases/services-tls-enabled/expect.jq | 1 + .../cases/services-tls-enabled/values.yaml | 1 + 7 files changed, 10 insertions(+), 26 deletions(-) create mode 100644 tests/render/cases/services-empty-objects/expect.jq create mode 100644 tests/render/cases/services-empty-objects/values.yaml create mode 100644 tests/render/cases/services-handshake-timeout/expect.jq create mode 100644 tests/render/cases/services-handshake-timeout/values.yaml create mode 100644 tests/render/cases/services-tls-enabled/expect.jq create mode 100644 tests/render/cases/services-tls-enabled/values.yaml diff --git a/templates/configmap.yaml b/templates/configmap.yaml index da2dbcf..222fdd3 100644 --- a/templates/configmap.yaml +++ b/templates/configmap.yaml @@ -82,35 +82,13 @@ data: {{ $k | quote }}: {{ $v | toJson }} {{- $first = false }} {{- end }} - {{- if kindIs "map" $s.tls }} + {{- range $name, $keys := dict "tls" (list "enabled" "certFile" "keyFile" "caFile" "requireClientCert" "minVersion" "cipherSuites") "compression" (list "enabled" "type" "level") "handshake" (list "timeout" "contentType") }} + {{- $obj := include "nofire-edge.pickSet" (dict "src" (get $s $name) "keys" $keys) | fromJson }} + {{- if $obj }} {{- if not $first }},{{ end }} - "tls": { - "enabled": {{ $s.tls.enabled | default false }}, - "certFile": {{ $s.tls.certFile | default "" | quote }}, - "keyFile": {{ $s.tls.keyFile | default "" | quote }}, - "caFile": {{ $s.tls.caFile | default "" | quote }}, - "requireClientCert": {{ $s.tls.requireClientCert | default false }}, - "minVersion": {{ $s.tls.minVersion | default "" | quote }}, - "cipherSuites": {{ $s.tls.cipherSuites | default list | toJson }} - } - {{- $first = false }} - {{- end }} - {{- if kindIs "map" $s.compression }} - {{- if not $first }},{{ end }} - "compression": { - "enabled": {{ $s.compression.enabled | default false }}, - "type": {{ $s.compression.type | default "" | quote }}, - "level": {{ $s.compression.level | default -1 }} - } + {{ $name | quote }}: {{ $obj | toJson }} {{- $first = false }} {{- end }} - {{- if kindIs "map" $s.handshake }} - {{- if not $first }},{{ end }} - "handshake": { - "timeout": {{ $s.handshake.timeout | default "" | quote }}, - "contentType": {{ $s.handshake.contentType | default "" | quote }} - } - {{- $first = false }} {{- end }} }, "enablePublishing": {{ .Values.config.enablePublishing }}, diff --git a/tests/render/cases/services-empty-objects/expect.jq b/tests/render/cases/services-empty-objects/expect.jq new file mode 100644 index 0000000..fc2af9b --- /dev/null +++ b/tests/render/cases/services-empty-objects/expect.jq @@ -0,0 +1 @@ +.services == {"workers": 4} diff --git a/tests/render/cases/services-empty-objects/values.yaml b/tests/render/cases/services-empty-objects/values.yaml new file mode 100644 index 0000000..8a76beb --- /dev/null +++ b/tests/render/cases/services-empty-objects/values.yaml @@ -0,0 +1 @@ +config: {services: {tls: {}, compression: null, handshake: {}}} diff --git a/tests/render/cases/services-handshake-timeout/expect.jq b/tests/render/cases/services-handshake-timeout/expect.jq new file mode 100644 index 0000000..a0b9fae --- /dev/null +++ b/tests/render/cases/services-handshake-timeout/expect.jq @@ -0,0 +1 @@ +.services.handshake == {"timeout": "5s"} diff --git a/tests/render/cases/services-handshake-timeout/values.yaml b/tests/render/cases/services-handshake-timeout/values.yaml new file mode 100644 index 0000000..ae90f86 --- /dev/null +++ b/tests/render/cases/services-handshake-timeout/values.yaml @@ -0,0 +1 @@ +config: {services: {handshake: {timeout: "5s"}}} diff --git a/tests/render/cases/services-tls-enabled/expect.jq b/tests/render/cases/services-tls-enabled/expect.jq new file mode 100644 index 0000000..f5e5c01 --- /dev/null +++ b/tests/render/cases/services-tls-enabled/expect.jq @@ -0,0 +1 @@ +.services == {"workers": 4, "tls": {"enabled": true}} diff --git a/tests/render/cases/services-tls-enabled/values.yaml b/tests/render/cases/services-tls-enabled/values.yaml new file mode 100644 index 0000000..079ed1e --- /dev/null +++ b/tests/render/cases/services-tls-enabled/values.yaml @@ -0,0 +1 @@ +config: {services: {tls: {enabled: true}}} From 1f4bbce2854ac7af8e47760776d796fd46c77d1a Mon Sep 17 00:00:00 2001 From: stheppi Date: Wed, 7 Oct 2026 22:40:57 +0100 Subject: [PATCH 08/19] test(chart): guard falsy services values from being dropped Covers compression.enabled=false, compression.level=0, workers=0 and empty cipherSuites. No template change: the pickSet-based rendering from the previous commit already handles them. Against the pre-fix template: compression.enabled=false rendered {enabled:false,type:"",level:-1}; level: 0 was rewritten to -1 by `default -1`; falsy-scalars compression rendered null. Signed-off-by: stheppi --- tests/render/cases/services-compression-disabled/expect.jq | 1 + tests/render/cases/services-compression-disabled/values.yaml | 1 + tests/render/cases/services-compression-level-zero/expect.jq | 1 + tests/render/cases/services-compression-level-zero/values.yaml | 1 + tests/render/cases/services-falsy-scalars/expect.jq | 1 + tests/render/cases/services-falsy-scalars/values.yaml | 1 + 6 files changed, 6 insertions(+) create mode 100644 tests/render/cases/services-compression-disabled/expect.jq create mode 100644 tests/render/cases/services-compression-disabled/values.yaml create mode 100644 tests/render/cases/services-compression-level-zero/expect.jq create mode 100644 tests/render/cases/services-compression-level-zero/values.yaml create mode 100644 tests/render/cases/services-falsy-scalars/expect.jq create mode 100644 tests/render/cases/services-falsy-scalars/values.yaml diff --git a/tests/render/cases/services-compression-disabled/expect.jq b/tests/render/cases/services-compression-disabled/expect.jq new file mode 100644 index 0000000..86ab706 --- /dev/null +++ b/tests/render/cases/services-compression-disabled/expect.jq @@ -0,0 +1 @@ +.services.compression == {"enabled": false} diff --git a/tests/render/cases/services-compression-disabled/values.yaml b/tests/render/cases/services-compression-disabled/values.yaml new file mode 100644 index 0000000..f9fa49a --- /dev/null +++ b/tests/render/cases/services-compression-disabled/values.yaml @@ -0,0 +1 @@ +config: {services: {compression: {enabled: false}}} diff --git a/tests/render/cases/services-compression-level-zero/expect.jq b/tests/render/cases/services-compression-level-zero/expect.jq new file mode 100644 index 0000000..899544f --- /dev/null +++ b/tests/render/cases/services-compression-level-zero/expect.jq @@ -0,0 +1 @@ +.services.compression == {"level": 0} diff --git a/tests/render/cases/services-compression-level-zero/values.yaml b/tests/render/cases/services-compression-level-zero/values.yaml new file mode 100644 index 0000000..3b6ae3a --- /dev/null +++ b/tests/render/cases/services-compression-level-zero/values.yaml @@ -0,0 +1 @@ +config: {services: {compression: {level: 0}}} diff --git a/tests/render/cases/services-falsy-scalars/expect.jq b/tests/render/cases/services-falsy-scalars/expect.jq new file mode 100644 index 0000000..31a7491 --- /dev/null +++ b/tests/render/cases/services-falsy-scalars/expect.jq @@ -0,0 +1 @@ +.services == {"workers": 0, "tls": {"requireClientCert": false, "cipherSuites": []}} diff --git a/tests/render/cases/services-falsy-scalars/values.yaml b/tests/render/cases/services-falsy-scalars/values.yaml new file mode 100644 index 0000000..dacf286 --- /dev/null +++ b/tests/render/cases/services-falsy-scalars/values.yaml @@ -0,0 +1 @@ +config: {services: {workers: 0, tls: {requireClientCert: false, cipherSuites: []}}} From 17760d027f0950cef4ad054c0674fcc8f28a14fa Mon Sep 17 00:00:00 2001 From: stheppi Date: Wed, 7 Oct 2026 22:42:04 +0100 Subject: [PATCH 09/19] refactor(chart): move pickSet helper to _helpers.tpl Signed-off-by: stheppi --- templates/_helpers.tpl | 17 ++++++++++++++++- templates/configmap.yaml | 14 -------------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/templates/_helpers.tpl b/templates/_helpers.tpl index 54f8229..4f8434a 100644 --- a/templates/_helpers.tpl +++ b/templates/_helpers.tpl @@ -135,4 +135,19 @@ Edge Proxy: configmap name */}} {{- define "nofire-edge.edgeProxy.configName" -}} {{- printf "%s-config" (include "nofire-edge.edgeProxy.fullname" .) }} -{{- end }} \ No newline at end of file +{{- end }} + +{{- /* +nofire-edge.pickSet: JSON object of the given keys that are set on src. A key is +kept when present, even if falsy (false, 0, "", []); an explicit null counts as unset. +*/ -}} +{{- define "nofire-edge.pickSet" -}} +{{- $out := dict -}} +{{- $src := .src | default dict -}} +{{- range $k := .keys -}} +{{- if and (hasKey $src $k) (not (kindIs "invalid" (index $src $k))) -}} +{{- $_ := set $out $k (index $src $k) -}} +{{- end -}} +{{- end -}} +{{- $out | toJson -}} +{{- end -}} diff --git a/templates/configmap.yaml b/templates/configmap.yaml index 222fdd3..a647363 100644 --- a/templates/configmap.yaml +++ b/templates/configmap.yaml @@ -1,17 +1,3 @@ -{{- /* -nofire-edge.pickSet: JSON object of the given keys that are set on src. A key is -kept when present, even if falsy (false, 0, "", []); an explicit null counts as unset. -*/ -}} -{{- define "nofire-edge.pickSet" -}} -{{- $out := dict -}} -{{- $src := .src | default dict -}} -{{- range $k := .keys -}} -{{- if and (hasKey $src $k) (not (kindIs "invalid" (index $src $k))) -}} -{{- $_ := set $out $k (index $src $k) -}} -{{- end -}} -{{- end -}} -{{- $out | toJson -}} -{{- end -}} apiVersion: v1 kind: ConfigMap metadata: From ab56e8db90c819d60ad0d5f99eac455aad3e9ce3 Mon Sep 17 00:00:00 2001 From: stheppi Date: Wed, 7 Oct 2026 22:42:24 +0100 Subject: [PATCH 10/19] docs(chart): document namespace scoping, netobs exclusions and services defaults Add commented config.kube.namespaceFilter, netobs.edgeExistenceTtl and netobs.excludeNamespaces examples (noting that excludeNamespaces replaces the Edge default). Correct the config.services comments to the Edge v3.6.0 defaults. The values-examples render case uncomments the examples and checks they render and pass the Edge's config loader. Signed-off-by: stheppi --- examples/production-values.yaml | 4 ++ tests/render/cases/values-examples/expect.jq | 6 +++ .../render/cases/values-examples/values.yaml | 37 +++++++++++++++ values.yaml | 47 +++++++++++++++---- 4 files changed, 84 insertions(+), 10 deletions(-) create mode 100644 tests/render/cases/values-examples/expect.jq create mode 100644 tests/render/cases/values-examples/values.yaml diff --git a/examples/production-values.yaml b/examples/production-values.yaml index 75bdb84..a2fc9b4 100644 --- a/examples/production-values.yaml +++ b/examples/production-values.yaml @@ -66,6 +66,10 @@ config: - "jobs" - "ingresses" - "namespaces" + # Scope the graph to specific namespaces (default: all). See values.yaml. + # namespaceFilter: + # mode: deny + # namespaces: ["kube-system"] # Enable publishing with real endpoints enablePublishing: false diff --git a/tests/render/cases/values-examples/expect.jq b/tests/render/cases/values-examples/expect.jq new file mode 100644 index 0000000..8bd87e0 --- /dev/null +++ b/tests/render/cases/values-examples/expect.jq @@ -0,0 +1,6 @@ +.kube.namespaceFilter.mode == "allow" +and .netobs.edgeExistenceTtl == "2h" +and .netobs.excludeNamespaces == ["kube-system", "monitoring"] +and .services.address == ":6000" +and .services.compression == {"enabled": true, "type": "gzip", "level": -1} +and (.services.tls.cipherSuites | length) == 4 diff --git a/tests/render/cases/values-examples/values.yaml b/tests/render/cases/values-examples/values.yaml new file mode 100644 index 0000000..a3db9f8 --- /dev/null +++ b/tests/render/cases/values-examples/values.yaml @@ -0,0 +1,37 @@ +# The documented examples from values.yaml, uncommented. +config: + kube: + namespaceFilter: + mode: allow + namespaces: + - production + - staging + netobs: + edgeExistenceTtl: "2h" + excludeNamespaces: + - kube-system + - monitoring + services: + address: ":6000" + maxConns: 100 + readTimeout: "30s" + writeTimeout: "10s" + tls: + enabled: false + certFile: "/etc/nofire/certs/tls.crt" + keyFile: "/etc/nofire/certs/tls.key" + caFile: "/etc/nofire/certs/ca.crt" + requireClientCert: false + minVersion: "1.2" + cipherSuites: + - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 + - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 + - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 + compression: + enabled: true + type: "gzip" + level: -1 + handshake: + timeout: "10s" + contentType: "protobuf:dnstap.Dnstap" diff --git a/values.yaml b/values.yaml index 55681de..6adc8d5 100644 --- a/values.yaml +++ b/values.yaml @@ -207,6 +207,17 @@ config: - "ingresses" - "namespaces" - "services" + # Restrict which namespaces enter the resource graph. Unset (or mode "none") + # means every namespace is graphed. The chart fails the render on invalid + # combinations, mirroring the Edge's own validation: + # - namespaces set with mode unset/""/"none" + # - mode "allow" or "deny" with an empty namespaces list + # - any other mode + # namespaceFilter: + # mode: allow # allow | deny | none + # namespaces: + # - production + # - staging # Causal analysis configuration enableCausalAnalysis: false causal: @@ -216,24 +227,31 @@ config: # Services configuration services: workers: 4 - # address: "127.0.0.1:6000" - # maxConns: 10 + # Keys you leave out use the Edge defaults shown below (set a key to override + # it; partial tls/compression/handshake objects are merged into the defaults). + # DNSTap listens on :6000 even when address is unset (matches service.dnstapPort). + # address: ":6000" + # maxConns: 100 # readTimeout: "30s" - # writeTimeout: "30s" + # writeTimeout: "10s" # tls: # enabled: false - # certFile: "" - # keyFile: "" - # caFile: "" + # certFile: "/etc/nofire/certs/tls.crt" + # keyFile: "/etc/nofire/certs/tls.key" + # caFile: "/etc/nofire/certs/ca.crt" # requireClientCert: false # minVersion: "1.2" - # cipherSuites: [] + # cipherSuites: # setting this replaces the default list + # - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 + # - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 + # - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + # - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 # compression: - # enabled: false - # type: "none" + # enabled: true + # type: "gzip" # none | gzip # level: -1 # handshake: - # timeout: "5s" + # timeout: "10s" # contentType: "protobuf:dnstap.Dnstap" # Network observability — L7 dependency detection via Hubble, Istio, or Caretta. @@ -244,6 +262,15 @@ config: prometheusUrl: "" # empty = in-cluster discovery source: "" # empty = auto-detect; or "hubble" | "istio" | "caretta" prometheusTimeout: "10s" + # How long a network edge is kept after it stops being observed (Edge default: 2h). + # edgeExistenceTtl: "2h" + # Namespaces whose network flows are dropped before collection. Edge default: + # [kube-system, monitoring]. Setting this REPLACES the default, it does not + # append (use [] to exclude nothing). Namespaces listed in a deny-mode + # kube.namespaceFilter are added by the Edge on top of this list. + # excludeNamespaces: + # - kube-system + # - monitoring # Publishing configuration enablePublishing: true From 46651cfe2107749efcbf14469cf765158b75f3ab Mon Sep 17 00:00:00 2001 From: stheppi Date: Wed, 7 Oct 2026 22:44:59 +0100 Subject: [PATCH 11/19] test(chart): add kind end-to-end test for the Edge ConfigMap Installs the chart on a throwaway kind cluster and checks the rendered ConfigMap, that the Edge accepts it and rolls out, that its startup log shows the namespace filter, that an upgrade to deny changes checksum/config and rolls the pod, and that an invalid filter fails at render. Signed-off-by: stheppi --- ci/kind-values.yaml | 15 +++++++ tests/e2e/kind.sh | 96 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 ci/kind-values.yaml create mode 100755 tests/e2e/kind.sh diff --git a/ci/kind-values.yaml b/ci/kind-values.yaml new file mode 100644 index 0000000..297a8be --- /dev/null +++ b/ci/kind-values.yaml @@ -0,0 +1,15 @@ +# Values for tests/e2e/kind.sh: a local kind cluster with no backend. +image: + pullPolicy: IfNotPresent + +config: + # Publishing needs a graph URL and API key; the e2e only needs the Edge to + # accept its config and start. + enablePublishing: false + kube: + namespaceFilter: + mode: allow + namespaces: [default] + netobs: + excludeNamespaces: [x] + edgeExistenceTtl: 1h diff --git a/tests/e2e/kind.sh b/tests/e2e/kind.sh new file mode 100755 index 0000000..dfbc375 --- /dev/null +++ b/tests/e2e/kind.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# End-to-end check on a throwaway kind cluster: the installed ConfigMap carries +# the namespace scoping keys, the Edge accepts the config and rolls out, a +# values change rolls the pod, and an invalid filter is rejected at render. +# Needs docker, kind, kubectl, helm, jq. Usage: tests/e2e/kind.sh +set -euo pipefail + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cluster=edge-chart-e2e +release=e2e +export KUBECONFIG +KUBECONFIG="$(mktemp)" + +cleanup() { + kind delete cluster --name "$cluster" >/dev/null 2>&1 || true + rm -f "$KUBECONFIG" +} +trap cleanup EXIT + +step() { echo "==> $*"; } +die() { echo "FAIL: $*" >&2; exit 1; } + +repo="$(yq -r .image.repository "$root/values.yaml")" +tag="$(yq -r .image.tag "$root/values.yaml")" +image="$repo:$tag" + +step "pull $image" +docker image inspect "$image" >/dev/null 2>&1 || docker pull "$image" + +step "create kind cluster $cluster" +kind delete cluster --name "$cluster" >/dev/null 2>&1 || true +kind create cluster --name "$cluster" --kubeconfig "$KUBECONFIG" --wait 120s +# `kind load docker-image` fails on multi-platform images in Docker's containerd +# store (missing content digest); save only the node's platform instead. +case "$(uname -m)" in arm64|aarch64) platform=linux/arm64 ;; *) platform=linux/amd64 ;; esac +archive="$(mktemp)" +docker save --platform "$platform" "$image" -o "$archive" 2>/dev/null || docker save "$image" -o "$archive" +kind load image-archive "$archive" --name "$cluster" +rm -f "$archive" + +cm_config() { + local cm + cm="$(kubectl get cm -l "app.kubernetes.io/instance=$release" -o name | grep -- '-config$')" + kubectl get "$cm" -o jsonpath='{.data.config\.json}' +} +checksum() { + kubectl get deploy -l "app.kubernetes.io/instance=$release" \ + -o jsonpath='{.items[0].spec.template.metadata.annotations.checksum/config}' +} +assert() { # description, config.json, jq expression + jq -e "$3" <<<"$2" >/dev/null || die "$1: jq '$3' failed on: $2" + echo "ok: $1" +} + +step "helm install (namespaceFilter allow [default])" +helm install "$release" "$root" -f "$root/ci/kind-values.yaml" --wait=false +cfg="$(cm_config)" +assert "installed ConfigMap has allow filter" "$cfg" \ + '.kube.namespaceFilter == {"mode":"allow","namespaces":["default"]}' +assert "installed ConfigMap has netobs keys" "$cfg" \ + '.netobs.edgeExistenceTtl == "1h" and .netobs.excludeNamespaces == ["x"]' +before="$(checksum)" + +step "rollout (the Edge accepted the config)" +kubectl rollout status deploy -l "app.kubernetes.io/instance=$release" --timeout=180s \ + || { kubectl logs -l "app.kubernetes.io/instance=$release" --tail=50 || true; die "rollout failed"; } +# The Edge logs its loaded config at startup (cmd/edge/main.go); there is no +# dedicated namespace-filter log line. +logs="$(kubectl logs -l "app.kubernetes.io/instance=$release" --tail=-1)" +grep -q 'Configuration loaded successfully' <<<"$logs" || die "no 'Configuration loaded' line in logs" +grep -Eq '"mode": ?"allow"' <<<"$logs" || die "startup log does not show namespaceFilter mode allow" +echo "ok: startup log shows the allow filter" + +step "helm upgrade to deny" +helm upgrade "$release" "$root" -f "$root/ci/kind-values.yaml" \ + --set config.kube.namespaceFilter.mode=deny +after="$(checksum)" +[[ -n "$before" && "$before" != "$after" ]] || die "checksum/config did not change ($before -> $after)" +echo "ok: checksum/config changed" +assert "upgraded ConfigMap has deny filter" "$(cm_config)" '.kube.namespaceFilter.mode == "deny"' +kubectl rollout status deploy -l "app.kubernetes.io/instance=$release" --timeout=180s \ + || die "rollout after upgrade failed" +logs="$(kubectl logs -l "app.kubernetes.io/instance=$release" --tail=-1)" +grep -Eq '"mode": ?"deny"' <<<"$logs" || die "pod log does not show mode deny after upgrade" +echo "ok: new pod runs with the deny filter" + +step "negative: allow with no namespaces is rejected at render" +if out="$(helm install bad "$root" -f "$root/ci/kind-values.yaml" \ + --set config.kube.namespaceFilter.namespaces=null 2>&1)"; then + die "install with an empty allow list succeeded" +fi +grep -q 'config.kube.namespaceFilter.mode is "allow" but config.kube.namespaceFilter.namespaces is empty' <<<"$out" \ + || die "unexpected error: $out" +echo "ok: invalid filter rejected" + +step "all e2e checks passed" From 1a124b1892e7b32286fcc651a2d1edc33c22dd69 Mon Sep 17 00:00:00 2001 From: stheppi Date: Wed, 7 Oct 2026 22:51:38 +0100 Subject: [PATCH 12/19] fix(chart): coerce services scalars to the types the Edge expects Values given as strings (--set-string, quoted YAML) rendered as JSON strings and crashed the Edge's decoder, e.g. workers "8". Each key is now coerced per type (int64, bool, string, list) with a clear failure for values that cannot be, and objects are built without the float64 fromJson round-trip. An empty services.address is treated as unset (Edge default :6000) as before, and non-map tls/compression/handshake fail with a clear message. Before: services-quoted-scalars FAIL (workers rendered "8"), services-empty-address FAIL (address "" written, disabling DNSTap), services-fail-tls-not-map / -tls-false FAIL (raw Go template error) Signed-off-by: stheppi --- templates/_helpers.tpl | 54 ++++++++++++++++--- templates/configmap.yaml | 19 +------ .../services-address-empty-objects/expect.jq | 1 + .../values.yaml | 1 + .../cases/services-empty-address/expect.jq | 1 + .../cases/services-empty-address/values.yaml | 1 + .../services-fail-bool-not-bool/expect-fail | 1 + .../services-fail-bool-not-bool/values.yaml | 1 + .../cases/services-fail-tls-false/expect-fail | 1 + .../cases/services-fail-tls-false/values.yaml | 1 + .../services-fail-tls-not-map/expect-fail | 1 + .../services-fail-tls-not-map/values.yaml | 1 + .../services-fail-workers-not-int/expect-fail | 1 + .../services-fail-workers-not-int/values.yaml | 1 + .../cases/services-quoted-scalars/expect.jq | 3 ++ .../cases/services-quoted-scalars/values.yaml | 6 +++ 16 files changed, 70 insertions(+), 24 deletions(-) create mode 100644 tests/render/cases/services-address-empty-objects/expect.jq create mode 100644 tests/render/cases/services-address-empty-objects/values.yaml create mode 100644 tests/render/cases/services-empty-address/expect.jq create mode 100644 tests/render/cases/services-empty-address/values.yaml create mode 100644 tests/render/cases/services-fail-bool-not-bool/expect-fail create mode 100644 tests/render/cases/services-fail-bool-not-bool/values.yaml create mode 100644 tests/render/cases/services-fail-tls-false/expect-fail create mode 100644 tests/render/cases/services-fail-tls-false/values.yaml create mode 100644 tests/render/cases/services-fail-tls-not-map/expect-fail create mode 100644 tests/render/cases/services-fail-tls-not-map/values.yaml create mode 100644 tests/render/cases/services-fail-workers-not-int/expect-fail create mode 100644 tests/render/cases/services-fail-workers-not-int/values.yaml create mode 100644 tests/render/cases/services-quoted-scalars/expect.jq create mode 100644 tests/render/cases/services-quoted-scalars/values.yaml diff --git a/templates/_helpers.tpl b/templates/_helpers.tpl index 4f8434a..5d86970 100644 --- a/templates/_helpers.tpl +++ b/templates/_helpers.tpl @@ -138,15 +138,57 @@ Edge Proxy: configmap name {{- end }} {{- /* -nofire-edge.pickSet: JSON object of the given keys that are set on src. A key is -kept when present, even if falsy (false, 0, "", []); an explicit null counts as unset. +nofire-edge.typedPick copies the keys named in .spec from .src into the dict +.out, coercing each to the type the Edge expects. Values given as strings (for +example via --set-string) would otherwise crash the Edge's JSON decoder. +A key is kept when present, even if falsy (false, 0, "", []); an explicit null +counts as unset. .spec maps key -> int | bool | string | nestring | strlist +(nestring: an empty string counts as unset). .path prefixes error messages. +Writes nothing itself: call it as `{{- $_ := include "nofire-edge.typedPick" (dict ...) }}`. */ -}} -{{- define "nofire-edge.pickSet" -}} -{{- $out := dict -}} +{{- define "nofire-edge.typedPick" -}} {{- $src := .src | default dict -}} -{{- range $k := .keys -}} +{{- $path := .path -}} +{{- range $k, $t := .spec -}} {{- if and (hasKey $src $k) (not (kindIs "invalid" (index $src $k))) -}} -{{- $_ := set $out $k (index $src $k) -}} +{{- $v := index $src $k -}} +{{- $p := printf "%s.%s" $path $k -}} +{{- if eq $t "int" -}} +{{- if not (regexMatch "^-?[0-9]+$" (toString $v)) -}}{{- fail (printf "%s must be an integer (got %v)" $p $v) -}}{{- end -}} +{{- $_ := set $.out $k (int64 $v) -}} +{{- else if eq $t "bool" -}} +{{- if kindIs "bool" $v -}}{{- $_ := set $.out $k $v -}} +{{- else if has (toString $v) (list "true" "false") -}}{{- $_ := set $.out $k (eq (toString $v) "true") -}} +{{- else -}}{{- fail (printf "%s must be a boolean (got %v)" $p $v) -}}{{- end -}} +{{- else if or (eq $t "string") (eq $t "nestring") -}} +{{- if or (kindIs "map" $v) (kindIs "slice" $v) -}}{{- fail (printf "%s must be a string" $p) -}}{{- end -}} +{{- if or (eq $t "string") (ne (toString $v) "") -}}{{- $_ := set $.out $k (toString $v) -}}{{- end -}} +{{- else if eq $t "strlist" -}} +{{- if not (kindIs "slice" $v) -}}{{- fail (printf "%s must be a list" $p) -}}{{- end -}} +{{- range $e := $v -}}{{- if not (kindIs "string" $e) -}}{{- fail (printf "%s entry %v must be a string" $p $e) -}}{{- end -}}{{- end -}} +{{- $_ := set $.out $k $v -}} +{{- end -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{- /* nofire-edge.servicesJson: the Edge "services" object, with only the keys the user set. */ -}} +{{- define "nofire-edge.servicesJson" -}} +{{- $s := .Values.config.services | default dict -}} +{{- if not (kindIs "map" $s) -}}{{- fail "config.services must be a map" -}}{{- end -}} +{{- $out := dict -}} +{{- $_ := include "nofire-edge.typedPick" (dict "src" $s "path" "config.services" "out" $out "spec" (dict "workers" "int" "address" "nestring" "maxConns" "int" "readTimeout" "string" "writeTimeout" "string")) -}} +{{- $objects := dict + "tls" (dict "enabled" "bool" "certFile" "string" "keyFile" "string" "caFile" "string" "requireClientCert" "bool" "minVersion" "string" "cipherSuites" "strlist") + "compression" (dict "enabled" "bool" "type" "string" "level" "int") + "handshake" (dict "timeout" "string" "contentType" "string") -}} +{{- range $name, $spec := $objects -}} +{{- $v := get $s $name -}} +{{- if and (hasKey $s $name) (not (kindIs "invalid" $v)) (not (kindIs "map" $v)) -}}{{- fail (printf "config.services.%s must be a map" $name) -}}{{- end -}} +{{- if kindIs "map" $v -}} +{{- $o := dict -}} +{{- $_ := include "nofire-edge.typedPick" (dict "src" $v "path" (printf "config.services.%s" $name) "out" $o "spec" $spec) -}} +{{- if $o -}}{{- $_ := set $out $name $o -}}{{- end -}} {{- end -}} {{- end -}} {{- $out | toJson -}} diff --git a/templates/configmap.yaml b/templates/configmap.yaml index a647363..9872a6e 100644 --- a/templates/configmap.yaml +++ b/templates/configmap.yaml @@ -59,24 +59,7 @@ data: "sampleRate": {{ .Values.config.sentry.sampleRate | default 1.0 }}, "tracesSampleRate": {{ .Values.config.sentry.tracesSampleRate | default 0.0 }} }, - "services": { - {{- $s := .Values.config.services | default dict }} - {{- $scalars := include "nofire-edge.pickSet" (dict "src" $s "keys" (list "workers" "address" "maxConns" "readTimeout" "writeTimeout")) | fromJson }} - {{- $first := true }} - {{- range $k, $v := $scalars }} - {{- if not $first }},{{ end }} - {{ $k | quote }}: {{ $v | toJson }} - {{- $first = false }} - {{- end }} - {{- range $name, $keys := dict "tls" (list "enabled" "certFile" "keyFile" "caFile" "requireClientCert" "minVersion" "cipherSuites") "compression" (list "enabled" "type" "level") "handshake" (list "timeout" "contentType") }} - {{- $obj := include "nofire-edge.pickSet" (dict "src" (get $s $name) "keys" $keys) | fromJson }} - {{- if $obj }} - {{- if not $first }},{{ end }} - {{ $name | quote }}: {{ $obj | toJson }} - {{- $first = false }} - {{- end }} - {{- end }} - }, + "services": {{ include "nofire-edge.servicesJson" . }}, "enablePublishing": {{ .Values.config.enablePublishing }}, "publisher": { "apiKey": {{ if .Values.config.publisher.apiKeySecret.enabled }}"{NOFIRE_API_KEY}"{{ else }}{{ .Values.config.publisher.apiKey | default "" | quote }}{{ end }}, diff --git a/tests/render/cases/services-address-empty-objects/expect.jq b/tests/render/cases/services-address-empty-objects/expect.jq new file mode 100644 index 0000000..d18c8fa --- /dev/null +++ b/tests/render/cases/services-address-empty-objects/expect.jq @@ -0,0 +1 @@ +.services == {"workers": 4, "address": ":6000"} diff --git a/tests/render/cases/services-address-empty-objects/values.yaml b/tests/render/cases/services-address-empty-objects/values.yaml new file mode 100644 index 0000000..fd3f703 --- /dev/null +++ b/tests/render/cases/services-address-empty-objects/values.yaml @@ -0,0 +1 @@ +config: {services: {address: ":6000", tls: {}, compression: {}, handshake: {}}} diff --git a/tests/render/cases/services-empty-address/expect.jq b/tests/render/cases/services-empty-address/expect.jq new file mode 100644 index 0000000..fc2af9b --- /dev/null +++ b/tests/render/cases/services-empty-address/expect.jq @@ -0,0 +1 @@ +.services == {"workers": 4} diff --git a/tests/render/cases/services-empty-address/values.yaml b/tests/render/cases/services-empty-address/values.yaml new file mode 100644 index 0000000..e5474a7 --- /dev/null +++ b/tests/render/cases/services-empty-address/values.yaml @@ -0,0 +1 @@ +config: {services: {address: ""}} diff --git a/tests/render/cases/services-fail-bool-not-bool/expect-fail b/tests/render/cases/services-fail-bool-not-bool/expect-fail new file mode 100644 index 0000000..aa85874 --- /dev/null +++ b/tests/render/cases/services-fail-bool-not-bool/expect-fail @@ -0,0 +1 @@ +config\.services\.tls\.enabled must be a boolean diff --git a/tests/render/cases/services-fail-bool-not-bool/values.yaml b/tests/render/cases/services-fail-bool-not-bool/values.yaml new file mode 100644 index 0000000..e84f3e8 --- /dev/null +++ b/tests/render/cases/services-fail-bool-not-bool/values.yaml @@ -0,0 +1 @@ +config: {services: {tls: {enabled: maybe}}} diff --git a/tests/render/cases/services-fail-tls-false/expect-fail b/tests/render/cases/services-fail-tls-false/expect-fail new file mode 100644 index 0000000..fe9aea3 --- /dev/null +++ b/tests/render/cases/services-fail-tls-false/expect-fail @@ -0,0 +1 @@ +config\.services\.tls must be a map diff --git a/tests/render/cases/services-fail-tls-false/values.yaml b/tests/render/cases/services-fail-tls-false/values.yaml new file mode 100644 index 0000000..5ad483c --- /dev/null +++ b/tests/render/cases/services-fail-tls-false/values.yaml @@ -0,0 +1 @@ +config: {services: {tls: false}} diff --git a/tests/render/cases/services-fail-tls-not-map/expect-fail b/tests/render/cases/services-fail-tls-not-map/expect-fail new file mode 100644 index 0000000..fe9aea3 --- /dev/null +++ b/tests/render/cases/services-fail-tls-not-map/expect-fail @@ -0,0 +1 @@ +config\.services\.tls must be a map diff --git a/tests/render/cases/services-fail-tls-not-map/values.yaml b/tests/render/cases/services-fail-tls-not-map/values.yaml new file mode 100644 index 0000000..3414dc5 --- /dev/null +++ b/tests/render/cases/services-fail-tls-not-map/values.yaml @@ -0,0 +1 @@ +config: {services: {tls: true}} diff --git a/tests/render/cases/services-fail-workers-not-int/expect-fail b/tests/render/cases/services-fail-workers-not-int/expect-fail new file mode 100644 index 0000000..abbddcf --- /dev/null +++ b/tests/render/cases/services-fail-workers-not-int/expect-fail @@ -0,0 +1 @@ +config\.services\.workers must be an integer diff --git a/tests/render/cases/services-fail-workers-not-int/values.yaml b/tests/render/cases/services-fail-workers-not-int/values.yaml new file mode 100644 index 0000000..1b5385e --- /dev/null +++ b/tests/render/cases/services-fail-workers-not-int/values.yaml @@ -0,0 +1 @@ +config: {services: {workers: abc}} diff --git a/tests/render/cases/services-quoted-scalars/expect.jq b/tests/render/cases/services-quoted-scalars/expect.jq new file mode 100644 index 0000000..8edfb80 --- /dev/null +++ b/tests/render/cases/services-quoted-scalars/expect.jq @@ -0,0 +1,3 @@ +.services == {"workers": 8, "maxConns": 100, + "tls": {"enabled": true, "requireClientCert": false}, + "compression": {"enabled": false, "level": -1}} diff --git a/tests/render/cases/services-quoted-scalars/values.yaml b/tests/render/cases/services-quoted-scalars/values.yaml new file mode 100644 index 0000000..d4321b9 --- /dev/null +++ b/tests/render/cases/services-quoted-scalars/values.yaml @@ -0,0 +1,6 @@ +config: + services: + workers: "8" + maxConns: "100" + tls: {enabled: "true", requireClientCert: "false"} + compression: {enabled: "false", level: "-1"} From f8b86a28d56674074321aa21e1f41e82491e50aa Mon Sep 17 00:00:00 2001 From: stheppi Date: Wed, 7 Oct 2026 22:52:03 +0100 Subject: [PATCH 13/19] fix(chart): validate namespace names and config types before rendering The namespaceFilter guard accepted names the Edge rejects (uppercase, underscores, non-strings, empty, over 63 chars), which crashlooped the pod. Each entry is now checked as a DNS-1123 label. Wrong-typed namespaceFilter, excludeNamespaces and edgeExistenceTtl fail with a clear message instead of a Go template error or a bad config, and null mode/namespaces count as unset. Expect-fail patterns and unittest patterns now match their own guard's wording so a test cannot pass on the wrong guard. run.sh normalises a relative EDGE_SRC. Before: ns-filter-fail-uppercase/-nonstring/-empty-entry/-too-long FAIL (render succeeded), ns-filter-null-fields FAIL (nulls rendered), exclude-ns-fail-string and ttl-fail-number FAIL (render succeeded) Signed-off-by: stheppi --- templates/configmap.yaml | 27 ++++++++++++++----- tests/configmap_test.yaml | 6 ++--- .../cases/exclude-ns-fail-string/expect-fail | 1 + .../cases/exclude-ns-fail-string/values.yaml | 1 + .../cases/ns-filter-duplicates/expect.jq | 1 + .../cases/ns-filter-duplicates/values.yaml | 1 + .../ns-filter-fail-allow-empty/expect-fail | 2 +- .../ns-filter-fail-deny-missing/expect-fail | 2 +- .../ns-filter-fail-empty-entry/expect-fail | 1 + .../ns-filter-fail-empty-entry/values.yaml | 1 + .../expect-fail | 2 +- .../expect-fail | 2 +- .../ns-filter-fail-nonstring/expect-fail | 1 + .../ns-filter-fail-nonstring/values.yaml | 1 + .../cases/ns-filter-fail-not-map/expect-fail | 1 + .../cases/ns-filter-fail-not-map/values.yaml | 1 + .../cases/ns-filter-fail-too-long/expect-fail | 1 + .../cases/ns-filter-fail-too-long/values.yaml | 1 + .../ns-filter-fail-unknown-mode/expect-fail | 2 +- .../ns-filter-fail-uppercase/expect-fail | 1 + .../ns-filter-fail-uppercase/values.yaml | 1 + .../cases/ns-filter-null-fields/expect.jq | 1 + .../cases/ns-filter-null-fields/values.yaml | 1 + .../render/cases/ttl-fail-number/expect-fail | 1 + .../render/cases/ttl-fail-number/values.yaml | 1 + tests/render/run.sh | 1 + 26 files changed, 47 insertions(+), 15 deletions(-) create mode 100644 tests/render/cases/exclude-ns-fail-string/expect-fail create mode 100644 tests/render/cases/exclude-ns-fail-string/values.yaml create mode 100644 tests/render/cases/ns-filter-duplicates/expect.jq create mode 100644 tests/render/cases/ns-filter-duplicates/values.yaml create mode 100644 tests/render/cases/ns-filter-fail-empty-entry/expect-fail create mode 100644 tests/render/cases/ns-filter-fail-empty-entry/values.yaml create mode 100644 tests/render/cases/ns-filter-fail-nonstring/expect-fail create mode 100644 tests/render/cases/ns-filter-fail-nonstring/values.yaml create mode 100644 tests/render/cases/ns-filter-fail-not-map/expect-fail create mode 100644 tests/render/cases/ns-filter-fail-not-map/values.yaml create mode 100644 tests/render/cases/ns-filter-fail-too-long/expect-fail create mode 100644 tests/render/cases/ns-filter-fail-too-long/values.yaml create mode 100644 tests/render/cases/ns-filter-fail-uppercase/expect-fail create mode 100644 tests/render/cases/ns-filter-fail-uppercase/values.yaml create mode 100644 tests/render/cases/ns-filter-null-fields/expect.jq create mode 100644 tests/render/cases/ns-filter-null-fields/values.yaml create mode 100644 tests/render/cases/ttl-fail-number/expect-fail create mode 100644 tests/render/cases/ttl-fail-number/values.yaml diff --git a/templates/configmap.yaml b/templates/configmap.yaml index 9872a6e..6f7e448 100644 --- a/templates/configmap.yaml +++ b/templates/configmap.yaml @@ -24,14 +24,16 @@ data: {{ $resource | quote }} {{- end }} ] - {{- $nf := pick (.Values.config.kube.namespaceFilter | default dict) "mode" "namespaces" }} + {{- $nfRaw := .Values.config.kube.namespaceFilter }} + {{- if and (hasKey .Values.config.kube "namespaceFilter") (not (kindIs "invalid" $nfRaw)) (not (kindIs "map" $nfRaw)) }} + {{- fail "config.kube.namespaceFilter must be a map" }} + {{- end }} + {{- $nf := dict }} + {{- $_ := include "nofire-edge.typedPick" (dict "src" $nfRaw "path" "config.kube.namespaceFilter" "out" $nf "spec" (dict "mode" "string" "namespaces" "strlist")) }} {{- if $nf }} {{- /* Mirror the Edge's validation so a bad filter fails at install, not as a crashloop. */}} - {{- $mode := printf "%v" (get $nf "mode" | default "") }} + {{- $mode := get $nf "mode" | default "" }} {{- $namespaces := get $nf "namespaces" | default list }} - {{- if not (kindIs "slice" $namespaces) }} - {{- fail "config.kube.namespaceFilter.namespaces must be a list" }} - {{- end }} {{- if has $mode (list "" "none") }} {{- if $namespaces }} {{- fail (printf "config.kube.namespaceFilter.namespaces is set but mode %q disables filtering; set config.kube.namespaceFilter.mode to 'allow' or 'deny'" $mode) }} @@ -40,6 +42,11 @@ data: {{- if not $namespaces }} {{- fail (printf "config.kube.namespaceFilter.mode is %q but config.kube.namespaceFilter.namespaces is empty" $mode) }} {{- end }} + {{- range $ns := $namespaces }} + {{- if not (and (le (len $ns) 63) (regexMatch "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" $ns)) }} + {{- fail (printf "config.kube.namespaceFilter.namespaces entry %q is not a valid namespace name (a lowercase DNS-1123 label of at most 63 characters)" $ns) }} + {{- end }} + {{- end }} {{- else }} {{- fail (printf "config.kube.namespaceFilter.mode must be one of '', 'none', 'allow', 'deny' (got %q)" $mode) }} {{- end }}, @@ -82,10 +89,16 @@ data: "prometheusUrl": {{ .Values.config.netobs.prometheusUrl | default "" | quote }}, "source": {{ .Values.config.netobs.source | default "" | quote }}, "prometheusTimeout": {{ .Values.config.netobs.prometheusTimeout | default "10s" | quote }} - {{- if and (hasKey .Values.config.netobs "edgeExistenceTtl") (not (kindIs "invalid" .Values.config.netobs.edgeExistenceTtl)) }}, + {{- if and (hasKey .Values.config.netobs "edgeExistenceTtl") (not (kindIs "invalid" .Values.config.netobs.edgeExistenceTtl)) }} + {{- if not (kindIs "string" .Values.config.netobs.edgeExistenceTtl) }} + {{- fail (printf "config.netobs.edgeExistenceTtl must be a duration string like 2h (got %v)" .Values.config.netobs.edgeExistenceTtl) }} + {{- end }}, "edgeExistenceTtl": {{ .Values.config.netobs.edgeExistenceTtl | quote }} {{- end }} - {{- if and (hasKey .Values.config.netobs "excludeNamespaces") (not (kindIs "invalid" .Values.config.netobs.excludeNamespaces)) }}, + {{- if and (hasKey .Values.config.netobs "excludeNamespaces") (not (kindIs "invalid" .Values.config.netobs.excludeNamespaces)) }} + {{- if not (kindIs "slice" .Values.config.netobs.excludeNamespaces) }} + {{- fail "config.netobs.excludeNamespaces must be a list" }} + {{- end }}, "excludeNamespaces": {{ .Values.config.netobs.excludeNamespaces | toJson }} {{- end }} }, diff --git a/tests/configmap_test.yaml b/tests/configmap_test.yaml index 69e1669..4ea8714 100644 --- a/tests/configmap_test.yaml +++ b/tests/configmap_test.yaml @@ -17,14 +17,14 @@ tests: config.kube.namespaceFilter.namespaces: [a] asserts: - failedTemplate: - errorPattern: config\.kube\.namespaceFilter\.mode + errorPattern: disables filtering - it: fails when mode is allow with no namespaces set: config.kube.namespaceFilter.mode: allow asserts: - failedTemplate: - errorPattern: config\.kube\.namespaceFilter\.namespaces + errorPattern: mode is "allow" but - it: fails on an unknown mode set: @@ -32,4 +32,4 @@ tests: config.kube.namespaceFilter.namespaces: [a] asserts: - failedTemplate: - errorPattern: config\.kube\.namespaceFilter\.mode + errorPattern: must be one of diff --git a/tests/render/cases/exclude-ns-fail-string/expect-fail b/tests/render/cases/exclude-ns-fail-string/expect-fail new file mode 100644 index 0000000..3d6562f --- /dev/null +++ b/tests/render/cases/exclude-ns-fail-string/expect-fail @@ -0,0 +1 @@ +config\.netobs\.excludeNamespaces must be a list diff --git a/tests/render/cases/exclude-ns-fail-string/values.yaml b/tests/render/cases/exclude-ns-fail-string/values.yaml new file mode 100644 index 0000000..3573fb9 --- /dev/null +++ b/tests/render/cases/exclude-ns-fail-string/values.yaml @@ -0,0 +1 @@ +config: {netobs: {excludeNamespaces: foo}} diff --git a/tests/render/cases/ns-filter-duplicates/expect.jq b/tests/render/cases/ns-filter-duplicates/expect.jq new file mode 100644 index 0000000..ab4cd07 --- /dev/null +++ b/tests/render/cases/ns-filter-duplicates/expect.jq @@ -0,0 +1 @@ +.kube.namespaceFilter == {"mode": "allow", "namespaces": ["a", "a"]} diff --git a/tests/render/cases/ns-filter-duplicates/values.yaml b/tests/render/cases/ns-filter-duplicates/values.yaml new file mode 100644 index 0000000..2582b66 --- /dev/null +++ b/tests/render/cases/ns-filter-duplicates/values.yaml @@ -0,0 +1 @@ +config: {kube: {namespaceFilter: {mode: allow, namespaces: [a, a]}}} diff --git a/tests/render/cases/ns-filter-fail-allow-empty/expect-fail b/tests/render/cases/ns-filter-fail-allow-empty/expect-fail index 4a0503c..3f19409 100644 --- a/tests/render/cases/ns-filter-fail-allow-empty/expect-fail +++ b/tests/render/cases/ns-filter-fail-allow-empty/expect-fail @@ -1 +1 @@ -config\.kube\.namespaceFilter\.namespaces +mode is "allow" but diff --git a/tests/render/cases/ns-filter-fail-deny-missing/expect-fail b/tests/render/cases/ns-filter-fail-deny-missing/expect-fail index 4a0503c..47a7bbb 100644 --- a/tests/render/cases/ns-filter-fail-deny-missing/expect-fail +++ b/tests/render/cases/ns-filter-fail-deny-missing/expect-fail @@ -1 +1 @@ -config\.kube\.namespaceFilter\.namespaces +mode is "deny" but diff --git a/tests/render/cases/ns-filter-fail-empty-entry/expect-fail b/tests/render/cases/ns-filter-fail-empty-entry/expect-fail new file mode 100644 index 0000000..0673c0a --- /dev/null +++ b/tests/render/cases/ns-filter-fail-empty-entry/expect-fail @@ -0,0 +1 @@ +entry "" is not a valid namespace name diff --git a/tests/render/cases/ns-filter-fail-empty-entry/values.yaml b/tests/render/cases/ns-filter-fail-empty-entry/values.yaml new file mode 100644 index 0000000..161e456 --- /dev/null +++ b/tests/render/cases/ns-filter-fail-empty-entry/values.yaml @@ -0,0 +1 @@ +config: {kube: {namespaceFilter: {mode: deny, namespaces: [""]}}} diff --git a/tests/render/cases/ns-filter-fail-namespaces-no-mode/expect-fail b/tests/render/cases/ns-filter-fail-namespaces-no-mode/expect-fail index 2bc2cc0..41564e5 100644 --- a/tests/render/cases/ns-filter-fail-namespaces-no-mode/expect-fail +++ b/tests/render/cases/ns-filter-fail-namespaces-no-mode/expect-fail @@ -1 +1 @@ -config\.kube\.namespaceFilter\.mode +disables filtering diff --git a/tests/render/cases/ns-filter-fail-none-with-namespaces/expect-fail b/tests/render/cases/ns-filter-fail-none-with-namespaces/expect-fail index 2bc2cc0..c64ccf7 100644 --- a/tests/render/cases/ns-filter-fail-none-with-namespaces/expect-fail +++ b/tests/render/cases/ns-filter-fail-none-with-namespaces/expect-fail @@ -1 +1 @@ -config\.kube\.namespaceFilter\.mode +mode "none" disables filtering diff --git a/tests/render/cases/ns-filter-fail-nonstring/expect-fail b/tests/render/cases/ns-filter-fail-nonstring/expect-fail new file mode 100644 index 0000000..5f9e0b7 --- /dev/null +++ b/tests/render/cases/ns-filter-fail-nonstring/expect-fail @@ -0,0 +1 @@ +namespaces entry .* must be a string diff --git a/tests/render/cases/ns-filter-fail-nonstring/values.yaml b/tests/render/cases/ns-filter-fail-nonstring/values.yaml new file mode 100644 index 0000000..6d8ed46 --- /dev/null +++ b/tests/render/cases/ns-filter-fail-nonstring/values.yaml @@ -0,0 +1 @@ +config: {kube: {namespaceFilter: {mode: allow, namespaces: [1, true]}}} diff --git a/tests/render/cases/ns-filter-fail-not-map/expect-fail b/tests/render/cases/ns-filter-fail-not-map/expect-fail new file mode 100644 index 0000000..dacf41c --- /dev/null +++ b/tests/render/cases/ns-filter-fail-not-map/expect-fail @@ -0,0 +1 @@ +config\.kube\.namespaceFilter must be a map diff --git a/tests/render/cases/ns-filter-fail-not-map/values.yaml b/tests/render/cases/ns-filter-fail-not-map/values.yaml new file mode 100644 index 0000000..8b2af3d --- /dev/null +++ b/tests/render/cases/ns-filter-fail-not-map/values.yaml @@ -0,0 +1 @@ +config: {kube: {namespaceFilter: foo}} diff --git a/tests/render/cases/ns-filter-fail-too-long/expect-fail b/tests/render/cases/ns-filter-fail-too-long/expect-fail new file mode 100644 index 0000000..d94f3bf --- /dev/null +++ b/tests/render/cases/ns-filter-fail-too-long/expect-fail @@ -0,0 +1 @@ +is not a valid namespace name diff --git a/tests/render/cases/ns-filter-fail-too-long/values.yaml b/tests/render/cases/ns-filter-fail-too-long/values.yaml new file mode 100644 index 0000000..fb5c4c0 --- /dev/null +++ b/tests/render/cases/ns-filter-fail-too-long/values.yaml @@ -0,0 +1 @@ +config: {kube: {namespaceFilter: {mode: allow, namespaces: ["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]}}} diff --git a/tests/render/cases/ns-filter-fail-unknown-mode/expect-fail b/tests/render/cases/ns-filter-fail-unknown-mode/expect-fail index e052513..d6d1c8a 100644 --- a/tests/render/cases/ns-filter-fail-unknown-mode/expect-fail +++ b/tests/render/cases/ns-filter-fail-unknown-mode/expect-fail @@ -1 +1 @@ -config\.kube\.namespaceFilter\.mode.*bogus +must be one of.*bogus diff --git a/tests/render/cases/ns-filter-fail-uppercase/expect-fail b/tests/render/cases/ns-filter-fail-uppercase/expect-fail new file mode 100644 index 0000000..65862e3 --- /dev/null +++ b/tests/render/cases/ns-filter-fail-uppercase/expect-fail @@ -0,0 +1 @@ +entry "Prod_1" is not a valid namespace name diff --git a/tests/render/cases/ns-filter-fail-uppercase/values.yaml b/tests/render/cases/ns-filter-fail-uppercase/values.yaml new file mode 100644 index 0000000..4363ca5 --- /dev/null +++ b/tests/render/cases/ns-filter-fail-uppercase/values.yaml @@ -0,0 +1 @@ +config: {kube: {namespaceFilter: {mode: allow, namespaces: ["Prod_1"]}}} diff --git a/tests/render/cases/ns-filter-null-fields/expect.jq b/tests/render/cases/ns-filter-null-fields/expect.jq new file mode 100644 index 0000000..2e56e3c --- /dev/null +++ b/tests/render/cases/ns-filter-null-fields/expect.jq @@ -0,0 +1 @@ +.kube | has("namespaceFilter") | not diff --git a/tests/render/cases/ns-filter-null-fields/values.yaml b/tests/render/cases/ns-filter-null-fields/values.yaml new file mode 100644 index 0000000..e87d923 --- /dev/null +++ b/tests/render/cases/ns-filter-null-fields/values.yaml @@ -0,0 +1 @@ +config: {kube: {namespaceFilter: {mode: null, namespaces: null}}} diff --git a/tests/render/cases/ttl-fail-number/expect-fail b/tests/render/cases/ttl-fail-number/expect-fail new file mode 100644 index 0000000..dd25218 --- /dev/null +++ b/tests/render/cases/ttl-fail-number/expect-fail @@ -0,0 +1 @@ +config\.netobs\.edgeExistenceTtl must be a duration string like 2h diff --git a/tests/render/cases/ttl-fail-number/values.yaml b/tests/render/cases/ttl-fail-number/values.yaml new file mode 100644 index 0000000..ad84291 --- /dev/null +++ b/tests/render/cases/ttl-fail-number/values.yaml @@ -0,0 +1 @@ +config: {netobs: {edgeExistenceTtl: 2}} diff --git a/tests/render/run.sh b/tests/render/run.sh index 4c4e13e..cb79dfb 100755 --- a/tests/render/run.sh +++ b/tests/render/run.sh @@ -12,6 +12,7 @@ set -euo pipefail here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" chart="$(cd "$here/../.." && pwd)" filter="${1:-}" +[[ -n "${EDGE_SRC:-}" ]] && EDGE_SRC="$(cd "$EDGE_SRC" && pwd)" tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT From 9d48affd8be592bc809a992ab216f81a5cf95ae9 Mon Sep 17 00:00:00 2001 From: stheppi Date: Wed, 7 Oct 2026 22:52:34 +0100 Subject: [PATCH 14/19] fix(chart): make configMapCapture/envCapture nil-safe (PR #13 code) This fixes code introduced by PR #13, not by this change. helm upgrade --reuse-values from 0.5.2 reuses 0.5.2's values, which have no config.configMapCapture or config.envCapture maps, so the template hit a nil pointer. Default-values output is byte-identical to before. run.sh gains a replace-values marker: the case values.yaml replaces the chart defaults, which is what --reuse-values does. Before: case reuse-values-0.5.2 FAIL: nil pointer evaluating .Values.config.configMapCapture.clearText Signed-off-by: stheppi --- templates/configmap.yaml | 14 +- .../render/cases/reuse-values-0.5.2/expect.jq | 2 + .../cases/reuse-values-0.5.2/replace-values | 0 .../cases/reuse-values-0.5.2/values.yaml | 425 ++++++++++++++++++ tests/render/run.sh | 17 +- 5 files changed, 449 insertions(+), 9 deletions(-) create mode 100644 tests/render/cases/reuse-values-0.5.2/expect.jq create mode 100644 tests/render/cases/reuse-values-0.5.2/replace-values create mode 100644 tests/render/cases/reuse-values-0.5.2/values.yaml diff --git a/templates/configmap.yaml b/templates/configmap.yaml index 6f7e448..04f747d 100644 --- a/templates/configmap.yaml +++ b/templates/configmap.yaml @@ -102,21 +102,23 @@ data: "excludeNamespaces": {{ .Values.config.netobs.excludeNamespaces | toJson }} {{- end }} }, + {{- $cmc := .Values.config.configMapCapture | default dict }} "configMapCapture": { - "clearText": {{ .Values.config.configMapCapture.clearText | default false }}, - "captureCap": {{ .Values.config.configMapCapture.captureCap | default 4096 }}, + "clearText": {{ $cmc.clearText | default false }}, + "captureCap": {{ $cmc.captureCap | default 4096 }}, "redactKeyPatterns": [ - {{- range $index, $pattern := .Values.config.configMapCapture.redactKeyPatterns }} + {{- range $index, $pattern := $cmc.redactKeyPatterns }} {{- if $index }},{{ end }} {{ $pattern | quote }} {{- end }} ] }, + {{- $envc := .Values.config.envCapture | default dict }} "envCapture": { - "clearText": {{ .Values.config.envCapture.clearText | default false }}, - "captureCap": {{ .Values.config.envCapture.captureCap | default 4096 }}, + "clearText": {{ $envc.clearText | default false }}, + "captureCap": {{ $envc.captureCap | default 4096 }}, "redactKeyPatterns": [ - {{- range $index, $pattern := .Values.config.envCapture.redactKeyPatterns }} + {{- range $index, $pattern := $envc.redactKeyPatterns }} {{- if $index }},{{ end }} {{ $pattern | quote }} {{- end }} diff --git a/tests/render/cases/reuse-values-0.5.2/expect.jq b/tests/render/cases/reuse-values-0.5.2/expect.jq new file mode 100644 index 0000000..b46204d --- /dev/null +++ b/tests/render/cases/reuse-values-0.5.2/expect.jq @@ -0,0 +1,2 @@ +.configMapCapture == {"clearText": false, "captureCap": 4096, "redactKeyPatterns": []} +and .envCapture == {"clearText": false, "captureCap": 4096, "redactKeyPatterns": []} diff --git a/tests/render/cases/reuse-values-0.5.2/replace-values b/tests/render/cases/reuse-values-0.5.2/replace-values new file mode 100644 index 0000000..e69de29 diff --git a/tests/render/cases/reuse-values-0.5.2/values.yaml b/tests/render/cases/reuse-values-0.5.2/values.yaml new file mode 100644 index 0000000..c761e1f --- /dev/null +++ b/tests/render/cases/reuse-values-0.5.2/values.yaml @@ -0,0 +1,425 @@ +# Default values for nofire-edge. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# Image configuration +image: + repository: nofireai/edge + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "latest" + +# Image pull secrets +imagePullSecrets: [] + +# Override name and fullname +nameOverride: "" +fullnameOverride: "" + +# Number of replicas +replicaCount: 1 + +# Deployment strategy +strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 1 + maxSurge: 1 + +# Service account configuration +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +# RBAC configuration +rbac: + # Specifies whether RBAC resources should be created + create: true + # Additional cluster role rules + extraRules: [] + +# Pod annotations +podAnnotations: {} + +# Pod labels +podLabels: {} + +# Pod security context +podSecurityContext: + {} + # fsGroup: 2000 + +# Security context for containers +securityContext: + {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +# Service configuration +service: + type: ClusterIP + # clusterIP: "" # Optional: Set a static ClusterIP + port: 8080 + dnstapPort: 6000 + targetPort: 8080 + annotations: {} + +# Ingress configuration +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: k8s-resource-graph-client.local + paths: + - path: / + pathType: Prefix + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +# Resource limits and requests +resources: + limits: + cpu: 200m + memory: 256Mi + requests: + cpu: 100m + memory: 128Mi + +# Horizontal Pod Autoscaler +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Node selector +nodeSelector: {} + +# Tolerations +tolerations: [] + +# Affinity +affinity: {} + +# Topology spread constraints +topologySpreadConstraints: [] + +# Pod disruption budget +podDisruptionBudget: + enabled: false + minAvailable: 1 + # maxUnavailable: 1 + +# Liveness probe configuration +livenessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + +# Readiness probe configuration +readinessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + +# Monitoring configuration +monitoring: + # Enable ServiceMonitor for Prometheus Operator + serviceMonitor: + enabled: false + namespace: "" + labels: + release: prometheus + annotations: {} + interval: 30s + path: /metrics + targetLabels: [] + metricRelabelings: [] + relabelings: [] + +# Client configuration +config: + # Logging configuration + logLevel: "info" + + # Server configuration + serverPort: 8080 + + # Sentry configuration + sentry: + dsn: "https://0d3d5e19f07e0173eaadeb930a405150@o4507775482789888.ingest.de.sentry.io/4510431321718864" + environment: "production" + release: "" + debug: false + sampleRate: 1.0 + tracesSampleRate: 0.2 + + + # Graph configuration + graph: + pruneInterval: "1h" + maxPruneAge: "24h" + + # Kubernetes configuration + kube: + configPath: "" + resyncInterval: "5m" + clusterName: "default-cluster" + resources: + - "pods" + - "nodes" + - "k8services" + - "configmaps" + - "secrets" + - "persistentvolumeclaims" + - "persistentvolumes" + - "deployments" + - "replicasets" + - "statefulsets" + - "daemonsets" + - "jobs" + - "ingresses" + - "namespaces" + - "services" + # Causal analysis configuration + enableCausalAnalysis: false + causal: + algorithm: "counterfactual" + interval: "15m" + + # Services configuration + services: + workers: 4 + # address: "127.0.0.1:6000" + # maxConns: 10 + # readTimeout: "30s" + # writeTimeout: "30s" + # tls: + # enabled: false + # certFile: "" + # keyFile: "" + # caFile: "" + # requireClientCert: false + # minVersion: "1.2" + # cipherSuites: [] + # compression: + # enabled: false + # type: "none" + # level: -1 + # handshake: + # timeout: "5s" + # contentType: "protobuf:dnstap.Dnstap" + + # Network observability — L7 dependency detection via Hubble, Istio, or Caretta. + # Auto-detects available source when source is empty (hubble > istio > caretta). + netobs: + enabled: false + interval: "60s" + prometheusUrl: "" # empty = in-cluster discovery + source: "" # empty = auto-detect; or "hubble" | "istio" | "caretta" + prometheusTimeout: "10s" + + # Publishing configuration + enablePublishing: true + publisher: + # REQUIRED: API key for authentication (must be set to a valid value) + # apiKey: "your-api-key-here" + # Source the API key from an existing Secret (via the NOFIRE_API_KEY env var) + # instead of baking apiKey into the config.json ConfigMap in plaintext. When + # enabled the chart does NOT render templates/secret.yaml — the named Secret is + # expected to already exist (e.g. created by External Secrets Operator). + apiKeySecret: + enabled: false + name: nofire-credentials + key: api-key + timeout: "30s" + maxRetries: 3 + retryDelay: "5s" + enableCompression: true + graph: + url: "https://my.nofire.ai/api/edge" + interval: "5m" + heartbeat: + url: "" + interval: "10s" + +# Environment variables +env: [] +# - name: NODE_NAME +# valueFrom: +# fieldRef: +# fieldPath: spec.nodeName + +# Additional volumes +extraVolumes: [] + +# Additional volume mounts +extraVolumeMounts: [] + +# Extra containers +extraContainers: [] + +# Init containers +initContainers: [] + +# Node name configuration (for DaemonSet-like behavior) +nodeName: + # Enable automatic node name injection + enabled: true + # Field path for node name + fieldPath: "spec.nodeName" + +# ============================================================================= +# Edge Proxy Configuration +# ============================================================================= +# The edge proxy connects to Brain via gRPC and proxies observability queries +# to on-prem data sources (Prometheus, Loki, Tempo, etc.). +# Set enabled: true to deploy the edge proxy alongside the main agent. + +edgeProxy: + enabled: false + + image: + repository: nofireai/edge-proxy + # tag: defaults to .Values.image.tag (shared version with main agent) + # pullPolicy: defaults to .Values.image.pullPolicy + + replicaCount: 1 + + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "256Mi" + cpu: "500m" + + # Stream configuration (Brain gRPC connection) + stream: + serverAddress: "edge.nofire.ai:443" + useTLS: true + reconnectDelay: "1s" + maxReconnectDelay: "60s" + heartbeatInterval: "30s" + maxMessageSize: 16777216 # 16MB + maxConcurrent: 10 + keepaliveTime: "30s" + keepaliveTimeout: "10s" + protocolVersion: 1 + + # HTTP client settings for backend connections + http: + timeout: "30s" + maxIdleConns: 100 + retryMaxAttempts: 3 + + # PII redaction before data leaves customer infrastructure + redaction: + enabled: true + standardPII: true + + # Health/metrics HTTP server port + serverPort: 8081 + + # Base path for K8s secret volume mounts inside the pod + secretsPath: "/etc/nofire/secrets" + + # Service configuration + service: + type: ClusterIP + port: 8081 + annotations: {} + + # Probes + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 5 + +# On-premises connections map (used when edgeProxy.enabled: true) +# +# Supported types: prometheus, loki, tempo, grafana, elasticsearch, +# open_search, mongodb_atlas, datadog, cloudwatch, google_cloud_logs, +# jaeger, alertmanager, github, gitlab, bitbucket, slack, custom_http, postgres +# +# Each connection supports: +# type: (required) backend type from list above +# enabled: (optional, default: true) whether connection is active +# name: (optional, default: key name) human-friendly display name +# secretName: (optional) K8s secret name for credentials (username/password/token) +# url: (required for most types) backend service base URL +# orgId: (optional) Loki multi-tenant org ID (X-Scope-OrgID header) +# index: (optional) Elasticsearch/OpenSearch index pattern +# datasourceUID: (optional) Tempo datasource UID +# tlsEnabled: (optional) enable TLS for backend connection +# tlsSkipVerify: (optional) skip backend TLS certificate verification +# caCertPath: (optional) path to CA cert for backend TLS verification +# +# Postgres connections use DB fields instead of url: +# dbHost: (required) database hostname +# dbPort: (optional, default: 5432) database port +# dbName: (required) database name +# dbSSLMode: (optional) sslmode (e.g. disable, require, verify-full) +# +# Example: +# onPremConnections: +# prod-prometheus: +# type: prometheus +# enabled: true +# name: "Production Prometheus" +# secretName: prometheus-creds +# url: "http://prometheus:9090" +# prod-loki: +# type: loki +# enabled: true +# name: "Production Loki" +# secretName: loki-creds +# url: "http://loki:3100" +# orgId: "tenant1" +# prod-postgres: +# type: postgres +# enabled: true +# name: "Production Postgres" +# secretName: postgres-creds +# dbHost: "postgres.default.svc.cluster.local" +# dbPort: 5432 +# dbName: "app" +# dbSSLMode: "require" +# internal-api: +# type: custom_http +# enabled: true +# name: "Internal API" +# secretName: internal-api-creds +# url: "https://api.internal.example.com" +onPremConnections: {} diff --git a/tests/render/run.sh b/tests/render/run.sh index cb79dfb..c267349 100755 --- a/tests/render/run.sh +++ b/tests/render/run.sh @@ -5,12 +5,14 @@ # A case dir holds values.yaml plus either: # expect.jq a jq program run against config.json; must print true # expect-fail a regex the `helm template` stderr must match (render must fail) +# A case with a `replace-values` file renders with values.yaml INSTEAD of the +# chart defaults, like `helm upgrade --reuse-values` from an older release. # With EDGE_SRC set to an Edge checkout, every rendered config.json must also be # accepted by the Edge's own config loader (see edgecheck/). set -euo pipefail here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -chart="$(cd "$here/../.." && pwd)" +chart_root="$(cd "$here/../.." && pwd)" filter="${1:-}" [[ -n "${EDGE_SRC:-}" ]] && EDGE_SRC="$(cd "$EDGE_SRC" && pwd)" tmp="$(mktemp -d)" @@ -36,8 +38,17 @@ for dir in "$here"/cases/*/; do name="$(basename "$dir")" [[ -n "$filter" && "$name" != *"$filter"* ]] && continue + chart="$chart_root" + vals=(-f "$dir/values.yaml") + if [[ -f "$dir/replace-values" ]]; then + chart="$tmp/chart-$name" + cp -R "$chart_root" "$chart" + cp "$dir/values.yaml" "$chart/values.yaml" + vals=() + fi + if [[ -f "$dir/expect-fail" ]]; then - if out="$(helm template t "$chart" -f "$dir/values.yaml" -s templates/configmap.yaml 2>&1)"; then + if out="$(helm template t "$chart" ${vals[@]+"${vals[@]}"} -s templates/configmap.yaml 2>&1)"; then report FAIL "$name" "render succeeded, expected failure" elif grep -Eq "$(cat "$dir/expect-fail")" <<<"$out"; then report PASS "$name" @@ -47,7 +58,7 @@ for dir in "$here"/cases/*/; do continue fi - if ! out="$(helm template t "$chart" -f "$dir/values.yaml" -s templates/configmap.yaml 2>&1)"; then + if ! out="$(helm template t "$chart" ${vals[@]+"${vals[@]}"} -s templates/configmap.yaml 2>&1)"; then report FAIL "$name" "render failed: $out" continue fi From 530084bead3160a833fc8eba5eb976b217bf0fee Mon Sep 17 00:00:00 2001 From: stheppi Date: Wed, 7 Oct 2026 22:53:38 +0100 Subject: [PATCH 15/19] test(chart): check the new pod's logs after upgrade in the kind test After helm upgrade, pick the pod by its checksum/config annotation instead of reading logs from every pod of the release, so a leftover old pod cannot satisfy the deny assertion. Select the deployment by chart name too, and list yq in the dependencies. Signed-off-by: stheppi --- tests/e2e/kind.sh | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/tests/e2e/kind.sh b/tests/e2e/kind.sh index dfbc375..b38b48c 100755 --- a/tests/e2e/kind.sh +++ b/tests/e2e/kind.sh @@ -2,7 +2,7 @@ # End-to-end check on a throwaway kind cluster: the installed ConfigMap carries # the namespace scoping keys, the Edge accepts the config and rolls out, a # values change rolls the pod, and an invalid filter is rejected at render. -# Needs docker, kind, kubectl, helm, jq. Usage: tests/e2e/kind.sh +# Needs docker, kind, kubectl, helm, jq, yq. Usage: tests/e2e/kind.sh set -euo pipefail root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" @@ -43,10 +43,16 @@ cm_config() { cm="$(kubectl get cm -l "app.kubernetes.io/instance=$release" -o name | grep -- '-config$')" kubectl get "$cm" -o jsonpath='{.data.config\.json}' } +selector="app.kubernetes.io/instance=$release,app.kubernetes.io/name=nofire-edge" checksum() { - kubectl get deploy -l "app.kubernetes.io/instance=$release" \ + kubectl get deploy -l "$selector" \ -o jsonpath='{.items[0].spec.template.metadata.annotations.checksum/config}' } +# Name of the live pod whose checksum/config annotation equals $1. +pod_with_checksum() { + kubectl get pods -l "$selector" -o json | jq -r --arg c "$1" \ + '.items[] | select(.metadata.deletionTimestamp == null and .metadata.annotations["checksum/config"] == $c) | .metadata.name' | head -1 +} assert() { # description, config.json, jq expression jq -e "$3" <<<"$2" >/dev/null || die "$1: jq '$3' failed on: $2" echo "ok: $1" @@ -62,11 +68,13 @@ assert "installed ConfigMap has netobs keys" "$cfg" \ before="$(checksum)" step "rollout (the Edge accepted the config)" -kubectl rollout status deploy -l "app.kubernetes.io/instance=$release" --timeout=180s \ - || { kubectl logs -l "app.kubernetes.io/instance=$release" --tail=50 || true; die "rollout failed"; } +kubectl rollout status deploy -l "$selector" --timeout=180s \ + || { kubectl logs -l "$selector" --tail=50 || true; die "rollout failed"; } # The Edge logs its loaded config at startup (cmd/edge/main.go); there is no # dedicated namespace-filter log line. -logs="$(kubectl logs -l "app.kubernetes.io/instance=$release" --tail=-1)" +pod="$(pod_with_checksum "$before")" +[[ -n "$pod" ]] || die "no pod carries checksum $before" +logs="$(kubectl logs "$pod")" grep -q 'Configuration loaded successfully' <<<"$logs" || die "no 'Configuration loaded' line in logs" grep -Eq '"mode": ?"allow"' <<<"$logs" || die "startup log does not show namespaceFilter mode allow" echo "ok: startup log shows the allow filter" @@ -78,10 +86,13 @@ after="$(checksum)" [[ -n "$before" && "$before" != "$after" ]] || die "checksum/config did not change ($before -> $after)" echo "ok: checksum/config changed" assert "upgraded ConfigMap has deny filter" "$(cm_config)" '.kube.namespaceFilter.mode == "deny"' -kubectl rollout status deploy -l "app.kubernetes.io/instance=$release" --timeout=180s \ +kubectl rollout status deploy -l "$selector" --timeout=180s \ || die "rollout after upgrade failed" -logs="$(kubectl logs -l "app.kubernetes.io/instance=$release" --tail=-1)" -grep -Eq '"mode": ?"deny"' <<<"$logs" || die "pod log does not show mode deny after upgrade" +pod="$(pod_with_checksum "$after")" +[[ -n "$pod" ]] || die "no pod carries the new checksum $after" +logs="$(kubectl logs "$pod")" +grep -Eq '"mode": ?"deny"' <<<"$logs" || die "new pod $pod log does not show mode deny" +! grep -Eq '"mode": ?"allow"' <<<"$logs" || die "new pod $pod log still shows mode allow" echo "ok: new pod runs with the deny filter" step "negative: allow with no namespaces is rejected at render" From f0ea3d17e15fe2af8b61ef392c49cb6988f7c3eb Mon Sep 17 00:00:00 2001 From: stheppi Date: Thu, 8 Oct 2026 00:43:12 +0100 Subject: [PATCH 16/19] fix(chart): reject non-string netobs.excludeNamespaces entries excludeNamespaces [1, 2] rendered and the Edge then failed to unmarshal a number into a string. Entries are now type-checked (strings only). Names are deliberately not validated further: the Edge accepts e.g. "Prod_1" here, which exclude-ns-uppercase documents. Before: case exclude-ns-fail-number FAIL (render succeeded, expected failure); unittest 'fails on a non-string excludeNamespaces entry' failed Signed-off-by: stheppi --- templates/configmap.yaml | 9 ++++----- tests/configmap_test.yaml | 7 +++++++ tests/render/cases/exclude-ns-fail-number/expect-fail | 1 + tests/render/cases/exclude-ns-fail-number/values.yaml | 1 + tests/render/cases/exclude-ns-uppercase/expect.jq | 1 + tests/render/cases/exclude-ns-uppercase/values.yaml | 1 + 6 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 tests/render/cases/exclude-ns-fail-number/expect-fail create mode 100644 tests/render/cases/exclude-ns-fail-number/values.yaml create mode 100644 tests/render/cases/exclude-ns-uppercase/expect.jq create mode 100644 tests/render/cases/exclude-ns-uppercase/values.yaml diff --git a/templates/configmap.yaml b/templates/configmap.yaml index 04f747d..930e592 100644 --- a/templates/configmap.yaml +++ b/templates/configmap.yaml @@ -95,11 +95,10 @@ data: {{- end }}, "edgeExistenceTtl": {{ .Values.config.netobs.edgeExistenceTtl | quote }} {{- end }} - {{- if and (hasKey .Values.config.netobs "excludeNamespaces") (not (kindIs "invalid" .Values.config.netobs.excludeNamespaces)) }} - {{- if not (kindIs "slice" .Values.config.netobs.excludeNamespaces) }} - {{- fail "config.netobs.excludeNamespaces must be a list" }} - {{- end }}, - "excludeNamespaces": {{ .Values.config.netobs.excludeNamespaces | toJson }} + {{- $netobsEx := dict }} + {{- $_ := include "nofire-edge.typedPick" (dict "src" .Values.config.netobs "path" "config.netobs" "out" $netobsEx "spec" (dict "excludeNamespaces" "strlist")) }} + {{- if hasKey $netobsEx "excludeNamespaces" }}, + "excludeNamespaces": {{ $netobsEx.excludeNamespaces | toJson }} {{- end }} }, {{- $cmc := .Values.config.configMapCapture | default dict }} diff --git a/tests/configmap_test.yaml b/tests/configmap_test.yaml index 4ea8714..e6b0abc 100644 --- a/tests/configmap_test.yaml +++ b/tests/configmap_test.yaml @@ -33,3 +33,10 @@ tests: asserts: - failedTemplate: errorPattern: must be one of + + - it: fails on a non-string excludeNamespaces entry + set: + config.netobs.excludeNamespaces: [1, 2] + asserts: + - failedTemplate: + errorPattern: excludeNamespaces entry 1 must be a string diff --git a/tests/render/cases/exclude-ns-fail-number/expect-fail b/tests/render/cases/exclude-ns-fail-number/expect-fail new file mode 100644 index 0000000..262647d --- /dev/null +++ b/tests/render/cases/exclude-ns-fail-number/expect-fail @@ -0,0 +1 @@ +config\.netobs\.excludeNamespaces entry 1 must be a string diff --git a/tests/render/cases/exclude-ns-fail-number/values.yaml b/tests/render/cases/exclude-ns-fail-number/values.yaml new file mode 100644 index 0000000..eb64042 --- /dev/null +++ b/tests/render/cases/exclude-ns-fail-number/values.yaml @@ -0,0 +1 @@ +config: {netobs: {excludeNamespaces: [1, 2]}} diff --git a/tests/render/cases/exclude-ns-uppercase/expect.jq b/tests/render/cases/exclude-ns-uppercase/expect.jq new file mode 100644 index 0000000..31cb922 --- /dev/null +++ b/tests/render/cases/exclude-ns-uppercase/expect.jq @@ -0,0 +1 @@ +.netobs.excludeNamespaces == ["Prod_1"] diff --git a/tests/render/cases/exclude-ns-uppercase/values.yaml b/tests/render/cases/exclude-ns-uppercase/values.yaml new file mode 100644 index 0000000..5b57e0d --- /dev/null +++ b/tests/render/cases/exclude-ns-uppercase/values.yaml @@ -0,0 +1 @@ +config: {netobs: {excludeNamespaces: ["Prod_1"]}} From 206d63b00e221ce16eb2f134aa5c237c8c5d97f6 Mon Sep 17 00:00:00 2001 From: stheppi Date: Thu, 8 Oct 2026 00:43:22 +0100 Subject: [PATCH 17/19] fix(chart): validate netobs.edgeExistenceTtl as a Go duration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "not-a-duration" rendered and the Edge rejected it at startup. The value is now matched against Go duration syntax (sign, fractions, compound 1h30m, ns/us/µs/ms/s/m/h, and plain "0") and fails the render otherwise. The ttl-ok-* cases pass the Edge loader, including "-1h" which it accepts. Before: ttl-fail-invalid and ttl-fail-missing-unit FAIL (render succeeded); two unittest cases failed Signed-off-by: stheppi --- templates/configmap.yaml | 4 ++++ tests/configmap_test.yaml | 22 +++++++++++++++++++ .../render/cases/ttl-fail-invalid/expect-fail | 1 + .../render/cases/ttl-fail-invalid/values.yaml | 1 + .../cases/ttl-fail-missing-unit/expect-fail | 1 + .../cases/ttl-fail-missing-unit/values.yaml | 1 + tests/render/cases/ttl-ok-1/expect.jq | 1 + tests/render/cases/ttl-ok-1/values.yaml | 1 + tests/render/cases/ttl-ok-2/expect.jq | 1 + tests/render/cases/ttl-ok-2/values.yaml | 1 + tests/render/cases/ttl-ok-3/expect.jq | 1 + tests/render/cases/ttl-ok-3/values.yaml | 1 + tests/render/cases/ttl-ok-4/expect.jq | 1 + tests/render/cases/ttl-ok-4/values.yaml | 1 + tests/render/cases/ttl-ok-5/expect.jq | 1 + tests/render/cases/ttl-ok-5/values.yaml | 1 + tests/render/cases/ttl-ok-6/expect.jq | 1 + tests/render/cases/ttl-ok-6/values.yaml | 1 + 18 files changed, 42 insertions(+) create mode 100644 tests/render/cases/ttl-fail-invalid/expect-fail create mode 100644 tests/render/cases/ttl-fail-invalid/values.yaml create mode 100644 tests/render/cases/ttl-fail-missing-unit/expect-fail create mode 100644 tests/render/cases/ttl-fail-missing-unit/values.yaml create mode 100644 tests/render/cases/ttl-ok-1/expect.jq create mode 100644 tests/render/cases/ttl-ok-1/values.yaml create mode 100644 tests/render/cases/ttl-ok-2/expect.jq create mode 100644 tests/render/cases/ttl-ok-2/values.yaml create mode 100644 tests/render/cases/ttl-ok-3/expect.jq create mode 100644 tests/render/cases/ttl-ok-3/values.yaml create mode 100644 tests/render/cases/ttl-ok-4/expect.jq create mode 100644 tests/render/cases/ttl-ok-4/values.yaml create mode 100644 tests/render/cases/ttl-ok-5/expect.jq create mode 100644 tests/render/cases/ttl-ok-5/values.yaml create mode 100644 tests/render/cases/ttl-ok-6/expect.jq create mode 100644 tests/render/cases/ttl-ok-6/values.yaml diff --git a/templates/configmap.yaml b/templates/configmap.yaml index 930e592..02c59cc 100644 --- a/templates/configmap.yaml +++ b/templates/configmap.yaml @@ -92,6 +92,10 @@ data: {{- if and (hasKey .Values.config.netobs "edgeExistenceTtl") (not (kindIs "invalid" .Values.config.netobs.edgeExistenceTtl)) }} {{- if not (kindIs "string" .Values.config.netobs.edgeExistenceTtl) }} {{- fail (printf "config.netobs.edgeExistenceTtl must be a duration string like 2h (got %v)" .Values.config.netobs.edgeExistenceTtl) }} + {{- end }} + {{- /* Go duration syntax: optional sign, then number+unit pairs ("1h30m", "1.5h"), or plain "0". */}} + {{- if not (regexMatch "^([-+]?(([0-9]+[.]?[0-9]*|[.][0-9]+)(ns|us|µs|μs|ms|s|m|h))+|[-+]?0)$" .Values.config.netobs.edgeExistenceTtl) }} + {{- fail (printf "config.netobs.edgeExistenceTtl %q is not a valid duration; use Go syntax such as 2h, 90m or 1h30m" .Values.config.netobs.edgeExistenceTtl) }} {{- end }}, "edgeExistenceTtl": {{ .Values.config.netobs.edgeExistenceTtl | quote }} {{- end }} diff --git a/tests/configmap_test.yaml b/tests/configmap_test.yaml index e6b0abc..60b3a69 100644 --- a/tests/configmap_test.yaml +++ b/tests/configmap_test.yaml @@ -40,3 +40,25 @@ tests: asserts: - failedTemplate: errorPattern: excludeNamespaces entry 1 must be a string + + - it: keeps excludeNamespaces entries the Edge accepts, such as uppercase + set: + config.netobs.excludeNamespaces: ["Prod_1"] + asserts: + - matchRegex: + path: data["config.json"] + pattern: '"excludeNamespaces": \["Prod_1"\]' + + - it: fails on an edgeExistenceTtl that is not a duration + set: + config.netobs.edgeExistenceTtl: not-a-duration + asserts: + - failedTemplate: + errorPattern: edgeExistenceTtl "not-a-duration" is not a valid duration + + - it: fails on an edgeExistenceTtl without a unit + set: + config.netobs.edgeExistenceTtl: "30" + asserts: + - failedTemplate: + errorPattern: edgeExistenceTtl "30" is not a valid duration diff --git a/tests/render/cases/ttl-fail-invalid/expect-fail b/tests/render/cases/ttl-fail-invalid/expect-fail new file mode 100644 index 0000000..7ca5f45 --- /dev/null +++ b/tests/render/cases/ttl-fail-invalid/expect-fail @@ -0,0 +1 @@ +config\.netobs\.edgeExistenceTtl "not-a-duration" is not a valid duration diff --git a/tests/render/cases/ttl-fail-invalid/values.yaml b/tests/render/cases/ttl-fail-invalid/values.yaml new file mode 100644 index 0000000..5d0c664 --- /dev/null +++ b/tests/render/cases/ttl-fail-invalid/values.yaml @@ -0,0 +1 @@ +config: {netobs: {edgeExistenceTtl: "not-a-duration"}} diff --git a/tests/render/cases/ttl-fail-missing-unit/expect-fail b/tests/render/cases/ttl-fail-missing-unit/expect-fail new file mode 100644 index 0000000..46116cd --- /dev/null +++ b/tests/render/cases/ttl-fail-missing-unit/expect-fail @@ -0,0 +1 @@ +edgeExistenceTtl "30" is not a valid duration diff --git a/tests/render/cases/ttl-fail-missing-unit/values.yaml b/tests/render/cases/ttl-fail-missing-unit/values.yaml new file mode 100644 index 0000000..178d3fd --- /dev/null +++ b/tests/render/cases/ttl-fail-missing-unit/values.yaml @@ -0,0 +1 @@ +config: {netobs: {edgeExistenceTtl: "30"}} diff --git a/tests/render/cases/ttl-ok-1/expect.jq b/tests/render/cases/ttl-ok-1/expect.jq new file mode 100644 index 0000000..3d301ea --- /dev/null +++ b/tests/render/cases/ttl-ok-1/expect.jq @@ -0,0 +1 @@ +.netobs.edgeExistenceTtl == "-1h" diff --git a/tests/render/cases/ttl-ok-1/values.yaml b/tests/render/cases/ttl-ok-1/values.yaml new file mode 100644 index 0000000..331bf8a --- /dev/null +++ b/tests/render/cases/ttl-ok-1/values.yaml @@ -0,0 +1 @@ +config: {netobs: {edgeExistenceTtl: "-1h"}} diff --git a/tests/render/cases/ttl-ok-2/expect.jq b/tests/render/cases/ttl-ok-2/expect.jq new file mode 100644 index 0000000..eedf093 --- /dev/null +++ b/tests/render/cases/ttl-ok-2/expect.jq @@ -0,0 +1 @@ +.netobs.edgeExistenceTtl == "1h30m" diff --git a/tests/render/cases/ttl-ok-2/values.yaml b/tests/render/cases/ttl-ok-2/values.yaml new file mode 100644 index 0000000..4b72e3c --- /dev/null +++ b/tests/render/cases/ttl-ok-2/values.yaml @@ -0,0 +1 @@ +config: {netobs: {edgeExistenceTtl: "1h30m"}} diff --git a/tests/render/cases/ttl-ok-3/expect.jq b/tests/render/cases/ttl-ok-3/expect.jq new file mode 100644 index 0000000..f966553 --- /dev/null +++ b/tests/render/cases/ttl-ok-3/expect.jq @@ -0,0 +1 @@ +.netobs.edgeExistenceTtl == "1.5h" diff --git a/tests/render/cases/ttl-ok-3/values.yaml b/tests/render/cases/ttl-ok-3/values.yaml new file mode 100644 index 0000000..9823b64 --- /dev/null +++ b/tests/render/cases/ttl-ok-3/values.yaml @@ -0,0 +1 @@ +config: {netobs: {edgeExistenceTtl: "1.5h"}} diff --git a/tests/render/cases/ttl-ok-4/expect.jq b/tests/render/cases/ttl-ok-4/expect.jq new file mode 100644 index 0000000..d6a4d83 --- /dev/null +++ b/tests/render/cases/ttl-ok-4/expect.jq @@ -0,0 +1 @@ +.netobs.edgeExistenceTtl == "500us" diff --git a/tests/render/cases/ttl-ok-4/values.yaml b/tests/render/cases/ttl-ok-4/values.yaml new file mode 100644 index 0000000..34fbc4c --- /dev/null +++ b/tests/render/cases/ttl-ok-4/values.yaml @@ -0,0 +1 @@ +config: {netobs: {edgeExistenceTtl: "500us"}} diff --git a/tests/render/cases/ttl-ok-5/expect.jq b/tests/render/cases/ttl-ok-5/expect.jq new file mode 100644 index 0000000..61c14f3 --- /dev/null +++ b/tests/render/cases/ttl-ok-5/expect.jq @@ -0,0 +1 @@ +.netobs.edgeExistenceTtl == "500µs" diff --git a/tests/render/cases/ttl-ok-5/values.yaml b/tests/render/cases/ttl-ok-5/values.yaml new file mode 100644 index 0000000..6727894 --- /dev/null +++ b/tests/render/cases/ttl-ok-5/values.yaml @@ -0,0 +1 @@ +config: {netobs: {edgeExistenceTtl: "500µs"}} diff --git a/tests/render/cases/ttl-ok-6/expect.jq b/tests/render/cases/ttl-ok-6/expect.jq new file mode 100644 index 0000000..5998b33 --- /dev/null +++ b/tests/render/cases/ttl-ok-6/expect.jq @@ -0,0 +1 @@ +.netobs.edgeExistenceTtl == "0" diff --git a/tests/render/cases/ttl-ok-6/values.yaml b/tests/render/cases/ttl-ok-6/values.yaml new file mode 100644 index 0000000..8afcbcb --- /dev/null +++ b/tests/render/cases/ttl-ok-6/values.yaml @@ -0,0 +1 @@ +config: {netobs: {edgeExistenceTtl: "0"}} From 446f0cb6fbaa49a6d68163eebf37300e264637a9 Mon Sep 17 00:00:00 2001 From: stheppi Date: Thu, 8 Oct 2026 00:44:03 +0100 Subject: [PATCH 18/19] fix(chart): reject unknown keys under kube.namespaceFilter namespaceFilter {node: allow} (a typo for mode) rendered nothing and silently left every namespace in scope. Any key other than mode and namespaces now fails the render and names the key. Before: cases ns-filter-fail-unknown-key and -valid-mode FAIL (render succeeded); unittest 'fails on an unknown namespaceFilter key' failed Signed-off-by: stheppi --- templates/configmap.yaml | 5 +++++ tests/configmap_test.yaml | 15 +++++++++++++++ .../expect-fail | 1 + .../values.yaml | 1 + .../cases/ns-filter-fail-unknown-key/expect-fail | 1 + .../cases/ns-filter-fail-unknown-key/values.yaml | 1 + 6 files changed, 24 insertions(+) create mode 100644 tests/render/cases/ns-filter-fail-unknown-key-valid-mode/expect-fail create mode 100644 tests/render/cases/ns-filter-fail-unknown-key-valid-mode/values.yaml create mode 100644 tests/render/cases/ns-filter-fail-unknown-key/expect-fail create mode 100644 tests/render/cases/ns-filter-fail-unknown-key/values.yaml diff --git a/templates/configmap.yaml b/templates/configmap.yaml index 02c59cc..0afcd77 100644 --- a/templates/configmap.yaml +++ b/templates/configmap.yaml @@ -28,6 +28,11 @@ data: {{- if and (hasKey .Values.config.kube "namespaceFilter") (not (kindIs "invalid" $nfRaw)) (not (kindIs "map" $nfRaw)) }} {{- fail "config.kube.namespaceFilter must be a map" }} {{- end }} + {{- range $key, $_ := $nfRaw }} + {{- if not (has $key (list "mode" "namespaces")) }} + {{- fail (printf "config.kube.namespaceFilter has unknown key %q; supported keys are mode and namespaces" $key) }} + {{- end }} + {{- end }} {{- $nf := dict }} {{- $_ := include "nofire-edge.typedPick" (dict "src" $nfRaw "path" "config.kube.namespaceFilter" "out" $nf "spec" (dict "mode" "string" "namespaces" "strlist")) }} {{- if $nf }} diff --git a/tests/configmap_test.yaml b/tests/configmap_test.yaml index 60b3a69..edc495a 100644 --- a/tests/configmap_test.yaml +++ b/tests/configmap_test.yaml @@ -62,3 +62,18 @@ tests: asserts: - failedTemplate: errorPattern: edgeExistenceTtl "30" is not a valid duration + + - it: accepts Go duration syntax for edgeExistenceTtl + set: + config.netobs.edgeExistenceTtl: -1h30m + asserts: + - matchRegex: + path: data["config.json"] + pattern: '"edgeExistenceTtl": "-1h30m"' + + - it: fails on an unknown namespaceFilter key + set: + config.kube.namespaceFilter.node: allow + asserts: + - failedTemplate: + errorPattern: namespaceFilter has unknown key "node" diff --git a/tests/render/cases/ns-filter-fail-unknown-key-valid-mode/expect-fail b/tests/render/cases/ns-filter-fail-unknown-key-valid-mode/expect-fail new file mode 100644 index 0000000..b1f0860 --- /dev/null +++ b/tests/render/cases/ns-filter-fail-unknown-key-valid-mode/expect-fail @@ -0,0 +1 @@ +has unknown key "extra" diff --git a/tests/render/cases/ns-filter-fail-unknown-key-valid-mode/values.yaml b/tests/render/cases/ns-filter-fail-unknown-key-valid-mode/values.yaml new file mode 100644 index 0000000..3949d36 --- /dev/null +++ b/tests/render/cases/ns-filter-fail-unknown-key-valid-mode/values.yaml @@ -0,0 +1 @@ +config: {kube: {namespaceFilter: {mode: allow, namespaces: [a], extra: 1}}} diff --git a/tests/render/cases/ns-filter-fail-unknown-key/expect-fail b/tests/render/cases/ns-filter-fail-unknown-key/expect-fail new file mode 100644 index 0000000..fff031c --- /dev/null +++ b/tests/render/cases/ns-filter-fail-unknown-key/expect-fail @@ -0,0 +1 @@ +config\.kube\.namespaceFilter has unknown key "node" diff --git a/tests/render/cases/ns-filter-fail-unknown-key/values.yaml b/tests/render/cases/ns-filter-fail-unknown-key/values.yaml new file mode 100644 index 0000000..b4806ad --- /dev/null +++ b/tests/render/cases/ns-filter-fail-unknown-key/values.yaml @@ -0,0 +1 @@ +config: {kube: {namespaceFilter: {node: allow}}} From f4ca8f67506270539e8accff7c35a0b88a579baa Mon Sep 17 00:00:00 2001 From: stheppi Date: Thu, 8 Oct 2026 00:44:40 +0100 Subject: [PATCH 19/19] test(chart): keep helm stderr out of rendered YAML; run CI on main run.sh merged stderr into the manifest it parsed, so a helm warning on a successful render (e.g. a symlink in the chart) broke yq. stdout and stderr are now captured separately; stderr is matched only for expect-fail cases and printed on failure. replace-values cases copy only chart files (no .git, node_modules or tests). harness_test.sh covers this with a symlink in a temp copy of the chart and runs in CI, which now also triggers on push to main. Before: harness_test.sh FAIL: yq 'bad file -: mapping values are not allowed' (helm's 'found symbolic link in path' warning parsed as YAML) Signed-off-by: stheppi --- .github/workflows/ci.yaml | 3 +++ tests/render/harness_test.sh | 20 ++++++++++++++++++++ tests/render/run.sh | 19 +++++++++++++------ 3 files changed, 36 insertions(+), 6 deletions(-) create mode 100755 tests/render/harness_test.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 76ed0c3..0677dc0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2,6 +2,8 @@ name: CI on: pull_request: + push: + branches: [main] jobs: test: @@ -20,3 +22,4 @@ jobs: - run: helm lint . - run: helm unittest . - run: tests/render/run.sh + - run: tests/render/harness_test.sh diff --git a/tests/render/harness_test.sh b/tests/render/harness_test.sh new file mode 100755 index 0000000..3c0013b --- /dev/null +++ b/tests/render/harness_test.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Tests for run.sh itself. A symlink inside the chart makes `helm template` +# print a warning on stderr while still succeeding; run.sh must not feed that +# warning to yq as part of the rendered YAML. +set -euo pipefail + +chart_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT + +(cd "$chart_root" && tar --exclude=.git --exclude=node_modules -cf - .) | (mkdir "$tmp/chart" && cd "$tmp/chart" && tar -xf -) +ln -s ../values.yaml "$tmp/chart/templates/stderr-warning-link" + +if ! out="$("$tmp/chart/tests/render/run.sh" defaults 2>&1)"; then + echo "FAIL: run.sh did not pass a case when helm warned on stderr" + echo "$out" + exit 1 +fi +grep -q '^PASS defaults' <<<"$out" || { echo "FAIL: unexpected output: $out"; exit 1; } +echo "PASS harness: stderr warnings do not leak into the rendered config" diff --git a/tests/render/run.sh b/tests/render/run.sh index c267349..bf865b0 100755 --- a/tests/render/run.sh +++ b/tests/render/run.sh @@ -42,26 +42,33 @@ for dir in "$here"/cases/*/; do vals=(-f "$dir/values.yaml") if [[ -f "$dir/replace-values" ]]; then chart="$tmp/chart-$name" - cp -R "$chart_root" "$chart" + mkdir "$chart" + (cd "$chart_root" && tar --exclude=.git --exclude=node_modules --exclude=tests -cf - .) | (cd "$chart" && tar -xf -) cp "$dir/values.yaml" "$chart/values.yaml" vals=() fi + # stdout is the manifest; stderr (helm warnings, errors) is kept apart so + # warnings on a successful render never end up in the YAML handed to yq. + rc=0 + helm template t "$chart" ${vals[@]+"${vals[@]}"} -s templates/configmap.yaml >"$tmp/out" 2>"$tmp/stderr" || rc=$? + if [[ -f "$dir/expect-fail" ]]; then - if out="$(helm template t "$chart" ${vals[@]+"${vals[@]}"} -s templates/configmap.yaml 2>&1)"; then + if [[ "$rc" -eq 0 ]]; then report FAIL "$name" "render succeeded, expected failure" - elif grep -Eq "$(cat "$dir/expect-fail")" <<<"$out"; then + elif grep -Eq "$(cat "$dir/expect-fail")" "$tmp/stderr"; then report PASS "$name" else - report FAIL "$name" "error did not match $(cat "$dir/expect-fail"): $out" + report FAIL "$name" "error did not match $(cat "$dir/expect-fail"): $(cat "$tmp/stderr")" fi continue fi - if ! out="$(helm template t "$chart" ${vals[@]+"${vals[@]}"} -s templates/configmap.yaml 2>&1)"; then - report FAIL "$name" "render failed: $out" + if [[ "$rc" -ne 0 ]]; then + report FAIL "$name" "render failed: $(cat "$tmp/stderr")" continue fi + out="$(cat "$tmp/out")" cfg="$tmp/$name.json" yq -r '.data["config.json"]' <<<"$out" > "$cfg" if ! jq -e . "$cfg" >/dev/null 2>"$tmp/err"; then