diff --git a/grafana/provisioning/datasources/ams-ledgers.yml b/grafana/provisioning/datasources/ams-ledgers.yml new file mode 100644 index 0000000000..6b03c3f00f --- /dev/null +++ b/grafana/provisioning/datasources/ams-ledgers.yml @@ -0,0 +1,29 @@ +# AMS (gittensory-miner) local SQLite ledger datasources (#5184). +# +# The miner keeps append-only SQLite ledgers under GITTENSORY_MINER_CONFIG_DIR (default ~/.config/gittensory-miner +# on a laptop, or /data/miner in the fleet Docker image). This wires two scrape-free `frser-sqlite-datasource` +# entries so a follow-on AMS usage dashboard can query them read-only — mirroring the maintainer `GittensoryDB` +# entry in sqlite.yml. This file is purely additive (it touches no existing datasource) and provisions NO +# dashboards or queries — that is the follow-on dashboard issue's scope. +# +# PATH — the absolute path varies per install, so `path:` below is a documented placeholder. Mount your miner's +# ledger directory (GITTENSORY_MINER_CONFIG_DIR) into the Grafana container at /ams-ledgers, e.g.: +# volumes: [ "${GITTENSORY_MINER_CONFIG_DIR:-~/.config/gittensory-miner}:/ams-ledgers:ro" ] +# then Grafana reads the files below. The `:ro` mount + this read-only query plugin mean Grafana can never write +# to the live ledgers. Adjust the two `path:` values if you mount elsewhere. +apiVersion: 1 +datasources: + - name: AMS Attempt Log + type: frser-sqlite-datasource + uid: ams-attempt-log + access: proxy + editable: false + jsonData: + path: /ams-ledgers/attempt-log.sqlite3 + - name: AMS Prediction Ledger + type: frser-sqlite-datasource + uid: ams-prediction-ledger + access: proxy + editable: false + jsonData: + path: /ams-ledgers/prediction-ledger.sqlite3 diff --git a/test/unit/ams-ledgers-datasource.test.ts b/test/unit/ams-ledgers-datasource.test.ts new file mode 100644 index 0000000000..a565998381 --- /dev/null +++ b/test/unit/ams-ledgers-datasource.test.ts @@ -0,0 +1,55 @@ +import { readFileSync, readdirSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { parse } from "yaml"; + +// Validation for the AMS ledger Grafana datasource provisioning (#5184). Config under grafana/provisioning/, not +// `src/**`, so Codecov doesn't gate it — but the read-only invariant (Grafana can only READ the miner's SQLite +// ledgers, never write) and the purely-additive contract (no UID collision, existing datasources untouched) are +// asserted here as real tests. +const DS_DIR = join(process.cwd(), "grafana/provisioning/datasources"); +const amsDoc = parse( + readFileSync(join(DS_DIR, "ams-ledgers.yml"), "utf8"), +) as Record; +const amsDatasources = (amsDoc.datasources as Array>) ?? []; + +describe("AMS ledger Grafana datasources (#5184)", () => { + it("provisions read-only frser-sqlite datasources for the attempt-log and prediction-ledger", () => { + expect(amsDatasources).toHaveLength(2); + const paths = amsDatasources.map((d) => d.jsonData?.path); + expect(paths).toContain("/ams-ledgers/attempt-log.sqlite3"); + expect(paths).toContain("/ams-ledgers/prediction-ledger.sqlite3"); + for (const ds of amsDatasources) { + expect(ds.type).toBe("frser-sqlite-datasource"); + } + }); + + it("is read-only and never grants write access to the ledgers", () => { + for (const ds of amsDatasources) { + expect(ds.editable).toBe(false); // provisioned datasources are immutable in the UI + expect(ds.access).toBe("proxy"); + // no write-enabling knob is present; the frser-sqlite query plugin only reads the file + expect(JSON.stringify(ds)).not.toMatch( + /write|allowUpdate|readonly"?:\s*false/i, + ); + } + }); + + it("uses fresh UIDs and leaves every existing datasource untouched (purely additive)", () => { + const existingUids = new Set(); + for (const file of readdirSync(DS_DIR)) { + if (file === "ams-ledgers.yml") continue; + const doc = parse(readFileSync(join(DS_DIR, file), "utf8")) as Record< + string, + any + >; + for (const ds of (doc.datasources as Array>) ?? []) + existingUids.add(ds.uid); + } + for (const ds of amsDatasources) { + expect(existingUids.has(ds.uid)).toBe(false); + } + // the maintainer GittensoryDB datasource still exists — this change added a file, it did not edit one + expect(existingUids.has("gittensory-db")).toBe(true); + }); +});