nix: run swarm-ui wherever swarm-controller runs
Remove deploy.swarm-ui.enable. Every reader (the swarm-ui vhost, the authelia operator rule for the swarm apex, hive-network's anyServiceEnabled) now reads deploy.swarm-controller.enable, and the controller's webhook public URL is gated on the swarm domain alone, since the vhost it names exists wherever the daemon does. Both spellings of the old switch (deploy.swarm-ui.enable and the already-renamed swarm.ui.enable) become mkRemovedOptionModule entries, so a host that still sets either fails evaluation with a message naming the controller toggle instead of silently changing. Ruling (mara, #3742): "swarm-ui always runs wherever controller runs". Part of #3742
This commit is contained in:
parent
70c13aee7a
commit
f9954b91b4
10 changed files with 83 additions and 59 deletions
|
|
@ -12,7 +12,7 @@ You rarely switch it on yourself. `gateway.enable` defaults to off, and every mo
|
||||||
|
|
||||||
| URL | upstream | declared by, when |
|
| URL | upstream | declared by, when |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `<swarm>/` | swarm-ui dist (static), behind an authelia subrequest | `swarm-ui.nix`, `deploy.swarm-ui.enable` |
|
| `<swarm>/` | swarm-ui dist (static), behind an authelia subrequest | `swarm-ui.nix`, `deploy.swarm-controller.enable` |
|
||||||
| `auth.<swarm>/` | authelia (`9091`) | `swarm-authelia.nix`, `deploy.authelia.enable` |
|
| `auth.<swarm>/` | authelia (`9091`) | `swarm-authelia.nix`, `deploy.authelia.enable` |
|
||||||
| `auth.<swarm>/bridge/*` | swarm-authelia-bridge (`9092`), prefix stripped; answers swarm-controller's client only | `swarm-authelia.nix`, `deploy.authelia.enable` |
|
| `auth.<swarm>/bridge/*` | swarm-authelia-bridge (`9092`), prefix stripped; answers swarm-controller's client only | `swarm-authelia.nix`, `deploy.authelia.enable` |
|
||||||
| `forge.<swarm>/` | forgejo (`3000`) | `hive-forge/`, `deploy.forgejo.enable` |
|
| `forge.<swarm>/` | forgejo (`3000`) | `hive-forge/`, `deploy.forgejo.enable` |
|
||||||
|
|
|
||||||
|
|
@ -334,10 +334,10 @@ Where each credential lives and who reads it:
|
||||||
## Enabling
|
## Enabling
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
services.hyperhive.deploy.swarm-ui.enable = true; # defaults to deploy.swarm-controller.enable
|
services.hyperhive.deploy.swarm-controller.enable = true; # the UI comes with it
|
||||||
```
|
```
|
||||||
|
|
||||||
Derived from the controller rather than from `allSwarmServices`:
|
The UI has no switch of its own, and doesn't follow `allSwarmServices`:
|
||||||
the UI is a view onto the controller's state and reaches it over that
|
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
|
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.
|
can serve the UI. A hive that merely _uses_ a swarm has nothing to serve.
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,13 @@
|
||||||
# spread across the service modules, so all of them have a single home
|
# 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
|
# and a single file to delete when the deprecation window closes — the
|
||||||
# shape ./swarm-peers-removed.nix already uses.
|
# shape ./swarm-peers-removed.nix already uses.
|
||||||
{ lib, config, ... }:
|
{ lib, ... }:
|
||||||
let
|
let
|
||||||
deployCfg = config.services.hyperhive.deploy;
|
# Shared by the two removed spellings of the swarm UI's own switch below.
|
||||||
|
swarmUiSwitchRemoved = ''
|
||||||
|
swarm-ui now runs wherever swarm-controller runs. Remove this
|
||||||
|
definition; services.hyperhive.deploy.swarm-controller.enable decides it.
|
||||||
|
'';
|
||||||
|
|
||||||
# Shared by the two removed spellings of the status-publishing secret below.
|
# Shared by the two removed spellings of the status-publishing secret below.
|
||||||
statusSecretRemoved = ''
|
statusSecretRemoved = ''
|
||||||
|
|
@ -94,10 +98,20 @@ in
|
||||||
be deleted.
|
be deleted.
|
||||||
''
|
''
|
||||||
)
|
)
|
||||||
(lib.mkRenamedOptionModule
|
(lib.mkRemovedOptionModule [
|
||||||
[ "services" "hyperhive" "swarm" "ui" "enable" ]
|
"services"
|
||||||
[ "services" "hyperhive" "deploy" "swarm-ui" "enable" ]
|
"hyperhive"
|
||||||
)
|
"swarm"
|
||||||
|
"ui"
|
||||||
|
"enable"
|
||||||
|
] swarmUiSwitchRemoved)
|
||||||
|
(lib.mkRemovedOptionModule [
|
||||||
|
"services"
|
||||||
|
"hyperhive"
|
||||||
|
"deploy"
|
||||||
|
"swarm-ui"
|
||||||
|
"enable"
|
||||||
|
] swarmUiSwitchRemoved)
|
||||||
(lib.mkRenamedOptionModule
|
(lib.mkRenamedOptionModule
|
||||||
[ "services" "hyperhive" "swarm" "authelia" "enable" ]
|
[ "services" "hyperhive" "swarm" "authelia" "enable" ]
|
||||||
[ "services" "hyperhive" "deploy" "authelia" "enable" ]
|
[ "services" "hyperhive" "deploy" "authelia" "enable" ]
|
||||||
|
|
@ -436,9 +450,8 @@ in
|
||||||
[ "services" "hyperhive" "swarm" "victoriametrics" "package" ]
|
[ "services" "hyperhive" "swarm" "victoriametrics" "package" ]
|
||||||
[ "services" "hyperhive" "deploy" "victoriametrics" "package" ]
|
[ "services" "hyperhive" "deploy" "victoriametrics" "package" ]
|
||||||
)
|
)
|
||||||
# With this one `services.hyperhive.swarm.ui` holds nothing at all — its
|
# With this one `services.hyperhive.swarm.ui` holds nothing at all: its
|
||||||
# two options are both renamed, so the namespace exists only as this
|
# `enable` is removed above and its `package` is renamed here.
|
||||||
# entry and the `enable` one above it.
|
|
||||||
(lib.mkRenamedOptionModule
|
(lib.mkRenamedOptionModule
|
||||||
[ "services" "hyperhive" "swarm" "ui" "package" ]
|
[ "services" "hyperhive" "swarm" "ui" "package" ]
|
||||||
[ "services" "hyperhive" "deploy" "swarm-ui" "package" ]
|
[ "services" "hyperhive" "deploy" "swarm-ui" "package" ]
|
||||||
|
|
@ -688,23 +701,11 @@ in
|
||||||
{option}`services.hyperhive.deploy.hive-controller.enable`: a swarm
|
{option}`services.hyperhive.deploy.hive-controller.enable`: a swarm
|
||||||
has one controller, so running it is a decision about this host
|
has one controller, so running it is a decision about this host
|
||||||
rather than about whether it runs a hive.
|
rather than about whether it runs a hive.
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
swarm-ui.enable = lib.mkOption {
|
Also serves the swarm UI from this host. The UI is a view onto the
|
||||||
type = lib.types.bool;
|
controller's state and reaches it over that daemon's unix socket,
|
||||||
default = deployCfg.swarm-controller.enable;
|
so the host that runs the controller is the only host that can
|
||||||
defaultText = lib.literalExpression "services.hyperhive.deploy.swarm-controller.enable";
|
serve it.
|
||||||
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.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -623,7 +623,7 @@ in
|
||||||
# swarm's host was the missing one, and every swarm-controller
|
# swarm's host was the missing one, and every swarm-controller
|
||||||
# delivery was refused for it.
|
# delivery was refused for it.
|
||||||
#
|
#
|
||||||
# Not gated on `deploy.swarm-ui.enable`: in a spread deployment the
|
# Not gated on `deploy.swarm-controller.enable`: in a spread deployment the
|
||||||
# forge and the swarm host are different machines, so that
|
# forge and the swarm host are different machines, so that
|
||||||
# per-host flag is false exactly where this entry is needed.
|
# per-host flag is false exactly where this entry is needed.
|
||||||
webhook.ALLOWED_HOST_LIST = lib.concatStringsSep "," (
|
webhook.ALLOWED_HOST_LIST = lib.concatStringsSep "," (
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,6 @@ let
|
||||||
lib.any lib.id [
|
lib.any lib.id [
|
||||||
d.hive-controller.enable
|
d.hive-controller.enable
|
||||||
d.swarm-controller.enable
|
d.swarm-controller.enable
|
||||||
d.swarm-ui.enable
|
|
||||||
d.forgejo.enable
|
d.forgejo.enable
|
||||||
d.ci-runner.enable
|
d.ci-runner.enable
|
||||||
d.matrix.enable
|
d.matrix.enable
|
||||||
|
|
|
||||||
|
|
@ -1107,7 +1107,7 @@ in
|
||||||
[ metricsRule ]
|
[ metricsRule ]
|
||||||
# Also guarded on the domain being set: without it a null
|
# Also guarded on the domain being set: without it a null
|
||||||
# apex would render a rule matching the string "null".
|
# apex would render a rule matching the string "null".
|
||||||
++ lib.optional (deployCfg.swarm-ui.enable && swarmDomain != null) {
|
++ lib.optional (deployCfg.swarm-controller.enable && swarmDomain != null) {
|
||||||
domain = swarmDomain;
|
domain = swarmDomain;
|
||||||
subject = [ "group:${operatorGroup}" ];
|
subject = [ "group:${operatorGroup}" ];
|
||||||
policy = "one_factor";
|
policy = "one_factor";
|
||||||
|
|
|
||||||
|
|
@ -204,9 +204,9 @@ let
|
||||||
};
|
};
|
||||||
|
|
||||||
# How the forge must address this controller to deliver a swarm-wide
|
# How the forge must address this controller to deliver a swarm-wide
|
||||||
# webhook. Gated on the swarm UI being served *here*, because that module
|
# webhook. ./swarm-ui.nix declares the vhost and the `/webhook/forge/`
|
||||||
# is what declares the vhost and the `/webhook/forge/` location inside it:
|
# location inside it on every host that runs this daemon, so the only gate
|
||||||
# absent, nothing outside this host can reach the endpoint.
|
# left is a swarm domain to name it by.
|
||||||
#
|
#
|
||||||
# 🔑 The gate is the point, not a detail. The daemon registers hooks only
|
# 🔑 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
|
# when this is set, and a hook whose `target_url` nothing answers is worse
|
||||||
|
|
@ -216,17 +216,15 @@ let
|
||||||
# ⚠️ Correct ONLY while the vhost and this daemon share a host, and they do
|
# ⚠️ Correct ONLY while the vhost and this daemon share a host, and they do
|
||||||
# by construction: the UI's `/api/` location proxies
|
# by construction: the UI's `/api/` location proxies
|
||||||
# `http://unix:<socketPath>`, a path that resolves nowhere else, and this
|
# `http://unix:<socketPath>`, a path that resolves nowhere else, and this
|
||||||
# daemon binds no TCP address at all (see the header). `deploy.swarm-ui`
|
# daemon binds no TCP address at all (see the header).
|
||||||
# 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,
|
# ⇒ The day this daemon grows a TCP listener so another host can front it,
|
||||||
# THIS LINE SILENTLY STOPS BEING RIGHT: the env goes unset on the controller
|
# THIS LINE SILENTLY STOPS BEING RIGHT: hooks get registered against a vhost
|
||||||
# host, registration quietly does not happen, and nothing errors. That is
|
# that may no longer be the one fronting the daemon, and nothing errors. That is
|
||||||
# the moment to add an explicit `publicUrl` option — not before, because
|
# the moment to add an explicit `publicUrl` option — not before, because
|
||||||
# until then there is exactly one derivable answer and an option would only
|
# until then there is exactly one derivable answer and an option would only
|
||||||
# be a second place to get it wrong.
|
# be a second place to get it wrong.
|
||||||
webhookEnv = lib.optionalAttrs (deployCfg.swarm-ui.enable && swarmDomain != null) {
|
webhookEnv = lib.optionalAttrs (swarmDomain != null) {
|
||||||
SWARM_CONTROLLER_PUBLIC_URL = "https://${swarmDomain}";
|
SWARM_CONTROLLER_PUBLIC_URL = "https://${swarmDomain}";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -469,8 +467,8 @@ in
|
||||||
`services.hyperhive.swarm.bao.ui.domain`, and the Matrix entry
|
`services.hyperhive.swarm.bao.ui.domain`, and the Matrix entry
|
||||||
from `services.hyperhive.swarm.matrix.gatewayHost` when it is set,
|
from `services.hyperhive.swarm.matrix.gatewayHost` when it is set,
|
||||||
so they are present whichever host runs each service.
|
so they are present whichever host runs each service.
|
||||||
`swarm-ui.nix` contributes its own API-docs entry wherever the
|
`swarm-ui.nix` contributes its own API-docs entry, since the
|
||||||
swarm UI is enabled on this host.
|
swarm UI runs wherever the controller does.
|
||||||
|
|
||||||
Read only on the host that runs the controller.
|
Read only on the host that runs the controller.
|
||||||
'';
|
'';
|
||||||
|
|
|
||||||
|
|
@ -66,10 +66,10 @@ let
|
||||||
apex = if swarmCfg.domain == null then "swarm.invalid" else swarmCfg.domain;
|
apex = if swarmCfg.domain == null then "swarm.invalid" else swarmCfg.domain;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
# `enable` is `services.hyperhive.deploy.swarm-ui.enable` — see
|
# No `enable` of its own: the UI is on wherever
|
||||||
# ./deploy.nix, where it derives from the controller's own deploy
|
# `services.hyperhive.deploy.swarm-controller.enable` is, because it reaches
|
||||||
# toggle for the same reason. There is no `domain` option: the UI answers
|
# that daemon over its unix socket. There is no `domain` option: the UI
|
||||||
# on the swarm apex, which the swarm already declares once.
|
# answers on the swarm apex, which the swarm already declares once.
|
||||||
imports = [
|
imports = [
|
||||||
(lib.mkRemovedOptionModule [ "services" "hyperhive" "swarm" "ui" "domain" ] ''
|
(lib.mkRemovedOptionModule [ "services" "hyperhive" "swarm" "ui" "domain" ] ''
|
||||||
The swarm UI answers on services.hyperhive.swarm.domain and nothing
|
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 two options,
|
# ⚠️ `services.hyperhive.swarm.ui` is not a namespace. Its `package` lives
|
||||||
# `enable` and `package`, live under `deploy.swarm-ui`, and the `swarm.ui`
|
# under `deploy.swarm-ui`, and the `swarm.ui` paths survive purely as
|
||||||
# paths survive purely as renames in ./deploy.nix.
|
# rename/removal entries in ./deploy.nix.
|
||||||
options.services.hyperhive.deploy.swarm-ui.package = lib.mkOption {
|
options.services.hyperhive.deploy.swarm-ui.package = lib.mkOption {
|
||||||
type = lib.types.package;
|
type = lib.types.package;
|
||||||
defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarm-ui";
|
defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarm-ui";
|
||||||
|
|
@ -102,7 +102,7 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf deployCfg.swarm-ui.enable {
|
config = lib.mkIf deployCfg.swarm-controller.enable {
|
||||||
assertions = [
|
assertions = [
|
||||||
{
|
{
|
||||||
# The `_` default server already answers for the hive domain
|
# The `_` default server already answers for the hive domain
|
||||||
|
|
|
||||||
|
|
@ -631,11 +631,11 @@ let
|
||||||
ok = !(bare.containers ? swarm-otel) && !(centralToggleOff.containers ? swarm-otel);
|
ok = !(bare.containers ? swarm-otel) && !(centralToggleOff.containers ? swarm-otel);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
# `deploy.swarm-ui.enable`, which defaults to the controller's toggle —
|
# The swarm UI follows `deploy.swarm-controller.enable` — a sibling
|
||||||
# derived from a sibling deployment decision, still not from the hive
|
# deployment decision, not the hive toggle. `t.local` is the fixtures'
|
||||||
# toggle. `t.local` is the fixtures' swarm domain, which is the apex the
|
# swarm domain, which is the apex the UI claims; the arm below is what
|
||||||
# UI claims; the arm below is what proves this vhost renders at all.
|
# proves this vhost renders at all.
|
||||||
name = "the swarm UI vhost is absent unless this host is told to serve it";
|
name = "the swarm UI vhost is absent unless this host runs the swarm controller";
|
||||||
ok =
|
ok =
|
||||||
!(bare.services.nginx.virtualHosts ? "t.local")
|
!(bare.services.nginx.virtualHosts ? "t.local")
|
||||||
&& !(centralToggleOff.services.nginx.virtualHosts ? "t.local");
|
&& !(centralToggleOff.services.nginx.virtualHosts ? "t.local");
|
||||||
|
|
|
||||||
|
|
@ -70,13 +70,19 @@ let
|
||||||
network.exposeHostPorts = [ 5432 ];
|
network.exposeHostPorts = [ 5432 ];
|
||||||
};
|
};
|
||||||
|
|
||||||
# The swarm UI where its own toggle is on — the control the absence arm
|
# The swarm UI on the controller's host — the control the absence arm
|
||||||
# needs, since nothing else in this suite renders this vhost and an 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.
|
# 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.
|
# Package stubbed per this file's header: the vhost roots at it.
|
||||||
|
#
|
||||||
|
# Also sets both removed UI switches to `false`: the vhost must render
|
||||||
|
# regardless, and each must fail evaluation through its assertion. Neither
|
||||||
|
# is read anywhere else, so they change nothing the other arms see.
|
||||||
swarmUiHere = hive {
|
swarmUiHere = hive {
|
||||||
deploy.swarm-ui.enable = true;
|
deploy.swarm-controller.enable = true;
|
||||||
deploy.swarm-ui.package = pkgs.emptyDirectory;
|
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
|
# The swarm's shared services hosted HERE without the all-local mode — a
|
||||||
|
|
@ -224,7 +230,7 @@ let
|
||||||
name = "hosting the swarm's shared services does not make a hive the swarm's control plane";
|
name = "hosting the swarm's shared services does not make a hive the swarm's control plane";
|
||||||
ok =
|
ok =
|
||||||
!swarmServicesHere.services.hyperhive.deploy.swarm-controller.enable
|
!swarmServicesHere.services.hyperhive.deploy.swarm-controller.enable
|
||||||
&& !swarmServicesHere.services.hyperhive.deploy.swarm-ui.enable;
|
&& !(swarmServicesHere.services.nginx.virtualHosts ? "t.local");
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
# Control for the arm above, and the one place this slice is not inert:
|
# Control for the arm above, and the one place this slice is not inert:
|
||||||
|
|
@ -235,9 +241,29 @@ let
|
||||||
ok = builtins.elem 5432 (bridgePortsOrNone exposedPortsNoHive);
|
ok = builtins.elem 5432 (bridgePortsOrNone exposedPortsNoHive);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
name = "the swarm UI claims the swarm apex where this host serves it";
|
name = "the swarm UI claims the swarm apex wherever this host runs the swarm controller";
|
||||||
ok = swarmUiHere.services.nginx.virtualHosts ? "t.local";
|
ok = swarmUiHere.services.nginx.virtualHosts ? "t.local";
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
# Control: `swarmServicesHere` sets neither removed switch.
|
||||||
|
name = "a definition of a removed swarm UI switch fails evaluation, naming the option and the controller";
|
||||||
|
ok =
|
||||||
|
let
|
||||||
|
refused =
|
||||||
|
m: path:
|
||||||
|
lib.any (
|
||||||
|
a:
|
||||||
|
!a.assertion
|
||||||
|
&& lib.hasInfix "services.hyperhive.${path}" a.message
|
||||||
|
&& lib.hasInfix "swarm-ui now runs wherever swarm-controller runs" a.message
|
||||||
|
) m.assertions;
|
||||||
|
paths = [
|
||||||
|
"deploy.swarm-ui.enable"
|
||||||
|
"swarm.ui.enable"
|
||||||
|
];
|
||||||
|
in
|
||||||
|
lib.all (refused swarmUiHere) paths && !(lib.any (refused swarmServicesHere) paths);
|
||||||
|
}
|
||||||
{
|
{
|
||||||
# The swarm-services toggle enables them explicitly, from its own
|
# The swarm-services toggle enables them explicitly, from its own
|
||||||
# module rather than from any of their defaults.
|
# module rather than from any of their defaults.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue