From ca193ad52b2ee2985af079be0159cc679c914f02 Mon Sep 17 00:00:00 2001 From: Tim Keller Date: Thu, 27 Aug 2026 14:16:02 -0500 Subject: generate the dashboards at build time instead of committing them The JSON is machine-written and the generator beside it is the only source of truth, so committing both only created a way for them to drift -- which is what the dashboards-current check existed to police. modules/dashboards.nix now runs gen_zfs.py / gen_smart.py in the derivation, so a stale dashboard cannot be built at all and the check is gone. Note there is no third option: flakes copy only git-tracked files into the store, so a gitignored dashboards/*.json cannot be read from disk either. It is generate, or commit. Verified equivalent -- both generated dashboards are byte-identical to the JSON this commit deletes. Adds a dashboards-nonempty check. Factoring the shared jq invocation out first introduced a bug it caught: the multi-line string ended in a newline, so the callers' `< $src > "$out"` landed on a line of its own as a separate command, truncating every dashboard to zero bytes while the build still succeeded. All four provisioned dashboards were empty. jqFixup is now a single line, and the check asserts each dashboard is non-empty and has a title, uid and panels. Both failure modes are exercised: an empty dashboard and one with no panels each fail the check. It also asserts it found four files, because the first version globbed *.json against linkFarm entries named without the suffix and passed vacuously. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01JKczN1f66kmLxNBLX37voQ --- flake.nix | 125 ++++++++++++++++++++++++++++++++------------------------------ 1 file changed, 65 insertions(+), 60 deletions(-) (limited to 'flake.nix') diff --git a/flake.nix b/flake.nix index 1a9c661..a8b5101 100644 --- a/flake.nix +++ b/flake.nix @@ -29,68 +29,73 @@ in collectors // { default = collectors.zfs-metrics; } ); - checks = forAllSystems (pkgs: { - # promtool over the recording rules. The server module also runs this at build - # time via services.prometheus.checkConfig, but that only fires for people who - # actually deploy it. - prometheus-rules = pkgs.runCommand "check-prometheus-rules" - { nativeBuildInputs = [ pkgs.prometheus.cli ]; } - '' - promtool check rules ${./rules}/*.yml - touch "$out" - ''; + checks = forAllSystems (pkgs: + let + dashboardsForCheck = pkgs.linkFarm "dashboards" (pkgs.lib.mapAttrsToList + # The .json suffix matters: the check globs *.json, and a + # linkFarm named without it silently matches nothing. + (name: drv: { name = "${name}.json"; path = drv; }) + # callPackages, not callPackage: the latter adds override/ + # overrideDerivation attrs that are functions, not derivations. + (pkgs.callPackages ./modules/dashboards.nix { + dsUid = { prometheus = "prometheus"; loki = "loki"; }; + })); + in + { + # promtool over the recording rules. The server module also runs this at build + # time via services.prometheus.checkConfig, but that only fires for people who + # actually deploy it. + prometheus-rules = pkgs.runCommand "check-prometheus-rules" + { nativeBuildInputs = [ pkgs.prometheus.cli ]; } + '' + promtool check rules ${./rules}/*.yml + touch "$out" + ''; - # The dashboard JSON is generated by the scripts next to it and committed, so it - # can rot silently if someone edits a generator without re-running it. Regenerate - # here and diff. - dashboards-current = pkgs.runCommand "check-dashboards-current" - { nativeBuildInputs = [ pkgs.python3 pkgs.diffutils ]; } - '' - cp -r ${./dashboards} work - chmod -R u+w work - cd work + # An empty or malformed dashboard does not fail any build on its own -- it + # provisions fine and simply renders nothing -- so assert on the content. + dashboards-nonempty = pkgs.runCommand "check-dashboards-nonempty" + { nativeBuildInputs = [ pkgs.jq ]; } + '' + shopt -s nullglob + found=0 + for f in ${dashboardsForCheck}/*.json; do + found=$((found + 1)) + name=$(basename "$f") + [ -s "$f" ] || { echo "$name is empty"; exit 1; } + jq -e '.title and .uid and (.panels | length > 0)' "$f" >/dev/null \ + || { echo "$name has no title, uid or panels"; exit 1; } + echo "$name: $(jq -r '"\(.title) — \(.panels | length) panels"' "$f")" + done + # A glob that matches nothing would otherwise make this check vacuous. + [ "$found" -eq 4 ] || { echo "expected 4 dashboards, found $found"; exit 1; } + touch "$out" + ''; - # Note the loop variable is not called "out": that is the derivation's own - # output path, and shadowing it makes the final touch silently write - # somewhere else and the build fail with "failed to produce output path". - check() { - local generator="$1" committed="$2" - python3 "$generator" > regenerated.json - if ! diff -u "$committed" regenerated.json; then - echo - echo "dashboards/$committed is stale." - echo "Re-run: python3 dashboards/$generator > dashboards/$committed" - exit 1 - fi - } - - check gen_smart.py smart.json - check gen_zfs.py zfs-pools.json - touch "$out" - ''; - - # Both halves have to evaluate, separately and together. Catches option typos - # and missing mkIf guards without needing a real machine. - modules-eval = (nixpkgs.lib.nixosSystem { - system = pkgs.stdenv.hostPlatform.system; - modules = [ - self.nixosModules.default - ({ lib, ... }: { - services.telemetry.collectors.enable = true; - services.telemetry.server = { - enable = true; - collectorHosts = [ "one" "two" ]; - proxy.enable = true; - # Never read during evaluation; only has to satisfy the assertion. - grafana.secretKeyFile = "/run/secrets/grafana-secret-key"; - }; - boot.loader.grub.devices = [ "/dev/sda" ]; - fileSystems."/" = { device = "/dev/sda1"; fsType = "ext4"; }; - system.stateVersion = "25.11"; - }) - ]; - }).config.system.build.toplevel; - }); + # Both halves have to evaluate, separately and together. Catches option typos + # and missing mkIf guards without needing a real machine -- and because the + # dashboards are generated during the build, this also runs both generators and + # fails if either one crashes or emits something jq cannot parse. + modules-eval = (nixpkgs.lib.nixosSystem { + system = pkgs.stdenv.hostPlatform.system; + modules = [ + self.nixosModules.default + ({ lib, ... }: { + services.telemetry.collectors.enable = true; + services.telemetry.server = { + enable = true; + collectorHosts = [ "one" "two" ]; + proxy.enable = true; + # Never read during evaluation; only has to satisfy the assertion. + grafana.secretKeyFile = "/run/secrets/grafana-secret-key"; + }; + boot.loader.grub.devices = [ "/dev/sda" ]; + fileSystems."/" = { device = "/dev/sda1"; fsType = "ext4"; }; + system.stateVersion = "25.11"; + }) + ]; + }).config.system.build.toplevel; + }); devShells = forAllSystems (pkgs: { default = pkgs.mkShell { -- cgit v1.3.1