Skip to content
Merged
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
12 changes: 11 additions & 1 deletion grafana/provisioning/dashboards/provider.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,17 @@ providers:
- name: loopover
folder: LoopOver
type: file
disableDeletion: true
# false (2026-07-14, #orb-grafana-dashboard-uid-collision): `true` tells Grafana's file-provisioner
# to NEVER remove a dashboard from its own database even after its uid disappears from this
# directory's file set. That silently orphans an old-uid entry every time a dashboard is renamed
# here (e.g. the gittensory-* -> loopover-* rebrand) — Grafana's unified-storage backend then
# hard-fails re-provisioning the NEW uid with a "deprecatedInternalID already in use" collision
# against the orphan, which recurs on every subsequent deploy/restart until someone manually
# reconciles Grafana's DB. `false` lets the provisioner clean up an orphaned dashboard the moment
# its file/uid is gone, matching every other config-as-code source of truth in this stack (if it's
# not in the repo, it shouldn't exist in Grafana). Does not touch the underlying Prometheus/Postgres
# metrics data — only the dashboard JSON registration itself.
disableDeletion: false
editable: false
options:
path: /var/lib/grafana/dashboards
36 changes: 36 additions & 0 deletions test/unit/selfhost-grafana-provisioning-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { parse } from "yaml";
import { describe, expect, it } from "vitest";

type DashboardProvider = { name: string; folder: string; type: string; disableDeletion: boolean; editable: boolean; options: { path: string } };
type ProviderConfig = { apiVersion: number; providers: DashboardProvider[] };

const providerPath = join(process.cwd(), "grafana/provisioning/dashboards/provider.yml");

function readProviderConfig(): ProviderConfig {
return parse(readFileSync(providerPath, "utf8")) as ProviderConfig;
}

describe("LoopOver — Grafana dashboard file-provisioner config (#orb-grafana-dashboard-uid-collision)", () => {
it("REGRESSION: disableDeletion stays false so a renamed/removed dashboard's stale uid is cleaned up", () => {
// disableDeletion: true silently orphans the OLD uid in Grafana's own database forever whenever a
// dashboard file's uid changes (e.g. a future rebrand or dashboard rename) -- the provisioner then
// hard-fails re-provisioning the NEW uid with an internal-id collision against that orphan, on every
// subsequent deploy/restart, until someone manually reconciles Grafana's DB by hand. false lets the
// provisioner delete an orphaned dashboard the moment its file/uid disappears, so this stack's
// Grafana dashboards stay reconciled to the git-tracked file set the way every other config-as-code
// source of truth here already works.
const config = readProviderConfig();
const provider = config.providers.find((p) => p.name === "loopover");
expect(provider?.disableDeletion).toBe(false);
});

it("still points at the bind-mounted dashboards directory and the LoopOver folder", () => {
const config = readProviderConfig();
const provider = config.providers.find((p) => p.name === "loopover");
expect(provider?.type).toBe("file");
expect(provider?.folder).toBe("LoopOver");
expect(provider?.options.path).toBe("/var/lib/grafana/dashboards");
});
});
Loading