Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
446d883
feat(chart): grant Endpoints access + informer, expose configMap & en…
alextoulps Aug 7, 2026
7e8108e
feat(chart): pin Edge to v3.6.0 and release chart 0.6.0
stheppi Aug 11, 2026
e4346bd
test(chart): add render test harness and CI
stheppi Oct 7, 2026
7ac44a5
fix(chart): render kube.namespaceFilter into config.json
stheppi Oct 7, 2026
ddbc37f
fix(chart): fail render on an invalid kube.namespaceFilter
stheppi Oct 7, 2026
9351817
fix(chart): render netobs.edgeExistenceTtl into config.json
stheppi Oct 7, 2026
744e519
fix(chart): render netobs.excludeNamespaces into config.json
stheppi Oct 7, 2026
02c8b66
fix(chart): render services.address without the full services block
stheppi Oct 7, 2026
68e39ad
fix(chart): render partial services tls/compression/handshake objects
stheppi Oct 7, 2026
1f4bbce
test(chart): guard falsy services values from being dropped
stheppi Oct 7, 2026
17760d0
refactor(chart): move pickSet helper to _helpers.tpl
stheppi Oct 7, 2026
ab56e8d
docs(chart): document namespace scoping, netobs exclusions and servic…
stheppi Oct 7, 2026
46651cf
test(chart): add kind end-to-end test for the Edge ConfigMap
stheppi Oct 7, 2026
1a124b1
fix(chart): coerce services scalars to the types the Edge expects
stheppi Oct 7, 2026
f8b86a2
fix(chart): validate namespace names and config types before rendering
stheppi Oct 7, 2026
9d48aff
fix(chart): make configMapCapture/envCapture nil-safe (PR #13 code)
stheppi Oct 7, 2026
530084b
test(chart): check the new pod's logs after upgrade in the kind test
stheppi Oct 7, 2026
f0ea3d1
fix(chart): reject non-string netobs.excludeNamespaces entries
stheppi Oct 7, 2026
206d63b
fix(chart): validate netobs.edgeExistenceTtl as a Go duration
stheppi Oct 7, 2026
446f0cb
fix(chart): reject unknown keys under kube.namespaceFilter
stheppi Oct 7, 2026
f4ca8f6
test(chart): keep helm stderr out of rendered YAML; run CI on main
stheppi Oct 7, 2026
5eb7078
Merge pull request #18 from NOFireAI/fix/configmap-namespace-filter
stheppi Oct 8, 2026
e119c23
fix(chart): accept whole-number floats in typedPick int fields
stheppi Oct 8, 2026
ca9cb0f
fix(chart): type-check configMapCapture and envCapture values
stheppi Oct 8, 2026
0ca8e42
fix(examples): add endpoints to the production resources list
stheppi Oct 8, 2026
5950730
fix(manifests): open endpoints gates and fix invalid config.json
stheppi Oct 8, 2026
8b79007
test(e2e): cover capture config and endpoints RBAC on kind
stheppi Oct 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: CI

on:
pull_request:

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 PR description says "this repo has no PR-triggered CI — release.yml fires on push-to-main and workflow_dispatch only, so no checks will report here", but this diff adds a workflow with on: pull_request.

The description is the stale side: .github/workflows/ci.yaml:4 triggers on every pull request and runs helm lint, helm unittest, tests/render/run.sh and tests/render/harness_test.sh. Update the Testing section so a reviewer does not skip checks that will in fact report.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Right, the description was the stale side. The Testing section is updated: CI runs on every PR.

push:
branches: [main]

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
- run: tests/render/harness_test.sh
- run: tests/manifests/check.sh
3 changes: 3 additions & 0 deletions .helmignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ docs/
.idea/
*.tmproj
.vscode/
# Tests and CI helpers are not chart content
tests/
ci/
4 changes: 2 additions & 2 deletions Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions ci/kind-values.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# 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
# 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
7 changes: 6 additions & 1 deletion examples/production-values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
image:
#repository: localhost:50001/nofire-edge
repository: nofireai/edge
tag: "latest"
tag: "v3.6.0"
pullPolicy: Always

imagePullSecrets: []
Expand Down Expand Up @@ -54,6 +54,7 @@ config:
- "pods"
- "nodes"
- "k8services"
- "endpoints"
- "services"
- "configmaps"
- "secrets"
Expand All @@ -66,6 +67,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
Expand Down
9 changes: 5 additions & 4 deletions manifests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -141,7 +141,8 @@ data:
"resources": [
"pods",
"nodes",
"services",
"k8services",
"endpoints",
"configmaps",
"secrets",
"persistentvolumeclaims",
Expand All @@ -152,7 +153,7 @@ data:
"daemonsets",
"jobs",
"ingresses",
"namespaces",
"namespaces"
]
},
"enableCausalAnalysis": true,
Expand Down
90 changes: 89 additions & 1 deletion templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,92 @@ Edge Proxy: configmap name
*/}}
{{- define "nofire-edge.edgeProxy.configName" -}}
{{- printf "%s-config" (include "nofire-edge.edgeProxy.fullname" .) }}
{{- end }}
{{- end }}

{{- /*
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.typedPick" -}}
{{- $src := .src | default dict -}}
{{- $path := .path -}}
{{- range $k, $t := .spec -}}
{{- if and (hasKey $src $k) (not (kindIs "invalid" (index $src $k))) -}}
{{- $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") -}}
{{- 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 -}}
{{- 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 -}}
85 changes: 52 additions & 33 deletions templates/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,39 @@ data:
{{ $resource | quote }}
{{- end }}
]
{{- $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 }}
{{- 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 }}
{{- /* Mirror the Edge's validation so a bad filter fails at install, not as a crashloop. */}}
{{- $mode := get $nf "mode" | default "" }}
{{- $namespaces := get $nf "namespaces" | default list }}
{{- 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 }}
{{- 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 }},
"namespaceFilter": {{ $nf | toJson }}
{{- end }}
},
"enableCausalAnalysis": {{ .Values.config.enableCausalAnalysis }},
"causal": {
Expand All @@ -38,38 +71,7 @@ data:
"sampleRate": {{ .Values.config.sentry.sampleRate | default 1.0 }},
"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 }},
"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 }}
]
},
"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 }}
},
"handshake": {
"timeout": {{ .Values.config.services.handshake.timeout | default "" | quote }},
"contentType": {{ .Values.config.services.handshake.contentType | default "" | quote }}
}
{{- 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 }},
Expand All @@ -92,5 +94,22 @@ 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 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 }}
{{- $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 }}
},
"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") }}
}
2 changes: 1 addition & 1 deletion templates/rbac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
Loading
Loading