diff --git a/docs/swarm/README.md b/docs/swarm/README.md index 2f845496..aa0d07c9 100644 --- a/docs/swarm/README.md +++ b/docs/swarm/README.md @@ -338,10 +338,8 @@ nothing has to re-publish. ### Making a hive report -Three options, on the **hive**, set together or not at all — a -half-configured hive is an eval error rather than one that quietly never -reports. They sit in two namespaces, because two of them are facts about -_this machine_ and one is the swarm's single address: +Three options on the **hive**. They sit in two namespaces, because two of them +are facts about _this machine_ and one is the swarm's single address: | option | what to set it to | | ------------------------------------------------------- | --------------------------------------------- | @@ -349,10 +347,14 @@ _this machine_ and one is the swarm's single address: | `swarm.statusPublish.tokenEndpoint` | the swarm IdP's `/api/oidc/token` | | `deploy.hive-controller.statusPublish.clientSecretFile` | path to this hive's client secret, plaintext | -On a host that runs the queue and the IdP itself, all three default to -the local ones and there is nothing to set. Any other hive needs them -spelled out, and needs the secret to physically be there: the swarm does -not distribute it. Copy `hive-.secret` out of the swarm host's +The queue URL and the token endpoint default to the swarm's own addresses on +every hive, so there is nothing to set for them. The secret is what turns +publishing on: a hive without it doesn't publish. A secret without the other +two is an eval error rather than a hive that quietly never reports. + +On a host that runs the queue and the IdP, the secret defaults to the local one. +Any other hive needs the secret to physically be there, because the swarm +doesn't distribute it. Copy `hive-.secret` out of the swarm host's `deploy.authelia.hostClientSecretDir` with whatever secret management the deployment already uses. @@ -390,11 +392,12 @@ with the credential itself and aren't configurable. | `deploy.hive-controller.queue.agentNatsUrl` | where the queue listens, as an agent **container** reaches it | | `swarm.statusPublish.tokenEndpoint` | the swarm IdP's `/api/oidc/token` — the same one the hive uses | -On a host that runs the queue, `agentNatsUrl` defaults to the same -`tls://:` as the hive's own. Inside a -container the name resolves to the bridge address, where the firewall opens the -port. ⚠️ **Never a loopback address here**: an agent has its own network -namespace, so `127.0.0.1` reaches the agent. +On every hive, `agentNatsUrl` defaults to the same +`tls://:` as the hive's own. On the queue's +host the name resolves inside a container to the bridge address, where the +firewall opens the port; on any other hive it resolves through the host's DNS, +like the store's name. ⚠️ **Never a loopback address here**: an agent has its +own network namespace, so `127.0.0.1` reaches the agent. The harness sees four variables, and treats them as all-or-none: `HIVE_AGENT_NATS_URL` and `HIVE_AGENT_OIDC_TOKEN_ENDPOINT` from the two options diff --git a/nix/host-modules/hive-c0re/default.nix b/nix/host-modules/hive-c0re/default.nix index f587489e..52f1ce2f 100644 --- a/nix/host-modules/hive-c0re/default.nix +++ b/nix/host-modules/hive-c0re/default.nix @@ -133,12 +133,12 @@ in ./options.nix ./theme.nix # Hive-CA trust for this daemon's outbound TLS. Nothing it is given by - # default is https — the forge, matrix and queue URLs all resolve to - # plain http or loopback — so this changes nothing on an all-local - # hive. It matters for the split-host shape the options invite: - # `swarm.matrix.apiUrl`'s own example is `https://matrix.example.com`, - # and pointing it (or `deploy.hive-controller.statusPublish.natsUrl`) - # at another hive's + # default is https — the forge and matrix URLs resolve to plain http or + # loopback, and the queue's TLS is verified against + # `HIVE_C0RE_OIDC_CA_FILE` (../hive-tls.nix), not this bundle — so this + # changes nothing on an all-local hive. It matters for the split-host + # shape the options invite: `swarm.matrix.apiUrl`'s own example is + # `https://matrix.example.com`, and pointing it at another hive's # gateway means verifying a leaf signed by a CA generated at runtime, # which no build-time trust store can contain. # diff --git a/nix/host-modules/hive-c0re/environment.nix b/nix/host-modules/hive-c0re/environment.nix index d07b3e69..49271892 100644 --- a/nix/host-modules/hive-c0re/environment.nix +++ b/nix/host-modules/hive-c0re/environment.nix @@ -257,24 +257,30 @@ in # environment is a deployment bug the daemon refuses to treat as # "no queue configured", because the failure it would otherwise # produce is a hive that comes up fine and silently never reports. - # The three-option version of that same rule is asserted at eval in - # ./../swarm.nix, so this can only ever emit a complete set. + # The rule that a secret needs the other two options is asserted at + # eval in ./../swarm.nix, so this can only ever emit a complete set. # - # ⚠️ The guard and the value beside it read different namespaces on - # purpose: where the queue is and where its secret sits are this - # machine's, the token endpoint is the swarm's one address. The - # assertion covers all three, which is what keeps the guard honest. - lib.optionalAttrs (config.services.hyperhive.deploy.hive-controller.statusPublish.natsUrl != null) { - HIVE_C0RE_NATS_URL = config.services.hyperhive.deploy.hive-controller.statusPublish.natsUrl; - HIVE_C0RE_OIDC_TOKEN_ENDPOINT = config.services.hyperhive.swarm.statusPublish.tokenEndpoint; - # The identity swarm-authelia.nix already declares for every entry in - # `swarm.hives` — the hive does not choose its own name here, it uses - # the one the roster gave it. - HIVE_C0RE_OIDC_CLIENT_ID = "hive-${config.services.hyperhive.hiveName}"; - # `%d` is systemd's credentials directory — see the LoadCredential in - # ./default.nix. The daemon reads a path, never a value. - HIVE_C0RE_OIDC_CLIENT_SECRET_FILE = "%d/swarm-status-client.secret"; - } + # ⚠️ Gated on the SECRET as well as the URL. The URL defaults to the + # queue's name on every hive, so on its own it says where the queue is, + # not that this hive publishes; the secret is that claim. Gated on the + # URL alone, a hive with no secret would be handed a secret path that + # `LoadCredential` in ./default.nix never fills. + lib.optionalAttrs + ( + config.services.hyperhive.deploy.hive-controller.statusPublish.natsUrl != null + && config.services.hyperhive.deploy.hive-controller.statusPublish.clientSecretFile != null + ) + { + HIVE_C0RE_NATS_URL = config.services.hyperhive.deploy.hive-controller.statusPublish.natsUrl; + HIVE_C0RE_OIDC_TOKEN_ENDPOINT = config.services.hyperhive.swarm.statusPublish.tokenEndpoint; + # The identity swarm-authelia.nix already declares for every entry in + # `swarm.hives` — the hive does not choose its own name here, it uses + # the one the roster gave it. + HIVE_C0RE_OIDC_CLIENT_ID = "hive-${config.services.hyperhive.hiveName}"; + # `%d` is systemd's credentials directory — see the LoadCredential in + # ./default.nix. The daemon reads a path, never a value. + HIVE_C0RE_OIDC_CLIENT_SECRET_FILE = "%d/swarm-status-client.secret"; + } // { # Where ../glue-queue-agent-credential.nix lands the AGENTS' queue # credential. Read by `hive_c0re::lifecycle::host_config`, which stats the diff --git a/nix/host-modules/local-defaults.nix b/nix/host-modules/local-defaults.nix index 0b71cf45..3c6d80e1 100644 --- a/nix/host-modules/local-defaults.nix +++ b/nix/host-modules/local-defaults.nix @@ -86,31 +86,13 @@ in config.services.hyperhive.deploy.nats.autoGenerateCallout = lib.mkDefault cfg.deploy.singleHostSwarm; + # ⚠️ Every `swarm.*` default this mode sets lives INSIDE this attrset, not + # as a second `config.services.hyperhive.swarm.…` path beside it — written + # that way the two definitions of `swarm` collide and the nested one is + # silently lost. The gate caught exactly that on the controller's queue URL, + # when this mode still set it: mode on, `natsUrl` still "". config.services.hyperhive.swarm = { ca.autoConfigure = lib.mkDefault cfg.deploy.singleHostSwarm; - # The controller's queue coordinates. Kept with the mode, not in the - # options' own `default`, because the controller's minted client secret - # only exists on the host authelia ran its first boot on — so they belong - # to the mode that asserts this box is the whole deployment. - # - # Deriving them from `deploy.nats` / `deploy.authelia` - # inside those defaults is the mixing this file exists to prevent: the - # option would be describing a deployment shape instead of describing - # itself, and "what does all-local turn on?" would stop having one - # answer. - # - # ⚠️ Must live INSIDE this attrset, not as a second - # `config.services.hyperhive.swarm.…` path beside it — written that - # way the two definitions of `swarm` collide and the nested one is - # silently lost. The gate caught exactly that: mode on, `natsUrl` - # still "". - # - # The *requirement* stays in `swarm-controller.nix` as an assertion: - # needing a queue is the controller's own property in every topology, - # and only the convenience is local. - controller.queue.natsUrl = lib.mkIf cfg.deploy.singleHostSwarm ( - lib.mkDefault "tls://${config.services.hyperhive.swarm.nats.domain}:${toString config.services.hyperhive.swarm.nats.port}" - ); }; # The controller is asserted by the MODE and by nothing else. Its own diff --git a/nix/host-modules/swarm-controller.nix b/nix/host-modules/swarm-controller.nix index 49e0c837..998dd0f5 100644 --- a/nix/host-modules/swarm-controller.nix +++ b/nix/host-modules/swarm-controller.nix @@ -17,6 +17,7 @@ let cfg = config.services.hyperhive.swarm.controller; deployCfg = config.services.hyperhive.deploy; autheliaCfg = config.services.hyperhive.swarm.authelia; + natsCfg = config.services.hyperhive.swarm.nats; # Where the secret store is, and whether this host holds the controller's # own leaf for it. ⚠️ The controller's pair, NOT `deploy.bao.clientCertFile` @@ -364,18 +365,18 @@ in queue = { natsUrl = lib.mkOption { type = lib.types.str; - default = ""; + default = "tls://${natsCfg.domain}:${toString natsCfg.port}"; + defaultText = lib.literalExpression ''"tls://''${swarm.nats.domain}:''${swarm.nats.port}"''; example = "tls://nats.example.com:4222"; description = '' Where the controller reaches the swarm queue. + Defaults to the queue's name, which is the same URL on every + host: the queue's own host resolves it locally, and any other + through the operator's DNS. + Empty means unset, which the assertion below refuses — a controller with no queue is not a lighter controller. - - `singleHostSwarm` fills this in with - `tls://:`. That derivation lives with the - mode rather than here, so this option describes itself rather than - a deployment shape. ''; }; @@ -764,8 +765,8 @@ in message = '' services.hyperhive.swarm.controller.queue.natsUrl is unset. - `singleHostSwarm` fills it in with the queue's name. A controller - in any other deployment has to be told the URL. + It defaults to the queue's name; something in this configuration + set it to "". Remove that setting, or set the URL. ''; } { diff --git a/nix/host-modules/swarm.nix b/nix/host-modules/swarm.nix index 220a5a41..3c91b3f5 100644 --- a/nix/host-modules/swarm.nix +++ b/nix/host-modules/swarm.nix @@ -115,16 +115,15 @@ let lib.filterAttrs (_: hive: hive.certFingerprint != null) swarmCfg.hives ); - # Whether the queue and its minted secret are on THIS host — the two - # coordinates that are a statement about this machine's disk and - # netns, from one condition so a partial set is unrepresentable rather - # than merely detected. + # Whether this hive's minted status secret is on THIS host, which is the + # one status-publishing coordinate that is a statement about this + # machine's disk. # - # ⚠️ The token endpoint is NOT one of them any more. It is the swarm's - # one address, derived from `swarm.authelia.url` like - # ./swarm-controller.nix's own `queue.tokenEndpoint` already is, so it - # is correct for a remote provider and does not ask where anything - # runs. + # ⚠️ Neither the token endpoint nor the queue's URL is gated on it. Both + # are the swarm's one address: the endpoint derived from + # `swarm.authelia.url`, the queue from its name, which resolves on every + # hive. So they are correct wherever the queue and the IdP run, and do + # not ask where anything runs. queueLocal = deployCfg.nats.enable && deployCfg.authelia.enable && cfg.hiveName != null; in { @@ -373,18 +372,16 @@ in } { # Deliberately an assertion and not a silent "then publish - # nothing": a half-set trio is a config an operator believes is - # working, and its runtime failure mode is the expensive one — - # the daemon comes up fine, never connects, and the hive reads - # `never_reported` on a dashboard nobody is watching yet. + # nothing": a secret without the coordinates it is presented at is + # a config an operator believes is working, and its runtime failure + # mode is the expensive one — the daemon comes up fine, never + # connects, and the hive reads `never_reported` on a dashboard + # nobody is watching yet. # - # Safe to add to an existing deployment: every `statusPublish` - # default is either all-local or all-null, so no config that - # evaluates today can be caught by this. It also encodes a - # property of the code rather than an intended shape — hive-c0re - # genuinely cannot publish with two of three coordinates — which - # is the distinction the `serviceDomains'` comment at the top of - # this file was written about. + # It encodes a property of the code rather than an intended shape — + # hive-c0re genuinely cannot publish with two of three coordinates — + # which is the distinction the `serviceDomains'` comment at the top + # of this file was written about. # # The three coordinates live in two namespaces now: the token # endpoint is the swarm's one address, the other two are this @@ -392,23 +389,21 @@ in # operator told only the option names would look for them under # one prefix and find one of them. # - # ⚠️ Asymmetric on purpose. A token endpoint on its own is the - # normal state of every hive in a swarm that has an IdP — it says - # where the IdP is, not that this hive publishes. Only the two - # per-host coordinates are a claim to be publishing, and they - # need the endpoint to mean anything. + # ⚠️ Asymmetric on purpose. The token endpoint and the queue's URL + # both default to the swarm's one address on every hive: they say + # where the IdP and the queue are, not that this hive publishes, so + # either without the secret is publishing off rather than a half-set + # trio. Only the client secret is a claim to be publishing, and it + # needs the other two to mean anything. assertion = - let - local = lib.filter (v: v != null) [ - deployCfg.hive-controller.statusPublish.natsUrl - deployCfg.hive-controller.statusPublish.clientSecretFile - ]; - in - builtins.length local == 0 - || (builtins.length local == 2 && swarmCfg.statusPublish.tokenEndpoint != null); + deployCfg.hive-controller.statusPublish.clientSecretFile == null + || ( + deployCfg.hive-controller.statusPublish.natsUrl != null + && swarmCfg.statusPublish.tokenEndpoint != null + ); message = '' - This hive's status-publishing coordinates have to be set - together or not at all — it has only some of them. + This hive has a status-publishing client secret but not the + coordinates to publish with it. Currently: deploy.hive-controller.statusPublish.natsUrl @@ -419,7 +414,7 @@ in = ${toString deployCfg.hive-controller.statusPublish.clientSecretFile} Set the missing ones to publish this hive's status to the - swarm, or set all three to null to turn publishing off. + swarm, or set clientSecretFile to null to turn publishing off. ''; } (nameGuards.mustNotEqual { @@ -543,21 +538,22 @@ in options.services.hyperhive.deploy.hive-controller.statusPublish = { natsUrl = lib.mkOption { type = lib.types.nullOr lib.types.str; - default = - if queueLocal then "tls://${swarmCfg.nats.domain}:${toString swarmCfg.nats.port}" else null; - defaultText = lib.literalExpression ''"tls://''${swarm.nats.domain}:''${swarm.nats.port}" when this host runs the queue and the IdP, else null''; + default = "tls://${swarmCfg.nats.domain}:${toString swarmCfg.nats.port}"; + defaultText = lib.literalExpression ''"tls://''${swarm.nats.domain}:''${swarm.nats.port}"''; example = "tls://nats.example.com:4222"; description = '' Where the swarm queue listens, as seen from *this* hive. - Defaults to the queue's name when this host runs the queue and the - IdP. A hive that is not the swarm host sets the same URL: the name - resolves through the operator's DNS there. TLS only, and by name, - since the queue's certificate carries the name and no address. + Defaults to the queue's name on every hive. The queue's own host + resolves it locally, and any other hive through the operator's + DNS. TLS only, and by name, since the queue's certificate carries + the name and no address. - Null disables status publishing: this hive computes its own - readiness as always, and simply offers it to nobody. The swarm - controller then reports it `never_reported`, which is the honest + A URL alone does not publish. + {option}`services.hyperhive.deploy.hive-controller.statusPublish.clientSecretFile` + is what turns publishing on. Without it this hive computes its own + readiness as always and offers it to nobody, and the swarm + controller reports it `never_reported`, which is the honest reading. ''; }; @@ -579,6 +575,9 @@ in readable, and one in the environment is readable by anything that can open {file}`/proc//environ`. + Setting it is what makes this hive publish its status; null + turns publishing off. + Defaults to authelia's own minted secret when the IdP runs on this host. On any other hive the secret has to get here somehow, and the swarm does not distribute it — copy it out of the swarm @@ -598,25 +597,24 @@ in # owns `queue.agentCredentialDir` in the same namespace. options.services.hyperhive.deploy.hive-controller.queue.agentNatsUrl = lib.mkOption { type = lib.types.nullOr lib.types.str; - default = - if queueLocal then "tls://${swarmCfg.nats.domain}:${toString swarmCfg.nats.port}" else null; - defaultText = lib.literalExpression ''"tls://''${swarm.nats.domain}:''${swarm.nats.port}" when this host runs the queue and the IdP, else null''; + default = "tls://${swarmCfg.nats.domain}:${toString swarmCfg.nats.port}"; + defaultText = lib.literalExpression ''"tls://''${swarm.nats.domain}:''${swarm.nats.port}"''; example = "tls://nats.example.com:4222"; description = '' Where the swarm queue listens, as an agent *container* on this host reaches it. - Defaults to the queue's name when this host runs the queue. The agent - resolves it through the bridge, where dnsmasq answers it with the - bridge address. ⚠️ Never a loopback address: inside an agent's network - namespace `127.0.0.1` is the agent, not this host, and the queue's - certificate names no address anyway. + Defaults to the queue's name on every hive. The agent resolves it + through the bridge: on the queue's own host dnsmasq answers it with + the bridge address, and elsewhere it forwards to the operator's DNS, + as for the store's name. ⚠️ Never a loopback address: inside an + agent's network namespace `127.0.0.1` is the agent, not this host, + and the queue's certificate names no address anyway. Null means this hive's agents have not been given the queue's address. Together with {option}`services.hyperhive.swarm.statusPublish.tokenEndpoint` it is - what decides whether the harness is handed queue coordinates at all; a - hive whose queue is elsewhere names the address its containers route to. + what decides whether the harness is handed queue coordinates at all. ''; }; diff --git a/nix/module-eval/core-toggle.nix b/nix/module-eval/core-toggle.nix index 2d4a9dad..44e6c122 100644 --- a/nix/module-eval/core-toggle.nix +++ b/nix/module-eval/core-toggle.nix @@ -42,6 +42,10 @@ let # renders the same absences `bare` does. centralToggleOff = hive { enable = false; }; + # Every hive defaults the agents' queue address to the queue's name, so the + # only hive without one is a hive told to have none. + noAgentQueue = hive { deploy.hive-controller.queue.agentNatsUrl = null; }; + withCi = hive { deploy.forgejo.ci.enable = true; }; # A priority collision is a property of the *option*, not @@ -149,13 +153,13 @@ let } { # The absence arm, and what makes the two above able to fail: a hive - # with no queue address must forward neither coordinate, because half a - # pair reaches the harness as a partial configuration rather than as - # none. - name = "a hive with no swarm queue forwards no agent queue coordinates"; + # with no queue address for its agents must forward neither coordinate, + # because half a pair reaches the harness as a partial configuration + # rather than as none. + name = "a hive with no agent queue address forwards no agent queue coordinates"; ok = let - e = bare.systemd.services.hive-c0re.environment; + e = noAgentQueue.systemd.services.hive-c0re.environment; in !(e ? HIVE_AGENT_NATS_URL) && !(e ? HIVE_AGENT_OIDC_TOKEN_ENDPOINT); } diff --git a/nix/module-eval/nats-tls.nix b/nix/module-eval/nats-tls.nix index 9e649bd9..b68ae667 100644 --- a/nix/module-eval/nats-tls.nix +++ b/nix/module-eval/nats-tls.nix @@ -47,6 +47,40 @@ let deploy.nats.autoGenerateCallout = true; }; + # A hive that is not the queue's host, with nothing about the queue's + # address set by hand: what every hive but one in a multi-host swarm looks + # like. The controller is on too, since it may run away from the queue. + # + # The two secrets are the ones a hive away from authelia already has to be + # handed, and neither is an address; without them this hive would fail + # assertions that have nothing to do with the queue. + remoteSecrets = { + deploy.forgejo.sso.clientSecretFile = "/var/lib/forgejo-oidc/by-hand.secret"; + deploy.swarm-controller.queue.clientSecretFile = "/var/lib/secrets/swarm-controller.secret"; + }; + remote = hive (lib.recursiveUpdate remoteSecrets { deploy.swarm-controller.enable = true; }); + + # The same hive given its status secret by hand, which is what turns + # publishing on away from the IdP's host. + remotePublishing = hive ( + lib.recursiveUpdate remoteSecrets { + deploy.hive-controller.statusPublish.clientSecretFile = "/var/lib/secrets/hive-h1.secret"; + } + ); + + # A real half-config: the secret, with the URL it would be presented at + # taken away. + remoteSecretNoUrl = hive ( + lib.recursiveUpdate remoteSecrets { + deploy.hive-controller.statusPublish.clientSecretFile = "/var/lib/secrets/hive-h1.secret"; + deploy.hive-controller.statusPublish.natsUrl = null; + } + ); + + failedAssertions = m: lib.filter (a: !a.assertion) m.assertions; + refusedStatusSecret = + m: lib.any (a: lib.hasInfix "status-publishing client secret" a.message) (failedAssertions m); + policyScript = allLocal.systemd.services.swarm-bao-nats-tls-policy.script; leafUnit = allLocal.systemd.services.swarm-bao-nats-tls; natsContainer = allLocal.containers.swarm-nats.config; @@ -78,6 +112,8 @@ let containerUnits = lib.concatLists ( lib.mapAttrsToList (c: cc: fromUnits c cc.config.systemd.services) machine.containers ); + # The responder runs beside the queue only, so a hive without one has + # none to read. responder = map (u: { @@ -85,7 +121,9 @@ let value = u; }) ( - urlsIn machine.containers.swarm-nats.config.systemd.services.swarm-nats-auth.serviceConfig.ExecStart + lib.optionals (machine.containers ? swarm-nats) ( + urlsIn machine.containers.swarm-nats.config.systemd.services.swarm-nats-auth.serviceConfig.ExecStart + ) ); in lib.listToAttrs (fromUnits "host" machine.systemd.services ++ containerUnits ++ responder); @@ -212,6 +250,65 @@ let !(lib.elem "swarm-bao-nats-tls-policy.service" u.after) && !(lib.elem "swarm-bao-nats-tls-policy.service" u.wants); } + { + # Every hive dials the queue by the same name, so a hive away from it + # needs no URL of its own. Control and property in one: the scan must + # reach the controller and the agents' address here, and every URL it + # finds, like the options it reads through, is the name. + name = "a hive that is not the queue's host dials tls://:4222 with nothing set"; + ok = + let + s = clientUrls remote; + d = remote.services.hyperhive.deploy; + in + !d.nats.enable + && s ? "host/hive-c0re/HIVE_AGENT_NATS_URL" + && s ? "host/swarm-controller/SWARM_CONTROLLER_NATS_URL" + && lib.all (u: u == natsUrl) (lib.attrValues s) + && d.hive-controller.statusPublish.natsUrl == natsUrl + && d.hive-controller.queue.agentNatsUrl == natsUrl + && remote.services.hyperhive.swarm.controller.queue.natsUrl == natsUrl; + } + { + # The URL is set and the secret is not, which is every such hive until + # an operator places one: publishing is off, not misconfigured. + name = "that hive evaluates without an assertion failure"; + ok = failedAssertions remote == [ ]; + } + { + # Off means off: hive-c0re is handed no status coordinates, rather + # than a secret path nothing fills. + name = "without its status secret, that hive's hive-c0re is given no status coordinates"; + ok = + let + s = remote.systemd.services.hive-c0re; + in + !(s.environment ? HIVE_C0RE_NATS_URL) + && !(s.environment ? HIVE_C0RE_OIDC_CLIENT_SECRET_FILE) + && !(lib.any (lib.hasPrefix "swarm-status-client.secret:") ( + lib.toList (s.serviceConfig.LoadCredential or [ ]) + )); + } + { + # The secret alone turns publishing on, at the default URL. + name = "given its status secret, that hive publishes to the queue's name"; + ok = + let + s = remotePublishing.systemd.services.hive-c0re; + in + failedAssertions remotePublishing == [ ] + && s.environment.HIVE_C0RE_NATS_URL == natsUrl + && s.environment.HIVE_C0RE_OIDC_CLIENT_SECRET_FILE == "%d/swarm-status-client.secret" + && lib.elem "swarm-status-client.secret:/var/lib/secrets/hive-h1.secret" ( + lib.toList s.serviceConfig.LoadCredential + ); + } + { + # What the assertion was written for is still refused: a secret with + # nowhere to present it. + name = "a status secret without a queue URL is refused at eval"; + ok = refusedStatusSecret remoteSecretNoUrl && !(refusedStatusSecret remote); + } ]; in runGroup "nats-tls" cases