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 Helm hook Job that upserts exported HyperDX dashboard JSON into MongoDB on install/upgrade
26 changes: 26 additions & 0 deletions charts/clickstack/templates/configmaps/dashboard-configmap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{{- if and .Values.hyperdx.dashboards.enabled .Values.hyperdx.dashboards.configMaps }}
{{- /*
Dashboard ConfigMap — holds user-provided dashboard JSON files.
This must be a Helm hook (not a regular resource) because the provisioner
Job is itself a hook, and hooks can only reference other hook resources.
- pre-install,pre-upgrade: created before the post-install/post-upgrade Job runs
- hook-weight -5: ensures this ConfigMap exists before the Job starts
- before-hook-creation: old ConfigMap is deleted on the next upgrade so it
gets recreated with the latest dashboard definitions
*/ -}}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "clickstack.fullname" . }}-dashboards
labels:
{{- include "clickstack.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-5"
"helm.sh/hook-delete-policy": before-hook-creation
data:
{{- range $key, $value := .Values.hyperdx.dashboards.configMaps }}
{{ $key }}: |
{{- $value | nindent 4 }}
{{- end }}
{{- end }}
138 changes: 138 additions & 0 deletions charts/clickstack/templates/jobs/dashboard-provisioner.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
{{- if and .Values.hyperdx.dashboards.enabled .Values.hyperdx.dashboards.configMaps }}
{{- /*
Dashboard Provisioner Job — upserts dashboard JSON into MongoDB on install/upgrade.
Dashboards are matched by name for idempotency: existing dashboards are updated,
new ones are created. Manually-created dashboards are never deleted.
- post-install,post-upgrade: runs after all regular resources are deployed,
ensuring MongoDB is available
- hook-weight 5: runs after the dashboard ConfigMap (weight -5) is created
- before-hook-creation: cleans up the previous Job on the next upgrade
- hook-succeeded: auto-deletes the Job and Pod once provisioning completes
successfully, keeping the namespace clean
*/ -}}
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "clickstack.fullname" . }}-dashboard-provisioner
labels:
{{- include "clickstack.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": post-install,post-upgrade
"helm.sh/hook-weight": "5"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
backoffLimit: 3
activeDeadlineSeconds: 600
template:
metadata:
labels:
{{- include "clickstack.selectorLabels" . | nindent 8 }}
app: {{ include "clickstack.fullname" . }}-dashboard-provisioner
spec:
{{- with .Values.global.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
restartPolicy: OnFailure
containers:
- name: dashboard-provisioner
image: {{ .Values.mongodb.image }}
{{- with .Values.hyperdx.dashboards.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
env:
- name: MONGO_URI
value: "{{ tpl .Values.hyperdx.mongoUri . }}"
- name: MONGO_READY_RETRIES
value: {{ .Values.hyperdx.dashboards.mongoReadyRetries | quote }}
- name: MONGO_READY_INTERVAL
value: {{ .Values.hyperdx.dashboards.mongoReadyInterval | quote }}
volumeMounts:
- name: dashboards
mountPath: /dashboards
readOnly: true
command:
- /bin/bash
- -c
- |
set -e

echo "Waiting for MongoDB to be ready..."
RETRIES=0
until mongosh "${MONGO_URI}" --eval "db.runCommand({ ping: 1 })" --quiet > /dev/null 2>&1; do
RETRIES=$((RETRIES + 1))
if [ "$RETRIES" -ge "$MONGO_READY_RETRIES" ]; then
echo "ERROR: MongoDB not ready after ${MONGO_READY_RETRIES} attempts"
exit 1
fi
echo "MongoDB not ready yet (attempt ${RETRIES}/${MONGO_READY_RETRIES}), retrying in ${MONGO_READY_INTERVAL}s..."
sleep "$MONGO_READY_INTERVAL"
done
echo "MongoDB is ready."

# Get the team ID (first team in the database)
TEAM_ID=$(mongosh "${MONGO_URI}" --quiet --eval 'const t = db.teams.findOne(); if (t) print(t._id.toString()); else { print("ERROR: No team found"); quit(1); }')
if [ -z "$TEAM_ID" ] || echo "$TEAM_ID" | grep -q "ERROR"; then
echo "ERROR: Could not find a team in the database"
exit 1
fi
echo "Using team: ${TEAM_ID}"

# Write the provisioning script to a JS file so MongoDB operators ($set, $setOnInsert)
# are passed literally to mongosh without shell or Helm template escaping issues
cat > /tmp/provision.js << 'SCRIPT_EOF'
const fs = require('fs');
const path = require('path');
const teamId = ObjectId(process.env.TEAM_ID);
const now = new Date();
const dir = '/dashboards';
let success = 0;
let fail = 0;

const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
for (const file of files) {
const content = JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8'));
if (!content.name) {
print('WARN: Skipping ' + file + ' - no name field found');
fail++;
continue;
}
print('Provisioning dashboard: ' + content.name + '...');
try {
// Upsert: match by name+team for idempotency
const result = db.dashboards.updateOne(
{ name: content.name, team: teamId },
{
$set: {
name: content.name,
tiles: content.tiles || [],
tags: content.tags || [],
team: teamId,
updatedAt: now
},
$setOnInsert: {
createdAt: now
}
},
{ upsert: true }
);
print(' OK: matched=' + result.matchedCount + ' upserted=' + result.upsertedCount + ' modified=' + result.modifiedCount);
success++;
} catch (e) {
print(' FAILED: ' + e.message);
fail++;
}
}
print('');
print('Dashboard provisioning complete: ' + success + ' succeeded, ' + fail + ' failed');
if (fail > 0) quit(1);
SCRIPT_EOF

export TEAM_ID="${TEAM_ID}"
mongosh "${MONGO_URI}" --quiet --file /tmp/provision.js
volumes:
- name: dashboards
configMap:
name: {{ include "clickstack.fullname" . }}-dashboards
{{- end }}
Loading