Skip to content

fix(chart): render namespace scoping and netobs keys into the Edge config - #18

Merged
stheppi merged 19 commits into
feat/endpoints-access-and-configmap-capturefrom
fix/configmap-namespace-filter
Oct 8, 2026
Merged

stheppi merged 19 commits into
feat/endpoints-access-and-configmap-capturefrom
fix/configmap-namespace-filter

Conversation

@stheppi

@stheppi stheppi commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Chart 0.5.2 (and #13 as it stands) never writes config.kube.namespaceFilter, config.netobs.excludeNamespaces or config.netobs.edgeExistenceTtl into the Edge's config.json. Setting them through Helm does nothing, and nothing reports it. Setting config.services.address on its own also breaks helm template with a nil pointer.

This PR makes the ConfigMap template write those keys whenever they're set, and makes every services.* sub-key optional. It targets #13 so the fix ships in 0.6.0 rather than as a separate 0.5.3.

  • Namespace scoping: the template now writes kube.namespaceFilter when it's set. Invalid filters fail helm install/upgrade with a clear message instead of crashlooping the pod. That covers an unknown mode, a mode and namespace list that don't agree, names that aren't DNS-1123 labels, and wrong types. The checks mirror the Edge v3.6.0 validation.
  • Netobs keys: edgeExistenceTtl and excludeNamespaces are written when set and left out when unset, so the Edge defaults apply. Explicit false, 0 and [] are kept. null counts as unset.
  • services: each key is written only when you set it, and coerced to the type the Edge expects, so --set-string workers=8 gives a number. tls, compression and handshake render as partial objects, which the Edge merges into its defaults.
  • --reuse-values from 0.5.2: upgrading this way crashed on a nil pointer in feat(chart): enable Edge v3.6.0 features and pin to v3.6.0 (chart 0.6.0) #13's configMapCapture/envCapture blocks, because 0.5.2's values have no such maps. A separate commit makes those blocks nil-safe.
  • Docs: values.yaml documents the new keys and the real Edge defaults for services.

Testing

Each fix was written test-first; the failing output from before each change is in the commit bodies.

  • helm lint .: passes
  • helm unittest .: 10 of 10 pass (tests/configmap_test.yaml)
  • tests/render/run.sh: 53 of 53 cases pass. Each case renders the chart with its own values and checks the exact config.json with jq, or checks that rendering fails with the right message.
  • EDGE_SRC=<edge checkout at v3.6.0> tests/render/run.sh also feeds every rendered config to the Edge's own config.LoadConfigFromReader.
  • tests/e2e/kind.sh passes end to end on a local kind cluster:
    • installs with an allow filter and checks the installed ConfigMap and the Edge's startup log
    • runs helm upgrade to deny and checks that checksum/config changes and the new pod logs deny
    • checks that an invalid filter is rejected at render
  • With default values and with examples/production-values.yaml, the generated config.json is semantically identical to feat(chart): enable Edge v3.6.0 features and pin to v3.6.0 (chart 0.6.0) #13's (jq -S diff empty). It is not byte-identical: services is now written as one line of compact JSON, so the checksum/config hash changes and pods roll once on upgrade, even with unchanged values.
  • tests/render/harness_test.sh checks that helm's stderr warnings (e.g. a symlink in the chart) don't leak into the parsed output. It also runs in CI.
  • Revert check: restoring the pre-review template makes 4 unittests and 5 render cases fail.
  • New .github/workflows/ci.yaml runs lint, helm-unittest, the render cases and the harness test on every PR and on pushes to main. The Edge loader check and kind stay local-only.

Local kind evidence

These runs were on a local kind cluster against commit 530084b (re-run on f4ca8f6 after the review fixes: all checks pass):

  • Tools: helm v3.18.4, kind v0.29.0, Kubernetes node v1.33.1.
  • Images: nofireai/edge:v3.6.0 (sha256:4b577d0e…62bd32f).
  • Run date: 2026-10-07T22:20Z.
  • Values: both charts were installed side by side with the same values, ci/kind-values.yaml (namespaceFilter: allow [default], excludeNamespaces: [x], edgeExistenceTtl: 1h, publishing off).
  • Base: feat(chart): enable Edge v3.6.0 features and pin to v3.6.0 (chart 0.6.0) #13's chart, at commit 7e8108e.

1. Behaviour: what each Edge actually graphs

The cluster ran a web Deployment in default and another in payments. After a 45s initial sync, I port-forwarded to each Edge and counted the resources per namespace in its live GET /graph:

Chart Namespaces in /graph (resources) /graph/stats
#13 (before) default 14, kube-system 24, local-path-storage 6, payments 5, base 9, kube-public 2, kube-node-lease 1 69 nodes, 75 edges
This PR (after) default 14 16 nodes, 17 edges

With #13's chart the filter never reaches the Edge, so it graphs the whole cluster. With this PR it graphs only default.

2. The installed ConfigMap (kubectl get cm … -o jsonpath='{.data.config\.json}')

Before (#13): namespaceFilter=false excludeNamespaces=false edgeExistenceTtl=false
"kube":   { "configPath": "", "resyncInterval": "5m", "clusterName": "default-cluster" }
"netobs": { "enabled": false, "interval": "60s", "prometheusUrl": "", "source": "", "prometheusTimeout": "10s" }

helm template --set config.services.address=:6000 → nil pointer evaluating interface {}.enabled

After (this PR)
"kube": {
  "configPath": "", "resyncInterval": "5m", "clusterName": "default-cluster",
  "namespaceFilter": { "mode": "allow", "namespaces": ["default"] }
},
"netobs": {
  "enabled": false, "interval": "60s", "prometheusUrl": "", "source": "", "prometheusTimeout": "10s",
  "edgeExistenceTtl": "1h",
  "excludeNamespaces": ["x"]
},
"services": { "workers": 4 }

3. The Edge accepted it

The pod e2e-nofire-edge-7f7b7dcf89-9tg2j reached 1/1 Running with 0 restarts. The Edge's own startup log shows the filter it loaded, along with the Edge defaults it filled in for services:

"message":"Configuration loaded successfully"
"kube":{…,"namespaceFilter":{"mode":"allow","namespaces":["default"]}}
"services":{"workers":4,"address":":6000","maxConns":100,"readTimeout":"30s","writeTimeout":"10s",
            "compression":{"enabled":true,"type":"gzip","level":-1},"handshake":{"timeout":"10s",…}}

4. helm upgrade rolls the pod with the new config

helm upgrade --set config.kube.namespaceFilter.mode=deny:

checksum/config before: 7eb1d295a1a5a28ec467f71fe98e29db0c4aa0df6b349a8448d9405c1e9d95f8
checksum/config after:  5bd18119a4facc06bacf3910f7a05afc7a8d22f631cd4b4696a6d377dd336785
ConfigMap namespaceFilter: {"mode":"deny","namespaces":["default"]}
new pod e2e-nofire-edge-745fd47f7f-ff9v2 (1/1 Running, 0 restarts) logs "namespaceFilter":{"mode":"deny","namespaces":["default"]}

5. Invalid values fail at install, not in the pod

$ helm install bad1 … --set config.kube.namespaceFilter.namespaces=null
config.kube.namespaceFilter.mode is "allow" but config.kube.namespaceFilter.namespaces is empty
$ helm install bad2 … --set-json 'config.kube.namespaceFilter.namespaces=["Prod_1"]'
"Prod_1" is not a valid namespace name (a lowercase DNS-1123 label of at most 63 characters)

Neither release was created (helm list -A shows only base and e2e).

6. Scripted e2e

tests/e2e/kind.sh asserts sections 2 to 5 automatically and passed on 530084b:

==> helm install (namespaceFilter allow [default])
ok: installed ConfigMap has allow filter
ok: installed ConfigMap has netobs keys
==> rollout (the Edge accepted the config)
ok: startup log shows the allow filter
==> helm upgrade to deny
ok: checksum/config changed
ok: upgraded ConfigMap has deny filter
ok: new pod runs with the deny filter
==> negative: allow with no namespaces is rejected at render
ok: invalid filter rejected
==> all e2e checks passed

The /graph comparison in section 1 was a one-off script run, not part of kind.sh.

Notes

Merge this into #13 before #13 lands. release.yml publishes when Chart.yaml changes on main, and #13 already sets 0.6.0. If #13 merges first, 0.6.0 ships without this fix and it needs a 0.6.1.

Behaviour changes for existing users

  • Edge defaults instead of chart defaults: keys you don't set now get the Edge's own defaults rather than values the chart made up. Before, setting services.address also wrote compression as disabled with an empty type, an empty handshake content type, and empty TLS paths. Now the Edge defaults apply: compression on with gzip, a 10s handshake, maxConns 100.
  • Empty address: services.address: "" is treated as unset, so the Edge listens on its default :6000.
  • excludeNamespaces replaces the list: setting it replaces the Edge default [kube-system, monitoring] instead of adding to it. Namespaces from a deny filter are still added by the Edge.
  • Stricter render: invalid namespace-filter values and wrong types now fail at render instead of reaching the pod.

Out of scope

  • The chart has never rendered services.nameLabels, versionLabels, serviceNameEnvVars or enableOTEL, and this PR leaves them out too.
  • manifests.yaml (the static manifest at the repo root) is unchanged.

Follow-ups after 0.6.0 releases

  • Bump the dashboard's EDGE_CHART_VERSION to 0.6.0.
  • In NOFireAI/docs, replace the "Not passed through by chart 0.5.2" notes (NOFireAI/docs#43).

I implemented this with Claude Code: Sonnet wrote the changes test-first, and Opus reviewed the plan and the final diff adversarially. I re-ran every test locally before opening this PR.


Fixes #17
Refs: #13, NOFireAI/docs#42, NOFireAI/docs#43, NOFireAI/dashboard#915

stheppi added 15 commits October 7, 2026 22:37
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 <stefan.bocutiu@gmail.com>
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 <stefan.bocutiu@gmail.com>
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 <stefan.bocutiu@gmail.com>
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 <stefan.bocutiu@gmail.com>
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 <stefan.bocutiu@gmail.com>
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 <stefan.bocutiu@gmail.com>
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 <stefan.bocutiu@gmail.com>
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 <stefan.bocutiu@gmail.com>
Signed-off-by: stheppi <stefan.bocutiu@gmail.com>
…es 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 <stefan.bocutiu@gmail.com>
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 <stefan.bocutiu@gmail.com>
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 <stefan.bocutiu@gmail.com>
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 <stefan.bocutiu@gmail.com>
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 <stefan.bocutiu@gmail.com>
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 <stefan.bocutiu@gmail.com>
@stheppi stheppi self-assigned this Oct 7, 2026
@stheppi

stheppi commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator Author

@claude-fleet review

@claude-fleet

claude-fleet Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Review task queued.

Task: 86b67d97-f0fe-4ec5-845e-e2992f5f9698
Commit: 530084b
Model: claude-opus-5 (effort medium)
Executor: rp2
Status: done. Review posted.

@claude-fleet claude-fleet Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Workspace restored; git status shows only the pre-existing untracked node_modules.


Request changes — one narrow should_fix gap, the rest nits. The core of this PR is solid: the three unrendered keys now render, the validation is well-targeted, and the test suite genuinely covers it.

Findings:

  • should_fix, templates/configmap.yaml:102 — netobs.excludeNamespaces entries are not type-checked, so non-string entries reach the pod
  • nit, templates/configmap.yaml:93 — edgeExistenceTtl is checked as a string but not as a parseable duration
  • nit, templates/configmap.yaml:33 — a namespaceFilter map with only unrecognized keys silently renders no filter
  • nit, tests/render/run.sh:61 — stderr is folded into the YAML the harness parses, so any benign helm warning fails every case
  • nit, templates/configmap.yaml:69 — "byte-identical to #13's" in the description is not accurate

Verified:

  • kube.namespaceFilter, netobs.edgeExistenceTtl and netobs.excludeNamespaces are written only when set, and null counts as unset — templates/configmap.yaml:27-54, 92-103; confirmed by the defaults, ns-filter-null-fields, ttl-null and exclude-ns-null cases
  • Falsy values survive: workers: 0, cipherSuites: [], excludeNamespaces: [], edgeExistenceTtl: 0s, requireClientCert: false — templates/_helpers.tpl:146 keeps a key on hasKey and only kindIs "invalid" drops it
  • --set-string coercion works: workers: "8" renders 8, enabled: "true" renders true — templates/_helpers.tpl:152-159, case services-quoted-scalars
  • services.address alone no longer panics. Reverting templates/configmap.yaml:69 to #13's block reproduces the reported error verbatim: nil pointer evaluating interface {}.enabled at configmap.yaml:77
  • tls/compression/handshake render as partial objects and empty ones are dropped — templates/_helpers.tpl:186-192, cases services-empty-objects, services-tls-enabled
  • --reuse-values from 0.5.2 works, and not only for the ConfigMap: helm template of the whole chart against the 0.5.2 values file renders clean, so configMapCapture/envCapture at templates/configmap.yaml:105,116 are the only two sites that needed guarding. I grepped templates/ for other uses of the new #13 keys and found none
  • The rendered default config is semantically identical to #13's (jq -S diff empty) — see the nit below for the byte-level caveat
  • config.services is the only consumer of those values; nothing else in templates/ reads services.address or the filter

Checks run:

  • tests/render/run.sh → 41 passed, 0 failed (helm v3.18.4)
  • helm lint . → 0 failed; helm unittest . (plugin v1.2.1) → 4 of 4 pass
  • Revert check, namespace filter: deleted templates/configmap.yaml:27-54, ran run.sh ns-filter → 14 of 15 fail (all 10 expect-fail cases render successfully; ns-filter-allow/deny/none/duplicates fail their expect.jq). Restored.
  • Revert check, netobs: deleted templates/configmap.yaml:92-103, ran run.sh ttl and run.sh exclude-ns → 6 of 8 fail. Restored.
  • Revert check, services: replaced templates/configmap.yaml:69 with #13's inline block, ran run.sh services → 12 of 15 fail, including the nil-pointer crash. Restored; git status clean.

Smaller things:

  • .github/workflows/ci.yaml:4 triggers only on pull_request. Pushes straight to main (and the release path) run nothing.
  • tests/render/run.sh:45 does cp -R "$chart_root" for the replace-values case, which copies .git and any node_modules into the temp chart. git archive or --exclude would be cheaper.
  • templates/configmap.yaml:108,119: $cmc.captureCap | default 4096 means an explicit captureCap: 0 silently becomes 4096. Inherited from #13 and the semantics are unchanged here, but the line is in this diff and the rest of the PR is careful about exactly this.
  • tests/e2e/kind.sh reads image.repository/image.tag from values.yaml (lines 23-25), so it tracks the Chart.yaml pin automatically. Nice.

Not checked: the kind e2e run (no docker/kind in this environment), the EDGE_SRC loader path and tests/render/edgecheck/main.go (no Edge checkout reachable), and therefore the claim that the render-time checks mirror Edge v3.6.0's own validation — in particular whether the Edge accepts {"mode":"none"}, an empty mode, or duplicate namespaces, all of which this chart now renders.

Not checked:

  • tests/e2e/kind.sh was not executed: no docker or kind in this environment. Reviewed by reading only.
  • tests/render/edgecheck/main.go and the EDGE_SRC code path: no Edge checkout reachable, so go build -overlay and config.LoadConfigFromReader were not exercised.
  • The claim that the render-time namespace-filter checks mirror Edge v3.6.0's own validation. In particular, whether the Edge accepts {"mode":"none"}, an empty mode string, or duplicate namespace entries, all of which the chart now renders (tests/render/cases/ns-filter-none, ns-filter-duplicates).
  • The documented Edge defaults added to values.yaml (maxConns 100, writeTimeout 10s, compression gzip, handshake 10s) were not verified against the Edge source.

Task 86b67d97-f0fe-4ec5-845e-e2992f5f9698 on rp2, model claude-opus-5, effort medium.

Comment thread templates/configmap.yaml Outdated
{{- if not (kindIs "slice" .Values.config.netobs.excludeNamespaces) }}
{{- fail "config.netobs.excludeNamespaces must be a list" }}
{{- end }},
"excludeNamespaces": {{ .Values.config.netobs.excludeNamespaces | toJson }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should_fix: netobs.excludeNamespaces is checked for being a list but its entries are never type-checked, so non-string entries are passed straight through toJson into config.json. This is the one input in the PR that can still reach the pod malformed, which is the failure mode the rest of the change exists to prevent. The sibling key kube.namespaceFilter.namespaces goes through nofire-edge.typedPick's strlist, which rejects a non-string entry at render (templates/_helpers.tpl:164-166) and has a dedicated case (tests/render/cases/ns-filter-fail-nonstring).

Failure: config.netobs.excludeNamespaces: [1, 2] renders "excludeNamespaces":[1,2]; the Edge decodes that field into a []string and fails at startup, crashlooping the pod.

Evidence: I ran helm template t . -f /tmp/v1.yaml -s templates/configmap.yaml | yq -r '.data["config.json"]' | jq -c '.netobs.excludeNamespaces' with those values and got [1,2]. The only guard is the kindIs "slice" check at templates/configmap.yaml:99.

Fix: route it through the existing helper, e.g. {{- $ne := dict }}{{- $_ := include "nofire-edge.typedPick" (dict "src" .Values.config.netobs "path" "config.netobs" "out" $ne "spec" (dict "excludeNamespaces" "strlist")) }}. Validating entries as DNS-1123 labels too, as the filter does at line 46, would make the two lists consistent.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f0ea3d1. Every entry is now type-checked and the render fails on a non-string: excludeNamespaces: [1,2] gives config.netobs.excludeNamespaces entry 1 must be a string. I confirmed the Edge v3.6.0 loader rejects [1,2] (cannot unmarshal number … of type string).

I deliberately did not add the DNS-1123 check. The Edge loader accepts ["Prod_1"] here (unlike namespaceFilter.namespaces), and an unmatched name just excludes nothing. So the chart stays aligned with what the Edge accepts. The exclude-ns-uppercase case records that decision. Tests: unittest plus exclude-ns-fail-number, exclude-ns-fail-string, exclude-ns-uppercase, exclude-ns-empty, exclude-ns-null.

Comment thread templates/configmap.yaml
"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 not (kindIs "string" .Values.config.netobs.edgeExistenceTtl) }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: edgeExistenceTtl is validated as a string but not as a Go duration, so a typo renders successfully and only fails inside the Edge. Narrow, and it fails closed (the pod does not start with a wrong TTL), but the PR validates namespace names at render for exactly this reason.

Failure: config.netobs.edgeExistenceTtl: "not-a-duration" renders "edgeExistenceTtl":"not-a-duration" and the Edge's time.ParseDuration rejects it at startup.

Evidence: ran helm template with that value and jq -c '.netobs.edgeExistenceTtl' printed "not-a-duration". templates/configmap.yaml:93 only checks kindIs "string".

Fix: add regexMatch "^-?([0-9]+(\\.[0-9]+)?(ns|us|ms|s|m|h))+$" alongside the kind check.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 206d63b. edgeExistenceTtl must now match Go duration syntax. not-a-duration gives … is not a valid duration; use Go syntax such as 2h, 90m or 1h30m, and so does a bare 30 with no unit. The leading -, fractions, compound values, µs/us and "0" are still accepted, since time.ParseDuration accepts them. Every ttl-ok-* case also passes the Edge v3.6.0 loader.

Comment thread templates/configmap.yaml
{{- end }}
{{- $nf := dict }}
{{- $_ := include "nofire-edge.typedPick" (dict "src" $nfRaw "path" "config.kube.namespaceFilter" "out" $nf "spec" (dict "mode" "string" "namespaces" "strlist")) }}
{{- if $nf }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: {{- if $nf }} skips both the validation and the render when typedPick produced an empty dict, which happens when the user's namespaceFilter map contains only keys the spec does not know. The result is a silently ignored namespace filter: the same class of bug this PR fixes.

Failure: config.kube.namespaceFilter: {node: allow} (a plausible typo for mode) renders with no namespaceFilter key at all and no warning; the Edge graphs the whole cluster. A typo in only one of the two keys, such as {modes: allow, namespaces: [x]}, does fail, so the behaviour is inconsistent.

Evidence: ran helm template with namespaceFilter: {node: allow}; jq '.kube | has("namespaceFilter")' returned false.

Fix: when the source map is non-empty but $nf is empty, fail with the unrecognized key names.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 446f0cb. Any key other than mode/namespaces now fails the render: {node: allow} gives config.kube.namespaceFilter has unknown key "node"; supported keys are mode and namespaces. This applies with or without a valid mode alongside (cases ns-filter-fail-unknown-key, ns-filter-fail-unknown-key-valid-mode, plus a unittest).

Comment thread tests/render/run.sh Outdated
continue
fi

if ! out="$(helm template t "$chart" ${vals[@]+"${vals[@]}"} -s templates/configmap.yaml 2>&1)"; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: The success path captures stderr into $out (2>&1) and then feeds $out to yq as YAML at line 66. Any non-fatal warning helm writes to stderr corrupts the document and every case fails with a message that points nowhere near the cause.

Failure: with a symlink anywhere under the chart root, helm prints walk.go:75: found symbolic link in path: ... to stderr; the suite then aborts on the first case with Error: bad file '-': yaml: mapping values are not allowed in this context and reports nothing else. I hit this on the first run of the suite in this workspace.

Evidence: helm template t . -f tests/render/cases/defaults/values.yaml -s templates/configmap.yaml 2>&1 1>/dev/null prints the warning, confirming it is on stderr. After removing the symlink the suite reports 41 passed, 0 failed.

Fix: capture stderr separately on the success path (2>"$tmp/stderr") and only merge it in the expect-fail branch at line 51, which does need it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f4ca8f6. On the success path, stderr now goes to its own file and is printed only when a case fails; the expect-fail branch still matches against it. The new tests/render/harness_test.sh reproduces your symlink scenario in a temp copy of the chart and asserts the suite still passes. It runs in CI. The same commit replaces the cp -R for replace-values with a chart-only copy that skips .git, node_modules and tests.

Comment thread templates/configmap.yaml
}
{{- end }}
},
"services": {{ include "nofire-edge.servicesJson" . }},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: The description says the generated config.json is "byte-identical to #13's" with default values and with examples/production-values.yaml. It is semantically identical, not byte-identical: the services object is now emitted by toJson on one line.

Failure: none at runtime, but checksum/config in the Deployment is a hash of the ConfigMap text, so anyone already on a 0.6.0 pre-release gets a pod roll on upgrade even with unchanged values. Worth stating accurately in the body since the PR's upgrade notes are otherwise thorough.

Evidence: diff of the two renders shows "services": {\n "workers": 4\n }, becoming "services": {"workers":4}, for both value sets; diff <(jq -S . base.json) <(jq -S . head.json) is empty.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, that was inaccurate. I updated the description: it is semantically identical (jq -S diff empty), not byte-identical. The services line is now compact JSON, so checksum/config changes and pods roll once on upgrade, even with unchanged values.

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 <stefan.bocutiu@gmail.com>
"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 <stefan.bocutiu@gmail.com>
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 <stefan.bocutiu@gmail.com>
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 <stefan.bocutiu@gmail.com>
@stheppi

stheppi commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator Author

All five inline findings are addressed in f0ea3d1..f4ca8f6

Smaller points from the review body

Your "Not checked" list, now checked

  • Edge loader at v3.6.0 (EDGE_SRC): {"mode":"none"} and {"mode":"allow","namespaces":["a","a"]} are both accepted, and an empty mode is the Edge zero value. Unknown modes, empty lists and invalid names are rejected, matching the chart guards. All 53 render cases pass the loader.
  • values.yaml defaults: checked against DefaultConfig in Edge v3.6.0 internal/edge/config/config.go (maxConns 100, readTimeout 30s, writeTimeout 10s, compression enabled/gzip/-1, handshake 10s).
  • kind: tests/e2e/kind.sh passes on f4ca8f6. The description also shows a side-by-side /graph run: with allow [default], the Edge from feat(chart): enable Edge v3.6.0 features and pin to v3.6.0 (chart 0.6.0) #13 graphed 7 namespaces (69 nodes); with this PR it graphed only default (16 nodes).

Current numbers: helm lint clean, helm unittest 10/10, run.sh 53/53 (with and without EDGE_SRC), harness_test.sh passes. Revert check: restoring the pre-review template fails 4 unittests and 5 render cases.

@stheppi
stheppi merged commit 5eb7078 into feat/endpoints-access-and-configmap-capture Oct 8, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant