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 @@
---
"clickstack": minor
---

feat: add dashboard provisioning via k8s-sidecar that discovers labeled ConfigMaps across namespaces. Requires hyperdxio/hyperdx#1962 (file-based dashboard provisioner).
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 }}
hyperdx.io/dashboard: "true"
data:
{{- range $key, $value := .Values.hyperdx.dashboards.configMaps }}
{{ $key }}: |
{{- $value | nindent 4 }}
{{- end }}
{{- end }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{{- if .Values.hyperdx.dashboards.enabled }}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "clickstack.fullname" . }}-dashboard-provisioner
labels:
{{- include "clickstack.labels" . | nindent 4 }}
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["list", "get", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "clickstack.fullname" . }}-dashboard-provisioner
labels:
{{- include "clickstack.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "clickstack.fullname" . }}-dashboard-provisioner
subjects:
- kind: ServiceAccount
name: {{ .Values.hyperdx.serviceAccount.name | default (include "clickstack.hyperdx.fullname" .) }}
namespace: {{ .Release.Namespace }}
{{- end }}
46 changes: 45 additions & 1 deletion charts/clickstack/templates/hyperdx/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ spec:
{{- if .Values.hyperdx.deployment.priorityClassName }}
priorityClassName: {{ .Values.hyperdx.deployment.priorityClassName | quote }}
{{- end }}
{{- if or .Values.hyperdx.serviceAccount.create .Values.hyperdx.serviceAccount.name }}
{{- if or .Values.hyperdx.serviceAccount.create .Values.hyperdx.serviceAccount.name .Values.hyperdx.dashboards.enabled }}
serviceAccountName: {{ .Values.hyperdx.serviceAccount.name | default (include "clickstack.hyperdx.fullname" .) }}
{{- end }}
{{- if .Values.global.imagePullSecrets }}
Expand Down Expand Up @@ -125,6 +125,50 @@ spec:
value: {{ tpl .Values.hyperdx.deployment.defaultSources . | quote }}
{{- end }}
{{- end }}
{{- if .Values.hyperdx.dashboards.enabled }}
- name: DASHBOARD_PROVISIONER_DIR
value: "/dashboards"
- name: DASHBOARD_PROVISIONER_ALL_TEAMS
value: "true"
{{- end }}
{{- with .Values.hyperdx.deployment.env }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- if .Values.hyperdx.dashboards.enabled }}
volumeMounts:
- name: dashboards
mountPath: /dashboards
readOnly: true
{{- end }}
{{- if .Values.hyperdx.dashboards.enabled }}
- name: dashboard-watcher
image: {{ .Values.hyperdx.dashboards.sidecarImage }}
resources:
limits:
cpu: 50m
memory: 64Mi
requests:
cpu: 10m
memory: 32Mi
env:
- name: LABEL
value: "hyperdx.io/dashboard"
- name: LABEL_VALUE
value: "true"
- name: FOLDER
value: "/dashboards"
- name: RESOURCE
value: "configmap"
- name: NAMESPACE
value: "ALL"
- name: UNIQUE_FILENAMES
value: "true"
volumeMounts:
- name: dashboards
mountPath: /dashboards
{{- end }}
{{- if .Values.hyperdx.dashboards.enabled }}
volumes:
- name: dashboards
emptyDir: {}
{{- end }}
2 changes: 1 addition & 1 deletion charts/clickstack/templates/hyperdx/serviceaccount.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{{- if .Values.hyperdx.serviceAccount.create }}
{{- if or .Values.hyperdx.serviceAccount.create (and .Values.hyperdx.dashboards.enabled (not .Values.hyperdx.serviceAccount.name)) }}
apiVersion: v1
kind: ServiceAccount
metadata:
Expand Down
230 changes: 230 additions & 0 deletions charts/clickstack/tests/dashboard-provisioner_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
suite: Test Dashboard Provisioner
templates:
- hyperdx/deployment.yaml
- hyperdx/dashboard-configmap.yaml
- hyperdx/dashboard-provisioner-rbac.yaml
- hyperdx/serviceaccount.yaml
tests:
- it: should not render RBAC or ConfigMap when dashboards are disabled
set:
hyperdx:
dashboards:
enabled: false
asserts:
- hasDocuments:
count: 0
template: hyperdx/dashboard-configmap.yaml
- hasDocuments:
count: 0
template: hyperdx/dashboard-provisioner-rbac.yaml

- it: should not add sidecar when dashboards are disabled
set:
hyperdx:
dashboards:
enabled: false
asserts:
- lengthEqual:
path: spec.template.spec.containers
count: 1
template: hyperdx/deployment.yaml

- it: should add dashboard-watcher sidecar when enabled
set:
hyperdx:
dashboards:
enabled: true
asserts:
- lengthEqual:
path: spec.template.spec.containers
count: 2
template: hyperdx/deployment.yaml
- equal:
path: spec.template.spec.containers[1].name
value: dashboard-watcher
template: hyperdx/deployment.yaml

- it: should set DASHBOARD_PROVISIONER_DIR on the app container
set:
hyperdx:
dashboards:
enabled: true
asserts:
- contains:
path: spec.template.spec.containers[0].env
content:
name: DASHBOARD_PROVISIONER_DIR
value: "/dashboards"
template: hyperdx/deployment.yaml

- it: should use the k8s-sidecar image for watcher
set:
hyperdx:
dashboards:
enabled: true
sidecarImage: "kiwigrid/k8s-sidecar:2.5.0"
asserts:
- equal:
path: spec.template.spec.containers[1].image
value: "kiwigrid/k8s-sidecar:2.5.0"
template: hyperdx/deployment.yaml

- it: should configure watcher to discover labeled ConfigMaps across all namespaces
set:
hyperdx:
dashboards:
enabled: true
asserts:
- contains:
path: spec.template.spec.containers[1].env
content:
name: LABEL
value: "hyperdx.io/dashboard"
template: hyperdx/deployment.yaml
- contains:
path: spec.template.spec.containers[1].env
content:
name: NAMESPACE
value: "ALL"
template: hyperdx/deployment.yaml

- it: should share dashboards volume between app and watcher
set:
hyperdx:
dashboards:
enabled: true
asserts:
- contains:
path: spec.template.spec.volumes
content:
name: dashboards
emptyDir: {}
template: hyperdx/deployment.yaml
- contains:
path: spec.template.spec.containers[0].volumeMounts
content:
name: dashboards
mountPath: /dashboards
readOnly: true
template: hyperdx/deployment.yaml
- contains:
path: spec.template.spec.containers[1].volumeMounts
content:
name: dashboards
mountPath: /dashboards
template: hyperdx/deployment.yaml

- it: should auto-create SA when dashboards enabled and no user SA configured
set:
hyperdx:
dashboards:
enabled: true
serviceAccount:
create: false
name: ""
asserts:
- equal:
path: spec.template.spec.serviceAccountName
value: RELEASE-NAME-clickstack-app
template: hyperdx/deployment.yaml
- hasDocuments:
count: 1
template: hyperdx/serviceaccount.yaml

- it: should use existing user SA and not create a new one
set:
hyperdx:
dashboards:
enabled: true
serviceAccount:
create: false
name: "my-custom-sa"
asserts:
- equal:
path: spec.template.spec.serviceAccountName
value: my-custom-sa
template: hyperdx/deployment.yaml
- hasDocuments:
count: 0
template: hyperdx/serviceaccount.yaml

- it: should bind ClusterRole to the user SA when configured
set:
hyperdx:
dashboards:
enabled: true
serviceAccount:
name: "my-custom-sa"
asserts:
- equal:
path: subjects[0].name
value: my-custom-sa
documentIndex: 1
template: hyperdx/dashboard-provisioner-rbac.yaml

- it: should bind ClusterRole to the default SA when no user SA configured
set:
hyperdx:
dashboards:
enabled: true
asserts:
- equal:
path: subjects[0].name
value: RELEASE-NAME-clickstack-app
documentIndex: 1
template: hyperdx/dashboard-provisioner-rbac.yaml

- it: should grant cluster-wide configmap list, get, and watch permissions
set:
hyperdx:
dashboards:
enabled: true
asserts:
- equal:
path: rules[0].verbs
value: ["list", "get", "watch"]
documentIndex: 0
template: hyperdx/dashboard-provisioner-rbac.yaml

- it: should not render inline ConfigMap when configMaps is empty
set:
hyperdx:
dashboards:
enabled: true
configMaps: {}
asserts:
- hasDocuments:
count: 0
template: hyperdx/dashboard-configmap.yaml

- it: should render inline ConfigMap with discovery label
set:
hyperdx:
dashboards:
enabled: true
configMaps:
test.json: |
{ "name": "Test", "tiles": [] }
asserts:
- equal:
path: metadata.labels["hyperdx.io/dashboard"]
value: "true"
template: hyperdx/dashboard-configmap.yaml

- it: should include multiple dashboard files in inline ConfigMap
set:
hyperdx:
dashboards:
enabled: true
configMaps:
k8s-overview.json: |
{ "name": "Kubernetes Overview", "tiles": [] }
app-metrics.json: |
{ "name": "App Metrics", "tiles": [] }
asserts:
- isNotNull:
path: data["k8s-overview.json"]
template: hyperdx/dashboard-configmap.yaml
- isNotNull:
path: data["app-metrics.json"]
template: hyperdx/dashboard-configmap.yaml
25 changes: 25 additions & 0 deletions charts/clickstack/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,31 @@ hyperdx:
app: 3000
opamp: 4320

# ── Dashboard provisioning ────────────────────────────────
# Discovers and upserts dashboard JSON into MongoDB (matched by name, never deletes)
#
# Two ways to provide dashboards:
# 1. Inline: set configMaps below with dashboard JSON
# 2. External: any ConfigMap in the cluster with the label "hyperdx.io/dashboard: true"
# will be discovered automatically (ideal for application charts managing their own dashboards)
dashboards:
enabled: false
# Image for the k8s-sidecar that watches for labeled ConfigMaps
sidecarImage: "kiwigrid/k8s-sidecar:2.5.0"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What is the motivation for using a sidecar to store the ConfigMap content as a file vs using the built in ability of ConfigMaps to be used as a file? I am slightly concerned about expanding 3rd party dependencies and increasing the security attack surface area of a deployment.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The use case is: the team managing ClickStack deploys and manages the observability platform independently from application teams. Each application team manages their own HyperDX dashboards as ConfigMaps in their own namespace, deployed via their own Helm charts- GitOps style

Without the sidecar, the ClickStack team would need to track every application team's dashboards and redeploy ClickStack every time a dashboard is added or changed. The sidecar dynamically discovers labeled ConfigMaps across namespaces (hence the RBAC)

This pattern is directly taken from Grafana's Helm chart, which uses the same kiwigrid/k8s-sidecar image for the same purpose (see https://github.com/grafana-community/helm-charts/blob/main/charts/grafana/values.yaml)

# Inline dashboard definitions - key is filename, value is exported dashboard JSON
configMaps: {}
# Example:
# configMaps:
# k8s-overview.json: |
# { "name": "Kubernetes Overview", "tiles": [...] }
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 50m
memory: 64Mi

# ── K8s ConfigMap (clickstack-config) ────────────────────
# Shared non-sensitive environment variables. Used by HyperDX and OTEL collector via envFrom.
# All values support Helm template expressions (rendered via tpl).
Expand Down