Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dashboard-provisioning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"helm-charts": minor
---

feat: add dashboard provisioning via a k8s-sidecar that discovers ConfigMaps labeled `hyperdx.io/dashboard: true`. Discovery is scoped to the release namespace by default (a namespaced Role, no cluster-wide access); set `hyperdx.dashboards.namespaces` to also watch specific namespaces, or `hyperdx.dashboards.namespaces: [ALL]` for cluster-wide discovery. Requires hyperdxio/hyperdx#1962 (file-based dashboard provisioner).
10 changes: 9 additions & 1 deletion charts/clickstack/templates/NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,13 @@ Application Access:
directly to the internet without proper authentication and encryption.
{{- end }}

{{- if and .Values.hyperdx.dashboards.enabled (not .Values.hyperdx.dashboards.rbac.create) }}

WARNING: hyperdx.dashboards.rbac.create is false. The dashboard watcher needs
read access (list/get/watch) to ConfigMaps in its discovery scope. Grant it
out-of-band (e.g. a Role/RoleBinding bound to the HyperDX ServiceAccount), or the
watcher will hit 403 errors and CrashLoop.
{{- end }}

To verify the deployment status, run:
kubectl get pods -l "app.kubernetes.io/name={{ include "clickstack.name" . }}"
kubectl get pods -l "app.kubernetes.io/name={{ include "clickstack.name" . }}"
92 changes: 92 additions & 0 deletions charts/clickstack/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,98 @@ suffix is kept for backward compatibility.
{{- end -}}
{{- end -}}

{{/*
HyperDX ServiceAccount name. Shared by the Deployment, ServiceAccount, and
dashboard-provisioner RBAC subject so the three can never drift out of sync.
*/}}
{{- define "clickstack.hyperdx.serviceAccountName" -}}
{{- .Values.hyperdx.serviceAccount.name | default (include "clickstack.hyperdx.fullname" .) -}}
{{- end -}}

{{/*
Dashboard discovery label. Shared by the inline dashboard ConfigMap (producer)
and the watcher sidecar (consumer) so the selector can't drift between them.
*/}}
{{- define "clickstack.hyperdx.dashboardLabelKey" -}}hyperdx.io/dashboard{{- end -}}
{{- define "clickstack.hyperdx.dashboardLabelValue" -}}true{{- end -}}

{{/*
RBAC rules the dashboard watcher needs: read-only access to ConfigMaps. Shared by
the namespaced Role and the cluster-scoped ClusterRole branches.
*/}}
{{- define "clickstack.hyperdx.dashboardRbacRules" -}}
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["list", "get", "watch"]
{{- end -}}

