Repository navigation
Export the Grafana dashboards with one command (#100) - #274
Merged
Merged
Conversation
`make dashboards-export` pulls every dashboard back out of the running Grafana by uid and writes it over the file, so the loop is edit → one command → `git diff` rather than a hand copy out of the JSON Model panel. The issue's design could not work as written. It assumed `allowUiUpdates: false` meant a UI edit is discarded at the next restart; it actually means Grafana refuses to *store* one at all — `POST /api/dashboards/db` answers `400 Cannot save provisioned dashboard`. So the API could only ever return the file it was provisioned from, and every export would have been a clean no-op over the very edit it existed to capture: exit zero, write nothing, `git diff` reporting no change to something plainly different on screen. Measured against the running stack first — the API's copy of `homelab-docker` matched the committed file in every field but `id` and `version`. So `allowUiUpdates` is now `true`. The JSON stays the source of truth, since a file change re-provisions over Grafana's copy. What `false` bought for free — that the running dashboard and the committed one cannot disagree — is bought back rather than dropped: `ARGS=--check` writes nothing and exits non-zero when Grafana holds an edit git does not, and the new daily `dashboards-drift` timer runs it, so forgetting to export ages into a stale job with an alert behind it. The rules in `backup.rules.yaml` join against the `JOBS` table rather than naming jobs, so it needed no rule. Two findings would otherwise have made every diff unreadable. Grafana serialises keys alphabetically at every level while these files put `uid`, `title` and `description` first, so a naive write-back would have reordered every key in all seven and buried the line that changed; the committed order is preserved and only new keys are appended. And Grafana persists whatever the browser was showing at save time, so the time picker's range and each variable's selection are read back out of the file rather than taken from the API — otherwise one person's afternoon of debugging silently becomes everyone's default time range. `scripts/check_dashboard_roundtrip.sh` boots the pinned Grafana image, provisions the committed JSON and reads it back, which is the round-trip check the issue asked for. It also asserts a save to a provisioned dashboard is still accepted — the condition the feature depends on and cannot detect for itself, since flipping the flag back breaks the export silently. Verified in both directions: with `allowUiUpdates: false` it fails and names the setting. The dashboard JSON changes here are that normalisation, applied once: seven stale `"version": 1` literals dropped, and escaped em dashes written as UTF-8, which three of the seven files already did. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The new homelab-dashboards-drift.service is missing Docker service dependency/group settings needed for docker compose to work reliably under systemd.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR adds an automated “export dashboards from Grafana back into git” workflow for the observability stack, including a CI round-trip guard and a scheduled drift detector, and flips Grafana provisioning to allow UI saves so exports can capture edits.
Changes:
- Add
make dashboards-export(live stack) andmake check-dashboard-roundtrip(offline/CI) to ensure exports are meaningful and stable. - Flip Grafana dashboard provisioning to
allowUiUpdates: true, and introduce a dailydashboards-driftsystemd timer to detect unexported UI edits. - Normalize committed dashboards to the exporter’s canonical form (drop volatile
version, normalize em-dashes).
File summaries
| File | Description |
|---|---|
systemd/homelab-dashboards-drift.timer |
Adds daily scheduling for the drift check job. |
systemd/homelab-dashboards-drift.service |
Runs make dashboards-export ARGS=--check under systemd to detect drift. |
stacks/observability/README.md |
Updates operator guidance to use make dashboards-export and drift job. |
stacks/observability/grafana/provisioning/dashboards/dashboards.yaml |
Sets allowUiUpdates: true and documents the tradeoff and mitigations. |
stacks/observability/grafana/dashboards/ups-power.json |
Removes volatile version. |
stacks/observability/grafana/dashboards/security-overview.json |
Removes volatile version. |
stacks/observability/grafana/dashboards/network-snmp.json |
Removes volatile version. |
stacks/observability/grafana/dashboards/logs-explorer.json |
Removes volatile version. |
stacks/observability/grafana/dashboards/host-overview.json |
Removes volatile version and normalizes em-dash escapes. |
stacks/observability/grafana/dashboards/docker-containers.json |
Removes volatile version and normalizes em-dash escapes. |
stacks/observability/grafana/dashboards/observability-stack.json |
Removes volatile version and normalizes em-dash escapes. |
stacks/observability/grafana/dashboards/README.md |
Documents export workflow, preserved/dropped fields, and ordering guarantees. |
scripts/validate.sh |
Adds the dashboard round-trip check into make validate accounting. |
scripts/install-timers.sh |
Registers the new dashboards-drift job for timer installation/thresholds. |
scripts/export-dashboards.sh |
Live-stack exporter wrapper (auth, fetch, orphan sweep) calling canonicalizer. |
scripts/export_dashboards.py |
Canonicalizer: strips volatile fields, preserves session state, preserves key order, supports --check. |
scripts/check_dashboard_roundtrip.sh |
CI/offline validation that Grafana round-trips and accepts saves for provisioned dashboards. |
Makefile |
Adds dashboards-export and check-dashboard-roundtrip targets. |
docs/runbooks/schedule-maintenance.md |
Documents the new scheduled job and how to respond to drift. |
docs/roadmap.md |
Moves #100 into Done with the final implementation notes. |
docs/observability.md |
Updates dashboard maintenance docs to the new export workflow. |
.github/workflows/ci.yml |
Runs the dashboard round-trip check in CI. |
Review details
Suppressed comments (1)
systemd/homelab-dashboards-drift.service:34
dashboards-exportshells out to Docker; under systemd,User=robowon't inherit thedockergroup membership from a login session. WithoutSupplementaryGroups=docker, access to /var/run/docker.sock is likely to fail (the same issue the backup/verify units document).
User=robo
Group=robo
WorkingDirectory=/home/robo/code/Gerrrt/HomeLab
Environment=STACK=observability
- Files reviewed: 22/22 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+26
to
+27
| Wants=network-online.target | ||
| After=network-online.target docker.service |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #100.
make dashboards-exportpulls every dashboard back out of the running Grafana by uid and writes it over the file ingrafana/dashboards/, so the loop is edit → one command →git diffinstead of a hand copy out of Dashboard settings → JSON Model.The issue's design could not work as written
The plan was to pull each dashboard from the API and write it back. That is a guaranteed no-op while
allowUiUpdatesisfalse, and the issue's premise about what that flag does is not quite right: Grafana does not discard a UI edit at the next restart, it refuses to store one at all.So the API can only ever return the file it was provisioned from. Every export would have succeeded, exited zero, written nothing, and left
git diffreporting no change to something plainly different on the screen. Measured against the running stack before anything was built: the API's copy ofhomelab-dockerwas identical to the committed file in every field butidandversion.allowUiUpdatesis therefore nowtrue. The JSON stays the source of truth — a file change re-provisions over Grafana's copy — but an edit now survives long enough to be exported.(Two smaller corrections to the issue: it says
editable: false, but all seven dashboards carryeditable: true; onlyallowUiUpdateswas false.)What pays for the flip
falsebought a real property: the running dashboard and the committed one could not disagree. That is bought back explicitly rather than dropped.make dashboards-export ARGS=--checkwrites nothing and exits non-zero when Grafana holds an edit git does not. The new dailydashboards-drifttimer runs it, so forgetting to export ages into a stale job with an alert behind it rather than into work quietly lost weeks later. The rules inbackup.rules.yamljoin against theJOBStable rather than naming jobs, so this needed no rule.make check-dashboard-roundtripasserts in CI that a save to a provisioned dashboard is still accepted. Flipping the flag back breaks the export silently, so the setting is asserted rather than trusted.Two findings that would have made every diff unreadable
uid,titleanddescriptionfirst (and a panel'stypeandtitlefirst). A naive write-back would have reordered every key in all seven files and buried the one line that changed. The committed order is preserved; only keys Grafana added are appended.The round-trip check
scripts/check_dashboard_roundtrip.shboots the pinned Grafana image against a throwaway copy of the provisioning — no secret, no published port, no live stack — and asserts three things:export_dashboards.pyinto a file.Verified in both directions. With
allowUiUpdates: falseit fails and names the setting; with a hand-mangledups-power.jsonit fails and prints the diff. It also caught a real regression during this work — a dashboard I had reverted mid-testing.Verification
make validate— all checks passed, 2 skipped (both worktree-expected:systemd-analyze verifyand the deployment-host check).make check-dashboard-roundtrip— 3/3, ~45s.--checkclean on the second run, so it is idempotent."version": 1literals dropped and escaped em dashes written as UTF-8, which three of the seven files already did. 15 lines total.🤖 Generated with Claude Code