From f918cea957e8b47163b00ee82d8e937d291ff774 Mon Sep 17 00:00:00 2001 From: atlas Date: Fri, 11 Sep 2026 11:28:56 +0200 Subject: [PATCH] module-eval: evaluate the swarm.peers removal shim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `nix/host-modules/swarm-peers-removed.nix` exists to turn an "option does not exist" error into a warning that says where the entries went. That warning is its whole deliverable, and no fixture set `swarm.peers` — the string appeared 0 times in module-eval.nix, so the shim was never evaluated by anything. It differs from its ten siblings in what a broken shim looks like. The others re-route a value, so a failure shows up as a wrong rendered config. This one renders prose nothing reads back, so a `mkIf` that stops matching or a rename of the `swarm.hives` it points at fails silently, and lands on the one operator who needed it. The check already carries an old-path fixture for ten migrated namespaces (wireguard, forge, matrix, nats, authelia, controller, ui, stores, grafana, statusPublish). `swarm.peers` was the eleventh and the only uncovered one. Two peers, only one carrying `caCert`, because the module emits a second warning filtered on exactly that attribute — with a single peer the filtered list and `attrNames` are the same list, so a `withCaCert` that had collapsed into `attrNames` would still read green. The third case is the control: a hive that never set `peers` must get neither warning, without which the other two pass on any config whose warning list happens to carry the string. Closes #4188. --- nix/module-eval.nix | 54 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/nix/module-eval.nix b/nix/module-eval.nix index 91130618..839365b0 100644 --- a/nix/module-eval.nix +++ b/nix/module-eval.nix @@ -235,6 +235,25 @@ let swarm.statusPublish.clientSecretFile = "/run/secrets/status-client.secret"; }; + # The eleventh migration, and the only one of the eleven with no old-path + # fixture. It differs from its ten siblings in what a shim DOES: the others + # re-route a value, so a broken one shows up as a wrong rendered config. This + # one's entire deliverable is a warning, which nothing reads back — so an + # unevaluated shim is a warning nobody sees, and it fails silently onto the + # one operator who needed it. + # + # Two peers, only ONE carrying `caCert`, because the module emits a SECOND + # warning filtered on exactly that attribute. With a single peer the filtered + # list and `attrNames` are the same list, and a `withCaCert` that had + # collapsed into `attrNames` would still read green. + peersOldPath = hive { + swarm.peers.old-one.domain = "old-one.t.local"; + swarm.peers.old-two = { + domain = "old-two.t.local"; + caCert = "/etc/ssl/old-two-ca.pem"; + }; + }; + baoPkcs11 = hive { deploy.bao.enable = true; deploy.bao.seal = "pkcs11"; @@ -990,6 +1009,41 @@ let name = "a bootstrap token on a host that runs no store grants nothing"; ok = !(baoGrantNoStore.systemd.services ? swarm-bao-bootstrap-dir); } + { + # Matches the option path, not the surrounding prose: the warning should + # stay rewordable, but it must not stop naming what was set or where it + # went. + name = "a config still setting the removed swarm.peers is told where the entries moved"; + ok = + let + hit = lib.filter ( + w: lib.hasInfix "services.hyperhive.swarm.peers is removed" w + ) peersOldPath.warnings; + in + lib.length hit == 1 + && lib.hasInfix "services.hyperhive.swarm.hives" (lib.head hit) + && lib.hasInfix "old-one, old-two" (lib.head hit); + } + { + # The caCert half is a separately filtered warning, so it has to name + # ONLY the peer that set one. Asserting the peer that did NOT is absent + # is what distinguishes the filter from `attrNames`. + name = "the removed peers.caCert warns about only the peers that set one"; + ok = + let + hit = lib.filter (w: lib.hasInfix "caCert is removed" w) peersOldPath.warnings; + in + lib.length hit == 1 + && lib.hasInfix "Still set on: old-two" (lib.head hit) + && !(lib.hasInfix "old-one" (lib.head hit)); + } + { + # Control for both cases above. Without it they pass on any config whose + # warning list happens to carry the string, including one where the shim + # fires unconditionally — which is the same defect wearing the other sign. + name = "a hive that never set swarm.peers gets neither removal warning"; + ok = !(lib.any (w: lib.hasInfix "swarm.peers" w) bare.warnings); + } { name = "a config written against the pre-rename authelia usersFile still reaches the bridge"; ok =