diff --git a/docs/networking/gateway.md b/docs/networking/gateway.md index 5a20473f..c87c0320 100644 --- a/docs/networking/gateway.md +++ b/docs/networking/gateway.md @@ -12,7 +12,7 @@ You rarely switch it on yourself. `gateway.enable` defaults to off, and every mo | URL | upstream | declared by, when | | --- | --- | --- | -| `/` | swarm-ui dist (static), behind an authelia subrequest | `swarm-ui.nix`, `deploy.swarm-controller.enable` | +| `/` | swarm-ui dist (static), behind an authelia subrequest | `swarm-ui.nix`, `deploy.swarm-ui.enable` | | `auth./` | authelia (`9091`) | `swarm-authelia.nix`, `deploy.authelia.enable` | | `auth./bridge/*` | swarm-authelia-bridge (`9092`), prefix stripped; answers swarm-controller's client only | `swarm-authelia.nix`, `deploy.authelia.enable` | | `forge./` | forgejo (`3000`) | `hive-forge/`, `deploy.forgejo.enable` | diff --git a/docs/swarm/ui.md b/docs/swarm/ui.md index 06cd4ad4..fac28025 100644 --- a/docs/swarm/ui.md +++ b/docs/swarm/ui.md @@ -334,11 +334,10 @@ Where each credential lives and who reads it: ## Enabling ```nix -services.hyperhive.deploy.swarm-controller.enable = true; # the UI comes with it +services.hyperhive.deploy.swarm-ui.enable = true; # defaults to deploy.swarm-controller.enable ``` -The UI has no switch of its own (setting `deploy.swarm-ui.enable` or -`swarm.ui.enable` has no effect, and warns at evaluation), and doesn't follow `allSwarmServices`: +Derived from the controller rather than from `allSwarmServices`: the UI is a view onto the controller's state and reaches it over that daemon's socket, so the host that runs the controller is the host that can serve the UI. A hive that merely _uses_ a swarm has nothing to serve. diff --git a/nix/host-modules/deploy.nix b/nix/host-modules/deploy.nix index 0b9b460b..972c5079 100644 --- a/nix/host-modules/deploy.nix +++ b/nix/host-modules/deploy.nix @@ -28,23 +28,9 @@ # spread across the service modules, so all of them have a single home # and a single file to delete when the deprecation window closes — the # shape ./swarm-peers-removed.nix already uses. -{ config, lib, ... }: +{ lib, config, ... }: let - # The swarm UI's own switch, in both spellings. Accepted and ignored: a - # definition only produces a warning, so a host that still sets one keeps - # evaluating. `null` is "not set". - swarmUiSwitchIgnored = lib.mkOption { - type = lib.types.nullOr lib.types.bool; - default = null; - visible = false; - description = '' - No effect. swarm-ui runs wherever swarm-controller runs. - ''; - }; - swarmUiSwitchWarning = - path: value: - lib.optional (value != null) - "services.hyperhive.${path} no longer has any effect: swarm-ui now runs wherever swarm-controller runs. Remove this setting; services.hyperhive.deploy.swarm-controller.enable decides it."; + deployCfg = config.services.hyperhive.deploy; # Shared by the two removed spellings of the status-publishing secret below. statusSecretRemoved = '' @@ -108,6 +94,10 @@ in be deleted. '' ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "swarm" "ui" "enable" ] + [ "services" "hyperhive" "deploy" "swarm-ui" "enable" ] + ) (lib.mkRenamedOptionModule [ "services" "hyperhive" "swarm" "authelia" "enable" ] [ "services" "hyperhive" "deploy" "authelia" "enable" ] @@ -446,8 +436,9 @@ in [ "services" "hyperhive" "swarm" "victoriametrics" "package" ] [ "services" "hyperhive" "deploy" "victoriametrics" "package" ] ) - # With this one `services.hyperhive.swarm.ui` holds nothing at all: its - # `enable` is an ignored switch (declared below) and its `package` is renamed here. + # With this one `services.hyperhive.swarm.ui` holds nothing at all — its + # two options are both renamed, so the namespace exists only as this + # entry and the `enable` one above it. (lib.mkRenamedOptionModule [ "services" "hyperhive" "swarm" "ui" "package" ] [ "services" "hyperhive" "deploy" "swarm-ui" "package" ] @@ -486,14 +477,7 @@ in # ⚠️ Only `deploy.forgejo.enable` is declared here. The rest of # `deploy.forgejo` is in ./hive-forge/default.nix: the knobs live with the # module that reads them; this file stays the registry of toggles. - options.services.hyperhive.swarm.ui.enable = swarmUiSwitchIgnored; - - config.warnings = - swarmUiSwitchWarning "deploy.swarm-ui.enable" config.services.hyperhive.deploy.swarm-ui.enable - ++ swarmUiSwitchWarning "swarm.ui.enable" config.services.hyperhive.swarm.ui.enable; - options.services.hyperhive.deploy = { - swarm-ui.enable = swarmUiSwitchIgnored; forgejo.enable = lib.mkOption { type = lib.types.bool; default = false; @@ -704,11 +688,23 @@ in {option}`services.hyperhive.deploy.hive-controller.enable`: a swarm has one controller, so running it is a decision about this host rather than about whether it runs a hive. + ''; + }; - Also serves the swarm UI from this host. The UI is a view onto the - controller's state and reaches it over that daemon's unix socket, - so the host that runs the controller is the only host that can - serve it. + swarm-ui.enable = lib.mkOption { + type = lib.types.bool; + default = deployCfg.swarm-controller.enable; + defaultText = lib.literalExpression "services.hyperhive.deploy.swarm-controller.enable"; + example = true; + description = '' + Serve the swarm UI from this host. + + Derived from + {option}`services.hyperhive.deploy.swarm-controller.enable`: the + UI is a view onto the controller's state and reaches it over that + daemon's unix socket, so the host that runs the controller is the + host that can serve the UI. A hive that merely *uses* a swarm has + nothing to serve here. ''; }; }; diff --git a/nix/host-modules/hive-forge/default.nix b/nix/host-modules/hive-forge/default.nix index 684f5415..206093a6 100644 --- a/nix/host-modules/hive-forge/default.nix +++ b/nix/host-modules/hive-forge/default.nix @@ -623,7 +623,7 @@ in # swarm's host was the missing one, and every swarm-controller # delivery was refused for it. # - # Not gated on `deploy.swarm-controller.enable`: in a spread deployment the + # Not gated on `deploy.swarm-ui.enable`: in a spread deployment the # forge and the swarm host are different machines, so that # per-host flag is false exactly where this entry is needed. webhook.ALLOWED_HOST_LIST = lib.concatStringsSep "," ( diff --git a/nix/host-modules/hive-network.nix b/nix/host-modules/hive-network.nix index 1cc02e37..f5f42072 100644 --- a/nix/host-modules/hive-network.nix +++ b/nix/host-modules/hive-network.nix @@ -40,6 +40,7 @@ let lib.any lib.id [ d.hive-controller.enable d.swarm-controller.enable + d.swarm-ui.enable d.forgejo.enable d.ci-runner.enable d.matrix.enable diff --git a/nix/host-modules/swarm-authelia.nix b/nix/host-modules/swarm-authelia.nix index 014bc10f..2c465c9b 100644 --- a/nix/host-modules/swarm-authelia.nix +++ b/nix/host-modules/swarm-authelia.nix @@ -1107,7 +1107,7 @@ in [ metricsRule ] # Also guarded on the domain being set: without it a null # apex would render a rule matching the string "null". - ++ lib.optional (deployCfg.swarm-controller.enable && swarmDomain != null) { + ++ lib.optional (deployCfg.swarm-ui.enable && swarmDomain != null) { domain = swarmDomain; subject = [ "group:${operatorGroup}" ]; policy = "one_factor"; diff --git a/nix/host-modules/swarm-controller.nix b/nix/host-modules/swarm-controller.nix index f1e2d1de..ec5e8f82 100644 --- a/nix/host-modules/swarm-controller.nix +++ b/nix/host-modules/swarm-controller.nix @@ -204,9 +204,9 @@ let }; # How the forge must address this controller to deliver a swarm-wide - # webhook. ./swarm-ui.nix declares the vhost and the `/webhook/forge/` - # location inside it on every host that runs this daemon, so the only gate - # left is a swarm domain to name it by. + # webhook. Gated on the swarm UI being served *here*, because that module + # is what declares the vhost and the `/webhook/forge/` location inside it: + # absent, nothing outside this host can reach the endpoint. # # 🔑 The gate is the point, not a detail. The daemon registers hooks only # when this is set, and a hook whose `target_url` nothing answers is worse @@ -216,15 +216,17 @@ let # ⚠️ Correct ONLY while the vhost and this daemon share a host, and they do # by construction: the UI's `/api/` location proxies # `http://unix:`, a path that resolves nowhere else, and this - # daemon binds no TCP address at all (see the header). + # daemon binds no TCP address at all (see the header). `deploy.swarm-ui` + # therefore is not a guess about *some* host publishing the endpoint — it is + # the flag that declares that vhost, on the box holding the socket. # # ⇒ The day this daemon grows a TCP listener so another host can front it, - # THIS LINE SILENTLY STOPS BEING RIGHT: hooks get registered against a vhost - # that may no longer be the one fronting the daemon, and nothing errors. That is + # THIS LINE SILENTLY STOPS BEING RIGHT: the env goes unset on the controller + # host, registration quietly does not happen, and nothing errors. That is # the moment to add an explicit `publicUrl` option — not before, because # until then there is exactly one derivable answer and an option would only # be a second place to get it wrong. - webhookEnv = lib.optionalAttrs (swarmDomain != null) { + webhookEnv = lib.optionalAttrs (deployCfg.swarm-ui.enable && swarmDomain != null) { SWARM_CONTROLLER_PUBLIC_URL = "https://${swarmDomain}"; }; @@ -467,8 +469,8 @@ in `services.hyperhive.swarm.bao.ui.domain`, and the Matrix entry from `services.hyperhive.swarm.matrix.gatewayHost` when it is set, so they are present whichever host runs each service. - `swarm-ui.nix` contributes its own API-docs entry, since the - swarm UI runs wherever the controller does. + `swarm-ui.nix` contributes its own API-docs entry wherever the + swarm UI is enabled on this host. Read only on the host that runs the controller. ''; diff --git a/nix/host-modules/swarm-ui.nix b/nix/host-modules/swarm-ui.nix index 998286f7..7cba8a8d 100644 --- a/nix/host-modules/swarm-ui.nix +++ b/nix/host-modules/swarm-ui.nix @@ -66,10 +66,10 @@ let apex = if swarmCfg.domain == null then "swarm.invalid" else swarmCfg.domain; in { - # No `enable` of its own: the UI is on wherever - # `services.hyperhive.deploy.swarm-controller.enable` is, because it reaches - # that daemon over its unix socket. There is no `domain` option: the UI - # answers on the swarm apex, which the swarm already declares once. + # `enable` is `services.hyperhive.deploy.swarm-ui.enable` — see + # ./deploy.nix, where it derives from the controller's own deploy + # toggle for the same reason. There is no `domain` option: the UI answers + # on the swarm apex, which the swarm already declares once. imports = [ (lib.mkRemovedOptionModule [ "services" "hyperhive" "swarm" "ui" "domain" ] '' The swarm UI answers on services.hyperhive.swarm.domain and nothing @@ -83,9 +83,9 @@ in '') ]; - # ⚠️ `services.hyperhive.swarm.ui` is not a namespace. Its `package` lives - # under `deploy.swarm-ui`, and the `swarm.ui` paths survive purely as - # rename/removal entries in ./deploy.nix. + # ⚠️ `services.hyperhive.swarm.ui` is not a namespace. Its two options, + # `enable` and `package`, live under `deploy.swarm-ui`, and the `swarm.ui` + # 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"; @@ -102,7 +102,7 @@ in ''; }; - config = lib.mkIf deployCfg.swarm-controller.enable { + config = lib.mkIf deployCfg.swarm-ui.enable { assertions = [ { # The `_` default server already answers for the hive domain diff --git a/nix/module-eval/core-toggle.nix b/nix/module-eval/core-toggle.nix index 09d3bdf1..1a030fac 100644 --- a/nix/module-eval/core-toggle.nix +++ b/nix/module-eval/core-toggle.nix @@ -631,11 +631,11 @@ let ok = !(bare.containers ? swarm-otel) && !(centralToggleOff.containers ? swarm-otel); } { - # The swarm UI follows `deploy.swarm-controller.enable` — a sibling - # deployment decision, not the hive toggle. `t.local` is the fixtures' - # swarm domain, which is the apex the UI claims; the arm below is what - # proves this vhost renders at all. - name = "the swarm UI vhost is absent unless this host runs the swarm controller"; + # `deploy.swarm-ui.enable`, which defaults to the controller's toggle — + # derived from a sibling deployment decision, still not from the hive + # toggle. `t.local` is the fixtures' swarm domain, which is the apex the + # UI claims; the arm below is what proves this vhost renders at all. + name = "the swarm UI vhost is absent unless this host is told to serve it"; ok = !(bare.services.nginx.virtualHosts ? "t.local") && !(centralToggleOff.services.nginx.virtualHosts ? "t.local"); diff --git a/nix/module-eval/swarm-services-switch.nix b/nix/module-eval/swarm-services-switch.nix index 96182176..1b9d9b21 100644 --- a/nix/module-eval/swarm-services-switch.nix +++ b/nix/module-eval/swarm-services-switch.nix @@ -70,19 +70,13 @@ let network.exposeHostPorts = [ 5432 ]; }; - # The swarm UI on the controller's host — the control the absence arm + # The swarm UI where its own toggle is on — the control the absence arm # needs, since nothing else in this suite renders this vhost and an arm # saying "it is not there" would hold just as well if it were never there. # Package stubbed per this file's header: the vhost roots at it. - # - # Also sets both ignored UI switches to `false`: the vhost must render - # regardless, and each must evaluate and warn. Neither - # is read anywhere else, so they change nothing the other arms see. swarmUiHere = hive { - deploy.swarm-controller.enable = true; + deploy.swarm-ui.enable = true; deploy.swarm-ui.package = pkgs.emptyDirectory; - deploy.swarm-ui.enable = false; - swarm.ui.enable = false; }; # The swarm's shared services hosted HERE without the all-local mode — a @@ -230,7 +224,7 @@ let name = "hosting the swarm's shared services does not make a hive the swarm's control plane"; ok = !swarmServicesHere.services.hyperhive.deploy.swarm-controller.enable - && !(swarmServicesHere.services.nginx.virtualHosts ? "t.local"); + && !swarmServicesHere.services.hyperhive.deploy.swarm-ui.enable; } { # Control for the arm above, and the one place this slice is not inert: @@ -241,28 +235,9 @@ let ok = builtins.elem 5432 (bridgePortsOrNone exposedPortsNoHive); } { - name = "the swarm UI claims the swarm apex wherever this host runs the swarm controller"; + name = "the swarm UI claims the swarm apex where this host serves it"; ok = swarmUiHere.services.nginx.virtualHosts ? "t.local"; } - { - # Control: `swarmServicesHere` sets neither ignored switch. - name = "a definition of an ignored swarm UI switch evaluates and warns, naming the option and the controller"; - ok = - let - warned = - m: path: - lib.any ( - w: - lib.hasInfix "services.hyperhive.${path} no longer has any effect" w - && lib.hasInfix "swarm-ui now runs wherever swarm-controller runs" w - ) m.warnings; - paths = [ - "deploy.swarm-ui.enable" - "swarm.ui.enable" - ]; - in - lib.all (warned swarmUiHere) paths && !(lib.any (warned swarmServicesHere) paths); - } { # The swarm-services toggle enables them explicitly, from its own # module rather than from any of their defaults.