diff --git a/docs/integrations/forge.md b/docs/integrations/forge.md index 546bc764..d249f914 100644 --- a/docs/integrations/forge.md +++ b/docs/integrations/forge.md @@ -3,10 +3,9 @@ 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 — names, ports, URLs) plus +forge IS to every hive — its package, names, ports, URLs) plus `services.hyperhive.deploy.forgejo.*` (what the host running it decides — -which build it runs, how it's served, what it mirrors, where its -host-local secrets sit). +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 37b4414f..9ee832b3 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.deploy.matrix.gui.package`) served by the hive-gateway +`services.hyperhive.swarm.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 9e6d2ad4..cefc5a2f 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.deploy.swarm-controller.package = + services.hyperhive.swarm.controller.package = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.swarm-controller; - services.hyperhive.deploy.swarm-controller.swarmctlPackage = + services.hyperhive.swarm.controller.swarmctlPackage = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.swarmctl; - services.hyperhive.deploy.swarm-ui.package = + services.hyperhive.swarm.ui.package = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.swarm-ui; - services.hyperhive.deploy.nats.authPackage = + services.hyperhive.swarm.nats.authPackage = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.swarm-nats-auth; - services.hyperhive.deploy.authelia.bridgePackage = + services.hyperhive.swarm.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 0d491143..a4e23af7 100644 --- a/nix/host-modules/deploy.nix +++ b/nix/host-modules/deploy.nix @@ -164,10 +164,9 @@ in ) # The queue's callout identity. What stays under `swarm.nats` is what the - # 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. + # 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. # # ⚠️ 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 @@ -196,10 +195,9 @@ in ) # The rest of the forge split. What stays under `swarm.forge` is what the - # 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 + # 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 # 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. # @@ -229,10 +227,9 @@ in ) # The homeserver, split the same way. `swarm.matrix` keeps what it IS to - # 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 + # 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 # 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 @@ -262,9 +259,8 @@ 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 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. + # agrees on. The two stores keep everything else — package, domain, port + # — in `swarm.*`, because a client hive needs those to reach them. (lib.mkRenamedOptionModule [ "services" "hyperhive" "swarm" "victoriametrics" "retentionPeriod" ] [ "services" "hyperhive" "deploy" "victoriametrics" "retentionPeriod" ] @@ -339,68 +335,6 @@ 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 3562fbd5..a171553f 100644 --- a/nix/host-modules/hive-forge/default.nix +++ b/nix/host-modules/hive-forge/default.nix @@ -232,6 +232,21 @@ 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; @@ -279,28 +294,13 @@ in }; }; - # 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. + # 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. 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 ${deployCfg.forgejo.package.data}/. $out/ + cp -r --no-preserve=mode,ownership ${cfg.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 = deployCfg.forgejo.package; + package = cfg.package; database.type = "sqlite3"; lfs.enable = true; settings = { @@ -1036,7 +1036,7 @@ in FORGEJO_CUSTOM = "/var/lib/forgejo/custom"; }; path = [ - deployCfg.forgejo.package + cfg.package pkgs.coreutils pkgs.gnugrep ]; @@ -1126,7 +1126,7 @@ in FORGEJO_CUSTOM = "/var/lib/forgejo/custom"; }; path = [ - deployCfg.forgejo.package + cfg.package pkgs.coreutils pkgs.gnugrep ]; diff --git a/nix/host-modules/hive-matrix.nix b/nix/host-modules/hive-matrix.nix index 8d087b0a..d1964cb8 100644 --- a/nix/host-modules/hive-matrix.nix +++ b/nix/host-modules/hive-matrix.nix @@ -172,9 +172,8 @@ 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. What stays under - # `swarm.matrix` is the homeserver's identity as every hive sees it — - # its server name, its addresses, its federation policy. + # namespace every host in the swarm agrees on. The rest of the matrix + # settings genuinely are swarm-wide and stay where they are. (lib.mkRenamedOptionModule [ "services" "hyperhive" "swarm" "matrix" "enable" ] [ "services" "hyperhive" "deploy" "matrix" "enable" ] @@ -192,6 +191,17 @@ 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; @@ -328,6 +338,26 @@ 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 @@ -368,24 +398,13 @@ in }; # What stays above is what the homeserver IS from any hive's point of view: - # 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 + # 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 # ./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; @@ -490,28 +509,11 @@ 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 - `deploy.matrix.gui.package` — which client, as opposed to whether + `swarm.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; @@ -632,7 +634,7 @@ in # fluffychat at sub-domain root, SPA-fallback via the # Accept-header `$matrix_spa_target` map above. "/" = { - alias = "${deployCfg.matrix.gui.package}/"; + alias = "${cfg.gui.package}/"; extraConfig = '' try_files $uri $uri/ $matrix_spa_target =404; ''; @@ -997,7 +999,7 @@ in services.matrix-tuwunel = { enable = true; - package = deployCfg.matrix.package; + package = cfg.package; settings.global = { server_name = effectiveServerName; # `address` + `port` are upstream `listOf` — wrap singles. @@ -1082,7 +1084,7 @@ in "oidc_client_secret:${toString deployCfg.matrix.sso.clientSecretFile}" ]; - environment.systemPackages = [ deployCfg.matrix.package ]; + environment.systemPackages = [ cfg.package ]; }; }; diff --git a/nix/host-modules/swarm-authelia.nix b/nix/host-modules/swarm-authelia.nix index a2309257..0c0e7e30 100644 --- a/nix/host-modules/swarm-authelia.nix +++ b/nix/host-modules/swarm-authelia.nix @@ -320,12 +320,22 @@ let ''; in { - # `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. + # `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. 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; @@ -689,6 +699,17 @@ 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; @@ -729,37 +750,16 @@ 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 — 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. + # 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. # # ⚠️ `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 = [ deployCfg.authelia.package ]; + environment.systemPackages = [ cfg.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" ]; - # `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 + # `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 # the exact form authelia will later verify. path = [ pkgs.coreutils ] ++ lib.optionals oidcEnabled [ pkgs.openssl - deployCfg.authelia.package + cfg.package ]; serviceConfig = { Type = "oneshot"; @@ -1216,7 +1216,7 @@ in "${unitName}.service" ]; serviceConfig = { - ExecStart = "${deployCfg.authelia.bridgePackage}/bin/swarm-authelia-bridge"; + ExecStart = "${cfg.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 = "${deployCfg.authelia.package}/bin/authelia"; + SWARM_AUTHELIA_BRIDGE_AUTHELIA_BIN = "${cfg.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 = deployCfg.authelia.package; + package = cfg.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 29d445ec..85d9bee6 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 = "${deployCfg.authelia.package}/bin/authelia"; + SWARMCTL_AUTHELIA_BIN = "${autheliaCfg.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 = [ deployCfg.swarm-controller.swarmctlPackage ]; + paths = [ cfg.swarmctlPackage ]; nativeBuildInputs = [ pkgs.makeWrapper ]; postBuild = '' wrapProgram $out/bin/swarmctl ${ @@ -243,8 +243,30 @@ 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. Both packages moved to that same - # `deploy.swarm-controller` block. What stays here is what the daemon IS. + # `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. + ''; + }; staleAfterSeconds = lib.mkOption { type = lib.types.ints.positive; @@ -349,39 +371,16 @@ in }; # What stays above is what the controller IS to the swarm: the quick-links - # 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. + # 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. # # ⚠️ `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"; @@ -592,7 +591,7 @@ in after = [ "network.target" ]; serviceConfig = { - ExecStart = "${deployCfg.swarm-controller.package}/bin/swarm-controller"; + ExecStart = "${cfg.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 08ebc799..58e7fce0 100644 --- a/nix/host-modules/swarm-grafana.nix +++ b/nix/host-modules/swarm-grafana.nix @@ -111,13 +111,20 @@ 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: 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`. + # 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`. 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; @@ -210,22 +217,15 @@ in }; # What stays above is what the service IS to every hive. What the host - # 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. + # 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. # # ⚠️ 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 = deployCfg.grafana.package; + package = cfg.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 9acd2e56..031eeedc 100644 --- a/nix/host-modules/swarm-nats.nix +++ b/nix/host-modules/swarm-nats.nix @@ -217,14 +217,12 @@ in # ./deploy.nix. What stays here is what the queue IS: its domain, # ports, accounts and callout wiring. - # ⚠️ 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.) + # ⚠️ 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. port = lib.mkOption { type = lib.types.port; @@ -286,19 +284,6 @@ 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"; @@ -306,15 +291,25 @@ in The auth-callout responder package. ⚠️ Named `authPackage`, not `package`, on purpose: this module - 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. + 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. ''; }; + }; + # 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; @@ -713,7 +708,7 @@ in wantedBy = [ "multi-user.target" ]; serviceConfig = { ExecStart = lib.concatStringsSep " " [ - "${deployCfg.nats.authPackage}/bin/swarm-nats-auth" + "${cfg.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 f50b3eff..dd389558 100644 --- a/nix/host-modules/swarm-ui.nix +++ b/nix/host-modules/swarm-ui.nix @@ -13,6 +13,7 @@ ... }: let + cfg = config.services.hyperhive.swarm.ui; deployCfg = config.services.hyperhive.deploy; gatewayCfg = config.services.hyperhive.gateway; autheliaCfg = config.services.hyperhive.swarm.authelia; @@ -21,11 +22,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 `deployCfg.swarm-ui.package`'s own. + # ever served a themed `colors.css` in place of `cfg.package`'s own. # Detection + CSS-generation is shared (`./stylix-theme.nix`). # # No package-copy derivation: this vhost has exactly one location - # serving `deployCfg.swarm-ui.package` (unlike hive-c0re's `servedFrontend`, which + # serving `cfg.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 @@ -78,23 +79,22 @@ in '') ]; - # ⚠️ `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. + 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. - 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. - ''; + 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. + ''; + }; }; config = lib.mkIf (config.services.hyperhive.enable && deployCfg.swarm-ui.enable) { @@ -178,7 +178,7 @@ in extraConfig = gatewayCfg.lib.securityHeaders; locations = { "/" = { - root = "${deployCfg.swarm-ui.package}"; + root = "${cfg.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 `deployCfg.swarm-ui.package`'s own static root, rather + # this one file from `cfg.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 ddead0d2..4709e3eb 100644 --- a/nix/host-modules/swarm-victorialogs.nix +++ b/nix/host-modules/swarm-victorialogs.nix @@ -56,11 +56,17 @@ let ''; in { - # 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.*`. + # 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.*`. 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; @@ -110,13 +116,6 @@ 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"; @@ -260,7 +259,7 @@ in services.victorialogs = { enable = true; - package = deployCfg.victorialogs.package; + package = cfg.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 604e9d3d..7e080d97 100644 --- a/nix/host-modules/swarm-victoriametrics.nix +++ b/nix/host-modules/swarm-victoriametrics.nix @@ -30,11 +30,17 @@ 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: the - # name it answers on and the port. `enable`, `package` 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: its + # package, the name it answers on, the port. `enable` 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; @@ -73,13 +79,6 @@ 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"; @@ -214,7 +213,7 @@ in services.victoriametrics = { enable = true; - package = deployCfg.victoriametrics.package; + package = cfg.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 53f2d2fc..610ae2b8 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 (`deploy.swarm-ui.package = pkgs.emptyDirectory`). +# file must stub what it drags in (`swarm.ui.package = pkgs.emptyDirectory`). { pkgs, lib, @@ -72,12 +72,11 @@ let swarm.wireguard.privateKeyFile = "/etc/wireguard/hive.key"; }; - # Same shape for the forge, which SPLIT rather than moving whole: the six + # Same shape for the forge, which SPLIT rather than moving whole: the five # host-side options are set here through their pre-rename paths while the - # rest of `swarm.forge` stays put. All six are defined so that dropping any + # rest of `swarm.forge` stays put. All five 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"; @@ -90,12 +89,11 @@ let ]; }; - # The homeserver's turn to split. All eight host-side options are set through + # The homeserver's turn to split. All six 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, 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. + # 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. matrixOldPath = hive { deploy.matrix.enable = true; swarm.matrix.openFirewall = true; @@ -103,33 +101,26 @@ 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. 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. + # 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. natsOldPath = hive { - swarm.nats.enable = true; + deploy.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, 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 + # 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 # 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 @@ -138,8 +129,6 @@ 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"; @@ -154,47 +143,20 @@ 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 { - swarm.controller.enable = true; - swarm.controller.package = pkgs.emptyDirectory; - swarm.controller.swarmctlPackage = pkgs.emptyDirectory; + deploy.swarm-controller.enable = true; 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 @@ -510,43 +472,6 @@ 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); }