{{/*
Reject contradictory discovery scope. The "ALL" sentinel already grants cluster-wide
discovery, so combining it with named namespaces would silently escalate to cluster
scope — fail fast instead of granting more than the operator likely intended.
*/}}
{{- define "clickstack.hyperdx.validateDashboards" -}}
{{- if .Values.hyperdx.dashboards.enabled -}}
{{- $namespaces := .Values.hyperdx.dashboards.namespaces -}}
{{- if and (eq (include "clickstack.hyperdx.dashboardsClusterWide" .) "true") (gt (len $namespaces) 1) -}}
{{- fail "hyperdx.dashboards.namespaces: \"ALL\" cannot be combined with specific namespaces (it already grants cluster-wide discovery)" -}}
{{- end -}}
{{- range $namespaces -}}
{{- if not (trim .) -}}
{{- fail "hyperdx.dashboards.namespaces: entries must be non-empty namespace names" -}}
{{- end -}}
{{- if and (ne . "ALL") (eq (upper (trim .)) "ALL") -}}
{{- fail "hyperdx.dashboards.namespaces: use exactly \"ALL\" (uppercase, no surrounding spaces) for cluster-wide discovery" -}}
{{- end -}}
{{- if and (ne . "ALL") (not (regexMatch "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" (trim .))) -}}
{{- fail "hyperdx.dashboards.namespaces: entries must be valid DNS-1123 labels (lowercase alphanumeric and '-')" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}

{{/*
Namespaces the watcher discovers in for the non-ALL scope: the release namespace
plus any configured extras, deduped and comma-joined. Single source for both the
watcher NAMESPACE env and the per-namespace RoleBindings so watch scope and granted
scope can't drift.
*/}}
{{- define "clickstack.hyperdx.effectiveNamespaces" -}}
{{- $trimmed := list -}}
{{- range (concat (list .Release.Namespace) .Values.hyperdx.dashboards.namespaces) -}}
{{- $trimmed = append $trimmed (trim .) -}}
{{- end -}}
{{- $trimmed | uniq | join "," -}}
{{- end -}}

{{/*
Whether dashboard discovery is cluster-wide (the ALL sentinel). Single source for the
Role-vs-ClusterRole choice in the RBAC template and the watcher NAMESPACE env in the
Deployment, so the granted scope and the watched scope can't diverge. Renders "true" or "".
*/}}
{{- define "clickstack.hyperdx.dashboardsClusterWide" -}}
{{- if has "ALL" .Values.hyperdx.dashboards.namespaces }}true{{- end -}}
{{- end -}}

{{/*
Whether discovery spans more than one namespace (the effective, deduped set is larger
than just the release namespace). Selects the scoped ClusterRole + per-namespace
RoleBinding path over a plain namespaced Role. Renders "true" or "".
*/}}
{{- define "clickstack.hyperdx.dashboardsCrossNamespace" -}}
{{- if gt (len (include "clickstack.hyperdx.effectiveNamespaces" . | splitList ",")) 1 }}true{{- end -}}
{{- end -}}

{{/*
Whether this chart creates the HyperDX ServiceAccount: when explicitly requested, or when
dashboards need one and no external name is supplied. Shared by the ServiceAccount template
and the Deployment's serviceAccountName guard so the "the named SA must exist" invariant
can't drift between them. Renders "true" or "".
*/}}
{{- define "clickstack.hyperdx.createServiceAccount" -}}
{{- if or .Values.hyperdx.serviceAccount.create (and .Values.hyperdx.dashboards.enabled (not .Values.hyperdx.serviceAccount.name)) }}true{{- end -}}
{{- end -}}

{{/*
Create chart name and version as used by the chart label.
*/}}
Expand Down
18 changes: 18 additions & 0 deletions charts/clickstack/templates/hyperdx/dashboard-configmap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{{- if and .Values.hyperdx.dashboards.enabled .Values.hyperdx.dashboards.configMaps }}
{{- /*
Inline dashboard ConfigMap; labeled for discovery by the dashboard provisioner
alongside any external dashboard ConfigMaps from application charts.
*/ -}}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "clickstack.fullname" . }}-dashboards
labels:
{{- include "clickstack.labels" . | nindent 4 }}
{{ include "clickstack.hyperdx.dashboardLabelKey" . }}: {{ include "clickstack.hyperdx.dashboardLabelValue" . | quote }}
data:
{{- range $key, $value := .Values.hyperdx.dashboards.configMaps }}
{{ $key | quote }}: |
{{- $value | nindent 4 }}
{{- end }}
{{- end }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
{{- if and .Values.hyperdx.dashboards.enabled .Values.hyperdx.dashboards.rbac.create }}
{{- $fullname := include "clickstack.fullname" . -}}
{{- $saName := include "clickstack.hyperdx.serviceAccountName" . -}}
{{- /* Cluster-scoped objects are namespace-qualified so two same-named releases don't collide. RBAC names allow 253 chars (not 63), so the qualifier always survives. */}}
{{- $clusterName := printf "%s-%s-dashboard-provisioner" $fullname .Release.Namespace | trunc 253 | trimSuffix "-" -}}
{{- if eq (include "clickstack.hyperdx.dashboardsClusterWide" .) "true" }}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ $clusterName }}
labels:
{{- include "clickstack.labels" . | nindent 4 }}
rules:
{{- include "clickstack.hyperdx.dashboardRbacRules" . | nindent 2 }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ $clusterName }}
labels:
{{- include "clickstack.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ $clusterName }}
subjects:
- kind: ServiceAccount
name: {{ $saName }}
namespace: {{ .Release.Namespace }}
{{- else if eq (include "clickstack.hyperdx.dashboardsCrossNamespace" .) "true" }}
{{- /* Scoped cross-namespace discovery (effective set spans >1 namespace): one ClusterRole as a rule template, bound per namespace via RoleBindings so read access never becomes cluster-wide. */}}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ $clusterName }}
labels:
{{- include "clickstack.labels" . | nindent 4 }}
rules:
{{- include "clickstack.hyperdx.dashboardRbacRules" . | nindent 2 }}
{{- range $ns := (include "clickstack.hyperdx.effectiveNamespaces" . | splitList ",") }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ $clusterName }}
namespace: {{ $ns }}
labels:
{{- include "clickstack.labels" $ | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ $clusterName }}
subjects:
- kind: ServiceAccount
name: {{ $saName }}
namespace: {{ $.Release.Namespace }}
{{- end }}
{{- else }}
{{- /* Default: discovery scoped to the release namespace only — a namespaced Role, no cluster-scoped objects. */}}
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ $fullname }}-dashboard-provisioner
namespace: {{ .Release.Namespace }}
labels:
{{- include "clickstack.labels" . | nindent 4 }}
rules:
{{- include "clickstack.hyperdx.dashboardRbacRules" . | nindent 2 }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ $fullname }}-dashboard-provisioner
namespace: {{ .Release.Namespace }}
labels:
{{- include "clickstack.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ $fullname }}-dashboard-provisioner
subjects:
- kind: ServiceAccount
name: {{ $saName }}
namespace: {{ .Release.Namespace }}
{{- end }}
{{- end }}
65 changes: 61 additions & 4 deletions charts/clickstack/templates/hyperdx/deployment.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{{- include "clickstack.hyperdx.validateDashboards" . -}}
apiVersion: apps/v1
kind: Deployment
metadata:
Expand Down Expand Up @@ -45,8 +46,8 @@ spec:
{{- if .Values.hyperdx.deployment.priorityClassName }}
priorityClassName: {{ .Values.hyperdx.deployment.priorityClassName | quote }}
{{- end }}
{{- if or .Values.hyperdx.serviceAccount.create .Values.hyperdx.serviceAccount.name }}
serviceAccountName: {{ .Values.hyperdx.serviceAccount.name | default (include "clickstack.hyperdx.fullname" .) }}
{{- if or (eq (include "clickstack.hyperdx.createServiceAccount" .) "true") .Values.hyperdx.serviceAccount.name }}
serviceAccountName: {{ include "clickstack.hyperdx.serviceAccountName" . }}
{{- end }}
{{- if .Values.global.imagePullSecrets }}
imagePullSecrets:
Expand All @@ -64,9 +65,15 @@ spec:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
{{- with .Values.hyperdx.deployment.volumes }}
{{- if or .Values.hyperdx.deployment.volumes .Values.hyperdx.dashboards.enabled }}
volumes:
{{- with .Values.hyperdx.deployment.volumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.hyperdx.dashboards.enabled }}
- name: dashboards
emptyDir: {}
{{- end }}
{{- end }}
containers:
- name: app
Expand Down Expand Up @@ -103,9 +110,18 @@ spec:
timeoutSeconds: {{ .Values.hyperdx.deployment.readinessProbe.timeoutSeconds }}
failureThreshold: {{ .Values.hyperdx.deployment.readinessProbe.failureThreshold }}
{{- end }}
{{- with .Values.hyperdx.deployment.volumeMounts }}
{{- if or .Values.hyperdx.deployment.volumeMounts .Values.hyperdx.dashboards.enabled }}
volumeMounts:
{{- with .Values.hyperdx.deployment.volumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- if .Values.hyperdx.dashboards.enabled }}
# read-only: the HyperDX provisioner only reads .json files here, never writes
# (hyperdxio/hyperdx#1962); the watcher sidecar owns writes to the shared volume.
- name: dashboards
mountPath: /dashboards
readOnly: true
{{- end }}
{{- end }}
envFrom:
- configMapRef:
Expand Down Expand Up @@ -138,6 +154,47 @@ spec:
value: {{ tpl .Values.hyperdx.deployment.defaultSources . | quote }}
{{- end }}
{{- end }}
{{- if .Values.hyperdx.dashboards.enabled }}
- name: DASHBOARD_PROVISIONER_DIR
value: "/dashboards"
# Provision discovered dashboards for all HyperDX teams: the watcher has no
# team context, so per-team provisioning isn't expressible from a ConfigMap.
- name: DASHBOARD_PROVISIONER_ALL_TEAMS
value: "true"
{{- end }}
{{- with .Values.hyperdx.deployment.env }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- if .Values.hyperdx.dashboards.enabled }}
- name: dashboard-watcher
image: {{ .Values.hyperdx.dashboards.sidecarImage | quote }}
{{- with .Values.hyperdx.dashboards.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
env:
- name: LABEL
value: {{ include "clickstack.hyperdx.dashboardLabelKey" . | quote }}
- name: LABEL_VALUE
value: {{ include "clickstack.hyperdx.dashboardLabelValue" . | quote }}
- name: FOLDER
value: "/dashboards"
- name: RESOURCE
value: "configmap"
# Continuously watch for ConfigMap changes (also the image default) so
# dashboards added after startup are picked up, not just at boot.
- name: METHOD
value: "WATCH"
{{- if eq (include "clickstack.hyperdx.dashboardsClusterWide" .) "true" }}
- name: NAMESPACE
value: "ALL"
{{- else }}
- name: NAMESPACE
value: {{ include "clickstack.hyperdx.effectiveNamespaces" . | quote }}
{{- end }}
- name: UNIQUE_FILENAMES
value: "true"
volumeMounts:
- name: dashboards
mountPath: /dashboards
{{- end }}
4 changes: 2 additions & 2 deletions charts/clickstack/templates/hyperdx/serviceaccount.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{{- if .Values.hyperdx.serviceAccount.create }}
{{- if eq (include "clickstack.hyperdx.createServiceAccount" .) "true" }}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ .Values.hyperdx.serviceAccount.name | default (include "clickstack.hyperdx.fullname" .) }}
name: {{ include "clickstack.hyperdx.serviceAccountName" . }}
labels:
{{- include "clickstack.labels" . | nindent 4 }}
{{- with .Values.hyperdx.serviceAccount.annotations }}
Expand Down
28 changes: 28 additions & 0 deletions charts/clickstack/tests/dashboard-notes_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
suite: Test Dashboard Provisioner NOTES
templates:
- NOTES.txt
tests:
- it: should warn when rbac.create is false with dashboards enabled
set:
hyperdx:
dashboards:
enabled: true
rbac:
create: false
asserts:
- matchRegexRaw:
pattern: "hyperdx.dashboards.rbac.create is false"

- it: should not warn when rbac.create is true and dashboards enabled
set:
hyperdx:
dashboards:
enabled: true
asserts:
- notMatchRegexRaw:
pattern: "hyperdx.dashboards.rbac.create is false"

- it: should not warn when dashboards are disabled
asserts:
- notMatchRegexRaw:
pattern: "hyperdx.dashboards.rbac.create is false"
Loading
Loading