diff --git a/grafana/provisioning/dashboards/provider.yml b/grafana/provisioning/dashboards/provider.yml index 2c8f679a1c..9940723a17 100644 --- a/grafana/provisioning/dashboards/provider.yml +++ b/grafana/provisioning/dashboards/provider.yml @@ -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 diff --git a/test/unit/selfhost-grafana-provisioning-config.test.ts b/test/unit/selfhost-grafana-provisioning-config.test.ts new file mode 100644 index 0000000000..0f8ee6921f --- /dev/null +++ b/test/unit/selfhost-grafana-provisioning-config.test.ts @@ -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"); + }); +});