diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..4daf0ca --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,26 @@ +name: CI + +on: + pull_request: + 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 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/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/ci/kind-values.yaml b/ci/kind-values.yaml new file mode 100644 index 0000000..043b5a4 --- /dev/null +++ b/ci/kind-values.yaml @@ -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 diff --git a/examples/production-values.yaml b/examples/production-values.yaml index 56033ed..334ac32 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: [] @@ -54,6 +54,7 @@ config: - "pods" - "nodes" - "k8services" + - "endpoints" - "services" - "configmaps" - "secrets" @@ -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 diff --git a/manifests.yaml b/manifests.yaml index c646116..36ce8cd 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"] @@ -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/templates/_helpers.tpl b/templates/_helpers.tpl index 54f8229..c1d247f 100644 --- a/templates/_helpers.tpl +++ b/templates/_helpers.tpl @@ -135,4 +135,92 @@ 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.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 -}} diff --git a/templates/configmap.yaml b/templates/configmap.yaml index 45bd809..b182705 100644 --- a/templates/configmap.yaml +++ b/templates/configmap.yaml @@ -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": { @@ -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 }}, @@ -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") }} } 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/tests/configmap_test.yaml b/tests/configmap_test.yaml new file mode 100644 index 0000000..c33a0fc --- /dev/null +++ b/tests/configmap_test.yaml @@ -0,0 +1,159 @@ +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"] + + - it: fails when namespaces are set without a mode + set: + config.kube.namespaceFilter.namespaces: [a] + asserts: + - failedTemplate: + errorPattern: disables filtering + + - it: fails when mode is allow with no namespaces + set: + config.kube.namespaceFilter.mode: allow + asserts: + - failedTemplate: + errorPattern: mode is "allow" but + + - it: fails on an unknown mode + set: + config.kube.namespaceFilter.mode: bogus + config.kube.namespaceFilter.namespaces: [a] + 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 + + - 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 + + - 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" + + - 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' + + - 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/e2e/kind.sh b/tests/e2e/kind.sh new file mode 100755 index 0000000..28fcc07 --- /dev/null +++ b/tests/e2e/kind.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +# End-to-end check on a throwaway kind cluster: the installed ConfigMap carries +# 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 + +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}' +} +selector="app.kubernetes.io/instance=$release,app.kubernetes.io/name=nofire-edge" +checksum() { + 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" +} + +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"]' +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)" +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. +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" +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" \ + --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 "$selector" --timeout=180s \ + || die "rollout after upgrade failed" +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" +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 "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" 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/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" 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] 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/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/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/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-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-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/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-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"]}} 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]}} 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-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 new file mode 100644 index 0000000..3f19409 --- /dev/null +++ b/tests/render/cases/ns-filter-fail-allow-empty/expect-fail @@ -0,0 +1 @@ +mode is "allow" but 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..47a7bbb --- /dev/null +++ b/tests/render/cases/ns-filter-fail-deny-missing/expect-fail @@ -0,0 +1 @@ +mode is "deny" but 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-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 new file mode 100644 index 0000000..41564e5 --- /dev/null +++ b/tests/render/cases/ns-filter-fail-namespaces-no-mode/expect-fail @@ -0,0 +1 @@ +disables filtering 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..c64ccf7 --- /dev/null +++ b/tests/render/cases/ns-filter-fail-none-with-namespaces/expect-fail @@ -0,0 +1 @@ +mode "none" disables filtering 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-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-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}}} 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..d6d1c8a --- /dev/null +++ b/tests/render/cases/ns-filter-fail-unknown-mode/expect-fail @@ -0,0 +1 @@ +must be one of.*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] 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-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 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/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/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-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-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-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-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-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-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-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-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: []}}} 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" 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-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/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"} 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}}} 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-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-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/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-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"}} 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}} 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/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/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 new file mode 100755 index 0000000..bf865b0 --- /dev/null +++ b/tests/render/run.sh @@ -0,0 +1,90 @@ +#!/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) +# 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_root="$(cd "$here/../.." && pwd)" +filter="${1:-}" +[[ -n "${EDGE_SRC:-}" ]] && EDGE_SRC="$(cd "$EDGE_SRC" && pwd)" +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT + +edgecheck="" +if [[ -n "${EDGE_SRC:-}" ]]; then + cat > "$tmp/overlay.json" <"$tmp/out" 2>"$tmp/stderr" || rc=$? + + if [[ -f "$dir/expect-fail" ]]; then + if [[ "$rc" -eq 0 ]]; then + report FAIL "$name" "render succeeded, expected failure" + 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"): $(cat "$tmp/stderr")" + fi + continue + fi + + 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 + 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 ]] 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 diff --git a/values.yaml b/values.yaml index c761e1f..96bbae1 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: [] @@ -192,6 +194,7 @@ config: - "pods" - "nodes" - "k8services" + - "endpoints" - "configmaps" - "secrets" - "persistentvolumeclaims" @@ -204,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: @@ -213,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. @@ -241,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 @@ -266,6 +296,35 @@ 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. + # 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 + # 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). + # 0, negative or unset uses the Edge default of 4096. + captureCap: 4096 + # Environment variables env: [] # - name: NODE_NAME