From d363a1f159c754980b0d5dc33e404b6fc70c3b65 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Tue, 14 Jul 2026 10:40:45 +0800 Subject: [PATCH 1/7] feat(ngxdig): add Helm chart for the ngxdig eBPF diagnosis agent --- .github/workflows/ci.yaml | 6 +- AGENTS.md | 1 + charts/ngxdig/.helmignore | 23 ++++ charts/ngxdig/Chart.yaml | 21 ++++ charts/ngxdig/README.md | 56 +++++++++ charts/ngxdig/ci/ct-values.yaml | 5 + charts/ngxdig/templates/NOTES.txt | 32 +++++ charts/ngxdig/templates/_helpers.tpl | 63 ++++++++++ charts/ngxdig/templates/daemonset.yaml | 113 +++++++++++++++++ charts/ngxdig/templates/service.yaml | 20 +++ charts/ngxdig/templates/serviceaccount.yaml | 13 ++ charts/ngxdig/values.yaml | 128 ++++++++++++++++++++ 12 files changed, 479 insertions(+), 2 deletions(-) create mode 100644 charts/ngxdig/.helmignore create mode 100644 charts/ngxdig/Chart.yaml create mode 100644 charts/ngxdig/README.md create mode 100644 charts/ngxdig/ci/ct-values.yaml create mode 100644 charts/ngxdig/templates/NOTES.txt create mode 100644 charts/ngxdig/templates/_helpers.tpl create mode 100644 charts/ngxdig/templates/daemonset.yaml create mode 100644 charts/ngxdig/templates/service.yaml create mode 100644 charts/ngxdig/templates/serviceaccount.yaml create mode 100644 charts/ngxdig/values.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0587d5f4..d001cd31 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -37,7 +37,8 @@ jobs: --charts charts/api7 \ --charts charts/gateway \ --charts charts/ingress-controller \ - --charts charts/aisix-cp' + --charts charts/aisix-cp \ + --charts charts/ngxdig' - name: Verify Chart.lock files run: | @@ -76,7 +77,8 @@ jobs: && helm repo add jaegertracing https://jaegertracing.github.io/helm-charts \ && ct install \ --charts charts/api7 \ - --charts charts/ingress-controller' + --charts charts/ingress-controller \ + --charts charts/ngxdig' - name: Test gateway charts run: | diff --git a/AGENTS.md b/AGENTS.md index d43d39a9..72242956 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,6 +9,7 @@ This repo (`api7/api7-helm-chart`) holds the Helm charts for API7 EE components | `api7-ingress-controller` | `charts/ingress-controller` | no — independent product version | | `developer-portal-fe` | `charts/developer-portal-fe` | no — independent product version | | `aisix-cp` (AISIX private-deployment control plane) | `charts/aisix-cp` | no — independent product version (source of truth: `api7/AISIX-Cloud` `helm/aisix-cp`) | +| `ngxdig` (eBPF flame-graph / diagnosis agent, DaemonSet) | `charts/ngxdig` | no — independent product version (source of truth: `api7/ngx-flame`) | ## Multi-line maintenance model diff --git a/charts/ngxdig/.helmignore b/charts/ngxdig/.helmignore new file mode 100644 index 00000000..0e8a0eb3 --- /dev/null +++ b/charts/ngxdig/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/ngxdig/Chart.yaml b/charts/ngxdig/Chart.yaml new file mode 100644 index 00000000..ae91e355 --- /dev/null +++ b/charts/ngxdig/Chart.yaml @@ -0,0 +1,21 @@ +apiVersion: v2 +name: ngxdig +description: eBPF on-CPU flame graphs and guided CPU/memory/latency diagnosis for OpenResty/Nginx, deployed as a per-node collector +type: application +version: 0.1.0 +appVersion: "0.1.0" +keywords: + - ebpf + - profiling + - flamegraph + - openresty + - nginx + - apisix + - diagnostics +home: https://github.com/api7/ngx-flame +sources: + - https://github.com/api7/ngx-flame +maintainers: + - name: API7 + email: support@api7.ai + url: https://api7.ai diff --git a/charts/ngxdig/README.md b/charts/ngxdig/README.md new file mode 100644 index 00000000..f1292d93 --- /dev/null +++ b/charts/ngxdig/README.md @@ -0,0 +1,56 @@ +# ngxdig + +![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.1.0](https://img.shields.io/badge/AppVersion-0.1.0-informational?style=flat-square) + +eBPF on-CPU flame graphs and guided CPU/memory/latency diagnosis for OpenResty/Nginx, deployed as a per-node collector + +**Homepage:** + +## Maintainers + +| Name | Email | Url | +| ---- | ------ | --- | +| API7 | | | + +## Source Code + +* + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| affinity | object | `{}` | Affinity rules for the DaemonSet Pods | +| extraArgs | list | `[]` | Extra arguments appended to the `ngxdig ui` command | +| extraEnvVars | list | `[]` | Extra environment variables for the collector container | +| extraVolumeMounts | list | `[]` | Extra volume mounts for the collector container | +| extraVolumes | list | `[]` | Extra volumes for the collector Pod | +| fullnameOverride | string | `""` | Override the fully qualified release name | +| hostBtf.enabled | bool | `true` | Mount the node's kernel BTF read-only. Required by the eBPF collector to resolve kernel types without DWARF | +| hostBtf.path | string | `"/sys/kernel/btf"` | Host path to the kernel BTF directory | +| hostPID | bool | `true` | Share the host PID namespace so the collector can see Nginx/OpenResty worker processes elsewhere on the node. Required to profile targets outside the collector's own Pod | +| image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | +| image.repository | string | `"api7/ngxdig"` | ngxdig image repository | +| image.tag | string | `"latest"` | Image tag. Defaults to the chart appVersion when left empty | +| imagePullSecrets | list | `[]` | Image pull secrets for a private registry | +| livenessProbe | object | `{"httpGet":{"path":"/","port":"http"},"initialDelaySeconds":10,"periodSeconds":15}` | Liveness probe for the collector container | +| nameOverride | string | `""` | Override the chart name | +| nodeSelector | object | `{}` | Node selector for the DaemonSet Pods | +| output.path | string | `"/out"` | Directory inside the container where run artifacts are written | +| output.sizeLimit | string | `""` | sizeLimit for the artifact emptyDir volume. No limit when empty | +| podAnnotations | object | `{}` | Additional annotations for the Pods | +| podLabels | object | `{}` | Additional labels for the Pods | +| podSecurityContext | object | `{}` | Pod-level security context | +| readinessProbe | object | `{"httpGet":{"path":"/","port":"http"},"initialDelaySeconds":5,"periodSeconds":10}` | Readiness probe for the collector container | +| resources | object | `{}` | Resource requests and limits for the collector container | +| securityContext | object | `{"appArmorProfile":{"type":"Unconfined"},"capabilities":{"add":["BPF","PERFMON","SYS_PTRACE"]},"runAsUser":0,"seccompProfile":{"type":"Unconfined"}}` | Container security context. The defaults grant exactly what the eBPF collector needs: run as root, the CAP_BPF / CAP_PERFMON / CAP_SYS_PTRACE capabilities, and unconfined seccomp/AppArmor so BPF, perf events, and ptrace of target processes are permitted | +| service.annotations | object | `{}` | Service annotations | +| service.containerPort | int | `8080` | Container port the UI listens on | +| service.port | int | `8080` | Service port | +| service.type | string | `"ClusterIP"` | Service type. The debug UI is not a hardened multi-user service; keep it ClusterIP and reach a specific node's Pod with `kubectl port-forward` | +| serviceAccount.annotations | object | `{}` | Annotations to add to the ServiceAccount | +| serviceAccount.create | bool | `true` | Create a ServiceAccount for the collector | +| serviceAccount.name | string | `""` | Name of the ServiceAccount to use. Generated from the fullname when empty | +| tolerations | list | `[]` | Tolerations for the DaemonSet Pods (e.g. to also run on control-plane nodes) | +| updateStrategy | object | `{}` | Update strategy for the DaemonSet | + diff --git a/charts/ngxdig/ci/ct-values.yaml b/charts/ngxdig/ci/ct-values.yaml new file mode 100644 index 00000000..c6494d5e --- /dev/null +++ b/charts/ngxdig/ci/ct-values.yaml @@ -0,0 +1,5 @@ +# Values used by chart-testing (`ct install`) to validate the chart on a +# single-node kind cluster. Tolerate any node taint so the DaemonSet Pod is +# guaranteed to schedule on the CI node. +tolerations: + - operator: Exists diff --git a/charts/ngxdig/templates/NOTES.txt b/charts/ngxdig/templates/NOTES.txt new file mode 100644 index 00000000..9cf159f7 --- /dev/null +++ b/charts/ngxdig/templates/NOTES.txt @@ -0,0 +1,32 @@ +ngxdig is installed as a DaemonSet — one collector Pod per node. + +Each Pod runs the ngxdig debug UI and can profile the OpenResty/Nginx (and +APISIX) worker processes running on its own node. + +1. List the collector Pods and the node each one runs on: + + kubectl get pods --namespace {{ .Release.Namespace }} \ + -l "app.kubernetes.io/name={{ include "ngxdig.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" \ + -o wide + +2. Open the debug UI for the node whose workload you want to inspect. The UI is + not a hardened multi-user service, so reach it over a port-forward rather + than exposing it: + + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} \ + -l "app.kubernetes.io/name={{ include "ngxdig.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" \ + -o jsonpath="{.items[0].metadata.name}") + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:{{ .Values.service.containerPort }} + + Then browse to http://127.0.0.1:8080/ , pick a worker PID, and run a + diagnostic action (profile / cpu-on / memsnap / latency). + +3. Check host readiness for eBPF collection on a node: + + kubectl --namespace {{ .Release.Namespace }} exec $POD_NAME -- ngxdig doctor + +Requirements: eBPF collection needs the node to expose kernel BTF +({{ .Values.hostBtf.path }}){{ if not .Values.hostBtf.enabled }} — currently disabled via hostBtf.enabled=false{{ end }}, and the +collector must keep CAP_BPF, CAP_PERFMON, and CAP_SYS_PTRACE with unconfined +seccomp/AppArmor (the chart defaults grant these). If a diagnostic fails before +collecting, `ngxdig doctor` reports the exact fix. diff --git a/charts/ngxdig/templates/_helpers.tpl b/charts/ngxdig/templates/_helpers.tpl new file mode 100644 index 00000000..ce544182 --- /dev/null +++ b/charts/ngxdig/templates/_helpers.tpl @@ -0,0 +1,63 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "ngxdig.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "ngxdig.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "ngxdig.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "ngxdig.labels" -}} +helm.sh/chart: {{ include "ngxdig.chart" . }} +{{ include "ngxdig.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/component: collector +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "ngxdig.selectorLabels" -}} +app.kubernetes.io/name: {{ include "ngxdig.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "ngxdig.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "ngxdig.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/charts/ngxdig/templates/daemonset.yaml b/charts/ngxdig/templates/daemonset.yaml new file mode 100644 index 00000000..cbbaea85 --- /dev/null +++ b/charts/ngxdig/templates/daemonset.yaml @@ -0,0 +1,113 @@ +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: {{ include "ngxdig.fullname" . }} + namespace: {{ .Release.Namespace | quote }} + labels: + {{- include "ngxdig.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + {{- include "ngxdig.selectorLabels" . | nindent 6 }} + {{- with .Values.updateStrategy }} + updateStrategy: + {{- toYaml . | nindent 4 }} + {{- end }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "ngxdig.selectorLabels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "ngxdig.serviceAccountName" . }} + hostPID: {{ .Values.hostPID }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + args: + - ui + - --listen + - ":{{ .Values.service.containerPort }}" + - --output + - {{ .Values.output.path | quote }} + - --ngxdig + - /usr/local/bin/ngxdig + {{- with .Values.extraArgs }} + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: http + containerPort: {{ .Values.service.containerPort }} + protocol: TCP + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.extraEnvVars }} + env: + {{- toYaml . | nindent 12 }} + {{- end }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - name: output + mountPath: {{ .Values.output.path }} + {{- if .Values.hostBtf.enabled }} + - name: host-btf + mountPath: {{ .Values.hostBtf.path }} + readOnly: true + {{- end }} + {{- with .Values.extraVolumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + volumes: + - name: output + {{- if .Values.output.sizeLimit }} + emptyDir: + sizeLimit: {{ .Values.output.sizeLimit | quote }} + {{- else }} + emptyDir: {} + {{- end }} + {{- if .Values.hostBtf.enabled }} + - name: host-btf + hostPath: + path: {{ .Values.hostBtf.path }} + {{- end }} + {{- with .Values.extraVolumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/ngxdig/templates/service.yaml b/charts/ngxdig/templates/service.yaml new file mode 100644 index 00000000..33027618 --- /dev/null +++ b/charts/ngxdig/templates/service.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "ngxdig.fullname" . }} + namespace: {{ .Release.Namespace | quote }} + labels: + {{- include "ngxdig.labels" . | nindent 4 }} + {{- with .Values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "ngxdig.selectorLabels" . | nindent 4 }} diff --git a/charts/ngxdig/templates/serviceaccount.yaml b/charts/ngxdig/templates/serviceaccount.yaml new file mode 100644 index 00000000..533d0230 --- /dev/null +++ b/charts/ngxdig/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "ngxdig.serviceAccountName" . }} + namespace: {{ .Release.Namespace | quote }} + labels: + {{- include "ngxdig.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/ngxdig/values.yaml b/charts/ngxdig/values.yaml new file mode 100644 index 00000000..1e91471e --- /dev/null +++ b/charts/ngxdig/values.yaml @@ -0,0 +1,128 @@ +# ============================================================ +# Image +# ============================================================ +image: + # -- ngxdig image repository + repository: api7/ngxdig + # -- Image pull policy + pullPolicy: IfNotPresent + # -- Image tag. Defaults to the chart appVersion when left empty + tag: "latest" + +# -- Image pull secrets for a private registry +imagePullSecrets: [] + +# -- Override the chart name +nameOverride: "" +# -- Override the fully qualified release name +fullnameOverride: "" + +serviceAccount: + # -- Create a ServiceAccount for the collector + create: true + # -- Annotations to add to the ServiceAccount + annotations: {} + # -- Name of the ServiceAccount to use. Generated from the fullname when empty + name: "" + +# ============================================================ +# Collector (DaemonSet) +# ============================================================ +# ngxdig runs as a DaemonSet — one collector Pod per node — because it profiles +# the OpenResty/Nginx worker processes running on that node, so it must be +# co-located with its targets. There is no replicaCount. + +# -- Share the host PID namespace so the collector can see Nginx/OpenResty +# worker processes elsewhere on the node. Required to profile targets outside +# the collector's own Pod +hostPID: true + +# -- Update strategy for the DaemonSet +updateStrategy: {} + +# -- Extra arguments appended to the `ngxdig ui` command +extraArgs: [] + +# -- Extra environment variables for the collector container +extraEnvVars: [] + +# -- Pod-level security context +podSecurityContext: {} + +# -- Container security context. The defaults grant exactly what the eBPF +# collector needs: run as root, the CAP_BPF / CAP_PERFMON / CAP_SYS_PTRACE +# capabilities, and unconfined seccomp/AppArmor so BPF, perf events, and ptrace +# of target processes are permitted +securityContext: + runAsUser: 0 + capabilities: + add: + - BPF + - PERFMON + - SYS_PTRACE + seccompProfile: + type: Unconfined + appArmorProfile: + type: Unconfined + +hostBtf: + # -- Mount the node's kernel BTF read-only. Required by the eBPF collector to + # resolve kernel types without DWARF + enabled: true + # -- Host path to the kernel BTF directory + path: /sys/kernel/btf + +output: + # -- Directory inside the container where run artifacts are written + path: /out + # -- sizeLimit for the artifact emptyDir volume. No limit when empty + sizeLimit: "" + +# ============================================================ +# Service (Debug UI) +# ============================================================ +service: + # -- Service type. The debug UI is not a hardened multi-user service; keep it + # ClusterIP and reach a specific node's Pod with `kubectl port-forward` + type: ClusterIP + # -- Service port + port: 8080 + # -- Container port the UI listens on + containerPort: 8080 + # -- Service annotations + annotations: {} + +# -- Liveness probe for the collector container +livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 10 + periodSeconds: 15 +# -- Readiness probe for the collector container +readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + +# -- Resource requests and limits for the collector container +resources: {} + +# -- Node selector for the DaemonSet Pods +nodeSelector: {} +# -- Tolerations for the DaemonSet Pods (e.g. to also run on control-plane nodes) +tolerations: [] +# -- Affinity rules for the DaemonSet Pods +affinity: {} + +# -- Additional annotations for the Pods +podAnnotations: {} +# -- Additional labels for the Pods +podLabels: {} + +# -- Extra volumes for the collector Pod +extraVolumes: [] +# -- Extra volume mounts for the collector container +extraVolumeMounts: [] From 8283b9fe48028d48693bd0183f46fdb47742d562 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Tue, 14 Jul 2026 10:49:01 +0800 Subject: [PATCH 2/7] feat(ngxdig): pull the moving latest tag with imagePullPolicy Always --- charts/ngxdig/README.md | 2 +- charts/ngxdig/values.yaml | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/charts/ngxdig/README.md b/charts/ngxdig/README.md index f1292d93..646d7eb1 100644 --- a/charts/ngxdig/README.md +++ b/charts/ngxdig/README.md @@ -29,7 +29,7 @@ eBPF on-CPU flame graphs and guided CPU/memory/latency diagnosis for OpenResty/N | hostBtf.enabled | bool | `true` | Mount the node's kernel BTF read-only. Required by the eBPF collector to resolve kernel types without DWARF | | hostBtf.path | string | `"/sys/kernel/btf"` | Host path to the kernel BTF directory | | hostPID | bool | `true` | Share the host PID namespace so the collector can see Nginx/OpenResty worker processes elsewhere on the node. Required to profile targets outside the collector's own Pod | -| image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | +| image.pullPolicy | string | `"Always"` | Image pull policy. Defaults to Always because the image tag is the moving `latest`, so nodes must re-pull to pick up newer builds | | image.repository | string | `"api7/ngxdig"` | ngxdig image repository | | image.tag | string | `"latest"` | Image tag. Defaults to the chart appVersion when left empty | | imagePullSecrets | list | `[]` | Image pull secrets for a private registry | diff --git a/charts/ngxdig/values.yaml b/charts/ngxdig/values.yaml index 1e91471e..87902451 100644 --- a/charts/ngxdig/values.yaml +++ b/charts/ngxdig/values.yaml @@ -4,8 +4,9 @@ image: # -- ngxdig image repository repository: api7/ngxdig - # -- Image pull policy - pullPolicy: IfNotPresent + # -- Image pull policy. Defaults to Always because the image tag is the moving + # `latest`, so nodes must re-pull to pick up newer builds + pullPolicy: Always # -- Image tag. Defaults to the chart appVersion when left empty tag: "latest" From c0578b01345c2963d201f5b709a9fd2561f3424f Mon Sep 17 00:00:00 2001 From: Jarvis Date: Tue, 14 Jul 2026 10:54:36 +0800 Subject: [PATCH 3/7] docs(ngxdig): document pinning the DaemonSet to specific nodes --- charts/ngxdig/README.md | 45 ++++++++++++++++++++++++++- charts/ngxdig/README.md.gotmpl | 57 ++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 charts/ngxdig/README.md.gotmpl diff --git a/charts/ngxdig/README.md b/charts/ngxdig/README.md index 646d7eb1..d45afb96 100644 --- a/charts/ngxdig/README.md +++ b/charts/ngxdig/README.md @@ -16,6 +16,50 @@ eBPF on-CPU flame graphs and guided CPU/memory/latency diagnosis for OpenResty/N * +## Install + +```sh +helm repo add api7 https://charts.api7.ai +helm repo update + +helm install ngxdig api7/ngxdig --namespace ngxdig --create-namespace +``` + +ngxdig is deployed as a DaemonSet — one collector Pod per node — so it can +profile the OpenResty/Nginx (and APISIX) worker processes running on that node. + +### Run on specific node(s) only + +Usually you only want to profile the node running the workload you are +debugging, not every node in the cluster. Restrict the DaemonSet with a +`nodeSelector`; the DaemonSet controller then creates a collector Pod only on +matching nodes. + +Pin to a single node by its built-in `kubernetes.io/hostname` label (the value +is the node name shown by `kubectl get nodes`): + +```sh +helm install ngxdig api7/ngxdig --namespace ngxdig --create-namespace \ + --set nodeSelector."kubernetes\.io/hostname"= +``` + +To target a group of nodes, label them first and select by that label: + +```sh +kubectl label node ngxdig=enabled +``` + +```yaml +# values.yaml +nodeSelector: + ngxdig: enabled +``` + +For rules that `nodeSelector` cannot express, use `affinity` instead. Note that +for a DaemonSet you should not set the Pod's `.spec.nodeName` directly — node +placement is owned by the DaemonSet controller, and `nodeSelector`/`affinity` +is the supported way to constrain it. + ## Values | Key | Type | Default | Description | @@ -53,4 +97,3 @@ eBPF on-CPU flame graphs and guided CPU/memory/latency diagnosis for OpenResty/N | serviceAccount.name | string | `""` | Name of the ServiceAccount to use. Generated from the fullname when empty | | tolerations | list | `[]` | Tolerations for the DaemonSet Pods (e.g. to also run on control-plane nodes) | | updateStrategy | object | `{}` | Update strategy for the DaemonSet | - diff --git a/charts/ngxdig/README.md.gotmpl b/charts/ngxdig/README.md.gotmpl new file mode 100644 index 00000000..7455b98e --- /dev/null +++ b/charts/ngxdig/README.md.gotmpl @@ -0,0 +1,57 @@ +{{ template "chart.header" . }} + +{{ template "chart.badgesSection" . }} + +{{ template "chart.description" . }} + +{{ template "chart.homepageLine" . }} + +{{ template "chart.maintainersSection" . }} + +{{ template "chart.sourcesSection" . }} + +## Install + +```sh +helm repo add api7 https://charts.api7.ai +helm repo update + +helm install ngxdig api7/ngxdig --namespace ngxdig --create-namespace +``` + +ngxdig is deployed as a DaemonSet — one collector Pod per node — so it can +profile the OpenResty/Nginx (and APISIX) worker processes running on that node. + +### Run on specific node(s) only + +Usually you only want to profile the node running the workload you are +debugging, not every node in the cluster. Restrict the DaemonSet with a +`nodeSelector`; the DaemonSet controller then creates a collector Pod only on +matching nodes. + +Pin to a single node by its built-in `kubernetes.io/hostname` label (the value +is the node name shown by `kubectl get nodes`): + +```sh +helm install ngxdig api7/ngxdig --namespace ngxdig --create-namespace \ + --set nodeSelector."kubernetes\.io/hostname"= +``` + +To target a group of nodes, label them first and select by that label: + +```sh +kubectl label node ngxdig=enabled +``` + +```yaml +# values.yaml +nodeSelector: + ngxdig: enabled +``` + +For rules that `nodeSelector` cannot express, use `affinity` instead. Note that +for a DaemonSet you should not set the Pod's `.spec.nodeName` directly — node +placement is owned by the DaemonSet controller, and `nodeSelector`/`affinity` +is the supported way to constrain it. + +{{ template "chart.valuesSection" . }} From 5c7419fa22c600d33b67c693e0d6641a5738c48b Mon Sep 17 00:00:00 2001 From: Jarvis Date: Tue, 14 Jul 2026 11:04:48 +0800 Subject: [PATCH 4/7] feat(ngxdig): optional NodePort exposure and CAP_SYS_ADMIN for uprobe collectors --- charts/ngxdig/README.md | 56 ++++++++++++++++++++++---- charts/ngxdig/README.md.gotmpl | 47 ++++++++++++++++++--- charts/ngxdig/ci/ct-values.yaml | 5 ++- charts/ngxdig/templates/NOTES.txt | 28 ++++++++++--- charts/ngxdig/templates/daemonset.yaml | 4 +- charts/ngxdig/templates/service.yaml | 8 ++++ charts/ngxdig/values.yaml | 36 +++++++++++++---- 7 files changed, 154 insertions(+), 30 deletions(-) diff --git a/charts/ngxdig/README.md b/charts/ngxdig/README.md index d45afb96..8b571e33 100644 --- a/charts/ngxdig/README.md +++ b/charts/ngxdig/README.md @@ -28,12 +28,49 @@ helm install ngxdig api7/ngxdig --namespace ngxdig --create-namespace ngxdig is deployed as a DaemonSet — one collector Pod per node — so it can profile the OpenResty/Nginx (and APISIX) worker processes running on that node. -### Run on specific node(s) only +## Accessing the debug UI -Usually you only want to profile the node running the workload you are -debugging, not every node in the cluster. Restrict the DaemonSet with a -`nodeSelector`; the DaemonSet controller then creates a collector Pod only on -matching nodes. +The debug UI is not a hardened multi-user service, and each collector only sees +its own node's processes. So the access method has to target one specific node. + +### Recommended: port-forward to the target node's Pod + +This needs no Service, exposes nothing on the network, and is scoped to exactly +the node you want. Select the collector Pod by node name and forward to it: + +```sh +NODE= # from `kubectl get nodes` +POD=$(kubectl get pod -n ngxdig \ + -l app.kubernetes.io/name=ngxdig \ + --field-selector spec.nodeName=$NODE \ + -o jsonpath='{.items[0].metadata.name}') +kubectl -n ngxdig port-forward $POD 8080:8080 +# then open http://127.0.0.1:8080/ +``` + +### Standing endpoint: NodePort with externalTrafficPolicy: Local + +A plain `ClusterIP` Service is unhelpful here — it load-balances across every +node's collector, so the UI you reach hops between nodes. If you need a lasting +endpoint, expose a `NodePort` with `externalTrafficPolicy: Local` so +`:` deterministically reaches that node's own collector: + +```yaml +# values.yaml +service: + enabled: true + type: NodePort + externalTrafficPolicy: Local + # nodePort: 30080 # optional; auto-assigned when omitted +``` + +Only do this on a trusted network — the UI has no authentication. + +## Run on specific node(s) only + +Usually you only want a collector on the node running the workload you are +debugging, not every node. Restrict the DaemonSet with a `nodeSelector`; the +controller then creates a Pod only on matching nodes. Pin to a single node by its built-in `kubernetes.io/hostname` label (the value is the node name shown by `kubectl get nodes`): @@ -65,6 +102,7 @@ is the supported way to constrain it. | Key | Type | Default | Description | |-----|------|---------|-------------| | affinity | object | `{}` | Affinity rules for the DaemonSet Pods | +| containerPort | int | `8080` | Port the ngxdig UI listens on inside the container | | extraArgs | list | `[]` | Extra arguments appended to the `ngxdig ui` command | | extraEnvVars | list | `[]` | Extra environment variables for the collector container | | extraVolumeMounts | list | `[]` | Extra volume mounts for the collector container | @@ -87,11 +125,13 @@ is the supported way to constrain it. | podSecurityContext | object | `{}` | Pod-level security context | | readinessProbe | object | `{"httpGet":{"path":"/","port":"http"},"initialDelaySeconds":5,"periodSeconds":10}` | Readiness probe for the collector container | | resources | object | `{}` | Resource requests and limits for the collector container | -| securityContext | object | `{"appArmorProfile":{"type":"Unconfined"},"capabilities":{"add":["BPF","PERFMON","SYS_PTRACE"]},"runAsUser":0,"seccompProfile":{"type":"Unconfined"}}` | Container security context. The defaults grant exactly what the eBPF collector needs: run as root, the CAP_BPF / CAP_PERFMON / CAP_SYS_PTRACE capabilities, and unconfined seccomp/AppArmor so BPF, perf events, and ptrace of target processes are permitted | +| securityContext | object | `{"appArmorProfile":{"type":"Unconfined"},"capabilities":{"add":["BPF","PERFMON","SYS_PTRACE","SYS_ADMIN"]},"runAsUser":0,"seccompProfile":{"type":"Unconfined"}}` | Container security context. The defaults grant exactly what the eBPF collector needs: run as root, and the CAP_BPF / CAP_PERFMON / CAP_SYS_PTRACE / CAP_SYS_ADMIN capabilities with unconfined seccomp/AppArmor so BPF, perf events, ptrace, and the perf_uprobe PMU attachments used by `cpu-on` and `latency` are all permitted. CAP_SYS_ADMIN is the smallest addition that satisfies the uprobe perf_event check — `--privileged` is not required | | service.annotations | object | `{}` | Service annotations | -| service.containerPort | int | `8080` | Container port the UI listens on | +| service.enabled | bool | `false` | Expose the UI through a Service. Off by default: a plain ClusterIP Service load-balances across every node's collector, and each collector only sees its own node's processes, so a shared Service address hops between nodes. When you do need a standing endpoint, use type NodePort with externalTrafficPolicy Local (below) so : deterministically reaches that node's own collector | +| service.externalTrafficPolicy | string | `"Local"` | externalTrafficPolicy for NodePort/LoadBalancer. Local keeps traffic on the receiving node, so : reaches that node's own collector instead of being load-balanced to another node | +| service.nodePort | string | `""` | Static node port for type NodePort. Auto-assigned when empty | | service.port | int | `8080` | Service port | -| service.type | string | `"ClusterIP"` | Service type. The debug UI is not a hardened multi-user service; keep it ClusterIP and reach a specific node's Pod with `kubectl port-forward` | +| service.type | string | `"ClusterIP"` | Service type. NodePort (with externalTrafficPolicy Local) is the useful one for this DaemonSet; ClusterIP only makes sense when pinned to one node | | serviceAccount.annotations | object | `{}` | Annotations to add to the ServiceAccount | | serviceAccount.create | bool | `true` | Create a ServiceAccount for the collector | | serviceAccount.name | string | `""` | Name of the ServiceAccount to use. Generated from the fullname when empty | diff --git a/charts/ngxdig/README.md.gotmpl b/charts/ngxdig/README.md.gotmpl index 7455b98e..d62ee4ba 100644 --- a/charts/ngxdig/README.md.gotmpl +++ b/charts/ngxdig/README.md.gotmpl @@ -22,12 +22,49 @@ helm install ngxdig api7/ngxdig --namespace ngxdig --create-namespace ngxdig is deployed as a DaemonSet — one collector Pod per node — so it can profile the OpenResty/Nginx (and APISIX) worker processes running on that node. -### Run on specific node(s) only +## Accessing the debug UI -Usually you only want to profile the node running the workload you are -debugging, not every node in the cluster. Restrict the DaemonSet with a -`nodeSelector`; the DaemonSet controller then creates a collector Pod only on -matching nodes. +The debug UI is not a hardened multi-user service, and each collector only sees +its own node's processes. So the access method has to target one specific node. + +### Recommended: port-forward to the target node's Pod + +This needs no Service, exposes nothing on the network, and is scoped to exactly +the node you want. Select the collector Pod by node name and forward to it: + +```sh +NODE= # from `kubectl get nodes` +POD=$(kubectl get pod -n ngxdig \ + -l app.kubernetes.io/name=ngxdig \ + --field-selector spec.nodeName=$NODE \ + -o jsonpath='{.items[0].metadata.name}') +kubectl -n ngxdig port-forward $POD 8080:8080 +# then open http://127.0.0.1:8080/ +``` + +### Standing endpoint: NodePort with externalTrafficPolicy: Local + +A plain `ClusterIP` Service is unhelpful here — it load-balances across every +node's collector, so the UI you reach hops between nodes. If you need a lasting +endpoint, expose a `NodePort` with `externalTrafficPolicy: Local` so +`:` deterministically reaches that node's own collector: + +```yaml +# values.yaml +service: + enabled: true + type: NodePort + externalTrafficPolicy: Local + # nodePort: 30080 # optional; auto-assigned when omitted +``` + +Only do this on a trusted network — the UI has no authentication. + +## Run on specific node(s) only + +Usually you only want a collector on the node running the workload you are +debugging, not every node. Restrict the DaemonSet with a `nodeSelector`; the +controller then creates a Pod only on matching nodes. Pin to a single node by its built-in `kubernetes.io/hostname` label (the value is the node name shown by `kubectl get nodes`): diff --git a/charts/ngxdig/ci/ct-values.yaml b/charts/ngxdig/ci/ct-values.yaml index c6494d5e..b38839df 100644 --- a/charts/ngxdig/ci/ct-values.yaml +++ b/charts/ngxdig/ci/ct-values.yaml @@ -1,5 +1,8 @@ # Values used by chart-testing (`ct install`) to validate the chart on a # single-node kind cluster. Tolerate any node taint so the DaemonSet Pod is -# guaranteed to schedule on the CI node. +# guaranteed to schedule on the CI node, and enable the (default-off) Service so +# its template is exercised too. tolerations: - operator: Exists +service: + enabled: true diff --git a/charts/ngxdig/templates/NOTES.txt b/charts/ngxdig/templates/NOTES.txt index 9cf159f7..31c483d5 100644 --- a/charts/ngxdig/templates/NOTES.txt +++ b/charts/ngxdig/templates/NOTES.txt @@ -10,16 +10,31 @@ APISIX) worker processes running on its own node. -o wide 2. Open the debug UI for the node whose workload you want to inspect. The UI is - not a hardened multi-user service, so reach it over a port-forward rather - than exposing it: + not a hardened multi-user service, so the recommended access is a + port-forward to the collector Pod on that exact node — no Service, nothing + exposed on the network: + export NODE= # a node from step 1 / `kubectl get nodes` export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} \ -l "app.kubernetes.io/name={{ include "ngxdig.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" \ + --field-selector spec.nodeName=$NODE \ -o jsonpath="{.items[0].metadata.name}") - kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:{{ .Values.service.containerPort }} + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:{{ .Values.containerPort }} Then browse to http://127.0.0.1:8080/ , pick a worker PID, and run a diagnostic action (profile / cpu-on / memsnap / latency). +{{- if .Values.service.enabled }} + + A Service is enabled (type {{ .Values.service.type }}). + {{- if eq .Values.service.type "NodePort" }} With externalTrafficPolicy + {{ .Values.service.externalTrafficPolicy }}, reach a node's own collector at + :: + + kubectl get svc --namespace {{ .Release.Namespace }} {{ include "ngxdig.fullname" . }} -o jsonpath="{.spec.ports[0].nodePort}" + {{- else }} Note it load-balances across every node's collector, so it is + only unambiguous when the DaemonSet is pinned to a single node. + {{- end }} +{{- end }} 3. Check host readiness for eBPF collection on a node: @@ -27,6 +42,7 @@ APISIX) worker processes running on its own node. Requirements: eBPF collection needs the node to expose kernel BTF ({{ .Values.hostBtf.path }}){{ if not .Values.hostBtf.enabled }} — currently disabled via hostBtf.enabled=false{{ end }}, and the -collector must keep CAP_BPF, CAP_PERFMON, and CAP_SYS_PTRACE with unconfined -seccomp/AppArmor (the chart defaults grant these). If a diagnostic fails before -collecting, `ngxdig doctor` reports the exact fix. +collector must keep CAP_BPF, CAP_PERFMON, CAP_SYS_PTRACE, and CAP_SYS_ADMIN with +unconfined seccomp/AppArmor (the chart defaults grant these; CAP_SYS_ADMIN is +needed for the perf_uprobe attachments in `cpu-on`/`latency`). If a diagnostic +fails before collecting, `ngxdig doctor` reports the exact fix. diff --git a/charts/ngxdig/templates/daemonset.yaml b/charts/ngxdig/templates/daemonset.yaml index cbbaea85..99ab26b6 100644 --- a/charts/ngxdig/templates/daemonset.yaml +++ b/charts/ngxdig/templates/daemonset.yaml @@ -42,7 +42,7 @@ spec: args: - ui - --listen - - ":{{ .Values.service.containerPort }}" + - ":{{ .Values.containerPort }}" - --output - {{ .Values.output.path | quote }} - --ngxdig @@ -52,7 +52,7 @@ spec: {{- end }} ports: - name: http - containerPort: {{ .Values.service.containerPort }} + containerPort: {{ .Values.containerPort }} protocol: TCP {{- with .Values.securityContext }} securityContext: diff --git a/charts/ngxdig/templates/service.yaml b/charts/ngxdig/templates/service.yaml index 33027618..ea9074ad 100644 --- a/charts/ngxdig/templates/service.yaml +++ b/charts/ngxdig/templates/service.yaml @@ -1,3 +1,4 @@ +{{- if .Values.service.enabled -}} apiVersion: v1 kind: Service metadata: @@ -11,10 +12,17 @@ metadata: {{- end }} spec: type: {{ .Values.service.type }} + {{- if and .Values.service.externalTrafficPolicy (or (eq .Values.service.type "NodePort") (eq .Values.service.type "LoadBalancer")) }} + externalTrafficPolicy: {{ .Values.service.externalTrafficPolicy }} + {{- end }} ports: - port: {{ .Values.service.port }} targetPort: http protocol: TCP name: http + {{- if and (eq .Values.service.type "NodePort") .Values.service.nodePort }} + nodePort: {{ .Values.service.nodePort }} + {{- end }} selector: {{- include "ngxdig.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/charts/ngxdig/values.yaml b/charts/ngxdig/values.yaml index 87902451..6302672f 100644 --- a/charts/ngxdig/values.yaml +++ b/charts/ngxdig/values.yaml @@ -51,9 +51,11 @@ extraEnvVars: [] podSecurityContext: {} # -- Container security context. The defaults grant exactly what the eBPF -# collector needs: run as root, the CAP_BPF / CAP_PERFMON / CAP_SYS_PTRACE -# capabilities, and unconfined seccomp/AppArmor so BPF, perf events, and ptrace -# of target processes are permitted +# collector needs: run as root, and the CAP_BPF / CAP_PERFMON / CAP_SYS_PTRACE / +# CAP_SYS_ADMIN capabilities with unconfined seccomp/AppArmor so BPF, perf +# events, ptrace, and the perf_uprobe PMU attachments used by `cpu-on` and +# `latency` are all permitted. CAP_SYS_ADMIN is the smallest addition that +# satisfies the uprobe perf_event check — `--privileged` is not required securityContext: runAsUser: 0 capabilities: @@ -61,6 +63,7 @@ securityContext: - BPF - PERFMON - SYS_PTRACE + - SYS_ADMIN seccompProfile: type: Unconfined appArmorProfile: @@ -80,16 +83,33 @@ output: sizeLimit: "" # ============================================================ -# Service (Debug UI) +# Debug UI # ============================================================ +# -- Port the ngxdig UI listens on inside the container +containerPort: 8080 + +# The recommended way to reach the UI is `kubectl port-forward` to the collector +# Pod on the node you want to inspect (see the chart README / NOTES) — it needs +# no Service, targets an exact node, and keeps the unhardened UI off the network. service: - # -- Service type. The debug UI is not a hardened multi-user service; keep it - # ClusterIP and reach a specific node's Pod with `kubectl port-forward` + # -- Expose the UI through a Service. Off by default: a plain ClusterIP Service + # load-balances across every node's collector, and each collector only sees its + # own node's processes, so a shared Service address hops between nodes. When you + # do need a standing endpoint, use type NodePort with externalTrafficPolicy + # Local (below) so : deterministically reaches that node's own + # collector + enabled: false + # -- Service type. NodePort (with externalTrafficPolicy Local) is the useful + # one for this DaemonSet; ClusterIP only makes sense when pinned to one node type: ClusterIP # -- Service port port: 8080 - # -- Container port the UI listens on - containerPort: 8080 + # -- Static node port for type NodePort. Auto-assigned when empty + nodePort: "" + # -- externalTrafficPolicy for NodePort/LoadBalancer. Local keeps traffic on the + # receiving node, so : reaches that node's own collector + # instead of being load-balanced to another node + externalTrafficPolicy: Local # -- Service annotations annotations: {} From 287acacd36e5a58c6062fc9e4aa282aae1a3fa83 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Tue, 14 Jul 2026 11:30:30 +0800 Subject: [PATCH 5/7] feat(ngxdig): always create the debug-UI Service (drop the enable toggle) --- charts/ngxdig/README.md | 46 ++++++++-------------------- charts/ngxdig/README.md.gotmpl | 41 +++++++------------------ charts/ngxdig/ci/ct-values.yaml | 5 +-- charts/ngxdig/templates/NOTES.txt | 15 ++------- charts/ngxdig/templates/service.yaml | 8 ----- charts/ngxdig/values.yaml | 23 +++----------- 6 files changed, 32 insertions(+), 106 deletions(-) diff --git a/charts/ngxdig/README.md b/charts/ngxdig/README.md index 8b571e33..3b126e11 100644 --- a/charts/ngxdig/README.md +++ b/charts/ngxdig/README.md @@ -30,41 +30,22 @@ profile the OpenResty/Nginx (and APISIX) worker processes running on that node. ## Accessing the debug UI -The debug UI is not a hardened multi-user service, and each collector only sees -its own node's processes. So the access method has to target one specific node. - -### Recommended: port-forward to the target node's Pod - -This needs no Service, exposes nothing on the network, and is scoped to exactly -the node you want. Select the collector Pod by node name and forward to it: +Most deployments run a single collector — one node, or the DaemonSet pinned to +one node (see below) — so the `ClusterIP` Service the chart creates points at +that one Pod. For quick local access, port-forward straight to the collector +Pod: ```sh -NODE= # from `kubectl get nodes` -POD=$(kubectl get pod -n ngxdig \ - -l app.kubernetes.io/name=ngxdig \ - --field-selector spec.nodeName=$NODE \ - -o jsonpath='{.items[0].metadata.name}') -kubectl -n ngxdig port-forward $POD 8080:8080 +kubectl -n ngxdig port-forward \ + $(kubectl -n ngxdig get pod -l app.kubernetes.io/name=ngxdig -o jsonpath='{.items[0].metadata.name}') \ + 8080:8080 # then open http://127.0.0.1:8080/ ``` -### Standing endpoint: NodePort with externalTrafficPolicy: Local - -A plain `ClusterIP` Service is unhelpful here — it load-balances across every -node's collector, so the UI you reach hops between nodes. If you need a lasting -endpoint, expose a `NodePort` with `externalTrafficPolicy: Local` so -`:` deterministically reaches that node's own collector: - -```yaml -# values.yaml -service: - enabled: true - type: NodePort - externalTrafficPolicy: Local - # nodePort: 30080 # optional; auto-assigned when omitted -``` - -Only do this on a trusted network — the UI has no authentication. +The UI is not a hardened multi-user service — keep it on a trusted network and +prefer a port-forward over a public ingress. When the DaemonSet spans several +nodes, each collector only sees its own node's processes, so select the Pod on +the node you want with `--field-selector spec.nodeName=`. ## Run on specific node(s) only @@ -127,11 +108,8 @@ is the supported way to constrain it. | resources | object | `{}` | Resource requests and limits for the collector container | | securityContext | object | `{"appArmorProfile":{"type":"Unconfined"},"capabilities":{"add":["BPF","PERFMON","SYS_PTRACE","SYS_ADMIN"]},"runAsUser":0,"seccompProfile":{"type":"Unconfined"}}` | Container security context. The defaults grant exactly what the eBPF collector needs: run as root, and the CAP_BPF / CAP_PERFMON / CAP_SYS_PTRACE / CAP_SYS_ADMIN capabilities with unconfined seccomp/AppArmor so BPF, perf events, ptrace, and the perf_uprobe PMU attachments used by `cpu-on` and `latency` are all permitted. CAP_SYS_ADMIN is the smallest addition that satisfies the uprobe perf_event check — `--privileged` is not required | | service.annotations | object | `{}` | Service annotations | -| service.enabled | bool | `false` | Expose the UI through a Service. Off by default: a plain ClusterIP Service load-balances across every node's collector, and each collector only sees its own node's processes, so a shared Service address hops between nodes. When you do need a standing endpoint, use type NodePort with externalTrafficPolicy Local (below) so : deterministically reaches that node's own collector | -| service.externalTrafficPolicy | string | `"Local"` | externalTrafficPolicy for NodePort/LoadBalancer. Local keeps traffic on the receiving node, so : reaches that node's own collector instead of being load-balanced to another node | -| service.nodePort | string | `""` | Static node port for type NodePort. Auto-assigned when empty | | service.port | int | `8080` | Service port | -| service.type | string | `"ClusterIP"` | Service type. NodePort (with externalTrafficPolicy Local) is the useful one for this DaemonSet; ClusterIP only makes sense when pinned to one node | +| service.type | string | `"ClusterIP"` | Service type | | serviceAccount.annotations | object | `{}` | Annotations to add to the ServiceAccount | | serviceAccount.create | bool | `true` | Create a ServiceAccount for the collector | | serviceAccount.name | string | `""` | Name of the ServiceAccount to use. Generated from the fullname when empty | diff --git a/charts/ngxdig/README.md.gotmpl b/charts/ngxdig/README.md.gotmpl index d62ee4ba..842abdf1 100644 --- a/charts/ngxdig/README.md.gotmpl +++ b/charts/ngxdig/README.md.gotmpl @@ -24,41 +24,22 @@ profile the OpenResty/Nginx (and APISIX) worker processes running on that node. ## Accessing the debug UI -The debug UI is not a hardened multi-user service, and each collector only sees -its own node's processes. So the access method has to target one specific node. - -### Recommended: port-forward to the target node's Pod - -This needs no Service, exposes nothing on the network, and is scoped to exactly -the node you want. Select the collector Pod by node name and forward to it: +Most deployments run a single collector — one node, or the DaemonSet pinned to +one node (see below) — so the `ClusterIP` Service the chart creates points at +that one Pod. For quick local access, port-forward straight to the collector +Pod: ```sh -NODE= # from `kubectl get nodes` -POD=$(kubectl get pod -n ngxdig \ - -l app.kubernetes.io/name=ngxdig \ - --field-selector spec.nodeName=$NODE \ - -o jsonpath='{.items[0].metadata.name}') -kubectl -n ngxdig port-forward $POD 8080:8080 +kubectl -n ngxdig port-forward \ + $(kubectl -n ngxdig get pod -l app.kubernetes.io/name=ngxdig -o jsonpath='{.items[0].metadata.name}') \ + 8080:8080 # then open http://127.0.0.1:8080/ ``` -### Standing endpoint: NodePort with externalTrafficPolicy: Local - -A plain `ClusterIP` Service is unhelpful here — it load-balances across every -node's collector, so the UI you reach hops between nodes. If you need a lasting -endpoint, expose a `NodePort` with `externalTrafficPolicy: Local` so -`:` deterministically reaches that node's own collector: - -```yaml -# values.yaml -service: - enabled: true - type: NodePort - externalTrafficPolicy: Local - # nodePort: 30080 # optional; auto-assigned when omitted -``` - -Only do this on a trusted network — the UI has no authentication. +The UI is not a hardened multi-user service — keep it on a trusted network and +prefer a port-forward over a public ingress. When the DaemonSet spans several +nodes, each collector only sees its own node's processes, so select the Pod on +the node you want with `--field-selector spec.nodeName=`. ## Run on specific node(s) only diff --git a/charts/ngxdig/ci/ct-values.yaml b/charts/ngxdig/ci/ct-values.yaml index b38839df..c6494d5e 100644 --- a/charts/ngxdig/ci/ct-values.yaml +++ b/charts/ngxdig/ci/ct-values.yaml @@ -1,8 +1,5 @@ # Values used by chart-testing (`ct install`) to validate the chart on a # single-node kind cluster. Tolerate any node taint so the DaemonSet Pod is -# guaranteed to schedule on the CI node, and enable the (default-off) Service so -# its template is exercised too. +# guaranteed to schedule on the CI node. tolerations: - operator: Exists -service: - enabled: true diff --git a/charts/ngxdig/templates/NOTES.txt b/charts/ngxdig/templates/NOTES.txt index 31c483d5..07724576 100644 --- a/charts/ngxdig/templates/NOTES.txt +++ b/charts/ngxdig/templates/NOTES.txt @@ -23,18 +23,9 @@ APISIX) worker processes running on its own node. Then browse to http://127.0.0.1:8080/ , pick a worker PID, and run a diagnostic action (profile / cpu-on / memsnap / latency). -{{- if .Values.service.enabled }} - - A Service is enabled (type {{ .Values.service.type }}). - {{- if eq .Values.service.type "NodePort" }} With externalTrafficPolicy - {{ .Values.service.externalTrafficPolicy }}, reach a node's own collector at - :: - - kubectl get svc --namespace {{ .Release.Namespace }} {{ include "ngxdig.fullname" . }} -o jsonpath="{.spec.ports[0].nodePort}" - {{- else }} Note it load-balances across every node's collector, so it is - only unambiguous when the DaemonSet is pinned to a single node. - {{- end }} -{{- end }} + + In-cluster clients can also reach the UI through the Service + {{ include "ngxdig.fullname" . }}:{{ .Values.service.port }} (type {{ .Values.service.type }}). 3. Check host readiness for eBPF collection on a node: diff --git a/charts/ngxdig/templates/service.yaml b/charts/ngxdig/templates/service.yaml index ea9074ad..33027618 100644 --- a/charts/ngxdig/templates/service.yaml +++ b/charts/ngxdig/templates/service.yaml @@ -1,4 +1,3 @@ -{{- if .Values.service.enabled -}} apiVersion: v1 kind: Service metadata: @@ -12,17 +11,10 @@ metadata: {{- end }} spec: type: {{ .Values.service.type }} - {{- if and .Values.service.externalTrafficPolicy (or (eq .Values.service.type "NodePort") (eq .Values.service.type "LoadBalancer")) }} - externalTrafficPolicy: {{ .Values.service.externalTrafficPolicy }} - {{- end }} ports: - port: {{ .Values.service.port }} targetPort: http protocol: TCP name: http - {{- if and (eq .Values.service.type "NodePort") .Values.service.nodePort }} - nodePort: {{ .Values.service.nodePort }} - {{- end }} selector: {{- include "ngxdig.selectorLabels" . | nindent 4 }} -{{- end }} diff --git a/charts/ngxdig/values.yaml b/charts/ngxdig/values.yaml index 6302672f..1053b07c 100644 --- a/charts/ngxdig/values.yaml +++ b/charts/ngxdig/values.yaml @@ -88,28 +88,15 @@ output: # -- Port the ngxdig UI listens on inside the container containerPort: 8080 -# The recommended way to reach the UI is `kubectl port-forward` to the collector -# Pod on the node you want to inspect (see the chart README / NOTES) — it needs -# no Service, targets an exact node, and keeps the unhardened UI off the network. +# The UI is exposed through a Service. Most deployments run a single collector +# (one node, or the DaemonSet pinned to one node), so the Service points at that +# one Pod. For quick local access you can also `kubectl port-forward` straight to +# the collector Pod — see the chart README / NOTES. service: - # -- Expose the UI through a Service. Off by default: a plain ClusterIP Service - # load-balances across every node's collector, and each collector only sees its - # own node's processes, so a shared Service address hops between nodes. When you - # do need a standing endpoint, use type NodePort with externalTrafficPolicy - # Local (below) so : deterministically reaches that node's own - # collector - enabled: false - # -- Service type. NodePort (with externalTrafficPolicy Local) is the useful - # one for this DaemonSet; ClusterIP only makes sense when pinned to one node + # -- Service type type: ClusterIP # -- Service port port: 8080 - # -- Static node port for type NodePort. Auto-assigned when empty - nodePort: "" - # -- externalTrafficPolicy for NodePort/LoadBalancer. Local keeps traffic on the - # receiving node, so : reaches that node's own collector - # instead of being load-balanced to another node - externalTrafficPolicy: Local # -- Service annotations annotations: {} From 490adf93794f6898b917225882af8f1c15280da2 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Tue, 14 Jul 2026 11:36:52 +0800 Subject: [PATCH 6/7] feat(ngxdig): support NodePort/externalTrafficPolicy for a node-IP route to the UI --- charts/ngxdig/README.md | 24 +++++++++++++++++++----- charts/ngxdig/README.md.gotmpl | 20 ++++++++++++++++---- charts/ngxdig/templates/NOTES.txt | 8 +++++++- charts/ngxdig/templates/service.yaml | 6 ++++++ charts/ngxdig/values.yaml | 18 +++++++++++++----- 5 files changed, 61 insertions(+), 15 deletions(-) diff --git a/charts/ngxdig/README.md b/charts/ngxdig/README.md index 3b126e11..8c427185 100644 --- a/charts/ngxdig/README.md +++ b/charts/ngxdig/README.md @@ -30,10 +30,11 @@ profile the OpenResty/Nginx (and APISIX) worker processes running on that node. ## Accessing the debug UI -Most deployments run a single collector — one node, or the DaemonSet pinned to -one node (see below) — so the `ClusterIP` Service the chart creates points at -that one Pod. For quick local access, port-forward straight to the collector -Pod: +The chart always creates a Service for the UI. Most deployments run a single +collector — one node, or the DaemonSet pinned to one node (see below) — so the +`ClusterIP` points at that one Pod. + +Quick local access — port-forward straight to the collector Pod: ```sh kubectl -n ngxdig port-forward \ @@ -42,6 +43,17 @@ kubectl -n ngxdig port-forward \ # then open http://127.0.0.1:8080/ ``` +Node-IP access — set the Service to `NodePort`; with `externalTrafficPolicy: +Local`, `:` reaches the collector on that same node: + +```yaml +# values.yaml +service: + type: NodePort + externalTrafficPolicy: Local + # nodePort: 30080 # optional; auto-assigned when omitted +``` + The UI is not a hardened multi-user service — keep it on a trusted network and prefer a port-forward over a public ingress. When the DaemonSet spans several nodes, each collector only sees its own node's processes, so select the Pod on @@ -108,8 +120,10 @@ is the supported way to constrain it. | resources | object | `{}` | Resource requests and limits for the collector container | | securityContext | object | `{"appArmorProfile":{"type":"Unconfined"},"capabilities":{"add":["BPF","PERFMON","SYS_PTRACE","SYS_ADMIN"]},"runAsUser":0,"seccompProfile":{"type":"Unconfined"}}` | Container security context. The defaults grant exactly what the eBPF collector needs: run as root, and the CAP_BPF / CAP_PERFMON / CAP_SYS_PTRACE / CAP_SYS_ADMIN capabilities with unconfined seccomp/AppArmor so BPF, perf events, ptrace, and the perf_uprobe PMU attachments used by `cpu-on` and `latency` are all permitted. CAP_SYS_ADMIN is the smallest addition that satisfies the uprobe perf_event check — `--privileged` is not required | | service.annotations | object | `{}` | Service annotations | +| service.externalTrafficPolicy | string | `"Local"` | externalTrafficPolicy for NodePort/LoadBalancer. Local keeps traffic on the receiving node, so : reaches that node's own collector instead of being load-balanced to another node | +| service.nodePort | string | `""` | Static node port for type NodePort. Auto-assigned when empty | | service.port | int | `8080` | Service port | -| service.type | string | `"ClusterIP"` | Service type | +| service.type | string | `"ClusterIP"` | Service type (ClusterIP, NodePort, or LoadBalancer) | | serviceAccount.annotations | object | `{}` | Annotations to add to the ServiceAccount | | serviceAccount.create | bool | `true` | Create a ServiceAccount for the collector | | serviceAccount.name | string | `""` | Name of the ServiceAccount to use. Generated from the fullname when empty | diff --git a/charts/ngxdig/README.md.gotmpl b/charts/ngxdig/README.md.gotmpl index 842abdf1..d62d69dc 100644 --- a/charts/ngxdig/README.md.gotmpl +++ b/charts/ngxdig/README.md.gotmpl @@ -24,10 +24,11 @@ profile the OpenResty/Nginx (and APISIX) worker processes running on that node. ## Accessing the debug UI -Most deployments run a single collector — one node, or the DaemonSet pinned to -one node (see below) — so the `ClusterIP` Service the chart creates points at -that one Pod. For quick local access, port-forward straight to the collector -Pod: +The chart always creates a Service for the UI. Most deployments run a single +collector — one node, or the DaemonSet pinned to one node (see below) — so the +`ClusterIP` points at that one Pod. + +Quick local access — port-forward straight to the collector Pod: ```sh kubectl -n ngxdig port-forward \ @@ -36,6 +37,17 @@ kubectl -n ngxdig port-forward \ # then open http://127.0.0.1:8080/ ``` +Node-IP access — set the Service to `NodePort`; with `externalTrafficPolicy: +Local`, `:` reaches the collector on that same node: + +```yaml +# values.yaml +service: + type: NodePort + externalTrafficPolicy: Local + # nodePort: 30080 # optional; auto-assigned when omitted +``` + The UI is not a hardened multi-user service — keep it on a trusted network and prefer a port-forward over a public ingress. When the DaemonSet spans several nodes, each collector only sees its own node's processes, so select the Pod on diff --git a/charts/ngxdig/templates/NOTES.txt b/charts/ngxdig/templates/NOTES.txt index 07724576..2c83cae6 100644 --- a/charts/ngxdig/templates/NOTES.txt +++ b/charts/ngxdig/templates/NOTES.txt @@ -24,8 +24,14 @@ APISIX) worker processes running on its own node. Then browse to http://127.0.0.1:8080/ , pick a worker PID, and run a diagnostic action (profile / cpu-on / memsnap / latency). - In-cluster clients can also reach the UI through the Service + The UI is also reachable through the Service {{ include "ngxdig.fullname" . }}:{{ .Values.service.port }} (type {{ .Values.service.type }}). +{{- if eq .Values.service.type "NodePort" }} + With type NodePort{{ if eq (toString .Values.service.externalTrafficPolicy) "Local" }} and externalTrafficPolicy Local{{ end }}, reach a + node's own collector at :: + + kubectl get svc --namespace {{ .Release.Namespace }} {{ include "ngxdig.fullname" . }} -o jsonpath="{.spec.ports[0].nodePort}" +{{- end }} 3. Check host readiness for eBPF collection on a node: diff --git a/charts/ngxdig/templates/service.yaml b/charts/ngxdig/templates/service.yaml index 33027618..756c1f78 100644 --- a/charts/ngxdig/templates/service.yaml +++ b/charts/ngxdig/templates/service.yaml @@ -11,10 +11,16 @@ metadata: {{- end }} spec: type: {{ .Values.service.type }} + {{- if and .Values.service.externalTrafficPolicy (or (eq .Values.service.type "NodePort") (eq .Values.service.type "LoadBalancer")) }} + externalTrafficPolicy: {{ .Values.service.externalTrafficPolicy }} + {{- end }} ports: - port: {{ .Values.service.port }} targetPort: http protocol: TCP name: http + {{- if and (eq .Values.service.type "NodePort") .Values.service.nodePort }} + nodePort: {{ .Values.service.nodePort }} + {{- end }} selector: {{- include "ngxdig.selectorLabels" . | nindent 4 }} diff --git a/charts/ngxdig/values.yaml b/charts/ngxdig/values.yaml index 1053b07c..9953e998 100644 --- a/charts/ngxdig/values.yaml +++ b/charts/ngxdig/values.yaml @@ -88,15 +88,23 @@ output: # -- Port the ngxdig UI listens on inside the container containerPort: 8080 -# The UI is exposed through a Service. Most deployments run a single collector -# (one node, or the DaemonSet pinned to one node), so the Service points at that -# one Pod. For quick local access you can also `kubectl port-forward` straight to -# the collector Pod — see the chart README / NOTES. +# The UI is exposed through a Service (always created). Most deployments run a +# single collector — one node, or the DaemonSet pinned to one node — so a +# ClusterIP points at that one Pod. Setting type to NodePort adds a +# : route; pair it with externalTrafficPolicy Local so that +# route reaches the collector on that same node. For quick local access you can +# also `kubectl port-forward` to the Pod — see the chart README / NOTES. service: - # -- Service type + # -- Service type (ClusterIP, NodePort, or LoadBalancer) type: ClusterIP # -- Service port port: 8080 + # -- Static node port for type NodePort. Auto-assigned when empty + nodePort: "" + # -- externalTrafficPolicy for NodePort/LoadBalancer. Local keeps traffic on the + # receiving node, so : reaches that node's own collector + # instead of being load-balanced to another node + externalTrafficPolicy: Local # -- Service annotations annotations: {} From e48dc1074ae36959e90bcc99cc878677130f1b0c Mon Sep 17 00:00:00 2001 From: Jarvis Date: Tue, 14 Jul 2026 11:41:49 +0800 Subject: [PATCH 7/7] docs(ngxdig): scope UI port-forward to the release and node, note Service is not node-specific --- charts/ngxdig/README.md | 21 ++++++++++++++------- charts/ngxdig/README.md.gotmpl | 21 ++++++++++++++------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/charts/ngxdig/README.md b/charts/ngxdig/README.md index 8c427185..ad643598 100644 --- a/charts/ngxdig/README.md +++ b/charts/ngxdig/README.md @@ -34,12 +34,17 @@ The chart always creates a Service for the UI. Most deployments run a single collector — one node, or the DaemonSet pinned to one node (see below) — so the `ClusterIP` points at that one Pod. -Quick local access — port-forward straight to the collector Pod: +Quick local access — port-forward to the collector Pod. Scope the selector to +this release, and on a multi-node install pick the Pod on the node you want with +a `spec.nodeName` field selector: ```sh -kubectl -n ngxdig port-forward \ - $(kubectl -n ngxdig get pod -l app.kubernetes.io/name=ngxdig -o jsonpath='{.items[0].metadata.name}') \ - 8080:8080 +NODE= # from `kubectl get nodes`; omit the field selector if single-node +POD=$(kubectl -n ngxdig get pod \ + -l app.kubernetes.io/name=ngxdig,app.kubernetes.io/instance=ngxdig \ + --field-selector spec.nodeName=$NODE \ + -o jsonpath='{.items[0].metadata.name}') +kubectl -n ngxdig port-forward $POD 8080:8080 # then open http://127.0.0.1:8080/ ``` @@ -55,9 +60,11 @@ service: ``` The UI is not a hardened multi-user service — keep it on a trusted network and -prefer a port-forward over a public ingress. When the DaemonSet spans several -nodes, each collector only sees its own node's processes, so select the Pod on -the node you want with `--field-selector spec.nodeName=`. +prefer a port-forward over a public ingress. Note the `ClusterIP` Service is not +node-specific: on a single-node or node-pinned install it points at the one +collector, but when the DaemonSet spans several nodes it load-balances across +collectors and each collector only sees its own node's processes — so target a +specific node's Pod (the `spec.nodeName` selector above) rather than the Service. ## Run on specific node(s) only diff --git a/charts/ngxdig/README.md.gotmpl b/charts/ngxdig/README.md.gotmpl index d62d69dc..cfb63006 100644 --- a/charts/ngxdig/README.md.gotmpl +++ b/charts/ngxdig/README.md.gotmpl @@ -28,12 +28,17 @@ The chart always creates a Service for the UI. Most deployments run a single collector — one node, or the DaemonSet pinned to one node (see below) — so the `ClusterIP` points at that one Pod. -Quick local access — port-forward straight to the collector Pod: +Quick local access — port-forward to the collector Pod. Scope the selector to +this release, and on a multi-node install pick the Pod on the node you want with +a `spec.nodeName` field selector: ```sh -kubectl -n ngxdig port-forward \ - $(kubectl -n ngxdig get pod -l app.kubernetes.io/name=ngxdig -o jsonpath='{.items[0].metadata.name}') \ - 8080:8080 +NODE= # from `kubectl get nodes`; omit the field selector if single-node +POD=$(kubectl -n ngxdig get pod \ + -l app.kubernetes.io/name=ngxdig,app.kubernetes.io/instance=ngxdig \ + --field-selector spec.nodeName=$NODE \ + -o jsonpath='{.items[0].metadata.name}') +kubectl -n ngxdig port-forward $POD 8080:8080 # then open http://127.0.0.1:8080/ ``` @@ -49,9 +54,11 @@ service: ``` The UI is not a hardened multi-user service — keep it on a trusted network and -prefer a port-forward over a public ingress. When the DaemonSet spans several -nodes, each collector only sees its own node's processes, so select the Pod on -the node you want with `--field-selector spec.nodeName=`. +prefer a port-forward over a public ingress. Note the `ClusterIP` Service is not +node-specific: on a single-node or node-pinned install it points at the one +collector, but when the DaemonSet spans several nodes it load-balances across +collectors and each collector only sees its own node's processes — so target a +specific node's Pod (the `spec.nodeName` selector above) rather than the Service. ## Run on specific node(s) only