diff --git a/nix/host-modules/deploy.nix b/nix/host-modules/deploy.nix index b75f9e09..1ae2abbc 100644 --- a/nix/host-modules/deploy.nix +++ b/nix/host-modules/deploy.nix @@ -50,10 +50,6 @@ in [ "services" "hyperhive" "swarm" "grafana" "enable" ] [ "services" "hyperhive" "deploy" "grafana" "enable" ] ) - (lib.mkRenamedOptionModule - [ "services" "hyperhive" "swarm" "grafana" "socketDir" ] - [ "services" "hyperhive" "deploy" "grafana" "socketDir" ] - ) (lib.mkRenamedOptionModule [ "services" "hyperhive" "swarm" "grafana" "datasourceUrl" ] [ "services" "hyperhive" "deploy" "grafana" "datasourceUrl" ] @@ -110,15 +106,19 @@ in [ "services" "hyperhive" "swarm" "authelia" "usersFile" ] [ "services" "hyperhive" "deploy" "authelia" "usersFile" ] ) - # `hostClientSecretDir` and `hostUsersFile` deliberately have NO entry - # here. A rename module contributes a - # *definition* to its target, and both are `readOnly`, which refuses a - # second one — so a shim for either makes every evaluation fail with + # `hostClientSecretDir`, `hostUsersFile` and `grafana.socketDir` + # deliberately have NO entry here. A rename module contributes a + # *definition* to its target, and all three are `readOnly`, which refuses a + # second one — so a shim for any of them makes every evaluation fail with # "set multiple times", naming the option's own default as the rival - # definition. Nothing could have set them anyway: both are derived and - # read-only, so the shim's only possible customer is a reader of the old - # path, and that reader now gets "option does not exist" instead, which - # says the same thing sooner. + # definition. + # + # The first two are derived, so nothing could ever have set them and a + # shim's only customer is a reader of the old path. `grafana.socketDir` was + # a plain settable option; only a test fixture set it in + # this tree, but a deployment outside it can have. That setter now fails + # evaluation with "option does not exist" at the old path, rather than + # being carried to the new one. (lib.mkRenamedOptionModule [ "services" "hyperhive" "swarm" "nats" "enable" ] [ "services" "hyperhive" "deploy" "nats" "enable" ] diff --git a/nix/host-modules/swarm-grafana.nix b/nix/host-modules/swarm-grafana.nix index c0fd3c5e..c2a9ad30 100644 --- a/nix/host-modules/swarm-grafana.nix +++ b/nix/host-modules/swarm-grafana.nix @@ -291,10 +291,15 @@ in socketDir = lib.mkOption { type = lib.types.str; default = "/run/swarm-grafana"; + readOnly = true; description = '' Directory holding the unix socket Grafana serves on, shared between the host (where nginx runs) and the container (where Grafana runs). + Fixed, not configurable: the directory's mode is what keeps the socket + private, so it has to be dedicated to this one socket, and a settable + path could be pointed at a directory that carries something else. + ⚠️ **Grafana takes no TCP port at all, and that is the point.** Every swarm service container shares the host's network namespace, so a port is a swarm-wide resource that two modules can silently both diff --git a/nix/module-eval/grafana.nix b/nix/module-eval/grafana.nix index 5fea6e90..a920b3f6 100644 --- a/nix/module-eval/grafana.nix +++ b/nix/module-eval/grafana.nix @@ -23,13 +23,20 @@ let grafanaOldPath = hive { deploy.grafana.enable = true; - swarm.grafana.socketDir = "/run/test-grafana-sock"; swarm.grafana.datasourceUrl = "http://127.0.0.1:19999"; swarm.grafana.logsDatasourceUrl = "http://127.0.0.1:19998"; swarm.grafana.plugins = [ ]; swarm.grafana.package = pkgs.emptyDirectory; }; + # A host that tries to move the socket directory; reading the option throws. + grafanaSocketDirSet = hive { + deploy.grafana.enable = true; + deploy.grafana.socketDir = "/run/test-grafana-sock"; + deploy.grafana.plugins = [ ]; + deploy.grafana.package = pkgs.emptyDirectory; + }; + # Same pairing `nix/module-eval/bao-otel-collector.nix` uses for its own # cases: the store's container with its own collector on, so # `.containers.swarm-bao.config` carries a real, evaluated @@ -101,6 +108,18 @@ let && lib.hasInfix option a.message ) m.assertions; cases = [ + { + # The socket's directory is its access control, so it must stay dedicated + # to this one socket. `readOnly` is what makes that inexpressible: a + # definition anywhere — here at the `deploy` path — has to fail the eval, + # and an unset host has to render the default into nginx's upstream. + name = "grafana's socket directory cannot be set and renders its default"; + ok = + !(builtins.tryEval grafanaSocketDirSet.services.hyperhive.deploy.grafana.socketDir).success + && + grafanaOldPath.services.nginx.virtualHosts."grafana-metrics".locations."= /metrics".proxyPass + == "http://unix:/run/swarm-grafana/grafana.sock:/metrics"; + } { # This fixture enables grafana and NOT authelia, so the login-form toggle # has to follow "grafana requires SSO" rather than "both services are on