diff --git a/docs/integrations/forge.md b/docs/integrations/forge.md index d249f914..546bc764 100644 --- a/docs/integrations/forge.md +++ b/docs/integrations/forge.md @@ -3,9 +3,10 @@ Private Forgejo instance running in a nixos-container, used as the swarm's persistent code-collaboration surface (issues, PRs, reviews, attachments). Configured via `services.hyperhive.swarm.forge.*` (what the -forge IS to every hive — its package, names, ports, URLs) plus +forge IS to every hive — names, ports, URLs) plus `services.hyperhive.deploy.forgejo.*` (what the host running it decides — -how it's served, what it mirrors, where its host-local secrets sit). +which build it runs, how it's served, what it mirrors, where its +host-local secrets sit). Container shape, ROOT_URL / sub-domain routing, and operator-vs-in-cluster URL handling live in [`docs/networking/gateway.md`](../networking/gateway.md); this file owns the diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index 9ee832b3..37b4414f 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -625,7 +625,7 @@ existing in the page DOM. A static matrix web client (default `pkgs.fluffychat-web` rebuilt with `--base-href /matrix/`, swappable via -`services.hyperhive.swarm.matrix.gui.package`) served by the hive-gateway +`services.hyperhive.deploy.matrix.gui.package`) served by the hive-gateway nginx container at `/matrix/` when `services.hyperhive.deploy.matrix.gui.enable` is on (defaults to `matrix.enable`). c0re signals availability via the diff --git a/flake.nix b/flake.nix index cefc5a2f..9e6d2ad4 100644 --- a/flake.nix +++ b/flake.nix @@ -142,19 +142,19 @@ agentBaseToplevel = lib.mkDefault self.packages.x86_64-linux.agent-base-toplevel; managerToplevel = lib.mkDefault self.packages.x86_64-linux.ruth-toplevel; }; - services.hyperhive.swarm.controller.package = + services.hyperhive.deploy.swarm-controller.package = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.swarm-controller; - services.hyperhive.swarm.controller.swarmctlPackage = + services.hyperhive.deploy.swarm-controller.swarmctlPackage = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.swarmctl; - services.hyperhive.swarm.ui.package = + services.hyperhive.deploy.swarm-ui.package = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.swarm-ui; - services.hyperhive.swarm.nats.authPackage = + services.hyperhive.deploy.nats.authPackage = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.swarm-nats-auth; - services.hyperhive.swarm.authelia.bridgePackage = + services.hyperhive.deploy.authelia.bridgePackage = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.swarm-authelia-bridge; services.hyperhive.gateway.swaggerUiTheme = diff --git a/nix/host-modules/deploy.nix b/nix/host-modules/deploy.nix index a4e23af7..0d491143 100644 --- a/nix/host-modules/deploy.nix +++ b/nix/host-modules/deploy.nix @@ -164,9 +164,10 @@ in ) # The queue's callout identity. What stays under `swarm.nats` is what the - # queue IS to every hive — the ports it answers on, the client id it is - # registered under, the responder package; these five are what the host - # running it decides. + # queue IS to every hive — the ports it answers on and the client id it is + # registered under; these five are what the host running it decides. The + # responder package is one of them now, further down with the other + # `*.package` moves. # # ⚠️ The two PUBLIC keys move with their seeds. A public key looks # swarm-wide, and the user key genuinely is published to every client that @@ -195,9 +196,10 @@ in ) # The rest of the forge split. What stays under `swarm.forge` is what the - # forge IS from any hive's point of view — its package, the names and ports - # it answers on, the URLs it advertises, the client id it is registered - # under; these five are what the host running it decides. `sso` splits + # forge IS from any hive's point of view — the names and ports it answers + # on, the URLs it advertises, the client id it is registered under; these + # five are what the host running it decides. Its package moved as well, + # further down with the other `*.package` moves. `sso` splits # because its two halves are different facts: the client id must match the # entry in authelia's register, the secret is a path on this machine. # @@ -227,9 +229,10 @@ in ) # The homeserver, split the same way. `swarm.matrix` keeps what it IS to - # every hive — its package, the name it answers to, the ports and URLs it - # is reached on, the client id it is registered under. These six are what - # the host running it decides. ⚠️ `enable` is NOT here: it was renamed + # every hive — the name it answers to, the ports and URLs it is reached + # on, the client id it is registered under. These six are what the host + # running it decides; its two packages moved as well, further down with + # the other `*.package` moves. ⚠️ `enable` is NOT here: it was renamed # earlier and its shim lives in ./hive-matrix.nix, so that file is the one # to check before assuming this table is the whole matrix story. (lib.mkRenamedOptionModule @@ -259,8 +262,9 @@ in # Retention is read only where the container is defined, so it is a # decision of the host running the store rather than something the swarm - # agrees on. The two stores keep everything else — package, domain, port - # — in `swarm.*`, because a client hive needs those to reach them. + # agrees on. The two stores keep their domain and port in `swarm.*`, + # because a client hive needs those to reach them; `package` followed + # `enable` and `retentionPeriod` across in a later slice. (lib.mkRenamedOptionModule [ "services" "hyperhive" "swarm" "victoriametrics" "retentionPeriod" ] [ "services" "hyperhive" "deploy" "victoriametrics" "retentionPeriod" ] @@ -335,6 +339,68 @@ in [ "services" "hyperhive" "swarm" "wireguard" "persistentKeepalive" ] [ "services" "hyperhive" "deploy" "wireguard" "persistentKeepalive" ] ) + + # Which build a service runs is a decision of the host that runs it, so + # every `*.package` follows its `enable` across. The split these replace + # was the sharpest case: `gui.enable` had already moved while + # `gui.package` stayed, so one client's two halves sat in two namespaces + # and ./hive-matrix.nix said so in a comment above the option. + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "swarm" "matrix" "package" ] + [ "services" "hyperhive" "deploy" "matrix" "package" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "swarm" "matrix" "gui" "package" ] + [ "services" "hyperhive" "deploy" "matrix" "gui" "package" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "swarm" "grafana" "package" ] + [ "services" "hyperhive" "deploy" "grafana" "package" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "swarm" "victorialogs" "package" ] + [ "services" "hyperhive" "deploy" "victorialogs" "package" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "swarm" "victoriametrics" "package" ] + [ "services" "hyperhive" "deploy" "victoriametrics" "package" ] + ) + # With this one `services.hyperhive.swarm.ui` holds nothing at all — it had + # exactly two options and both have moved, so the namespace now exists only + # as this entry and the `enable` one above it. + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "swarm" "ui" "package" ] + [ "services" "hyperhive" "deploy" "swarm-ui" "package" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "swarm" "nats" "authPackage" ] + [ "services" "hyperhive" "deploy" "nats" "authPackage" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "swarm" "authelia" "package" ] + [ "services" "hyperhive" "deploy" "authelia" "package" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "swarm" "authelia" "bridgePackage" ] + [ "services" "hyperhive" "deploy" "authelia" "bridgePackage" ] + ) + # ⚠️ `swarm-controller`, not `hive-controller` — see the note in + # ./swarm-controller.nix. Both namespaces exist, so the wrong target here + # lands on a live option in hive-c0re's tree instead of failing. + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "swarm" "controller" "package" ] + [ "services" "hyperhive" "deploy" "swarm-controller" "package" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "swarm" "controller" "swarmctlPackage" ] + [ "services" "hyperhive" "deploy" "swarm-controller" "swarmctlPackage" ] + ) + # ⚠️ `deploy.forgejo`, not `deploy.forge` — the deploy-side name differs + # from the swarm-side one here, as it does for the controller and the UI. + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "swarm" "forge" "package" ] + [ "services" "hyperhive" "deploy" "forgejo" "package" ] + ) ]; # ⚠️ `deploy.forgejo` is declared in ./hive-ci.nix, not here, and it is the diff --git a/nix/host-modules/hive-forge/default.nix b/nix/host-modules/hive-forge/default.nix index a171553f..3562fbd5 100644 --- a/nix/host-modules/hive-forge/default.nix +++ b/nix/host-modules/hive-forge/default.nix @@ -232,21 +232,6 @@ in ''; }; - package = lib.mkOption { - type = lib.types.package; - default = pkgs.forgejo; - defaultText = lib.literalExpression "pkgs.forgejo"; - description = '' - Forgejo package to run inside the container. Defaults to - `pkgs.forgejo` (the latest release line) rather than the - nixpkgs-module default of `pkgs.forgejo-lts`, because LTS - lags far behind on schema and the DB easily ends up "newer - than the binary" if the operator ever ran a non-LTS forgejo - against the same state dir. Override to `pkgs.forgejo-lts` - if you actively want the slower release train. - ''; - }; - rootUrl = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; @@ -294,13 +279,28 @@ in }; }; - # What stays above is what the forge IS from any hive's point of view: its - # package, the names and ports it answers on, the URLs it advertises, and - # the client id it is registered under. What lives here is what the host - # running it decides — how it is served, what it mirrors, and where its - # host-local secrets sit. Same rule as ./swarm-victorialogs.nix, and the - # renames are in ./deploy.nix with the rest. + # What stays above is what the forge IS from any hive's point of view: the + # names and ports it answers on, the URLs it advertises, and the client id + # it is registered under. What lives here is what the host running it + # decides — which build it runs, how it is served, what it mirrors, and + # where its host-local secrets sit. Same rule as ./swarm-victorialogs.nix, + # and the renames are in ./deploy.nix with the rest. options.services.hyperhive.deploy.forgejo = { + package = lib.mkOption { + type = lib.types.package; + default = pkgs.forgejo; + defaultText = lib.literalExpression "pkgs.forgejo"; + description = '' + Forgejo package to run inside the container. Defaults to + `pkgs.forgejo` (the latest release line) rather than the + nixpkgs-module default of `pkgs.forgejo-lts`, because LTS + lags far behind on schema and the DB easily ends up "newer + than the binary" if the operator ever ran a non-LTS forgejo + against the same state dir. Override to `pkgs.forgejo-lts` + if you actively want the slower release train. + ''; + }; + behindGateway = lib.mkOption { type = lib.types.bool; default = config.services.hyperhive.enable; @@ -675,7 +675,7 @@ in # the nix store — no separate hive-forge container rebuild needed, # and no persistent-state directory involved. staticRootWithTheme = pkgs.runCommand "forgejo-static-with-theme" { } '' - cp -r --no-preserve=mode,ownership ${cfg.package.data}/. $out/ + cp -r --no-preserve=mode,ownership ${deployCfg.forgejo.package.data}/. $out/ mkdir -p $out/public/assets/css cp ${./theme-catppuccin-vibec0re.css} \ $out/public/assets/css/theme-catppuccin-vibec0re.css @@ -747,7 +747,7 @@ in services.forgejo = { enable = true; - package = cfg.package; + package = deployCfg.forgejo.package; database.type = "sqlite3"; lfs.enable = true; settings = { @@ -1036,7 +1036,7 @@ in FORGEJO_CUSTOM = "/var/lib/forgejo/custom"; }; path = [ - cfg.package + deployCfg.forgejo.package pkgs.coreutils pkgs.gnugrep ]; @@ -1126,7 +1126,7 @@ in FORGEJO_CUSTOM = "/var/lib/forgejo/custom"; }; path = [ - cfg.package + deployCfg.forgejo.package pkgs.coreutils pkgs.gnugrep ]; diff --git a/nix/host-modules/hive-matrix.nix b/nix/host-modules/hive-matrix.nix index d1964cb8..8d087b0a 100644 --- a/nix/host-modules/hive-matrix.nix +++ b/nix/host-modules/hive-matrix.nix @@ -172,8 +172,9 @@ in [ "services" "hyperhive" "swarm" "matrix" ] ) # "does THIS host run it" is a per-host decision, and `swarm.*` is the - # namespace every host in the swarm agrees on. The rest of the matrix - # settings genuinely are swarm-wide and stay where they are. + # namespace every host in the swarm agrees on. What stays under + # `swarm.matrix` is the homeserver's identity as every hive sees it — + # its server name, its addresses, its federation policy. (lib.mkRenamedOptionModule [ "services" "hyperhive" "swarm" "matrix" "enable" ] [ "services" "hyperhive" "deploy" "matrix" "enable" ] @@ -191,17 +192,6 @@ in ]; options.services.hyperhive.swarm.matrix = { - package = lib.mkOption { - type = lib.types.package; - default = pkgs.matrix-tuwunel; - defaultText = lib.literalExpression "pkgs.matrix-tuwunel"; - description = '' - matrix-tuwunel package to run inside the container. Defaults - to nixpkgs's `pkgs.matrix-tuwunel`. Override to pin a - specific upstream if you need an unreleased feature. - ''; - }; - serverName = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; @@ -338,26 +328,6 @@ in ''; }; - gui = { - # Whether this host serves it is `deploy.matrix.gui.enable`. - package = lib.mkOption { - type = lib.types.package; - default = fluffychat-web-fixed; - defaultText = lib.literalMD '' - `pkgs.fluffychat-web` with a `postInstall` patch that adds - the three files `flutter341.buildFlutterApplication` skips. - ''; - description = '' - Static web client dist served at `gatewayHost`'s vhost - (`chat.` by default). Override - to swap fluffychat for hydrogen-web, cinny, element-web, or - an out-of-tree dist — any replacement is mounted at the - sub-domain root with the upstream-default ``, - no sub-path gymnastics needed. - ''; - }; - }; - # This homeserver always delegates login to the swarm's authelia, as # an OIDC relying party — matrix SSO (`m.login.sso`), offered # alongside password login. No toggle: a homeserver in a swarm is a @@ -398,13 +368,24 @@ in }; # What stays above is what the homeserver IS from any hive's point of view: - # its package, the name it answers to, the ports and URLs it is reached on, - # and the client id it is registered under. What lives here is what the host - # running it decides — whether it is exposed, which peers it trusts, how large - # a request it accepts, and where its host-local secrets sit. Same rule as + # the name it answers to, the ports and URLs it is reached on, and the client + # id it is registered under. What lives here is what the host running it + # decides — which build it runs, whether it is exposed, which peers it trusts, + # how large a request it accepts, and where its host-local secrets sit. Same rule as # ./swarm-victorialogs.nix; `enable` already lives in ./deploy.nix, which also # carries the renames. options.services.hyperhive.deploy.matrix = { + package = lib.mkOption { + type = lib.types.package; + default = pkgs.matrix-tuwunel; + defaultText = lib.literalExpression "pkgs.matrix-tuwunel"; + description = '' + matrix-tuwunel package to run inside the container. Defaults + to nixpkgs's `pkgs.matrix-tuwunel`. Override to pin a + specific upstream if you need an unreleased feature. + ''; + }; + openFirewall = lib.mkOption { type = lib.types.bool; default = false; @@ -509,11 +490,28 @@ in off, the dashboard's `M4TR1X →` tab is hidden. See `docs/networking/gateway.md` for the discovery flow that lets clients auto-find the sub-domain. The client build itself is - `swarm.matrix.gui.package` — which client, as opposed to whether + `deploy.matrix.gui.package` — which client, as opposed to whether this host serves it. ''; }; + gui.package = lib.mkOption { + type = lib.types.package; + default = fluffychat-web-fixed; + defaultText = lib.literalMD '' + `pkgs.fluffychat-web` with a `postInstall` patch that adds + the three files `flutter341.buildFlutterApplication` skips. + ''; + description = '' + Static web client dist served at `gatewayHost`'s vhost + (`chat.` by default). Override + to swap fluffychat for hydrogen-web, cinny, element-web, or + an out-of-tree dist — any replacement is mounted at the + sub-domain root with the upstream-default ``, + no sub-path gymnastics needed. + ''; + }; + sso.clientSecretFile = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; @@ -634,7 +632,7 @@ in # fluffychat at sub-domain root, SPA-fallback via the # Accept-header `$matrix_spa_target` map above. "/" = { - alias = "${cfg.gui.package}/"; + alias = "${deployCfg.matrix.gui.package}/"; extraConfig = '' try_files $uri $uri/ $matrix_spa_target =404; ''; @@ -999,7 +997,7 @@ in services.matrix-tuwunel = { enable = true; - package = cfg.package; + package = deployCfg.matrix.package; settings.global = { server_name = effectiveServerName; # `address` + `port` are upstream `listOf` — wrap singles. @@ -1084,7 +1082,7 @@ in "oidc_client_secret:${toString deployCfg.matrix.sso.clientSecretFile}" ]; - environment.systemPackages = [ cfg.package ]; + environment.systemPackages = [ deployCfg.matrix.package ]; }; }; diff --git a/nix/host-modules/swarm-authelia.nix b/nix/host-modules/swarm-authelia.nix index 0c0e7e30..a2309257 100644 --- a/nix/host-modules/swarm-authelia.nix +++ b/nix/host-modules/swarm-authelia.nix @@ -320,22 +320,12 @@ let ''; in { - # `enable` moved to `services.hyperhive.deploy.authelia.enable` — see - # ./deploy.nix. Whether this host runs the swarm's SSO provider is a - # deployment decision; what stays here is what authelia IS, including - # `url` and the OIDC client registry every hive needs as a *client* - # whether or not it runs the container. + # `enable` and both packages moved to `services.hyperhive.deploy.authelia` + # — see ./deploy.nix. Whether this host runs the swarm's SSO provider, and + # which build it runs, are deployment decisions; what stays here is what + # authelia IS, including `url` and the OIDC client registry every hive needs + # as a *client* whether or not it runs the container. options.services.hyperhive.swarm.authelia = { - package = lib.mkOption { - type = lib.types.package; - default = pkgs.authelia; - defaultText = lib.literalExpression "pkgs.authelia"; - description = '' - authelia package to run in the container. Defaults to - nixpkgs's; override to pin a specific upstream. - ''; - }; - port = lib.mkOption { type = lib.types.port; default = 9091; @@ -699,17 +689,6 @@ in ''; }; - bridgePackage = lib.mkOption { - type = lib.types.package; - defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarm-authelia-bridge"; - description = '' - `swarm-authelia-bridge` package — the only process allowed to - write `usersFile`. Wired by default from this flake's own - package set (see `flake.nix`); override to run a different - build. - ''; - }; - bridgePort = lib.mkOption { type = lib.types.port; default = 9092; @@ -750,16 +729,37 @@ in # What stays above is what authelia IS to every hive: where it answers # (`url`), the OIDC register every service checks itself against, its port. - # What the host running the container decides is here, and all three of - # these are the same kind of thing — a filesystem path that only exists on - # the machine that runs `swarm-authelia`. A hive that does not run it has - # nothing at any of them. `enable` already lives in ./deploy.nix, which - # also carries the renames. + # What the host running the container decides is here — which two builds it + # runs, and three filesystem paths that only exist on the machine running + # `swarm-authelia`. A hive that does not run it has nothing at any of those + # paths. `enable` already lives in ./deploy.nix, which also carries the + # renames. # # ⚠️ `usersFile` is a path *inside* the container and still belongs here: # a path's scope is the scope of the filesystem it names, and that # filesystem is this host's container root. options.services.hyperhive.deploy.authelia = { + package = lib.mkOption { + type = lib.types.package; + default = pkgs.authelia; + defaultText = lib.literalExpression "pkgs.authelia"; + description = '' + authelia package to run in the container. Defaults to + nixpkgs's; override to pin a specific upstream. + ''; + }; + + bridgePackage = lib.mkOption { + type = lib.types.package; + defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarm-authelia-bridge"; + description = '' + `swarm-authelia-bridge` package — the only process allowed to + write `usersFile`. Wired by default from this flake's own + package set (see `flake.nix`); override to run a different + build. + ''; + }; + usersFile = lib.mkOption { type = lib.types.str; default = "${stateDir}/users.yml"; @@ -1111,7 +1111,7 @@ in # password for the users file. Without it the container runs # authelia and cannot invoke it: the unit's ExecStart resolves # through the store path, and nothing puts the CLI on PATH. - environment.systemPackages = [ cfg.package ]; + environment.systemPackages = [ deployCfg.authelia.package ]; # This container shares the host netns, so its own # firewall.service would rewrite the HOST ruleset at every @@ -1140,16 +1140,16 @@ in wantedBy = [ "multi-user.target" ]; before = [ "${unitName}.service" ]; requiredBy = [ "${unitName}.service" ]; - # `cfg.package` is here for its CLI, not its daemon: the - # client secrets are minted with `authelia crypto hash - # generate`, which is the only way to produce a digest in + # `deployCfg.authelia.package` is here for its CLI, not its + # daemon: the client secrets are minted with `authelia crypto + # hash generate`, which is the only way to produce a digest in # the exact form authelia will later verify. path = [ pkgs.coreutils ] ++ lib.optionals oidcEnabled [ pkgs.openssl - cfg.package + deployCfg.authelia.package ]; serviceConfig = { Type = "oneshot"; @@ -1216,7 +1216,7 @@ in "${unitName}.service" ]; serviceConfig = { - ExecStart = "${cfg.bridgePackage}/bin/swarm-authelia-bridge"; + ExecStart = "${deployCfg.authelia.bridgePackage}/bin/swarm-authelia-bridge"; User = unitName; Group = unitName; Restart = "on-failure"; @@ -1235,7 +1235,7 @@ in # argon2 parameters baked into a hash have to match the # verifier's — same reasoning as `swarmctl`'s own # `SWARMCTL_AUTHELIA_BIN`. - SWARM_AUTHELIA_BRIDGE_AUTHELIA_BIN = "${cfg.package}/bin/authelia"; + SWARM_AUTHELIA_BRIDGE_AUTHELIA_BIN = "${deployCfg.authelia.package}/bin/authelia"; # By name through the gateway, not loopback. A loopback # literal encodes "authelia is in my netns" at the call site, # and authelia's OIDC endpoints are https-only in effect — @@ -1255,7 +1255,7 @@ in services.authelia.instances.${instance} = { enable = true; - package = cfg.package; + package = deployCfg.authelia.package; # Merged at RUNTIME alongside the nix-generated config, which # is the whole reason the client digests can exist at all: diff --git a/nix/host-modules/swarm-controller.nix b/nix/host-modules/swarm-controller.nix index 85d9bee6..29d445ec 100644 --- a/nix/host-modules/swarm-controller.nix +++ b/nix/host-modules/swarm-controller.nix @@ -28,7 +28,7 @@ let autheliaEnv = lib.optionalAttrs deployCfg.authelia.enable { # The CONFIGURED authelia, not whatever is on PATH: the argon2 # parameters baked into a hash have to match the verifier's. - SWARMCTL_AUTHELIA_BIN = "${autheliaCfg.package}/bin/authelia"; + SWARMCTL_AUTHELIA_BIN = "${deployCfg.authelia.package}/bin/authelia"; SWARMCTL_AUTHELIA_USERS_FILE = deployCfg.authelia.hostUsersFile; }; @@ -196,7 +196,7 @@ let # config back to it — and to get it wrong the day one of them changes. swarmctlConfigured = pkgs.symlinkJoin { name = "swarmctl-configured"; - paths = [ cfg.swarmctlPackage ]; + paths = [ deployCfg.swarm-controller.swarmctlPackage ]; nativeBuildInputs = [ pkgs.makeWrapper ]; postBuild = '' wrapProgram $out/bin/swarmctl ${ @@ -243,30 +243,8 @@ in # asserts it, and that was never an exception to "not derived from # services.hyperhive.enable": that mode says "this box is the whole # deployment", which answers the topology question outright, where - # `enable` alone never can. What stays here is what the daemon IS. - - package = lib.mkOption { - type = lib.types.package; - defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarm-controller"; - description = '' - swarm-controller package. Wired by default from this flake's own - package set (see `flake.nix`); override to run a different build. - ''; - }; - - swarmctlPackage = lib.mkOption { - type = lib.types.package; - defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarmctl"; - description = '' - `swarmctl` package — the swarm operator's CLI, installed on this - host alongside the daemon and wrapped with the paths it needs. - - A separate option from `package` rather than a second binary in - the same derivation: the CLI runs as root and acts directly, - the daemon runs unprivileged and serves a socket, and pinning one - without the other is a thing an operator may legitimately want. - ''; - }; + # `enable` alone never can. Both packages moved to that same + # `deploy.swarm-controller` block. What stays here is what the daemon IS. staleAfterSeconds = lib.mkOption { type = lib.types.ints.positive; @@ -371,16 +349,39 @@ in }; # What stays above is what the controller IS to the swarm: the quick-links - # it publishes, the client id it is registered under, how stale a snapshot - # may read, and its packages. What the host running it decides is here — - # where its socket lives, and the three credentials it reads off local - # disk. `enable` already lives in ./deploy.nix, which also carries the - # renames. + # it publishes, the client id it is registered under, and how stale a + # snapshot may read. What the host running it decides is here — which two + # builds it runs, where its socket lives, and the three credentials it reads + # off local disk. `enable` already lives in ./deploy.nix, which also carries + # the renames. # # ⚠️ `deploy.swarm-controller`, NOT `deploy.hive-controller`. Both exist: # the latter is hive-c0re's namespace, where `tls.*` lives. A definition on # the wrong one lands on a live, unrelated option rather than failing. options.services.hyperhive.deploy.swarm-controller = { + package = lib.mkOption { + type = lib.types.package; + defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarm-controller"; + description = '' + swarm-controller package. Wired by default from this flake's own + package set (see `flake.nix`); override to run a different build. + ''; + }; + + swarmctlPackage = lib.mkOption { + type = lib.types.package; + defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarmctl"; + description = '' + `swarmctl` package — the swarm operator's CLI, installed on this + host alongside the daemon and wrapped with the paths it needs. + + A separate option from `package` rather than a second binary in + the same derivation: the CLI runs as root and acts directly, + the daemon runs unprivileged and serves a socket, and pinning one + without the other is a thing an operator may legitimately want. + ''; + }; + socketPath = lib.mkOption { type = lib.types.str; default = "/run/swarm-controller/controller.sock"; @@ -591,7 +592,7 @@ in after = [ "network.target" ]; serviceConfig = { - ExecStart = "${cfg.package}/bin/swarm-controller"; + ExecStart = "${deployCfg.swarm-controller.package}/bin/swarm-controller"; # The two differ on purpose. The queue credential is # unconditional — the assertions above make its path a value that diff --git a/nix/host-modules/swarm-grafana.nix b/nix/host-modules/swarm-grafana.nix index 58e7fce0..08ebc799 100644 --- a/nix/host-modules/swarm-grafana.nix +++ b/nix/host-modules/swarm-grafana.nix @@ -111,20 +111,13 @@ in # `enable` moved to `services.hyperhive.deploy.grafana.enable` — see # ./deploy.nix. Whether this host runs the swarm's Grafana is a # deployment decision, and `swarm.*` has to be identical on every host. - # What stays here is what the service IS to every hive: its package, the - # name it is served under, the port its `/metrics` is re-served on, and - # the OIDC client it is registered as. What the host running it decides — - # where its datasources point, which plugins are in its store path, and - # the socket directory it shares with nginx — is below, under - # `deploy.grafana`. + # What stays here is what the service IS to every hive: the name it is + # served under, the port its `/metrics` is re-served on, and the OIDC + # client it is registered as. What the host running it decides — which + # build it runs, where its datasources point, which plugins are in its + # store path, and the socket directory it shares with nginx — is below, + # under `deploy.grafana`. options.services.hyperhive.swarm.grafana = { - package = lib.mkOption { - type = lib.types.package; - default = pkgs.grafana; - defaultText = lib.literalExpression "pkgs.grafana"; - description = "Grafana package to run."; - }; - machine = lib.mkOption { type = lib.types.str; readOnly = true; @@ -217,15 +210,22 @@ in }; # What stays above is what the service IS to every hive. What the host - # running it decides is here: where its datasources point, which plugins sit - # in its store path, and the directory it shares a socket with nginx - # through. `enable` already lives in ./deploy.nix, which also carries the - # renames. + # running it decides is here: which build it runs, where its datasources + # point, which plugins sit in its store path, and the directory it shares a + # socket with nginx through. `enable` already lives in ./deploy.nix, which + # also carries the renames. # # ⚠️ Both datasource URLs are wiring and still move. A URL's scope is the # scope of what it ADDRESSES, not the fact that it is a URL: both stores # bind loopback, so these can only ever mean "on this host". options.services.hyperhive.deploy.grafana = { + package = lib.mkOption { + type = lib.types.package; + default = pkgs.grafana; + defaultText = lib.literalExpression "pkgs.grafana"; + description = "Grafana package to run."; + }; + socketDir = lib.mkOption { type = lib.types.str; default = "/run/swarm-grafana"; @@ -624,7 +624,7 @@ in services.grafana = { enable = true; - package = cfg.package; + package = deployCfg.grafana.package; # Passed through unconditionally, empty default included. # Upstream distinguishes `null` from `[ ]`, and both differences diff --git a/nix/host-modules/swarm-nats.nix b/nix/host-modules/swarm-nats.nix index 031eeedc..9acd2e56 100644 --- a/nix/host-modules/swarm-nats.nix +++ b/nix/host-modules/swarm-nats.nix @@ -217,12 +217,14 @@ in # ./deploy.nix. What stays here is what the queue IS: its domain, # ports, accounts and callout wiring. - # ⚠️ Deliberately NO `package` option, unlike this module's siblings. - # `services.nats` upstream does not expose one — it resolves - # `pkgs.nats-server` itself — so an option here would either be - # ignored or need an overlay to mean anything, and an option that - # does not control what it names is worse than its absence. Pin the - # build with `nixpkgs.overlays` if you need to. + # ⚠️ Deliberately NO `package` option for the NATS server, anywhere — + # not here and not under `deploy.nats` either, where every other + # service's build now lives. `services.nats` upstream does not expose + # one; it resolves `pkgs.nats-server` itself, so an option would be + # ignored or need an overlay to mean anything, and an option that does + # not control what it names is worse than its absence. Pin the build + # with `nixpkgs.overlays` if you need to. (`deploy.nats.authPackage` + # is a different thing: the callout responder, which IS ours.) port = lib.mkOption { type = lib.types.port; @@ -284,6 +286,19 @@ in ''; }; + }; + + # What stays above is what the queue IS to every hive: the ports it answers + # on and the client id it is registered under. What the host running it + # decides is below — which responder build it runs, whether it mints its own + # keypairs, and where the seeds sit. `enable` already lives in ./deploy.nix, + # which also carries the renames. + # + # ⚠️ The two PUBLIC keys move with their seeds rather than staying: peers + # receive the user key over the wire when they connect (docs/swarm/secrets.md), + # they never configure it, and splitting a keypair across two namespaces is + # worse than either placement. + options.services.hyperhive.deploy.nats = { authPackage = lib.mkOption { type = lib.types.package; defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarm-nats-auth"; @@ -291,25 +306,15 @@ in The auth-callout responder package. ⚠️ Named `authPackage`, not `package`, on purpose: this module - deliberately has **no** `package` option for the server itself - (see the note above — upstream's `services.nats` resolves - `pkgs.nats-server` on its own), so a bare `package` here would - read as "the NATS package" and mean something else entirely. + deliberately has **no** `package` option for the NATS server + itself, because upstream's `services.nats` resolves + `pkgs.nats-server` on its own — see the note above + `options.services.hyperhive.swarm.nats`. A bare `package` here + would read as "the NATS package" and mean something else + entirely. ''; }; - }; - # What stays above is what the queue IS to every hive: the ports it answers - # on, the client id it is registered under, the responder package. The - # callout identity below is what the host running it decides — whether it - # mints its own keypairs, and where the seeds sit. `enable` already lives in - # ./deploy.nix, which also carries the renames. - # - # ⚠️ The two PUBLIC keys move with their seeds rather than staying: peers - # receive the user key over the wire when they connect (docs/swarm/secrets.md), - # they never configure it, and splitting a keypair across two namespaces is - # worse than either placement. - options.services.hyperhive.deploy.nats = { autoGenerateCallout = lib.mkOption { type = lib.types.bool; default = false; @@ -708,7 +713,7 @@ in wantedBy = [ "multi-user.target" ]; serviceConfig = { ExecStart = lib.concatStringsSep " " [ - "${cfg.authPackage}/bin/swarm-nats-auth" + "${deployCfg.nats.authPackage}/bin/swarm-nats-auth" "--nats-url nats://127.0.0.1:${toString cfg.port}" "--user-seed-file \${CREDENTIALS_DIRECTORY}/callout-user.seed" "--issuer-seed-file \${CREDENTIALS_DIRECTORY}/issuer.seed" diff --git a/nix/host-modules/swarm-ui.nix b/nix/host-modules/swarm-ui.nix index dd389558..f50b3eff 100644 --- a/nix/host-modules/swarm-ui.nix +++ b/nix/host-modules/swarm-ui.nix @@ -13,7 +13,6 @@ ... }: let - cfg = config.services.hyperhive.swarm.ui; deployCfg = config.services.hyperhive.deploy; gatewayCfg = config.services.hyperhive.gateway; autheliaCfg = config.services.hyperhive.swarm.authelia; @@ -22,11 +21,11 @@ let # this UI was left out of that overlay entirely (an operator with a # stylix-themed host saw the dashboard in their own colours but this # UI still on the default Catppuccin palette), because nothing here - # ever served a themed `colors.css` in place of `cfg.package`'s own. + # ever served a themed `colors.css` in place of `deployCfg.swarm-ui.package`'s own. # Detection + CSS-generation is shared (`./stylix-theme.nix`). # # No package-copy derivation: this vhost has exactly one location - # serving `cfg.package` (unlike hive-c0re's `servedFrontend`, which + # serving `deployCfg.swarm-ui.package` (unlike hive-c0re's `servedFrontend`, which # backs both the dashboard root AND every per-agent gateway route, so # a single swapped tree covers both) — an `= /static/colors.css` # exact-match location overriding just that one file, same idiom @@ -79,22 +78,23 @@ in '') ]; - options.services.hyperhive.swarm.ui = { - package = lib.mkOption { - type = lib.types.package; - defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarm-ui"; - description = '' - Static build of the swarm UI. nginx serves this store path - directly — there is no server-side component beyond the - controller's own API. + # ⚠️ `services.hyperhive.swarm.ui` no longer exists as a namespace. `enable` + # moved in an earlier slice and `package` in this one, and those were its + # only two options — so both paths survive purely as renames in ./deploy.nix. + options.services.hyperhive.deploy.swarm-ui.package = lib.mkOption { + type = lib.types.package; + defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarm-ui"; + description = '' + Static build of the swarm UI. nginx serves this store path + directly — there is no server-side component beyond the + controller's own API. - Wired by default from this flake's own package set (see - `flake.nix`), the same way `swarm.controller.package` is. There - is deliberately **no overlay** in this project, so a - `pkgs.swarm-ui` default here would name an attribute that does - not exist on any real deployment. - ''; - }; + Wired by default from this flake's own package set (see + `flake.nix`), the same way `deploy.swarm-controller.package` is. There + is deliberately **no overlay** in this project, so a + `pkgs.swarm-ui` default here would name an attribute that does + not exist on any real deployment. + ''; }; config = lib.mkIf (config.services.hyperhive.enable && deployCfg.swarm-ui.enable) { @@ -178,7 +178,7 @@ in extraConfig = gatewayCfg.lib.securityHeaders; locations = { "/" = { - root = "${cfg.package}"; + root = "${deployCfg.swarm-ui.package}"; extraConfig = '' ${swarmAuthRequest} # SPA: any path the bundle routes client-side is served the @@ -189,7 +189,7 @@ in } // lib.optionalAttrs (stylixThemeColors != null) { # Stylix theming (see the `let` block above): overrides just - # this one file from `cfg.package`'s own static root, rather + # this one file from `deployCfg.swarm-ui.package`'s own static root, rather # than copying the whole tree to change one file inside it — # nginx resolves the more specific `=` exact match over the # `/` prefix root above, so this simply doesn't exist (falling diff --git a/nix/host-modules/swarm-victorialogs.nix b/nix/host-modules/swarm-victorialogs.nix index 4709e3eb..ddead0d2 100644 --- a/nix/host-modules/swarm-victorialogs.nix +++ b/nix/host-modules/swarm-victorialogs.nix @@ -56,17 +56,11 @@ let ''; in { - # What stays here is what the store IS from any hive's point of view: its - # package, the name it answers on, the port. `enable` and `retentionPeriod` - # are decisions of the host that runs it and live under `deploy.*`. + # What stays here is what the store IS from any hive's point of view: the + # name it answers on and the port. `enable`, `package` and + # `retentionPeriod` are decisions of the host that runs it and live under + # `deploy.*`. options.services.hyperhive.swarm.victorialogs = { - package = lib.mkOption { - type = lib.types.package; - default = pkgs.victorialogs; - defaultText = lib.literalExpression "pkgs.victorialogs"; - description = "VictoriaLogs package to run."; - }; - machine = lib.mkOption { type = lib.types.str; readOnly = true; @@ -116,6 +110,13 @@ in # and a hive that is a *client* of the log store never consults it. That # makes it a `deploy.*` value by the same rule as the seal on the secret # store — options on the auto-deployed service itself. + options.services.hyperhive.deploy.victorialogs.package = lib.mkOption { + type = lib.types.package; + default = pkgs.victorialogs; + defaultText = lib.literalExpression "pkgs.victorialogs"; + description = "VictoriaLogs package to run."; + }; + options.services.hyperhive.deploy.victorialogs.retentionPeriod = lib.mkOption { type = lib.types.str; default = "30d"; @@ -259,7 +260,7 @@ in services.victorialogs = { enable = true; - package = cfg.package; + package = deployCfg.victorialogs.package; # ⚠️ PINNED TO LOOPBACK for the same reason the metrics store is, # and it matters more here: upstream's default listens on every diff --git a/nix/host-modules/swarm-victoriametrics.nix b/nix/host-modules/swarm-victoriametrics.nix index 7e080d97..604e9d3d 100644 --- a/nix/host-modules/swarm-victoriametrics.nix +++ b/nix/host-modules/swarm-victoriametrics.nix @@ -30,17 +30,11 @@ let domainBase = if swarmDomain == null then "invalid" else swarmDomain; in { - # What stays here is what the store IS from any hive's point of view: its - # package, the name it answers on, the port. `enable` and `retentionPeriod` - # are decisions of the host that runs it and live under `deploy.*`. + # What stays here is what the store IS from any hive's point of view: the + # name it answers on and the port. `enable`, `package` and + # `retentionPeriod` are decisions of the host that runs it and live under + # `deploy.*`. options.services.hyperhive.swarm.victoriametrics = { - package = lib.mkOption { - type = lib.types.package; - default = pkgs.victoriametrics; - defaultText = lib.literalExpression "pkgs.victoriametrics"; - description = "VictoriaMetrics package to run."; - }; - machine = lib.mkOption { type = lib.types.str; readOnly = true; @@ -79,6 +73,13 @@ in # Retention is a property of the store this host runs, not something the # swarm has to agree on: it is read only where the container is defined, # and a hive that is a *client* of the metrics store never consults it. + options.services.hyperhive.deploy.victoriametrics.package = lib.mkOption { + type = lib.types.package; + default = pkgs.victoriametrics; + defaultText = lib.literalExpression "pkgs.victoriametrics"; + description = "VictoriaMetrics package to run."; + }; + options.services.hyperhive.deploy.victoriametrics.retentionPeriod = lib.mkOption { type = lib.types.str; default = "5y"; @@ -213,7 +214,7 @@ in services.victoriametrics = { enable = true; - package = cfg.package; + package = deployCfg.victoriametrics.package; retentionPeriod = deployCfg.victoriametrics.retentionPeriod; # ⚠️ PINNED TO LOOPBACK, and this is a correction rather than a diff --git a/nix/module-eval.nix b/nix/module-eval.nix index 610ae2b8..53f2d2fc 100644 --- a/nix/module-eval.nix +++ b/nix/module-eval.nix @@ -16,7 +16,7 @@ # # ⚠️ **This evaluates, it does not execute** — a command line, a request or # a certificate needs something that *runs* it. A case needing a rendered -# file must stub what it drags in (`swarm.ui.package = pkgs.emptyDirectory`). +# file must stub what it drags in (`deploy.swarm-ui.package = pkgs.emptyDirectory`). { pkgs, lib, @@ -72,11 +72,12 @@ let swarm.wireguard.privateKeyFile = "/etc/wireguard/hive.key"; }; - # Same shape for the forge, which SPLIT rather than moving whole: the five + # Same shape for the forge, which SPLIT rather than moving whole: the six # host-side options are set here through their pre-rename paths while the - # rest of `swarm.forge` stays put. All five are defined so that dropping any + # rest of `swarm.forge` stays put. All six are defined so that dropping any # single shim entry fails the eval, not just the two the assertion reads. forgeOldPath = hive { + swarm.forge.package = pkgs.emptyDirectory; swarm.forge.behindGateway = true; swarm.forge.openFirewall = true; swarm.forge.hostSwarmControllerTokenFile = "/etc/forge/sc.token"; @@ -89,11 +90,12 @@ let ]; }; - # The homeserver's turn to split. All six host-side options are set through + # The homeserver's turn to split. All eight host-side options are set through # their pre-rename paths — including `gui.enable`, whose value is deliberately - # the opposite of its default so the definition has to actually land. All six - # so that dropping any single shim entry fails the eval, not just the two the - # assertion reads. + # the opposite of its default so the definition has to actually land, and both + # packages, stubbed to a derivation neither option defaults to for the same + # reason. All eight so that dropping any single shim entry fails the eval, not + # just the ones the assertions read. matrixOldPath = hive { deploy.matrix.enable = true; swarm.matrix.openFirewall = true; @@ -101,26 +103,33 @@ let swarm.matrix.maxRequestSize = 31457280; swarm.matrix.registrationTokenFile = "/etc/matrix/register.token"; swarm.matrix.gui.enable = false; + swarm.matrix.package = pkgs.emptyDirectory; + swarm.matrix.gui.package = pkgs.emptyDirectory; swarm.matrix.sso.clientSecretFile = "/etc/matrix/oidc.secret"; }; # The queue's callout identity, fourth split slice. `autoGenerateCallout` is # left FALSE on purpose: that is what makes the seed paths the thing deciding # `responderConfigured`, so the assertion below is about the seeds rather - # than about the auto-mint branch. All five old paths are defined, so - # dropping any single shim entry fails the eval, not just the arms read. + # than about the auto-mint branch. Every one of the seven old paths is + # defined — `enable` included, which is why it is spelled the old way here + # while the fixture below uses the new one — so dropping any single nats + # shim fails the eval, not just the arms read. natsOldPath = hive { - deploy.nats.enable = true; + swarm.nats.enable = true; swarm.nats.autoGenerateCallout = false; swarm.nats.calloutUserPublicKey = "UTESTUSERPUBKEYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; swarm.nats.calloutIssuerPublicKey = "ATESTISSUERPUBKEYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; swarm.nats.calloutUserSeedFile = "/run/secrets/nats-user.seed"; swarm.nats.calloutIssuerSeedFile = "/run/secrets/nats-issuer.seed"; + swarm.nats.authPackage = pkgs.emptyDirectory; }; - # Seventh split slice. Only `usersFile` has a rename entry: the other two - # movers are `readOnly`, and a rename module contributes a definition, which - # a read-only option refuses — see ./host-modules/deploy.nix. So the two arms + # Seventh split slice, plus slice 10's two authelia packages. Of slice 7's + # movers only `usersFile` has a rename entry — the other two are `readOnly`, + # and a rename module contributes a definition, which a read-only option + # refuses; see ./host-modules/deploy.nix. `package` and `bridgePackage` are + # ordinary options, so they do carry one. The two arms # below have different jobs. `usersFile` tests the rename; the nats one tests # that a reader repointed to the new namespace still renders the derived # path, which is the failure this slice could actually have shipped — seven @@ -129,6 +138,8 @@ let deploy.authelia.enable = true; deploy.nats.enable = true; swarm.authelia.usersFile = "/var/lib/test-authelia/users.yml"; + swarm.authelia.package = pkgs.emptyDirectory; + swarm.authelia.bridgePackage = pkgs.emptyDirectory; swarm.nats.autoGenerateCallout = false; swarm.nats.calloutUserPublicKey = "UTESTUSERPUBKEYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; swarm.nats.calloutIssuerPublicKey = "ATESTISSUERPUBKEYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; @@ -143,20 +154,47 @@ let deploy.matrix.maxRequestSize = 99000000; }; + # `enable` is spelled the OLD way like everything else here, so all seven + # of the controller's shims are exercised rather than six. controllerOldPath = hive { - deploy.swarm-controller.enable = true; + swarm.controller.enable = true; + swarm.controller.package = pkgs.emptyDirectory; + swarm.controller.swarmctlPackage = pkgs.emptyDirectory; swarm.controller.socketPath = "/run/test-ctrl/ctrl.sock"; swarm.controller.forgeTokenFile = "/run/secrets/ctrl-forge.token"; swarm.controller.authBridgeUrl = "http://127.0.0.1:19097"; swarm.controller.queue.clientSecretFile = "/run/secrets/ctrl-queue.secret"; }; + # The swarm UI, whose namespace this slice empties: `swarm.ui` had exactly + # two options and both have moved, so BOTH old paths are set here and the + # whole namespace now lives or dies by its two rename entries. + uiOldPath = hive { + swarm.ui.enable = true; + swarm.ui.package = pkgs.emptyDirectory; + }; + + # The two stores, which had NO old-path fixture at all until this slice — + # their `enable` and `retentionPeriod` shims have been uncovered since they + # landed, which is precisely the "a missing shim reads as a clean tree" + # failure ./module-eval.nix's wireguard fixture was written to catch. All six + # old paths are set, so dropping any single entry fails the eval. + storesOldPath = hive { + swarm.victoriametrics.enable = true; + swarm.victorialogs.enable = true; + swarm.victoriametrics.retentionPeriod = "3d"; + swarm.victorialogs.retentionPeriod = "5d"; + swarm.victoriametrics.package = pkgs.emptyDirectory; + swarm.victorialogs.package = pkgs.emptyDirectory; + }; + 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; }; # The first slice to leave options on BOTH sides of the split, so the @@ -472,6 +510,43 @@ let # host-side half depends on: no store here, so no bind mount and no unit. # Without it a hive that merely names a token would drag the store's # container config into its evaluation. + # Reads the vhost's rendered `root`, not the option: the UI is served + # straight out of a store path, so a shim that resolves but stops + # reaching the module would leave nginx pointing at the default build. + name = "a config written against the pre-rename swarm-ui paths still serves the operator's build"; + # + # ⚠️ `unsafeDiscardStringContext` on both sides, load-bearing rather than + # tidy: interpolating a derivation carries string *context*, and this + # suite renders its results into a `buildCommand` that may not reference + # store paths. Comparing the paths as plain text is the intent — the case + # asks "does nginx point HERE", not "depend on what it points at". + ok = + let + want = builtins.unsafeDiscardStringContext "${pkgs.emptyDirectory}"; + # `or ""` is NOT enough: nginx's `locations..root` is `nullOr`, so + # a vhost that HAS the attribute set to null skips the default and + # reaches the coercion. Filter nulls, then discard context. + roots = map builtins.unsafeDiscardStringContext ( + lib.filter (r: r != null) ( + map (v: v.locations."/".root or null) (builtins.attrValues uiOldPath.services.nginx.virtualHosts) + ) + ); + in + builtins.elem want roots; + } + { + # Reads the package the CONTAINER renders, not the option: a shim that + # resolves but stops reaching the module would leave the store running + # nixpkgs' default while the operator's override read back fine. + name = "a config written against the pre-rename store paths still picks the operator's package"; + ok = + storesOldPath.containers.swarm-victorialogs.config.services.victorialogs.package + == pkgs.emptyDirectory + && + storesOldPath.containers.swarm-victoriametrics.config.services.victoriametrics.package + == pkgs.emptyDirectory; + } + { name = "a bootstrap token on a host that runs no store grants nothing"; ok = !(baoGrantNoStore.systemd.services ? swarm-bao-bootstrap-dir); }