From 446d883f4ff7cf11a8e81b6e298d1b3cc839aea9 Mon Sep 17 00:00:00 2001 From: Alexandros Touloupis Date: Fri, 7 Aug 2026 17:17:35 +0300 Subject: [PATCH 01/26] feat(chart): grant Endpoints access + informer, expose configMap & env capture Adds the endpoints resource to the ClusterRole + informer gate (has_endpoints), and exposes configMapCapture and envCapture (hash-by-default value capture with clearText / redactKeyPatterns / captureCap knobs) in values.yaml + config.json. --- templates/configmap.yaml | 20 ++++++++++++++++++++ templates/rbac.yaml | 2 +- values.yaml | 28 ++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/templates/configmap.yaml b/templates/configmap.yaml index 45bd809..be2e70f 100644 --- a/templates/configmap.yaml +++ b/templates/configmap.yaml @@ -92,5 +92,25 @@ data: "prometheusUrl": {{ .Values.config.netobs.prometheusUrl | default "" | quote }}, "source": {{ .Values.config.netobs.source | default "" | quote }}, "prometheusTimeout": {{ .Values.config.netobs.prometheusTimeout | default "10s" | quote }} + }, + "configMapCapture": { + "clearText": {{ .Values.config.configMapCapture.clearText | default false }}, + "captureCap": {{ .Values.config.configMapCapture.captureCap | default 4096 }}, + "redactKeyPatterns": [ + {{- range $index, $pattern := .Values.config.configMapCapture.redactKeyPatterns }} + {{- if $index }},{{ end }} + {{ $pattern | quote }} + {{- end }} + ] + }, + "envCapture": { + "clearText": {{ .Values.config.envCapture.clearText | default false }}, + "captureCap": {{ .Values.config.envCapture.captureCap | default 4096 }}, + "redactKeyPatterns": [ + {{- range $index, $pattern := .Values.config.envCapture.redactKeyPatterns }} + {{- if $index }},{{ end }} + {{ $pattern | quote }} + {{- end }} + ] } } diff --git a/templates/rbac.yaml b/templates/rbac.yaml index 90fb326..3c4e7df 100644 --- a/templates/rbac.yaml +++ b/templates/rbac.yaml @@ -7,7 +7,7 @@ metadata: {{- include "nofire-edge.labels" . | nindent 4 }} rules: - apiGroups: [""] - resources: ["pods", "services", "configmaps", "secrets", "persistentvolumeclaims", "persistentvolumes", "nodes", "namespaces", "resourcequotas", "limitranges"] + resources: ["pods", "services", "endpoints", "configmaps", "secrets", "persistentvolumeclaims", "persistentvolumes", "nodes", "namespaces", "resourcequotas", "limitranges"] verbs: ["get", "list", "watch"] - apiGroups: ["apps"] resources: ["deployments", "statefulsets", "daemonsets", "replicasets"] diff --git a/values.yaml b/values.yaml index c761e1f..3205597 100644 --- a/values.yaml +++ b/values.yaml @@ -192,6 +192,7 @@ config: - "pods" - "nodes" - "k8services" + - "endpoints" - "configmaps" - "secrets" - "persistentvolumeclaims" @@ -266,6 +267,33 @@ config: url: "" interval: "10s" + # ConfigMap value capture. Hash-by-default so raw customer configuration never + # leaves the cluster. Set clearText: true to send values verbatim (still + # subject to redactKeyPatterns and captureCap). + configMapCapture: + # false (default): every ConfigMap value is sent as a sha256 hash. + # true: values are sent in the clear, except keys matching redactKeyPatterns. + clearText: false + # In clear-text mode, ConfigMap data keys matching any of these regexes are + # hashed instead of sent verbatim (e.g. secrets embedded in a ConfigMap). + redactKeyPatterns: [] + # - "(?i)(password|passwd|secret|token|apikey|api_key|private_key)" + # Per-value byte cap for clear-text values. Values over the cap are trimmed + # (Brain is told the value was trimmed). Hashes always cover the full value. + captureCap: 4096 + + # Container env-var value capture. Hash-by-default so a plaintext credential in + # an env var never leaves the cluster. Same model as configMapCapture. + envCapture: + # false (default): every literal env value is sent as a sha256 hash. + # true: literal values are sent in the clear, except names matching redactKeyPatterns. + clearText: false + # In clear-text mode, env var names matching any of these regexes stay hashed. + redactKeyPatterns: [] + # - "(?i)(password|passwd|secret|token|apikey|api_key|private_key)" + # Per-value byte cap for clear-text values (over-cap values trimmed, flagged). + captureCap: 4096 + # Environment variables env: [] # - name: NODE_NAME From 7e8108ebfa29d20fd5c2d977d8152300d4bc58e1 Mon Sep 17 00:00:00 2001 From: stheppi Date: Tue, 11 Aug 2026 10:12:54 +0100 Subject: [PATCH 02/26] feat(chart): pin Edge to v3.6.0 and release chart 0.6.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The chart has never pinned an Edge version: image.tag has been "latest" since the repo was created, and appVersion sat at a stale 2.1.0 that only ever reached the app.kubernetes.io/version label. Pin it to the release whose features the rest of this branch enables. image.tag is the load-bearing edit — edge-proxy inherits it through nofire-edge.edgeProxy.image, so one value pins both deployments. The tag carries the "v" prefix because edge's release workflow computes ${GITHUB_REF#refs/tags/}, which strips refs/tags/ but not the v; the registry has nofireai/edge:v3.6.0 and no bare 3.6.0. appVersion is set to the same v-prefixed string so the tag|default .Chart.AppVersion fallback stays resolvable if anyone clears image.tag. The chart version bump is mandatory, not cosmetic: the release workflow triggers only on paths: [Chart.yaml] and skips every publish step when a release for the current version already exists. Without it this branch would merge and ship nothing. Minor rather than patch, matching the repo's feature convention (0.4.0 -> 0.5.0, 0.3.0 -> 0.4.0). Also pins the production values example, which would otherwise override the default straight back to a floating tag, and the standalone raw manifest for consistency. pullPolicy stays Always so overriding the tag back to latest still works. --- Chart.yaml | 4 ++-- examples/production-values.yaml | 2 +- manifests.yaml | 2 +- values.yaml | 6 ++++-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Chart.yaml b/Chart.yaml index e31da99..3279928 100644 --- a/Chart.yaml +++ b/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: nofire-edge description: A Helm chart for Kubernetes Resource Graph & Causal Analysis type: application -version: 0.5.2 -appVersion: "2.1.0" +version: 0.6.0 +appVersion: "v3.6.0" keywords: - kubernetes - monitoring diff --git a/examples/production-values.yaml b/examples/production-values.yaml index 56033ed..75bdb84 100644 --- a/examples/production-values.yaml +++ b/examples/production-values.yaml @@ -2,7 +2,7 @@ image: #repository: localhost:50001/nofire-edge repository: nofireai/edge - tag: "latest" + tag: "v3.6.0" pullPolicy: Always imagePullSecrets: [] diff --git a/manifests.yaml b/manifests.yaml index c646116..ea1826b 100644 --- a/manifests.yaml +++ b/manifests.yaml @@ -19,7 +19,7 @@ spec: restartPolicy: Always containers: - name: client - image: nofireai/edge:latest + image: nofireai/edge:v3.6.0 imagePullPolicy: IfNotPresent command: ["/nofire-edge"] args: ["--config", "/etc/nofire-edge/config.json"] diff --git a/values.yaml b/values.yaml index 3205597..55681de 100644 --- a/values.yaml +++ b/values.yaml @@ -6,8 +6,10 @@ image: repository: nofireai/edge pullPolicy: Always - # Overrides the image tag whose default is the chart appVersion. - tag: "latest" + # Overrides the image tag, which otherwise defaults to the chart appVersion. + # Released tags carry the "v" prefix (nofireai/edge:v3.6.0) — a bare "3.6.0" + # does not exist in the registry. + tag: "v3.6.0" # Image pull secrets imagePullSecrets: [] From e4346bd701c9cd5dfd00c601d399b8b790b17268 Mon Sep 17 00:00:00 2001 From: stheppi Date: Wed, 7 Oct 2026 22:37:42 +0100 Subject: [PATCH 03/26] 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 04/26] 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 05/26] 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 06/26] 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 07/26] 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 08/26] 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 09/26] 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 10/26] 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 11/26] 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 12/26] 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 13/26] 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 14/26] 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 15/26] 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 16/26] 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 17/26] 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 18/26] 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 19/26] 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 20/26] 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 21/26] 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 From e119c23b8b132623d069ff7e765e8b64ee87c0a3 Mon Sep 17 00:00:00 2001 From: stheppi Date: Thu, 8 Oct 2026 23:52:55 +0100 Subject: [PATCH 22/26] fix(chart): accept whole-number floats in typedPick int fields A number read from a values file is a float64, and toString prints 1048576 as 1.048576e+06, so the integer check rejected it. Accept whole-number floats directly; fractions and strings still go through the regex check. Signed-off-by: stheppi --- templates/_helpers.tpl | 4 ++++ tests/configmap_test.yaml | 8 ++++++++ tests/render/cases/capture-cap-large/expect.jq | 1 + tests/render/cases/capture-cap-large/values.yaml | 3 +++ .../cases/services-fail-workers-fraction/expect-fail | 1 + .../cases/services-fail-workers-fraction/values.yaml | 1 + tests/render/cases/services-max-conns-large/expect.jq | 1 + tests/render/cases/services-max-conns-large/values.yaml | 1 + tests/values/large-ints.yaml | 3 +++ 9 files changed, 23 insertions(+) create mode 100644 tests/render/cases/capture-cap-large/expect.jq create mode 100644 tests/render/cases/capture-cap-large/values.yaml create mode 100644 tests/render/cases/services-fail-workers-fraction/expect-fail create mode 100644 tests/render/cases/services-fail-workers-fraction/values.yaml create mode 100644 tests/render/cases/services-max-conns-large/expect.jq create mode 100644 tests/render/cases/services-max-conns-large/values.yaml create mode 100644 tests/values/large-ints.yaml diff --git a/templates/_helpers.tpl b/templates/_helpers.tpl index 5d86970..4f498e4 100644 --- a/templates/_helpers.tpl +++ b/templates/_helpers.tpl @@ -154,8 +154,12 @@ Writes nothing itself: call it as `{{- $_ := include "nofire-edge.typedPick" (di {{- $v := index $src $k -}} {{- $p := printf "%s.%s" $path $k -}} {{- if eq $t "int" -}} +{{- /* A number from a values file is a float64; toString would print 1048576 as 1.048576e+06. */ -}} +{{- if and (kindIs "float64" $v) (eq $v (floor $v)) -}}{{- $_ := set $.out $k (int64 $v) -}} +{{- else -}} {{- if not (regexMatch "^-?[0-9]+$" (toString $v)) -}}{{- fail (printf "%s must be an integer (got %v)" $p $v) -}}{{- end -}} {{- $_ := set $.out $k (int64 $v) -}} +{{- end -}} {{- 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") -}} diff --git a/tests/configmap_test.yaml b/tests/configmap_test.yaml index edc495a..fc2571f 100644 --- a/tests/configmap_test.yaml +++ b/tests/configmap_test.yaml @@ -77,3 +77,11 @@ tests: asserts: - failedTemplate: errorPattern: namespaceFilter has unknown key "node" + + - it: accepts a whole number given as a float from a values file + values: + - values/large-ints.yaml + asserts: + - matchRegex: + path: data["config.json"] + pattern: '"maxConns":1048576' diff --git a/tests/render/cases/capture-cap-large/expect.jq b/tests/render/cases/capture-cap-large/expect.jq new file mode 100644 index 0000000..1095110 --- /dev/null +++ b/tests/render/cases/capture-cap-large/expect.jq @@ -0,0 +1 @@ +.configMapCapture.captureCap == 1048576 and .envCapture.captureCap == 1048576 diff --git a/tests/render/cases/capture-cap-large/values.yaml b/tests/render/cases/capture-cap-large/values.yaml new file mode 100644 index 0000000..085c9ac --- /dev/null +++ b/tests/render/cases/capture-cap-large/values.yaml @@ -0,0 +1,3 @@ +config: + configMapCapture: {captureCap: 1048576} + envCapture: {captureCap: 1048576} diff --git a/tests/render/cases/services-fail-workers-fraction/expect-fail b/tests/render/cases/services-fail-workers-fraction/expect-fail new file mode 100644 index 0000000..a7ff609 --- /dev/null +++ b/tests/render/cases/services-fail-workers-fraction/expect-fail @@ -0,0 +1 @@ +must be an integer diff --git a/tests/render/cases/services-fail-workers-fraction/values.yaml b/tests/render/cases/services-fail-workers-fraction/values.yaml new file mode 100644 index 0000000..b24707d --- /dev/null +++ b/tests/render/cases/services-fail-workers-fraction/values.yaml @@ -0,0 +1 @@ +config: {services: {workers: 1.5}} diff --git a/tests/render/cases/services-max-conns-large/expect.jq b/tests/render/cases/services-max-conns-large/expect.jq new file mode 100644 index 0000000..8a9fa75 --- /dev/null +++ b/tests/render/cases/services-max-conns-large/expect.jq @@ -0,0 +1 @@ +.services.maxConns == 1048576 diff --git a/tests/render/cases/services-max-conns-large/values.yaml b/tests/render/cases/services-max-conns-large/values.yaml new file mode 100644 index 0000000..dce0a3c --- /dev/null +++ b/tests/render/cases/services-max-conns-large/values.yaml @@ -0,0 +1 @@ +config: {services: {maxConns: 1048576}} diff --git a/tests/values/large-ints.yaml b/tests/values/large-ints.yaml new file mode 100644 index 0000000..bf57340 --- /dev/null +++ b/tests/values/large-ints.yaml @@ -0,0 +1,3 @@ +config: + services: + maxConns: 1048576 From ca9cb0fe202dffdcd4bbc1785da30faa4d8a26b7 Mon Sep 17 00:00:00 2001 From: stheppi Date: Thu, 8 Oct 2026 23:56:12 +0100 Subject: [PATCH 23/26] fix(chart): type-check configMapCapture and envCapture values The capture blocks were rendered by hand, so a string captureCap such as 4k produced invalid config.json and a mistyped key was silently ignored. The Edge skips an invalid redactKeyPatterns regex with only a warning, which with clearText sends the value in the clear. Render both blocks through typedPick, reject unknown keys and bad regexes at install, and keep an explicit captureCap of 0. The output is now compact JSON, so checksum/config changes on upgrade. Signed-off-by: stheppi --- templates/_helpers.tpl | 27 +++++++ templates/configmap.yaml | 24 +------ tests/configmap_test.yaml | 72 +++++++++++++++++++ tests/render/cases/capture-cap-zero/expect.jq | 1 + .../render/cases/capture-cap-zero/values.yaml | 3 + .../cases/capture-fail-bad-regex/expect-fail | 1 + .../cases/capture-fail-bad-regex/values.yaml | 4 ++ .../capture-fail-cap-not-int/expect-fail | 1 + .../capture-fail-cap-not-int/values.yaml | 3 + .../expect-fail | 1 + .../values.yaml | 3 + .../cases/capture-fail-not-map/expect-fail | 1 + .../cases/capture-fail-not-map/values.yaml | 2 + .../expect-fail | 1 + .../values.yaml | 3 + .../capture-fail-unknown-key/expect-fail | 1 + .../capture-fail-unknown-key/values.yaml | 3 + tests/render/cases/capture-full/expect.jq | 1 + tests/render/cases/capture-full/values.yaml | 9 +++ .../cases/capture-null-fields/expect.jq | 1 + .../cases/capture-null-fields/values.yaml | 3 + .../cases/capture-quoted-scalars/expect.jq | 1 + .../cases/capture-quoted-scalars/values.yaml | 3 + values.yaml | 2 + 24 files changed, 149 insertions(+), 22 deletions(-) create mode 100644 tests/render/cases/capture-cap-zero/expect.jq create mode 100644 tests/render/cases/capture-cap-zero/values.yaml create mode 100644 tests/render/cases/capture-fail-bad-regex/expect-fail create mode 100644 tests/render/cases/capture-fail-bad-regex/values.yaml create mode 100644 tests/render/cases/capture-fail-cap-not-int/expect-fail create mode 100644 tests/render/cases/capture-fail-cap-not-int/values.yaml create mode 100644 tests/render/cases/capture-fail-cleartext-not-bool/expect-fail create mode 100644 tests/render/cases/capture-fail-cleartext-not-bool/values.yaml create mode 100644 tests/render/cases/capture-fail-not-map/expect-fail create mode 100644 tests/render/cases/capture-fail-not-map/values.yaml create mode 100644 tests/render/cases/capture-fail-patterns-not-list/expect-fail create mode 100644 tests/render/cases/capture-fail-patterns-not-list/values.yaml create mode 100644 tests/render/cases/capture-fail-unknown-key/expect-fail create mode 100644 tests/render/cases/capture-fail-unknown-key/values.yaml create mode 100644 tests/render/cases/capture-full/expect.jq create mode 100644 tests/render/cases/capture-full/values.yaml create mode 100644 tests/render/cases/capture-null-fields/expect.jq create mode 100644 tests/render/cases/capture-null-fields/values.yaml create mode 100644 tests/render/cases/capture-quoted-scalars/expect.jq create mode 100644 tests/render/cases/capture-quoted-scalars/values.yaml diff --git a/templates/_helpers.tpl b/templates/_helpers.tpl index 4f498e4..c1d247f 100644 --- a/templates/_helpers.tpl +++ b/templates/_helpers.tpl @@ -197,3 +197,30 @@ Writes nothing itself: call it as `{{- $_ := include "nofire-edge.typedPick" (di {{- end -}} {{- $out | toJson -}} {{- end -}} + +{{- /* +nofire-edge.captureJson: the Edge configMapCapture / envCapture object for +.src (a values map; .path prefixes error messages). Starts from the Edge +defaults, so unset keys render as defaults. The Edge only logs and skips an +invalid redactKeyPatterns regex, which with clearText would send the value in +the clear, so unknown keys and bad regexes fail the render instead. +*/ -}} +{{- define "nofire-edge.captureJson" -}} +{{- $src := .src | default dict -}} +{{- $path := .path -}} +{{- if not (kindIs "map" $src) -}}{{- fail (printf "%s must be a map" $path) -}}{{- end -}} +{{- range $key, $_ := $src -}} +{{- if not (has $key (list "clearText" "captureCap" "redactKeyPatterns")) -}} +{{- fail (printf "%s has unknown key %q; supported keys are clearText, captureCap and redactKeyPatterns" $path $key) -}} +{{- end -}} +{{- end -}} +{{- $out := dict "clearText" false "captureCap" 4096 "redactKeyPatterns" list -}} +{{- $_ := include "nofire-edge.typedPick" (dict "src" $src "path" $path "out" $out "spec" (dict "clearText" "bool" "captureCap" "int" "redactKeyPatterns" "strlist")) -}} +{{- range $p := $out.redactKeyPatterns -}} +{{- /* regexMatch reports a pattern that does not compile as false, but "|^" matches "" otherwise. */ -}} +{{- if not (regexMatch (printf "(?:%s)|^" $p) "") -}}{{- fail (printf "%s.redactKeyPatterns entry %q is not a valid regex" $path $p) -}}{{- end -}} +{{- /* A pattern like "a)|(b" passes the check above; regexFind rejects it. */ -}} +{{- $_ := regexFind $p "" -}} +{{- end -}} +{{- $out | toJson -}} +{{- end -}} diff --git a/templates/configmap.yaml b/templates/configmap.yaml index 0afcd77..b182705 100644 --- a/templates/configmap.yaml +++ b/templates/configmap.yaml @@ -110,26 +110,6 @@ data: "excludeNamespaces": {{ $netobsEx.excludeNamespaces | toJson }} {{- end }} }, - {{- $cmc := .Values.config.configMapCapture | default dict }} - "configMapCapture": { - "clearText": {{ $cmc.clearText | default false }}, - "captureCap": {{ $cmc.captureCap | default 4096 }}, - "redactKeyPatterns": [ - {{- range $index, $pattern := $cmc.redactKeyPatterns }} - {{- if $index }},{{ end }} - {{ $pattern | quote }} - {{- end }} - ] - }, - {{- $envc := .Values.config.envCapture | default dict }} - "envCapture": { - "clearText": {{ $envc.clearText | default false }}, - "captureCap": {{ $envc.captureCap | default 4096 }}, - "redactKeyPatterns": [ - {{- range $index, $pattern := $envc.redactKeyPatterns }} - {{- if $index }},{{ end }} - {{ $pattern | quote }} - {{- end }} - ] - } + "configMapCapture": {{ include "nofire-edge.captureJson" (dict "src" .Values.config.configMapCapture "path" "config.configMapCapture") }}, + "envCapture": {{ include "nofire-edge.captureJson" (dict "src" .Values.config.envCapture "path" "config.envCapture") }} } diff --git a/tests/configmap_test.yaml b/tests/configmap_test.yaml index fc2571f..c33a0fc 100644 --- a/tests/configmap_test.yaml +++ b/tests/configmap_test.yaml @@ -85,3 +85,75 @@ tests: - matchRegex: path: data["config.json"] pattern: '"maxConns":1048576' + + - it: fails on a captureCap that is not an integer + set: + config.envCapture.captureCap: 4k + asserts: + - failedTemplate: + errorPattern: config.envCapture.captureCap must be an integer + + - it: fails on a clearText that is not a boolean + set: + config.configMapCapture.clearText: "yes" + asserts: + - failedTemplate: + errorPattern: config.configMapCapture.clearText must be a boolean + + - it: fails when redactKeyPatterns is not a list + set: + config.configMapCapture.redactKeyPatterns: foo + asserts: + - failedTemplate: + errorPattern: redactKeyPatterns must be a list + + - it: fails when a capture block is not a map + set: + config.configMapCapture: 5 + asserts: + - failedTemplate: + errorPattern: config.configMapCapture must be a map + + - it: fails on an unknown capture key + set: + config.envCapture.redactKeyPattern: [x] + asserts: + - failedTemplate: + errorPattern: config.envCapture has unknown key "redactKeyPattern" + + - it: fails on a redactKeyPatterns entry that is not a valid regex + set: + config.configMapCapture.redactKeyPatterns: ["(unclosed"] + asserts: + - failedTemplate: + errorPattern: is not a valid regex + + - it: keeps an explicit captureCap of 0 + set: + config.configMapCapture.captureCap: 0 + asserts: + - matchRegex: + path: data["config.json"] + pattern: '"configMapCapture": \{"captureCap":0,' + - matchRegex: + path: data["config.json"] + pattern: '"envCapture": \{"captureCap":4096,' + + - it: renders both capture blocks as typed JSON + set: + config.envCapture.clearText: true + config.envCapture.redactKeyPatterns: ["(?i)secret"] + asserts: + - matchRegex: + path: data["config.json"] + pattern: '"envCapture": \{"captureCap":4096,"clearText":true,"redactKeyPatterns":\["\(\?i\)secret"\]\}' + - matchRegex: + path: data["config.json"] + pattern: '"configMapCapture": \{"captureCap":4096,"clearText":false,"redactKeyPatterns":\[\]\}' + + - it: fails on a pattern that only compiles once wrapped in a group + set: + config.configMapCapture.redactKeyPatterns: ["a)|(b"] + asserts: + - failedTemplate: + errorPattern: error parsing regexp diff --git a/tests/render/cases/capture-cap-zero/expect.jq b/tests/render/cases/capture-cap-zero/expect.jq new file mode 100644 index 0000000..cfc79d5 --- /dev/null +++ b/tests/render/cases/capture-cap-zero/expect.jq @@ -0,0 +1 @@ +.configMapCapture.captureCap == 0 and .envCapture.captureCap == 0 diff --git a/tests/render/cases/capture-cap-zero/values.yaml b/tests/render/cases/capture-cap-zero/values.yaml new file mode 100644 index 0000000..9ecece4 --- /dev/null +++ b/tests/render/cases/capture-cap-zero/values.yaml @@ -0,0 +1,3 @@ +config: + configMapCapture: {captureCap: 0} + envCapture: {captureCap: 0} diff --git a/tests/render/cases/capture-fail-bad-regex/expect-fail b/tests/render/cases/capture-fail-bad-regex/expect-fail new file mode 100644 index 0000000..ef7e3c1 --- /dev/null +++ b/tests/render/cases/capture-fail-bad-regex/expect-fail @@ -0,0 +1 @@ +is not a valid regex diff --git a/tests/render/cases/capture-fail-bad-regex/values.yaml b/tests/render/cases/capture-fail-bad-regex/values.yaml new file mode 100644 index 0000000..f335de2 --- /dev/null +++ b/tests/render/cases/capture-fail-bad-regex/values.yaml @@ -0,0 +1,4 @@ +config: + configMapCapture: + clearText: true + redactKeyPatterns: ["(unclosed"] diff --git a/tests/render/cases/capture-fail-cap-not-int/expect-fail b/tests/render/cases/capture-fail-cap-not-int/expect-fail new file mode 100644 index 0000000..8c19928 --- /dev/null +++ b/tests/render/cases/capture-fail-cap-not-int/expect-fail @@ -0,0 +1 @@ +config\.envCapture\.captureCap must be an integer diff --git a/tests/render/cases/capture-fail-cap-not-int/values.yaml b/tests/render/cases/capture-fail-cap-not-int/values.yaml new file mode 100644 index 0000000..5ca6c42 --- /dev/null +++ b/tests/render/cases/capture-fail-cap-not-int/values.yaml @@ -0,0 +1,3 @@ +config: + envCapture: + captureCap: "4k" diff --git a/tests/render/cases/capture-fail-cleartext-not-bool/expect-fail b/tests/render/cases/capture-fail-cleartext-not-bool/expect-fail new file mode 100644 index 0000000..c2f10e5 --- /dev/null +++ b/tests/render/cases/capture-fail-cleartext-not-bool/expect-fail @@ -0,0 +1 @@ +config\.configMapCapture\.clearText must be a boolean diff --git a/tests/render/cases/capture-fail-cleartext-not-bool/values.yaml b/tests/render/cases/capture-fail-cleartext-not-bool/values.yaml new file mode 100644 index 0000000..b5afda1 --- /dev/null +++ b/tests/render/cases/capture-fail-cleartext-not-bool/values.yaml @@ -0,0 +1,3 @@ +config: + configMapCapture: + clearText: "yes" diff --git a/tests/render/cases/capture-fail-not-map/expect-fail b/tests/render/cases/capture-fail-not-map/expect-fail new file mode 100644 index 0000000..e00e9d3 --- /dev/null +++ b/tests/render/cases/capture-fail-not-map/expect-fail @@ -0,0 +1 @@ +config\.configMapCapture must be a map diff --git a/tests/render/cases/capture-fail-not-map/values.yaml b/tests/render/cases/capture-fail-not-map/values.yaml new file mode 100644 index 0000000..61bdef1 --- /dev/null +++ b/tests/render/cases/capture-fail-not-map/values.yaml @@ -0,0 +1,2 @@ +config: + configMapCapture: 5 diff --git a/tests/render/cases/capture-fail-patterns-not-list/expect-fail b/tests/render/cases/capture-fail-patterns-not-list/expect-fail new file mode 100644 index 0000000..48808a3 --- /dev/null +++ b/tests/render/cases/capture-fail-patterns-not-list/expect-fail @@ -0,0 +1 @@ +redactKeyPatterns must be a list diff --git a/tests/render/cases/capture-fail-patterns-not-list/values.yaml b/tests/render/cases/capture-fail-patterns-not-list/values.yaml new file mode 100644 index 0000000..538db09 --- /dev/null +++ b/tests/render/cases/capture-fail-patterns-not-list/values.yaml @@ -0,0 +1,3 @@ +config: + configMapCapture: + redactKeyPatterns: foo diff --git a/tests/render/cases/capture-fail-unknown-key/expect-fail b/tests/render/cases/capture-fail-unknown-key/expect-fail new file mode 100644 index 0000000..9ad9978 --- /dev/null +++ b/tests/render/cases/capture-fail-unknown-key/expect-fail @@ -0,0 +1 @@ +has unknown key "redactKeyPattern" diff --git a/tests/render/cases/capture-fail-unknown-key/values.yaml b/tests/render/cases/capture-fail-unknown-key/values.yaml new file mode 100644 index 0000000..631c16f --- /dev/null +++ b/tests/render/cases/capture-fail-unknown-key/values.yaml @@ -0,0 +1,3 @@ +config: + envCapture: + redactKeyPattern: [x] diff --git a/tests/render/cases/capture-full/expect.jq b/tests/render/cases/capture-full/expect.jq new file mode 100644 index 0000000..9f8aac9 --- /dev/null +++ b/tests/render/cases/capture-full/expect.jq @@ -0,0 +1 @@ +.configMapCapture == {"clearText": true, "captureCap": 100, "redactKeyPatterns": ["(?i)secret", "^tok"]} and .envCapture == {"clearText": true, "captureCap": 200, "redactKeyPatterns": ["(?i)password"]} diff --git a/tests/render/cases/capture-full/values.yaml b/tests/render/cases/capture-full/values.yaml new file mode 100644 index 0000000..9d1edac --- /dev/null +++ b/tests/render/cases/capture-full/values.yaml @@ -0,0 +1,9 @@ +config: + configMapCapture: + clearText: true + captureCap: 100 + redactKeyPatterns: ["(?i)secret", "^tok"] + envCapture: + clearText: true + captureCap: 200 + redactKeyPatterns: ["(?i)password"] diff --git a/tests/render/cases/capture-null-fields/expect.jq b/tests/render/cases/capture-null-fields/expect.jq new file mode 100644 index 0000000..c70032b --- /dev/null +++ b/tests/render/cases/capture-null-fields/expect.jq @@ -0,0 +1 @@ +.configMapCapture == {"clearText": false, "captureCap": 4096, "redactKeyPatterns": []} and .envCapture == {"clearText": false, "captureCap": 4096, "redactKeyPatterns": []} diff --git a/tests/render/cases/capture-null-fields/values.yaml b/tests/render/cases/capture-null-fields/values.yaml new file mode 100644 index 0000000..43a8382 --- /dev/null +++ b/tests/render/cases/capture-null-fields/values.yaml @@ -0,0 +1,3 @@ +config: + configMapCapture: {clearText: null, captureCap: null, redactKeyPatterns: null} + envCapture: null diff --git a/tests/render/cases/capture-quoted-scalars/expect.jq b/tests/render/cases/capture-quoted-scalars/expect.jq new file mode 100644 index 0000000..571d871 --- /dev/null +++ b/tests/render/cases/capture-quoted-scalars/expect.jq @@ -0,0 +1 @@ +.configMapCapture == {"clearText": true, "captureCap": 512, "redactKeyPatterns": []} and .envCapture == {"clearText": false, "captureCap": 8192, "redactKeyPatterns": []} diff --git a/tests/render/cases/capture-quoted-scalars/values.yaml b/tests/render/cases/capture-quoted-scalars/values.yaml new file mode 100644 index 0000000..1ddfd99 --- /dev/null +++ b/tests/render/cases/capture-quoted-scalars/values.yaml @@ -0,0 +1,3 @@ +config: + configMapCapture: {clearText: "true", captureCap: "512"} + envCapture: {clearText: "false", captureCap: "8192"} diff --git a/values.yaml b/values.yaml index 6adc8d5..96bbae1 100644 --- a/values.yaml +++ b/values.yaml @@ -309,6 +309,7 @@ config: # - "(?i)(password|passwd|secret|token|apikey|api_key|private_key)" # Per-value byte cap for clear-text values. Values over the cap are trimmed # (Brain is told the value was trimmed). Hashes always cover the full value. + # 0, negative or unset uses the Edge default of 4096. captureCap: 4096 # Container env-var value capture. Hash-by-default so a plaintext credential in @@ -321,6 +322,7 @@ config: redactKeyPatterns: [] # - "(?i)(password|passwd|secret|token|apikey|api_key|private_key)" # Per-value byte cap for clear-text values (over-cap values trimmed, flagged). + # 0, negative or unset uses the Edge default of 4096. captureCap: 4096 # Environment variables From 0ca8e42b9b63ff60e89bf8bfb90c9570982fa91a Mon Sep 17 00:00:00 2001 From: stheppi Date: Thu, 8 Oct 2026 23:56:21 +0100 Subject: [PATCH 24/26] fix(examples): add endpoints to the production resources list Helm replaces lists, so the example's kube.resources dropped the endpoints informer that the chart default enables. Cover the example and the default ClusterRole with unit tests. Signed-off-by: stheppi --- examples/production-values.yaml | 1 + tests/examples_test.yaml | 23 +++++++++++++++++++++++ tests/rbac_test.yaml | 15 +++++++++++++++ 3 files changed, 39 insertions(+) create mode 100644 tests/examples_test.yaml create mode 100644 tests/rbac_test.yaml diff --git a/examples/production-values.yaml b/examples/production-values.yaml index a2fc9b4..334ac32 100644 --- a/examples/production-values.yaml +++ b/examples/production-values.yaml @@ -54,6 +54,7 @@ config: - "pods" - "nodes" - "k8services" + - "endpoints" - "services" - "configmaps" - "secrets" diff --git a/tests/examples_test.yaml b/tests/examples_test.yaml new file mode 100644 index 0000000..7f3bb10 --- /dev/null +++ b/tests/examples_test.yaml @@ -0,0 +1,23 @@ +suite: examples +templates: + - templates/configmap.yaml + - templates/rbac.yaml +values: + - ../examples/production-values.yaml +tests: + - it: watches endpoints in the production example + template: templates/configmap.yaml + asserts: + - matchRegex: + path: data["config.json"] + pattern: '"endpoints"' + + - it: grants endpoints access in the production example + template: templates/rbac.yaml + documentSelector: + path: kind + value: ClusterRole + asserts: + - contains: + path: rules[0].resources + content: endpoints diff --git a/tests/rbac_test.yaml b/tests/rbac_test.yaml new file mode 100644 index 0000000..140b308 --- /dev/null +++ b/tests/rbac_test.yaml @@ -0,0 +1,15 @@ +suite: rbac +templates: + - templates/rbac.yaml +tests: + - it: lets the default ClusterRole read endpoints + documentSelector: + path: kind + value: ClusterRole + asserts: + - contains: + path: rules[0].resources + content: endpoints + - equal: + path: rules[0].verbs + value: [get, list, watch] From 59507301a44a6d015dfaa7911105add9b67cddf9 Mon Sep 17 00:00:00 2001 From: stheppi Date: Thu, 8 Oct 2026 23:56:35 +0100 Subject: [PATCH 25/26] fix(manifests): open endpoints gates and fix invalid config.json The standalone manifest pinned Edge v3.6.0 but left the endpoints gates closed: no ClusterRole rule and no informer entry. Its resources list named "services", which is not an Edge informer (k8services is), and a trailing comma made the embedded config.json invalid JSON. Add tests/manifests/check.sh and run it in CI. Signed-off-by: stheppi --- .github/workflows/ci.yaml | 1 + manifests.yaml | 7 ++++--- tests/manifests/check.sh | 31 +++++++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) create mode 100755 tests/manifests/check.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0677dc0..4daf0ca 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -23,3 +23,4 @@ jobs: - run: helm unittest . - run: tests/render/run.sh - run: tests/render/harness_test.sh + - run: tests/manifests/check.sh diff --git a/manifests.yaml b/manifests.yaml index ea1826b..36ce8cd 100644 --- a/manifests.yaml +++ b/manifests.yaml @@ -77,7 +77,7 @@ metadata: name: nofire-edge rules: - apiGroups: [""] - resources: ["pods", "services", "configmaps", "secrets", "persistentvolumeclaims", "persistentvolumes", "nodes", "namespaces", "resourcequotas", "limitranges"] + resources: ["pods", "services", "endpoints", "configmaps", "secrets", "persistentvolumeclaims", "persistentvolumes", "nodes", "namespaces", "resourcequotas", "limitranges"] verbs: ["get", "list", "watch"] - apiGroups: ["apps"] resources: ["deployments", "statefulsets", "daemonsets", "replicasets"] @@ -141,7 +141,8 @@ data: "resources": [ "pods", "nodes", - "services", + "k8services", + "endpoints", "configmaps", "secrets", "persistentvolumeclaims", @@ -152,7 +153,7 @@ data: "daemonsets", "jobs", "ingresses", - "namespaces", + "namespaces" ] }, "enableCausalAnalysis": true, diff --git a/tests/manifests/check.sh b/tests/manifests/check.sh new file mode 100755 index 0000000..e422637 --- /dev/null +++ b/tests/manifests/check.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Checks the standalone manifests.yaml: the embedded config.json is valid and +# watches the informers the chart does, the ClusterRole can read endpoints, and +# the image tag matches the chart appVersion. Needs yq and jq. +# Usage: tests/manifests/check.sh +set -euo pipefail + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +file="$root/manifests.yaml" +fail=0 +check() { # description, command... + local desc="$1" + shift + if "$@" >/dev/null 2>&1; then echo "ok: $desc"; else echo "FAIL: $desc" >&2; fail=1; fi +} + +cfg="$(yq 'select(.kind == "ConfigMap") | .data["config.json"]' "$file")" +check "config.json is valid JSON" jq -e . <<<"$cfg" +check "kube.resources has endpoints" jq -e '.kube.resources | index("endpoints")' <<<"$cfg" +check "kube.resources has k8services" jq -e '.kube.resources | index("k8services")' <<<"$cfg" +check "kube.resources has no services" jq -e '.kube.resources | index("services") | not' <<<"$cfg" + +role="$(yq -o=json 'select(.kind == "ClusterRole")' "$file")" +check "ClusterRole core rule has endpoints" \ + jq -e '.rules[] | select(.apiGroups == [""]) | .resources | index("endpoints")' <<<"$role" + +app_version="$(yq -r .appVersion "$root/Chart.yaml")" +images="$(yq -r 'select(.kind == "Deployment") | .spec.template.spec.containers[].image' "$file")" +check "nofireai/edge image tag is appVersion $app_version" grep -Fxq "nofireai/edge:$app_version" <<<"$images" + +exit "$fail" From 8b79007e55823585e6f014ffb915a52ff9df2de7 Mon Sep 17 00:00:00 2001 From: stheppi Date: Thu, 8 Oct 2026 23:59:15 +0100 Subject: [PATCH 26/26] test(e2e): cover capture config and endpoints RBAC on kind Install with explicit configMapCapture and envCapture values (captureCap 0, and 1048576 as a whole-number float) and check the rendered ConfigMap, the Edge's startup log, and that its service account can list endpoints. A non-integer captureCap must be rejected at render. Signed-off-by: stheppi --- ci/kind-values.yaml | 8 ++++++++ tests/e2e/kind.sh | 30 ++++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/ci/kind-values.yaml b/ci/kind-values.yaml index 297a8be..043b5a4 100644 --- a/ci/kind-values.yaml +++ b/ci/kind-values.yaml @@ -13,3 +13,11 @@ config: netobs: excludeNamespaces: [x] edgeExistenceTtl: 1h + # Exercises typed rendering: captureCap 0 must survive, and 1048576 is a + # whole-number float when read from a values file. + configMapCapture: + clearText: true + captureCap: 0 + redactKeyPatterns: ["(?i)secret"] + envCapture: + captureCap: 1048576 diff --git a/tests/e2e/kind.sh b/tests/e2e/kind.sh index b38b48c..28fcc07 100755 --- a/tests/e2e/kind.sh +++ b/tests/e2e/kind.sh @@ -1,7 +1,8 @@ #!/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. +# the namespace scoping and capture keys, the Edge accepts the config and rolls +# out, its service account can list endpoints, a values change rolls the pod, +# and an invalid filter or capture value is rejected at render. # Needs docker, kind, kubectl, helm, jq, yq. Usage: tests/e2e/kind.sh set -euo pipefail @@ -65,6 +66,10 @@ 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"]' +assert "installed ConfigMap has the capture blocks" "$cfg" \ + '.configMapCapture == {"clearText":true,"captureCap":0,"redactKeyPatterns":["(?i)secret"]} + and .envCapture == {"clearText":false,"captureCap":1048576,"redactKeyPatterns":[]}' +assert "installed ConfigMap watches endpoints" "$cfg" '.kube.resources | index("endpoints")' before="$(checksum)" step "rollout (the Edge accepted the config)" @@ -78,6 +83,19 @@ 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" +grep -Eq '"resources".*"endpoints"' <<<"$(tr -d '\n' <<<"$logs")" || die "startup log does not list the endpoints informer" +echo "ok: startup log lists the endpoints informer" +! grep -q 'skipping invalid redact key pattern' <<<"$logs" || die "Edge rejected a redactKeyPatterns entry" +echo "ok: Edge accepted the redact key patterns" + +step "RBAC: the Edge service account can list endpoints" +sa="$(kubectl get deploy -l "$selector" -o jsonpath='{.items[0].spec.template.spec.serviceAccountName}')" +ns="$(kubectl config view --minify -o jsonpath='{..namespace}')" +ns="${ns:-default}" +[[ -n "$sa" ]] || die "no service account on the deployment" +[[ "$(kubectl auth can-i list endpoints --all-namespaces --as="system:serviceaccount:$ns:$sa")" == yes ]] \ + || die "service account $ns:$sa cannot list endpoints" +echo "ok: $ns:$sa can list endpoints" step "helm upgrade to deny" helm upgrade "$release" "$root" -f "$root/ci/kind-values.yaml" \ @@ -104,4 +122,12 @@ grep -q 'config.kube.namespaceFilter.mode is "allow" but config.kube.namespaceFi || die "unexpected error: $out" echo "ok: invalid filter rejected" +step "negative: a non-integer captureCap is rejected at render" +if out="$(helm install bad "$root" -f "$root/ci/kind-values.yaml" \ + --set-string config.envCapture.captureCap=4k 2>&1)"; then + die "install with captureCap=4k succeeded" +fi +grep -q 'config.envCapture.captureCap must be an integer' <<<"$out" || die "unexpected error: $out" +echo "ok: invalid captureCap rejected" + step "all e2e checks passed"