diff --git a/nix/host-modules/deploy.nix b/nix/host-modules/deploy.nix index bf2e6cdb..a58de18f 100644 --- a/nix/host-modules/deploy.nix +++ b/nix/host-modules/deploy.nix @@ -57,6 +57,10 @@ in [ "services" "hyperhive" "swarm" "authelia" "enable" ] [ "services" "hyperhive" "deploy" "authelia" ] ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "swarm" "nats" "enable" ] + [ "services" "hyperhive" "deploy" "nats" ] + ) ]; options.services.hyperhive.deploy = { @@ -119,6 +123,19 @@ in ''; }; + nats = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Run the swarm's message queue in a `swarm-nats` container on this + host. A swarm has one queue, so this belongs on the same host as + the rest of the shared services. + + Off by default, and off means *absent*: no container is created + and nothing else in the evaluated config changes. + ''; + }; + controller = lib.mkOption { type = lib.types.bool; default = false; diff --git a/nix/host-modules/local-defaults.nix b/nix/host-modules/local-defaults.nix index 4cff68d3..8f8a927c 100644 --- a/nix/host-modules/local-defaults.nix +++ b/nix/host-modules/local-defaults.nix @@ -95,7 +95,7 @@ in # ran its first boot on — so they belong to the mode that asserts this # box is the whole deployment, not to the options' own `default`. # - # Deriving them from `swarm.nats.enable` / `deploy.authelia` + # 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 diff --git a/nix/host-modules/swarm-authelia.nix b/nix/host-modules/swarm-authelia.nix index 53aea22e..a9ddf5b9 100644 --- a/nix/host-modules/swarm-authelia.nix +++ b/nix/host-modules/swarm-authelia.nix @@ -467,8 +467,8 @@ in oidc.hiveIdentities = lib.mkOption { type = lib.types.bool; - default = hyperhiveCfg.swarm.nats.enable; - defaultText = lib.literalExpression "services.hyperhive.swarm.nats.enable"; + default = deployCfg.nats; + defaultText = lib.literalExpression "services.hyperhive.deploy.nats"; description = '' Mint one machine client per hive in {option}`services.hyperhive.swarm.hives`, so each hive can diff --git a/nix/host-modules/swarm-controller.nix b/nix/host-modules/swarm-controller.nix index 04840c5e..4c61eb9c 100644 --- a/nix/host-modules/swarm-controller.nix +++ b/nix/host-modules/swarm-controller.nix @@ -539,7 +539,7 @@ in services.hyperhive.swarm.controller.queue.natsUrl is unset. It defaults to loopback only when this host also runs the queue - (`services.hyperhive.swarm.nats.enable`). A controller on its + (`services.hyperhive.deploy.nats`). A controller on its own host has to be told where the queue is. ''; } diff --git a/nix/host-modules/swarm-nats.nix b/nix/host-modules/swarm-nats.nix index aed7c984..973f191f 100644 --- a/nix/host-modules/swarm-nats.nix +++ b/nix/host-modules/swarm-nats.nix @@ -49,7 +49,7 @@ let hostPath = name: "${secretDir}/${name}"; # The responder needs all three credentials. Gating on them rather than - # on `cfg.enable` keeps a half-configured hive at "queue up, denying + # on `deployCfg.nats` keeps a half-configured hive at "queue up, denying # everyone" instead of "unit crash-looping on a missing file". # # In auto mode the seeds are minted on this host before the container @@ -210,18 +210,9 @@ in # per-service vhost pattern do not apply. options.services.hyperhive.swarm.nats = { - enable = lib.mkOption { - type = lib.types.bool; - default = false; - description = '' - Run the swarm's message queue in a `swarm-nats` container on this - host. A swarm has one queue, so this belongs on the same host as - the rest of the shared services. - - Off by default, and off means *absent*: no container is created - and nothing else in the evaluated config changes. - ''; - }; + # `enable` moved to `services.hyperhive.deploy.nats` — see + # ./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 @@ -408,7 +399,7 @@ in }; }; - config = lib.mkIf cfg.enable { + config = lib.mkIf deployCfg.nats { # The responder as well as the server: a denial reaches the client as a # timeout, so the server's own log is the only place it is an error. services.hyperhive.swarm.otel.journaldUnits = [ @@ -423,7 +414,7 @@ in # several layers from "the operator never set the issuer". assertion = cfg.autoGenerateCallout || cfg.calloutIssuerPublicKey != ""; message = '' - services.hyperhive.swarm.nats.enable requires + services.hyperhive.deploy.nats requires nats.calloutIssuerPublicKey — the public half of the account nkey that signs user JWTs for this queue. @@ -440,7 +431,7 @@ in # somebody connects. assertion = cfg.autoGenerateCallout || cfg.calloutUserPublicKey != ""; message = '' - services.hyperhive.swarm.nats.enable requires + services.hyperhive.deploy.nats requires nats.calloutUserPublicKey — the public half of the user nkey the auth-callout responder authenticates as. @@ -485,7 +476,7 @@ in { assertion = autheliaUrl != null; message = '' - services.hyperhive.swarm.nats.enable requires + services.hyperhive.deploy.nats requires services.hyperhive.swarm.authelia.url — the queue authenticates clients by validating tokens that authelia issued. @@ -684,7 +675,7 @@ in # door nobody can open, which is the deliberate interim state. # # ⚠️ It is gated on the seeds being configured rather than on - # `cfg.enable`, so a half-configured hive gets a running, + # `deployCfg.nats`, so a half-configured hive gets a running, # refusing queue instead of a unit that crash-loops on a # missing file. A queue that denies everyone is a legible # failure; a restart loop is not. diff --git a/nix/host-modules/swarm-required-services.nix b/nix/host-modules/swarm-required-services.nix index 6ad0d4e7..545e4981 100644 --- a/nix/host-modules/swarm-required-services.nix +++ b/nix/host-modules/swarm-required-services.nix @@ -60,7 +60,6 @@ in # derives `nats.autoGenerateCallout` from the all-local mode, so that # mode was minting the queue's callout nkeys and then never starting # the queue they authenticate against. - nats.enable = lib.mkDefault swarmCfg.enableRequiredServices; # The metrics pair and the log store used to derive here too. They now # live under `deploy.*` (below, and ./deploy.nix) because "does THIS @@ -89,6 +88,9 @@ in # at whoever runs it. config.services.hyperhive.deploy.authelia = lib.mkDefault swarmCfg.enableRequiredServices; + # The queue. Same rule: once per swarm, optional. + config.services.hyperhive.deploy.nats = lib.mkDefault swarmCfg.enableRequiredServices; + # The metrics pair, deriving together on purpose: a store with no UI is # unreadable and a UI with no store is empty, so there is no sensible # deployment that takes one and not the other from this switch. An diff --git a/nix/host-modules/swarm.nix b/nix/host-modules/swarm.nix index 5682ac8b..5e8d6a5b 100644 --- a/nix/host-modules/swarm.nix +++ b/nix/host-modules/swarm.nix @@ -80,18 +80,23 @@ let # ONE condition for all three of them, deliberately. # # 🩸 They were three independent conditions first, and that was wrong in - # a way only an eval gate finds: `enableRequiredServices` turns on - # matrix and authelia but NOT nats (nats has no mode that enables it — - # see the auto-deploy question on the swarm-queue issue), so an ordinary - # all-local hive resolved authelia's two coordinates and not the queue - # URL. Two of three set is exactly what the assertion below rejects, so - # every `enableAllLocalDefaults` hive would have stopped evaluating. + # a way only an eval gate finds: at the time `enableRequiredServices` + # turned on matrix and authelia but NOT nats, so an ordinary all-local + # hive resolved authelia's two coordinates and not the queue URL. Two of + # three set is exactly what the assertion below rejects, so every + # `enableAllLocalDefaults` hive would have stopped evaluating. + # + # (The queue does derive from that switch now — ./swarm-required-services.nix + # — so the original asymmetry is gone. The reasoning below is why the + # single predicate stays anyway: it makes the partial state + # unrepresentable rather than merely detected, which does not depend on + # which services happen to derive together this month.) # # Deriving all three from one predicate makes the partial state # unrepresentable rather than merely detected: a default set is all or # nothing, and the assertion is then only ever about what an operator # typed. - queueLocal = swarmCfg.nats.enable && deployCfg.authelia && cfg.hiveName != null; + queueLocal = deployCfg.nats && deployCfg.authelia && cfg.hiveName != null; in { options.services.hyperhive.swarm.hives = lib.mkOption {