deploy: give every option an enable, and name the controller one

Two corrections from review, applied forward on this branch rather than
by rewriting it.

`deploy.<service>` was a bare bool, which makes
`deploy.forgejo = { enable; ci; }` unrepresentable -- the nested
CI-runner sub-option this namespace was designed around. Every entry is
now an attrset with an `enable`, so a second per-host deployment
decision becomes an ordinary addition rather than a migration.

`deploy.controller` is now `deploy.swarm-controller`, consistent with
`deploy.swarm-ui`, which was introduced in the same commit.

89 references rewritten across 24 files -- nix, Rust, docs, and the
repo's own CLAUDE.md.

The prefix-anchored sweep missed exactly one, and it was live code:
hive-tls.nix spells it `hyperhiveCfg.deploy.controller` -- the only
`hyperhiveCfg` prefix among 45 references. A suffix grep
(`\.deploy\.<name>`) finds it; a path-anchored one cannot, because the
head of a reference is whatever alias the reading file happens to bind.
This commit is contained in:
atlas 2026-08-30 04:05:28 +02:00 committed by mara
commit d3b40da1c8
24 changed files with 137 additions and 121 deletions

View file

@ -142,7 +142,7 @@ hand-maintained per-file tree drifts out of sync with the code.
- **`hive-metric/`** — small CLI to push a single labeled metric to the - **`hive-metric/`** — small CLI to push a single labeled metric to the
OTEL collector via the OpenTelemetry Rust SDK / OTLP HTTP exporter. OTEL collector via the OpenTelemetry Rust SDK / OTLP HTTP exporter.
- **`swarm-controller/`** — swarm-level daemon, opt-in per host - **`swarm-controller/`** — swarm-level daemon, opt-in per host
(`services.hyperhive.deploy.controller`). Where `hive-c0re` owns (`services.hyperhive.deploy.swarm-controller.enable`). Where `hive-c0re` owns
the agents on **one** host, this owns what is true **across** hives; a the agents on **one** host, this owns what is true **across** hives; a
swarm runs one of them, so most hives leave it off. Serves HTTP over a swarm runs one of them, so most hives leave it off. Serves HTTP over a
unix socket the gateway's nginx proxies to — ⚠️ **the socket's unix socket the gateway's nginx proxies to — ⚠️ **the socket's

View file

@ -472,7 +472,7 @@ snapshot contains and how the store authenticates a sender.
## `/var/lib/swarm-controller/` (swarm-controller host only) ## `/var/lib/swarm-controller/` (swarm-controller host only)
Only present on the one host running Only present on the one host running
`services.hyperhive.deploy.controller`. systemd `StateDirectory=`, `services.hyperhive.deploy.swarm-controller.enable`. systemd `StateDirectory=`,
so it survives restarts and redeploys. so it survives restarts and redeploys.
- `webhook-secret` — the HMAC key the swarm's forge webhooks are signed - `webhook-secret` — the HMAC key the swarm's forge webhooks are signed

View file

@ -276,7 +276,7 @@ migrating agent keeps one unbroken incremental chain. See
## Swarm controller ## Swarm controller
`services.hyperhive.deploy.controller` runs the `swarm-controller` `services.hyperhive.deploy.swarm-controller.enable` runs the `swarm-controller`
daemon on this host. **Off by default and deliberately not derived from daemon on this host. **Off by default and deliberately not derived from
`services.hyperhive.enable`**: a swarm has one controller, so enabling it `services.hyperhive.enable`**: a swarm has one controller, so enabling it
is a statement about swarm topology, not about whether hyperhive is is a statement about swarm topology, not about whether hyperhive is

View file

@ -74,8 +74,8 @@ together: a store with no UI is unreadable and a UI with no store is
empty. To run one without the other, set it directly: empty. To run one without the other, set it directly:
```nix ```nix
services.hyperhive.deploy.victoriametrics = true; services.hyperhive.deploy.victoriametrics.enable = true;
services.hyperhive.deploy.grafana = false; services.hyperhive.deploy.grafana.enable = false;
``` ```
⚠️ **This starts a database that grows for as long as the swarm runs.** ⚠️ **This starts a database that grows for as long as the swarm runs.**

View file

@ -9,7 +9,7 @@ answers for one host. This one is the view *across* hives.
## Enabling ## Enabling
```nix ```nix
services.hyperhive.deploy.swarm-ui = true; # defaults to deploy.controller services.hyperhive.deploy.swarm-ui.enable = true; # defaults to deploy.swarm-controller.enable
``` ```
Derived from the controller rather than from `enableRequiredServices`: Derived from the controller rather than from `enableRequiredServices`:

View file

@ -15,10 +15,14 @@
# thing that must differ. # thing that must differ.
# #
# Flat and named for the thing deployed — `deploy.forgejo`, not # Flat and named for the thing deployed — `deploy.forgejo`, not
# `deploy.swarmServices.forgejo`. Grouping by "swarm service" would # `deploy.swarmServices.forgejo`: grouping by "swarm service" re-encodes
# re-encode the service-side taxonomy into a layer that does not care # the service-side taxonomy into a layer that does not care about it.
# about it: from here, a host deploys forgejo, or a hive, or the lot, and #
# what *kind* of thing each one is belongs to the service module. # ⚠️ Each entry is an attrset with an `enable`, not a bare bool, so a
# service that grows a second *deployment* decision has somewhere to put
# it — `deploy.forgejo = { enable; ci; }` is then an ordinary addition
# rather than a migration. `ci` ("does this host run the runner too") is
# exactly that shape, and a bare bool leaves it unrepresentable.
# #
# ⚠️ The renames below are deliberately in this one file rather than # ⚠️ The renames below are deliberately in this one file rather than
# spread across the service modules, so the whole move has a single home # spread across the service modules, so the whole move has a single home
@ -35,40 +39,40 @@ in
# paths. Precedent: ./hive-forge/default.nix, ./hive-matrix.nix. # paths. Precedent: ./hive-forge/default.nix, ./hive-matrix.nix.
(lib.mkRenamedOptionModule (lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "grafana" "enable" ] [ "services" "hyperhive" "swarm" "grafana" "enable" ]
[ "services" "hyperhive" "deploy" "grafana" ] [ "services" "hyperhive" "deploy" "grafana" "enable" ]
) )
(lib.mkRenamedOptionModule (lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "victoriametrics" "enable" ] [ "services" "hyperhive" "swarm" "victoriametrics" "enable" ]
[ "services" "hyperhive" "deploy" "victoriametrics" ] [ "services" "hyperhive" "deploy" "victoriametrics" "enable" ]
) )
(lib.mkRenamedOptionModule (lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "victorialogs" "enable" ] [ "services" "hyperhive" "swarm" "victorialogs" "enable" ]
[ "services" "hyperhive" "deploy" "victorialogs" ] [ "services" "hyperhive" "deploy" "victorialogs" "enable" ]
) )
(lib.mkRenamedOptionModule (lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "controller" "enable" ] [ "services" "hyperhive" "swarm" "controller" "enable" ]
[ "services" "hyperhive" "deploy" "controller" ] [ "services" "hyperhive" "deploy" "swarm-controller" "enable" ]
) )
(lib.mkRenamedOptionModule (lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "ui" "enable" ] [ "services" "hyperhive" "swarm" "ui" "enable" ]
[ "services" "hyperhive" "deploy" "swarm-ui" ] [ "services" "hyperhive" "deploy" "swarm-ui" "enable" ]
) )
(lib.mkRenamedOptionModule (lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "authelia" "enable" ] [ "services" "hyperhive" "swarm" "authelia" "enable" ]
[ "services" "hyperhive" "deploy" "authelia" ] [ "services" "hyperhive" "deploy" "authelia" "enable" ]
) )
(lib.mkRenamedOptionModule (lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "nats" "enable" ] [ "services" "hyperhive" "swarm" "nats" "enable" ]
[ "services" "hyperhive" "deploy" "nats" ] [ "services" "hyperhive" "deploy" "nats" "enable" ]
) )
(lib.mkRenamedOptionModule (lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "otel" "enable" ] [ "services" "hyperhive" "swarm" "otel" "enable" ]
[ "services" "hyperhive" "deploy" "otel" ] [ "services" "hyperhive" "deploy" "otel" "enable" ]
) )
]; ];
options.services.hyperhive.deploy = { options.services.hyperhive.deploy = {
grafana = lib.mkOption { grafana.enable = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = false; default = false;
description = '' description = ''
@ -81,7 +85,7 @@ in
''; '';
}; };
victoriametrics = lib.mkOption { victoriametrics.enable = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = false; default = false;
description = '' description = ''
@ -89,14 +93,14 @@ in
Derives from Derives from
{option}`services.hyperhive.swarm.enableRequiredServices` together {option}`services.hyperhive.swarm.enableRequiredServices` together
with {option}`services.hyperhive.deploy.grafana`: a store with no with {option}`services.hyperhive.deploy.grafana.enable`: a store
UI is unreadable and a UI with no store is empty, so there is no with no UI is unreadable and a UI with no store is empty, so there
sensible deployment that takes one and not the other from that is no sensible deployment that takes one and not the other from
switch. Set either directly to run exactly one. that switch. Set either directly to run exactly one.
''; '';
}; };
victorialogs = lib.mkOption { victorialogs.enable = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = false; default = false;
description = '' description = ''
@ -109,7 +113,7 @@ in
''; '';
}; };
authelia = lib.mkOption { authelia.enable = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = false; default = false;
example = true; example = true;
@ -127,7 +131,7 @@ in
''; '';
}; };
otel = lib.mkOption { otel.enable = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = false; default = false;
description = '' description = ''
@ -147,7 +151,7 @@ in
''; '';
}; };
nats = lib.mkOption { nats.enable = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = false; default = false;
description = '' description = ''
@ -160,7 +164,7 @@ in
''; '';
}; };
controller = lib.mkOption { swarm-controller.enable = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = false; default = false;
description = '' description = ''
@ -173,15 +177,16 @@ in
''; '';
}; };
swarm-ui = lib.mkOption { swarm-ui.enable = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = deployCfg.controller; default = deployCfg.swarm-controller.enable;
defaultText = lib.literalExpression "services.hyperhive.deploy.controller"; defaultText = lib.literalExpression "services.hyperhive.deploy.swarm-controller.enable";
example = true; example = true;
description = '' description = ''
Serve the swarm UI from this host. Serve the swarm UI from this host.
Derived from {option}`services.hyperhive.deploy.controller` rather Derived from
{option}`services.hyperhive.deploy.swarm-controller.enable` rather
than from than from
{option}`services.hyperhive.swarm.enableRequiredServices`: the UI {option}`services.hyperhive.swarm.enableRequiredServices`: the UI
is a view onto the controller's state and reaches it over that is a view onto the controller's state and reaches it over that

View file

@ -30,7 +30,7 @@ let
# authelia, so the secret can be moved without an operator. The other # authelia, so the secret can be moved without an operator. The other
# two cases (swarm side via swarmctl, remote hive) leave # two cases (swarm side via swarmctl, remote hive) leave
# `clientSecretFile` to be set explicitly — see docs/swarm/. # `clientSecretFile` to be set explicitly — see docs/swarm/.
ssoLocal = deployCfg.authelia; ssoLocal = deployCfg.authelia.enable;
# Where the plaintext lands inside the forge container. Under # Where the plaintext lands inside the forge container. Under
# /var/lib rather than /run: the forge may start before the delivery # /var/lib rather than /run: the forge may start before the delivery

View file

@ -32,7 +32,7 @@ let
# authelia, so the secret can be moved without an operator. The other # authelia, so the secret can be moved without an operator. The other
# two cases (swarm side, remote hive) leave `clientSecretFile` to be set # two cases (swarm side, remote hive) leave `clientSecretFile` to be set
# explicitly — same split the forge module documents. # explicitly — same split the forge module documents.
ssoLocal = deployCfg.authelia; ssoLocal = deployCfg.authelia.enable;
# Where the plaintext lands inside the matrix container. Under /var/lib # Where the plaintext lands inside the matrix container. Under /var/lib
# rather than /run: the homeserver may start before the delivery unit on # rather than /run: the homeserver may start before the delivery unit on

View file

@ -669,6 +669,6 @@ in
# in review on this PR; it evaluates and builds clean either way, which # in review on this PR; it evaluates and builds clean either way, which
# is exactly why it needed a reviewer rather than a check. # is exactly why it needed a reviewer rather than a check.
systemd.services.swarm-controller.environment.SWARM_CONTROLLER_OIDC_CA_FILE = systemd.services.swarm-controller.environment.SWARM_CONTROLLER_OIDC_CA_FILE =
lib.mkIf hyperhiveCfg.deploy.controller "${cfg.stateDir}/trust-bundle.pem"; lib.mkIf hyperhiveCfg.deploy.swarm-controller.enable "${cfg.stateDir}/trust-bundle.pem";
}; };
} }

View file

@ -33,7 +33,7 @@ in
shared services shared services
(`services.hyperhive.swarm.enableRequiredServices`), the swarm (`services.hyperhive.swarm.enableRequiredServices`), the swarm
CA (`services.hyperhive.swarm.ca.autoConfigure`), the swarm CA (`services.hyperhive.swarm.ca.autoConfigure`), the swarm
controller (`services.hyperhive.deploy.controller`), and the controller (`services.hyperhive.deploy.swarm-controller.enable`), and the
host's `/etc/hosts` entries for the names this hive serves host's `/etc/hosts` entries for the names this hive serves
(`services.hyperhive.gateway.localHostsEntry`) with no real DNS (`services.hyperhive.gateway.localHostsEntry`) with no real DNS
for those names, the operator is browsing them from the same box for those names, the operator is browsing them from the same box

View file

@ -61,7 +61,7 @@ in
their own containers can reach. their own containers can reach.
That collector forwards to the swarm's That collector forwards to the swarm's
({option}`services.hyperhive.deploy.otel`), which holds the ({option}`services.hyperhive.deploy.otel.enable`), which holds the
upstream credential and writes the swarm's store. So an agent never upstream credential and writes the swarm's store. So an agent never
sees the credential, and neither does this tier. sees the credential, and neither does this tier.
@ -81,13 +81,13 @@ in
has left the swarm. has left the swarm.
Read by the swarm's collector Read by the swarm's collector
({option}`services.hyperhive.deploy.otel`), which is the only ({option}`services.hyperhive.deploy.otel.enable`), which is the only
tier that holds the upstream credential. An agent is handed the tier that holds the upstream credential. An agent is handed the
*first* hop instead this hive's own collector so this value is *first* hop instead this hive's own collector so this value is
never given to a container. never given to a container.
Optional. Leave it empty and the swarm's own metrics store Optional. Leave it empty and the swarm's own metrics store
({option}`services.hyperhive.deploy.victoriametrics`) is the ({option}`services.hyperhive.deploy.victoriametrics.enable`) is the
destination; that is a complete deployment, not a degraded one. destination; that is a complete deployment, not a degraded one.
Set both and telemetry goes to both. Set both and telemetry goes to both.
''; '';
@ -231,7 +231,9 @@ in
clientSecretFile = lib.mkOption { clientSecretFile = lib.mkOption {
type = lib.types.nullOr lib.types.str; type = lib.types.nullOr lib.types.str;
default = default =
if config.services.hyperhive.deploy.authelia && config.services.hyperhive.hiveName != null then if
config.services.hyperhive.deploy.authelia.enable && config.services.hyperhive.hiveName != null
then
"${config.services.hyperhive.swarm.authelia.hostClientSecretDir}/" "${config.services.hyperhive.swarm.authelia.hostClientSecretDir}/"
+ "${config.services.hyperhive.swarm.authelia.hiveClientPrefix}${config.services.hyperhive.hiveName}.secret" + "${config.services.hyperhive.swarm.authelia.hiveClientPrefix}${config.services.hyperhive.hiveName}.secret"
else else
@ -579,8 +581,8 @@ in
# Only when the minting container is on THIS host. Elsewhere the file # Only when the minting container is on THIS host. Elsewhere the file
# is operator-provided and there is no local unit to order against — # is operator-provided and there is no local unit to order against —
# naming one that does not exist orders nothing, silently. # naming one that does not exist orders nothing, silently.
after = lib.optional deployCfg.authelia "container@${autheliaCfg.machine}.service"; after = lib.optional deployCfg.authelia.enable "container@${autheliaCfg.machine}.service";
requires = lib.optional deployCfg.authelia "container@${autheliaCfg.machine}.service"; requires = lib.optional deployCfg.authelia.enable "container@${autheliaCfg.machine}.service";
serviceConfig = { serviceConfig = {
Type = "oneshot"; Type = "oneshot";
RemainAfterExit = true; RemainAfterExit = true;

View file

@ -345,7 +345,7 @@ let
''; '';
in in
{ {
# `enable` moved to `services.hyperhive.deploy.authelia` — see # `enable` moved to `services.hyperhive.deploy.authelia.enable` — see
# ./deploy.nix. Whether this host runs the swarm's SSO provider is a # ./deploy.nix. Whether this host runs the swarm's SSO provider is a
# deployment decision; what stays here is what authelia IS, including # deployment decision; what stays here is what authelia IS, including
# `url` and the OIDC client registry every hive needs as a *client* # `url` and the OIDC client registry every hive needs as a *client*
@ -427,7 +427,7 @@ in
url = lib.mkOption { url = lib.mkOption {
type = lib.types.nullOr lib.types.str; type = lib.types.nullOr lib.types.str;
default = if deployCfg.authelia then "https://${cfg.domain}" else null; default = if deployCfg.authelia.enable then "https://${cfg.domain}" else null;
defaultText = lib.literalExpression ''if enable then "https://''${domain}" else null''; defaultText = lib.literalExpression ''if enable then "https://''${domain}" else null'';
example = "https://auth.example.com"; example = "https://auth.example.com";
description = '' description = ''
@ -467,8 +467,8 @@ in
oidc.hiveIdentities = lib.mkOption { oidc.hiveIdentities = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = deployCfg.nats; default = deployCfg.nats.enable;
defaultText = lib.literalExpression "services.hyperhive.deploy.nats"; defaultText = lib.literalExpression "services.hyperhive.deploy.nats.enable";
description = '' description = ''
Mint one machine client per hive in Mint one machine client per hive in
{option}`services.hyperhive.swarm.hives`, so each hive can {option}`services.hyperhive.swarm.hives`, so each hive can
@ -810,7 +810,7 @@ in
bridgeUrl = lib.mkOption { bridgeUrl = lib.mkOption {
type = lib.types.nullOr lib.types.str; type = lib.types.nullOr lib.types.str;
readOnly = true; readOnly = true;
default = if deployCfg.authelia then "http://127.0.0.1:${toString cfg.bridgePort}" else null; default = if deployCfg.authelia.enable then "http://127.0.0.1:${toString cfg.bridgePort}" else null;
defaultText = lib.literalExpression ''if enable then "http://127.0.0.1:''${bridgePort}" else null''; defaultText = lib.literalExpression ''if enable then "http://127.0.0.1:''${bridgePort}" else null'';
description = '' description = ''
Where `swarm-authelia-bridge` answers, **as seen from this Where `swarm-authelia-bridge` answers, **as seen from this
@ -829,7 +829,7 @@ in
}; };
}; };
config = lib.mkIf (hyperhiveCfg.enable && deployCfg.authelia) { config = lib.mkIf (hyperhiveCfg.enable && deployCfg.authelia.enable) {
# The derived half of the client list, declared the same way an # The derived half of the client list, declared the same way an
# operator declares one. Everything downstream then reads a single # operator declares one. Everything downstream then reads a single
# uniformly-typed `cfg.oidc.clients` and cannot tell the parts apart — # uniformly-typed `cfg.oidc.clients` and cannot tell the parts apart —
@ -964,7 +964,7 @@ in
# Authelia's own gateway surface: the vhost that fronts it and the # Authelia's own gateway surface: the vhost that fronts it and the
# name the hive resolver answers for. Both live here rather than in # name the hive resolver answers for. Both live here rather than in
# the gateway, and both are inside `deployCfg.authelia` — that guard is the # the gateway, and both are inside `deployCfg.authelia.enable` — that guard is the
# load-bearing part. # load-bearing part.
# #
# ⚠️ Every hive in a swarm knows `authelia.url`, but only the host # ⚠️ Every hive in a swarm knows `authelia.url`, but only the host
@ -1009,9 +1009,11 @@ in
# Written out in full rather than through a `let` binding so the gate and # Written out in full rather than through a `let` binding so the gate and
# the option it gates are visibly the same path: gating the wrong one is # the option it gates are visibly the same path: gating the wrong one is
# not a build error, it is a target that is always declared or never is. # not a build error, it is a target that is always declared or never is.
services.hyperhive.swarm.otel.scrapeTargets = lib.mkIf config.services.hyperhive.deploy.otel { services.hyperhive.swarm.otel.scrapeTargets =
authelia = "127.0.0.1:${toString cfg.metricsPort}"; lib.mkIf config.services.hyperhive.deploy.otel.enable
}; {
authelia = "127.0.0.1:${toString cfg.metricsPort}";
};
# This swarm-ui quick-links entry, same guard as the vhost/DNS name # This swarm-ui quick-links entry, same guard as the vhost/DNS name
# above (only the host actually running the container claims it — # above (only the host actually running the container claims it —
@ -1397,7 +1399,7 @@ in
# collector is registered — see `metricsRule` above, # collector is registered — see `metricsRule` above,
# which is where the reasoning for both halves lives. # which is where the reasoning for both halves lives.
lib.optional forgeCfg.behindGateway metricsRule lib.optional forgeCfg.behindGateway metricsRule
++ lib.optional deployCfg.swarm-ui { ++ lib.optional deployCfg.swarm-ui.enable {
domain = uiCfg.domain; domain = uiCfg.domain;
subject = [ "group:${operatorGroup}" ]; subject = [ "group:${operatorGroup}" ];
policy = "one_factor"; policy = "one_factor";

View file

@ -25,7 +25,7 @@ let
# the right behaviour is for `swarmctl user add` to fail saying the # the right behaviour is for `swarmctl user add` to fail saying the
# value is unset. A guessed path would resolve cleanly and write a file # value is unset. A guessed path would resolve cleanly and write a file
# nothing reads, which is the failure mode that costs an afternoon. # nothing reads, which is the failure mode that costs an afternoon.
autheliaEnv = lib.optionalAttrs deployCfg.authelia { autheliaEnv = lib.optionalAttrs deployCfg.authelia.enable {
# The CONFIGURED authelia, not whatever is on PATH: the argon2 # The CONFIGURED authelia, not whatever is on PATH: the argon2
# parameters baked into a hash have to match the verifier's. # parameters baked into a hash have to match the verifier's.
SWARMCTL_AUTHELIA_BIN = "${autheliaCfg.package}/bin/authelia"; SWARMCTL_AUTHELIA_BIN = "${autheliaCfg.package}/bin/authelia";
@ -130,7 +130,7 @@ let
# 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 { webhookEnv = lib.optionalAttrs deployCfg.swarm-ui.enable {
SWARM_CONTROLLER_PUBLIC_URL = "https://${uiCfg.domain}"; SWARM_CONTROLLER_PUBLIC_URL = "https://${uiCfg.domain}";
}; };
@ -174,14 +174,14 @@ let
# so the two ends agree on exactly one path, `/v1/metrics`, arrived at # so the two ends agree on exactly one path, `/v1/metrics`, arrived at
# from opposite directions. # from opposite directions.
# #
# Gated on `deployCfg.otel` alone (a swarm-wide fact, not "enabled # Gated on `deployCfg.otel.enable` alone (a swarm-wide fact, not "enabled
# HERE"): the collector is reachable by name wherever it runs, so a # HERE"): the collector is reachable by name wherever it runs, so a
# controller not co-located with it now exports over https instead of # controller not co-located with it now exports over https instead of
# exporting nothing — the graceful-absence case left is "no swarm-otel # exporting nothing — the graceful-absence case left is "no swarm-otel
# anywhere in this swarm at all," which `forgeEnv` already models the # anywhere in this swarm at all," which `forgeEnv` already models the
# same way. # same way.
otelSwarmCfg = config.services.hyperhive.swarm.otel; otelSwarmCfg = config.services.hyperhive.swarm.otel;
otelEnv = lib.optionalAttrs deployCfg.otel { otelEnv = lib.optionalAttrs deployCfg.otel.enable {
OTEL_EXPORTER_OTLP_ENDPOINT = "https://${otelSwarmCfg.domain}/${otelSwarmCfg.producerName}"; OTEL_EXPORTER_OTLP_ENDPOINT = "https://${otelSwarmCfg.domain}/${otelSwarmCfg.producerName}";
# Checked by `swarm-otel.nix`'s `oidc/${producerName}` authenticator # Checked by `swarm-otel.nix`'s `oidc/${producerName}` authenticator
# against exactly this controller's own registered audience (see the # against exactly this controller's own registered audience (see the
@ -215,7 +215,7 @@ in
name = "swarm-controller"; name = "swarm-controller";
consumers = [ "swarm-controller" ]; consumers = [ "swarm-controller" ];
hostUnit = true; hostUnit = true;
enable = deployCfg.controller; enable = deployCfg.swarm-controller.enable;
}) })
]; ];
@ -239,7 +239,7 @@ in
''; '';
}; };
# `enable` moved to `services.hyperhive.deploy.controller` — see # `enable` moved to `services.hyperhive.deploy.swarm-controller.enable` — see
# ./deploy.nix. `services.hyperhive.enableAllLocalDefaults` still # ./deploy.nix. `services.hyperhive.enableAllLocalDefaults` still
# asserts it, and that was never an exception to "not derived from # asserts it, and that was never an exception to "not derived from
# services.hyperhive.enable": that mode says "this box is the whole # services.hyperhive.enable": that mode says "this box is the whole
@ -451,7 +451,7 @@ in
authBridgeUrl = lib.mkOption { authBridgeUrl = lib.mkOption {
type = lib.types.nullOr lib.types.str; type = lib.types.nullOr lib.types.str;
default = if deployCfg.authelia then autheliaCfg.bridgeUrl else null; default = if deployCfg.authelia.enable then autheliaCfg.bridgeUrl else null;
defaultText = lib.literalExpression '' defaultText = lib.literalExpression ''
authelia's own `bridgeUrl` when this host also runs authelia's own `bridgeUrl` when this host also runs
`swarm-authelia`, else null `swarm-authelia`, else null
@ -474,7 +474,7 @@ in
}; };
}; };
config = lib.mkIf (config.services.hyperhive.enable && deployCfg.controller) { config = lib.mkIf (config.services.hyperhive.enable && deployCfg.swarm-controller.enable) {
# The daemon and the oneshot that mints its credential — the second one # The daemon and the oneshot that mints its credential — the second one
# failing leaves the first running and unable to authenticate anywhere. # failing leaves the first running and unable to authenticate anywhere.
services.hyperhive.swarm.otel.journaldUnits = [ services.hyperhive.swarm.otel.journaldUnits = [
@ -499,7 +499,7 @@ in
# client list would be a second source of truth for a string whose # client list would be a second source of truth for a string whose
# mismatch is an opaque 401 from the token endpoint. Same shape as the # mismatch is an opaque 401 from the token endpoint. Same shape as the
# queue's own client declaration. # queue's own client declaration.
services.hyperhive.swarm.authelia.oidc.clients = lib.mkIf deployCfg.authelia [ services.hyperhive.swarm.authelia.oidc.clients = lib.mkIf deployCfg.authelia.enable [
{ {
id = queueClientId; id = queueClientId;
description = "HyperHive swarm controller"; description = "HyperHive swarm controller";
@ -539,7 +539,7 @@ in
services.hyperhive.swarm.controller.queue.natsUrl is unset. services.hyperhive.swarm.controller.queue.natsUrl is unset.
It defaults to loopback only when this host also runs the queue It defaults to loopback only when this host also runs the queue
(`services.hyperhive.deploy.nats`). A controller on its (`services.hyperhive.deploy.nats.enable`). A controller on its
own host has to be told where the queue is. own host has to be told where the queue is.
''; '';
} }

View file

@ -77,7 +77,7 @@ let
# The all-local case: this host runs BOTH Grafana and the swarm's authelia, # The all-local case: this host runs BOTH Grafana and the swarm's authelia,
# so the minted secret can be moved without an operator. Same split the # so the minted secret can be moved without an operator. Same split the
# forge and matrix modules document. # forge and matrix modules document.
ssoLocal = deployCfg.grafana && deployCfg.authelia; ssoLocal = deployCfg.grafana.enable && deployCfg.authelia.enable;
autheliaUrl = toString autheliaCfg.url; autheliaUrl = toString autheliaCfg.url;
# Where the plaintext lands inside the container. Under /var/lib rather # Where the plaintext lands inside the container. Under /var/lib rather
@ -107,7 +107,7 @@ let
in in
{ {
# `enable` moved to `services.hyperhive.deploy.grafana` — see # `enable` moved to `services.hyperhive.deploy.grafana.enable` — see
# ./deploy.nix. Whether this host runs the swarm's Grafana is a # ./deploy.nix. Whether this host runs the swarm's Grafana is a
# deployment decision, and `swarm.*` has to be identical on every host. # deployment decision, and `swarm.*` has to be identical on every host.
# What stays here is what the service IS: its package, domain, and # What stays here is what the service IS: its package, domain, and
@ -307,7 +307,7 @@ in
}; };
}; };
config = lib.mkIf (hyperhiveCfg.enable && deployCfg.grafana) { config = lib.mkIf (hyperhiveCfg.enable && deployCfg.grafana.enable) {
# The gateway name and the quick-link, both inside `deploy.grafana` — that # The gateway name and the quick-link, both inside `deploy.grafana` — that
# guard is the load-bearing part. Every hive in a swarm may know this UI # guard is the load-bearing part. Every hive in a swarm may know this UI
# exists, but only the host that RUNS it may claim the name; a client # exists, but only the host that RUNS it may claim the name; a client
@ -408,9 +408,11 @@ in
# Declared here rather than in the collector's module, per that option's # Declared here rather than in the collector's module, per that option's
# rule: an entry exists where the service that named it runs. # rule: an entry exists where the service that named it runs.
services.hyperhive.swarm.otel.scrapeTargets = lib.mkIf config.services.hyperhive.deploy.otel { services.hyperhive.swarm.otel.scrapeTargets =
grafana = "127.0.0.1:${toString cfg.metricsPort}"; lib.mkIf config.services.hyperhive.deploy.otel.enable
}; {
grafana = "127.0.0.1:${toString cfg.metricsPort}";
};
# Order the container after the host CA service so the bind source below # Order the container after the host CA service so the bind source below
# exists before nspawn sets the mount up. # exists before nspawn sets the mount up.

View file

@ -49,7 +49,7 @@ let
hostPath = name: "${secretDir}/${name}"; hostPath = name: "${secretDir}/${name}";
# The responder needs all three credentials. Gating on them rather than # The responder needs all three credentials. Gating on them rather than
# on `deployCfg.nats` keeps a half-configured hive at "queue up, denying # on `deployCfg.nats.enable` keeps a half-configured hive at "queue up, denying
# everyone" instead of "unit crash-looping on a missing file". # everyone" instead of "unit crash-looping on a missing file".
# #
# In auto mode the seeds are minted on this host before the container # In auto mode the seeds are minted on this host before the container
@ -210,7 +210,7 @@ in
# per-service vhost pattern do not apply. # per-service vhost pattern do not apply.
options.services.hyperhive.swarm.nats = { options.services.hyperhive.swarm.nats = {
# `enable` moved to `services.hyperhive.deploy.nats` — see # `enable` moved to `services.hyperhive.deploy.nats.enable` — see
# ./deploy.nix. What stays here is what the queue IS: its domain, # ./deploy.nix. What stays here is what the queue IS: its domain,
# ports, accounts and callout wiring. # ports, accounts and callout wiring.
@ -399,7 +399,7 @@ in
}; };
}; };
config = lib.mkIf deployCfg.nats { config = lib.mkIf deployCfg.nats.enable {
# The responder as well as the server: a denial reaches the client as a # 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. # timeout, so the server's own log is the only place it is an error.
services.hyperhive.swarm.otel.journaldUnits = [ services.hyperhive.swarm.otel.journaldUnits = [
@ -414,7 +414,7 @@ in
# several layers from "the operator never set the issuer". # several layers from "the operator never set the issuer".
assertion = cfg.autoGenerateCallout || cfg.calloutIssuerPublicKey != ""; assertion = cfg.autoGenerateCallout || cfg.calloutIssuerPublicKey != "";
message = '' message = ''
services.hyperhive.deploy.nats requires services.hyperhive.deploy.nats.enable requires
nats.calloutIssuerPublicKey the public half of the account nats.calloutIssuerPublicKey the public half of the account
nkey that signs user JWTs for this queue. nkey that signs user JWTs for this queue.
@ -431,7 +431,7 @@ in
# somebody connects. # somebody connects.
assertion = cfg.autoGenerateCallout || cfg.calloutUserPublicKey != ""; assertion = cfg.autoGenerateCallout || cfg.calloutUserPublicKey != "";
message = '' message = ''
services.hyperhive.deploy.nats requires services.hyperhive.deploy.nats.enable requires
nats.calloutUserPublicKey the public half of the user nkey nats.calloutUserPublicKey the public half of the user nkey
the auth-callout responder authenticates as. the auth-callout responder authenticates as.
@ -476,7 +476,7 @@ in
{ {
assertion = autheliaUrl != null; assertion = autheliaUrl != null;
message = '' message = ''
services.hyperhive.deploy.nats requires services.hyperhive.deploy.nats.enable requires
services.hyperhive.swarm.authelia.url the queue authenticates services.hyperhive.swarm.authelia.url the queue authenticates
clients by validating tokens that authelia issued. clients by validating tokens that authelia issued.
@ -506,7 +506,7 @@ in
# authorization grant 'client_credentials'`. Introspection — which # authorization grant 'client_credentials'`. Introspection — which
# is all the responder needs today — worked throughout, which is why # is all the responder needs today — worked throughout, which is why
# nothing was visibly broken while the comment was untrue. # nothing was visibly broken while the comment was untrue.
services.hyperhive.swarm.authelia.oidc.clients = lib.mkIf deployCfg.authelia [ services.hyperhive.swarm.authelia.oidc.clients = lib.mkIf deployCfg.authelia.enable [
{ {
id = cfg.clientId; id = cfg.clientId;
description = "HyperHive swarm queue"; description = "HyperHive swarm queue";
@ -535,9 +535,11 @@ in
# ⚠️ `swarm.otel`, not `hyperhive.otel` — two collectors one word apart, # ⚠️ `swarm.otel`, not `hyperhive.otel` — two collectors one word apart,
# and only this one reads `scrapeTargets`. Written in full so the gate # and only this one reads `scrapeTargets`. Written in full so the gate
# and the option it gates are visibly the same path. # and the option it gates are visibly the same path.
services.hyperhive.swarm.otel.scrapeTargets = lib.mkIf config.services.hyperhive.deploy.otel { services.hyperhive.swarm.otel.scrapeTargets =
nats = "127.0.0.1:${toString cfg.metricsPort}"; lib.mkIf config.services.hyperhive.deploy.otel.enable
}; {
nats = "127.0.0.1:${toString cfg.metricsPort}";
};
containers.swarm-nats = { containers.swarm-nats = {
autoStart = true; autoStart = true;
@ -675,7 +677,7 @@ in
# door nobody can open, which is the deliberate interim state. # door nobody can open, which is the deliberate interim state.
# #
# ⚠️ It is gated on the seeds being configured rather than on # ⚠️ It is gated on the seeds being configured rather than on
# `deployCfg.nats`, so a half-configured hive gets a running, # `deployCfg.nats.enable`, so a half-configured hive gets a running,
# refusing queue instead of a unit that crash-loops on a # refusing queue instead of a unit that crash-loops on a
# missing file. A queue that denies everyone is a legible # missing file. A queue that denies everyone is a legible
# failure; a restart loop is not. # failure; a restart loop is not.
@ -759,7 +761,7 @@ in
# being up says nothing about whether its in-container secrets unit # being up says nothing about whether its in-container secrets unit
# has finished. The wait in the script is what actually closes it; # has finished. The wait in the script is what actually closes it;
# this only stops us spinning for the full timeout on every boot. # this only stops us spinning for the full timeout on every boot.
++ lib.optional deployCfg.authelia "container@${autheliaCfg.machine}.service"; ++ lib.optional deployCfg.authelia.enable "container@${autheliaCfg.machine}.service";
requires = lib.optional cfg.autoGenerateCallout "swarm-nats-callout-keys.service"; requires = lib.optional cfg.autoGenerateCallout "swarm-nats-callout-keys.service";
serviceConfig = { serviceConfig = {
Type = "oneshot"; Type = "oneshot";

View file

@ -164,13 +164,14 @@ let
# Written once because "which exporters" is a property of this tier, not # Written once because "which exporters" is a property of this tier, not
# of which hive a sample came from. # of which hive a sample came from.
exporterNames = exporterNames =
upstreamExporters ++ lib.optional deployCfg.victoriametrics "otlphttp/victoriametrics"; upstreamExporters ++ lib.optional deployCfg.victoriametrics.enable "otlphttp/victoriametrics";
# The same fan-out for logs, and the local store is only ONE of its # The same fan-out for logs, and the local store is only ONE of its
# destinations. A deployment that turns the swarm's log store off and keeps # destinations. A deployment that turns the swarm's log store off and keeps
# an upstream endpoint still collects — the store is where logs may be kept, # an upstream endpoint still collects — the store is where logs may be kept,
# not the reason to read the journal at all. # not the reason to read the journal at all.
logExporterNames = upstreamExporters ++ lib.optional deployCfg.victorialogs "otlphttp/victorialogs"; logExporterNames =
upstreamExporters ++ lib.optional deployCfg.victorialogs.enable "otlphttp/victorialogs";
# Collect when there is anywhere to send it, and only then. A pipeline with # Collect when there is anywhere to send it, and only then. A pipeline with
# an empty exporter list is not a quiet no-op — the collector rejects it — # an empty exporter list is not a quiet no-op — the collector rejects it —
@ -179,7 +180,7 @@ let
collectLogs = logExporterNames != [ ]; collectLogs = logExporterNames != [ ];
in in
{ {
# `enable` moved to `services.hyperhive.deploy.otel` — see ./deploy.nix. # `enable` moved to `services.hyperhive.deploy.otel.enable` — see ./deploy.nix.
# ⚠️ That is the SWARM collector. The per-hive one keeps its own # ⚠️ That is the SWARM collector. The per-hive one keeps its own
# `services.hyperhive.otel.enable` (./otel.nix) and is a different # `services.hyperhive.otel.enable` (./otel.nix) and is a different
# option entirely — every hive runs that one. # option entirely — every hive runs that one.
@ -434,8 +435,8 @@ in
}; };
}; };
config = lib.mkIf (config.services.hyperhive.enable && deployCfg.otel) { config = lib.mkIf (config.services.hyperhive.enable && deployCfg.otel.enable) {
# The gateway name, inside `deployCfg.otel` — that guard is the load-bearing # The gateway name, inside `deployCfg.otel.enable` — that guard is the load-bearing
# part. Every hive in a swarm may know this collector exists, but only # part. Every hive in a swarm may know this collector exists, but only
# the host that RUNS it may claim the name; a client hive declaring the # the host that RUNS it may claim the name; a client hive declaring the
# vhost would answer for a service it does not have. # vhost would answer for a service it does not have.
@ -505,7 +506,7 @@ in
# declaration would break every hive that runs a collector and # declaration would break every hive that runs a collector and
# publishes nothing. # publishes nothing.
services.hyperhive.swarm.authelia.oidc.clients = services.hyperhive.swarm.authelia.oidc.clients =
lib.mkIf (deployCfg.authelia && cfg.publishedScrapeTargets != { }) lib.mkIf (deployCfg.authelia.enable && cfg.publishedScrapeTargets != { })
[ [
{ {
id = cfg.clientId; id = cfg.clientId;
@ -543,7 +544,7 @@ in
# after it. On a fresh swarm that is a permanent stall presenting as # after it. On a fresh swarm that is a permanent stall presenting as
# "metrics are broken", several layers from its cause. # "metrics are broken", several layers from its cause.
systemd.services.swarm-otel-oidc-secret = systemd.services.swarm-otel-oidc-secret =
lib.mkIf (deployCfg.authelia && cfg.publishedScrapeTargets != { }) lib.mkIf (deployCfg.authelia.enable && cfg.publishedScrapeTargets != { })
{ {
description = "deliver the swarm collector's OIDC client secret from authelia"; description = "deliver the swarm collector's OIDC client secret from authelia";
after = [ "container@${autheliaCfg.machine}.service" ]; after = [ "container@${autheliaCfg.machine}.service" ];
@ -598,12 +599,12 @@ in
# The tier exists to hold the upstream credential and to write the # The tier exists to hold the upstream credential and to write the
# swarm's store. With neither, it is a process that receives # swarm's store. With neither, it is a process that receives
# samples and drops them — which looks healthy and loses data. # samples and drops them — which looks healthy and loses data.
assertion = otelCfg.endpoint != "" || deployCfg.victoriametrics; assertion = otelCfg.endpoint != "" || deployCfg.victoriametrics.enable;
message = '' message = ''
services.hyperhive.deploy.otel is true but this collector services.hyperhive.deploy.otel.enable is true but this collector
has nowhere to send what it receives: has nowhere to send what it receives:
services.hyperhive.otel.endpoint is empty and services.hyperhive.otel.endpoint is empty and
services.hyperhive.deploy.victoriametrics is false. services.hyperhive.deploy.victoriametrics.enable is false.
Set the endpoint to export upstream, or enable the swarm's Set the endpoint to export upstream, or enable the swarm's
metrics store. metrics store.
@ -632,7 +633,7 @@ in
# collector would listen on nothing while looking configured. # collector would listen on nothing while looking configured.
assertion = hyperhiveCfg.swarm.hives != { }; assertion = hyperhiveCfg.swarm.hives != { };
message = '' message = ''
services.hyperhive.deploy.otel is true but services.hyperhive.deploy.otel.enable is true but
services.hyperhive.swarm.hives is empty: ingest is authenticated services.hyperhive.swarm.hives is empty: ingest is authenticated
per hive, so an empty roster means this collector accepts nothing per hive, so an empty roster means this collector accepts nothing
from anyone. from anyone.
@ -744,7 +745,7 @@ in
# issuer URL evaluates cleanly and refuses every hive at runtime. # issuer URL evaluates cleanly and refuses every hive at runtime.
assertion = autheliaCfg.url != null; assertion = autheliaCfg.url != null;
message = '' message = ''
services.hyperhive.deploy.otel is true but services.hyperhive.deploy.otel.enable is true but
services.hyperhive.swarm.authelia.url is null: every hive services.hyperhive.swarm.authelia.url is null: every hive
authenticates to this collector as itself, and the token comes authenticates to this collector as itself, and the token comes
from the swarm's identity provider. from the swarm's identity provider.
@ -770,7 +771,7 @@ in
cfg.producerPort cfg.producerPort
otelCfg.collector.port otelCfg.collector.port
] ]
++ lib.optional deployCfg.victoriametrics vmCfg.port; ++ lib.optional deployCfg.victoriametrics.enable vmCfg.port;
all = derived ++ others; all = derived ++ others;
in in
lib.length (lib.unique all) == lib.length all; lib.length (lib.unique all) == lib.length all;
@ -1005,7 +1006,7 @@ in
}; };
exporters = exporters =
lib.optionalAttrs deployCfg.victoriametrics { lib.optionalAttrs deployCfg.victoriametrics.enable {
# `metrics_endpoint`, NOT `endpoint`: the latter is a # `metrics_endpoint`, NOT `endpoint`: the latter is a
# base that otlphttp appends `/v1/metrics` to, while # base that otlphttp appends `/v1/metrics` to, while
# VictoriaMetrics serves OTLP at # VictoriaMetrics serves OTLP at
@ -1028,7 +1029,7 @@ in
} }
// lib.optionalAttrs (otelCfg.protocol == "http/json") { encoding = "json"; }; // lib.optionalAttrs (otelCfg.protocol == "http/json") { encoding = "json"; };
} }
// lib.optionalAttrs deployCfg.victorialogs { // lib.optionalAttrs deployCfg.victorialogs.enable {
# `logs_endpoint`, NOT `endpoint`, for exactly the reason the # `logs_endpoint`, NOT `endpoint`, for exactly the reason the
# metrics exporter above spells out — and the trap is worse # metrics exporter above spells out — and the trap is worse
# here, because the two stores' OTLP routes differ. `endpoint` # here, because the two stores' OTLP routes differ. `endpoint`

View file

@ -71,25 +71,27 @@ in
# authelia: a swarm has one SSO provider, and this says it lives here. # authelia: a swarm has one SSO provider, and this says it lives here.
# With it off the hive is a *client* — `swarm.authelia.url` still points # With it off the hive is a *client* — `swarm.authelia.url` still points
# at whoever runs it. # at whoever runs it.
config.services.hyperhive.deploy.authelia = lib.mkDefault swarmCfg.enableRequiredServices; config.services.hyperhive.deploy.authelia.enable = lib.mkDefault swarmCfg.enableRequiredServices;
# The queue. Same rule: once per swarm, optional. # The queue. Same rule: once per swarm, optional.
config.services.hyperhive.deploy.nats = lib.mkDefault swarmCfg.enableRequiredServices; config.services.hyperhive.deploy.nats.enable = lib.mkDefault swarmCfg.enableRequiredServices;
# The swarm collector that feeds the metrics pair, and the only tier # The swarm collector that feeds the metrics pair, and the only tier
# holding the upstream credential. ⚠️ NOT the per-hive collector below, # holding the upstream credential. ⚠️ NOT the per-hive collector below,
# which every hive runs. # which every hive runs.
config.services.hyperhive.deploy.otel = lib.mkDefault swarmCfg.enableRequiredServices; config.services.hyperhive.deploy.otel.enable = lib.mkDefault swarmCfg.enableRequiredServices;
# The metrics pair, deriving together on purpose: a store with no UI is # 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 # 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 # deployment that takes one and not the other from this switch. An
# operator who wants exactly one still sets it directly, which # operator who wants exactly one still sets it directly, which
# `mkDefault` allows. # `mkDefault` allows.
config.services.hyperhive.deploy.victoriametrics = lib.mkDefault swarmCfg.enableRequiredServices; config.services.hyperhive.deploy.victoriametrics.enable =
config.services.hyperhive.deploy.grafana = lib.mkDefault swarmCfg.enableRequiredServices; lib.mkDefault swarmCfg.enableRequiredServices;
config.services.hyperhive.deploy.grafana.enable = lib.mkDefault swarmCfg.enableRequiredServices;
# The log store, from the same switch for the same reason as the rest: a # The log store, from the same switch for the same reason as the rest: a
# hive that is not the service host is a *client* of it, not a second one. # hive that is not the service host is a *client* of it, not a second one.
config.services.hyperhive.deploy.victorialogs = lib.mkDefault swarmCfg.enableRequiredServices; config.services.hyperhive.deploy.victorialogs.enable =
lib.mkDefault swarmCfg.enableRequiredServices;
} }

View file

@ -57,7 +57,7 @@ let
hiveDomain = config.services.hyperhive.domain; hiveDomain = config.services.hyperhive.domain;
in in
{ {
# `enable` moved to `services.hyperhive.deploy.swarm-ui` — see # `enable` moved to `services.hyperhive.deploy.swarm-ui.enable` — see
# ./deploy.nix, where it still derives from the controller's own deploy # ./deploy.nix, where it still derives from the controller's own deploy
# toggle for the same reason. What stays here is what the UI IS: its # toggle for the same reason. What stays here is what the UI IS: its
# domain and wiring. # domain and wiring.
@ -99,7 +99,7 @@ in
}; };
}; };
config = lib.mkIf (config.services.hyperhive.enable && deployCfg.swarm-ui) { config = lib.mkIf (config.services.hyperhive.enable && deployCfg.swarm-ui.enable) {
assertions = [ assertions = [
{ {
# The `_` default server already answers for the hive domain # The `_` default server already answers for the hive domain

View file

@ -53,7 +53,7 @@ let
''; '';
in in
{ {
# `enable` moved to `services.hyperhive.deploy.victorialogs` — see # `enable` moved to `services.hyperhive.deploy.victorialogs.enable` — see
# ./deploy.nix. What stays here is what the store IS: its package, # ./deploy.nix. What stays here is what the store IS: its package,
# domain, retention and wiring. # domain, retention and wiring.
options.services.hyperhive.swarm.victorialogs = { options.services.hyperhive.swarm.victorialogs = {
@ -121,7 +121,7 @@ in
}; };
}; };
config = lib.mkIf (hyperhiveCfg.enable && deployCfg.victorialogs) { config = lib.mkIf (hyperhiveCfg.enable && deployCfg.victorialogs.enable) {
# This store publishes its own health as prometheus metrics on the same # This store publishes its own health as prometheus metrics on the same
# listener it serves queries on, so the swarm's collector scrapes it with # listener it serves queries on, so the swarm's collector scrapes it with
# no exporter and no extra port — same arrangement as the metrics store. # no exporter and no extra port — same arrangement as the metrics store.
@ -134,7 +134,7 @@ in
# hosts are separate evaluations. # hosts are separate evaluations.
services.hyperhive.swarm.otel.scrapeTargets.victorialogs = "127.0.0.1:${toString cfg.port}"; services.hyperhive.swarm.otel.scrapeTargets.victorialogs = "127.0.0.1:${toString cfg.port}";
# The gateway name and the quick-link, both inside `deployCfg.victorialogs` — same # The gateway name and the quick-link, both inside `deployCfg.victorialogs.enable` — same
# "only the host that runs the service may claim the name" guard every # "only the host that runs the service may claim the name" guard every
# sibling swarm-service module uses (`swarm-grafana.nix`, # sibling swarm-service module uses (`swarm-grafana.nix`,
# `swarm-victoriametrics.nix`). # `swarm-victoriametrics.nix`).

View file

@ -29,7 +29,7 @@ let
domainBase = if swarmDomain == null then "invalid" else swarmDomain; domainBase = if swarmDomain == null then "invalid" else swarmDomain;
in in
{ {
# `enable` moved to `services.hyperhive.deploy.victoriametrics` — see # `enable` moved to `services.hyperhive.deploy.victoriametrics.enable` — see
# ./deploy.nix. What stays here is what the store IS: its package, # ./deploy.nix. What stays here is what the store IS: its package,
# domain, retention and wiring. # domain, retention and wiring.
options.services.hyperhive.swarm.victoriametrics = { options.services.hyperhive.swarm.victoriametrics = {
@ -90,8 +90,8 @@ in
}; };
}; };
config = lib.mkIf (hyperhiveCfg.enable && deployCfg.victoriametrics) { config = lib.mkIf (hyperhiveCfg.enable && deployCfg.victoriametrics.enable) {
# The gateway name and the quick-link, both inside `deployCfg.victoriametrics` — that # The gateway name and the quick-link, both inside `deployCfg.victoriametrics.enable` — that
# guard is the load-bearing part. Every hive in a swarm may know this # guard is the load-bearing part. Every hive in a swarm may know this
# store exists, but only the host that RUNS it may claim the name; a # store exists, but only the host that RUNS it may claim the name; a
# client hive declaring the vhost would answer for a service it does not # client hive declaring the vhost would answer for a service it does not

View file

@ -46,7 +46,7 @@ let
# `forge.<apex>` is, and no CA in the hierarchy issues for it # `forge.<apex>` is, and no CA in the hierarchy issues for it
# implicitly. Left out, its vhost falls back to the hive leaf and the # implicitly. Left out, its vhost falls back to the hive leaf and the
# swarm's front page opens with a name mismatch. # swarm's front page opens with a name mismatch.
++ lib.optional deployCfg.swarm-ui swarmCfg.ui.domain ++ lib.optional deployCfg.swarm-ui.enable swarmCfg.ui.domain
# Every swarm service that claims a gateway name belongs here, and # Every swarm service that claims a gateway name belongs here, and
# these three were missing it. Membership is what `gateway.lib.tlsFor` # these three were missing it. Membership is what `gateway.lib.tlsFor`
# consults to pick the services leaf over the hive one, so a name # consults to pick the services leaf over the hive one, so a name
@ -58,16 +58,16 @@ let
# metrics UI and store looked fine for as long as only people opened # metrics UI and store looked fine for as long as only people opened
# them; the collector's exporter — same defect, no human in the loop — # them; the collector's exporter — same defect, no human in the loop —
# failed every POST and dropped the samples. # failed every POST and dropped the samples.
++ lib.optional deployCfg.grafana swarmCfg.grafana.domain ++ lib.optional deployCfg.grafana.enable swarmCfg.grafana.domain
++ lib.optional deployCfg.victoriametrics swarmCfg.victoriametrics.domain ++ lib.optional deployCfg.victoriametrics.enable swarmCfg.victoriametrics.domain
++ lib.optional deployCfg.otel swarmCfg.otel.domain ++ lib.optional deployCfg.otel.enable swarmCfg.otel.domain
# VictoriaLogs' vhost is new (was previously unpublished entirely — see # VictoriaLogs' vhost is new (was previously unpublished entirely — see
# swarm-victorialogs.nix's file-top comment) and needs the same # swarm-victorialogs.nix's file-top comment) and needs the same
# membership every gateway-published swarm service needs: absent from # membership every gateway-published swarm service needs: absent from
# this list, `gateway.lib.tlsFor` falls back to the hive leaf, which # this list, `gateway.lib.tlsFor` falls back to the hive leaf, which
# cannot cover a name under a different apex — see the ⚠️ above this # cannot cover a name under a different apex — see the ⚠️ above this
# list for what that looked like the last time a name was missed here. # list for what that looked like the last time a name was missed here.
++ lib.optional deployCfg.victorialogs swarmCfg.victorialogs.domain; ++ lib.optional deployCfg.victorialogs.enable swarmCfg.victorialogs.domain;
# Hives whose entry still carries the removed `certFingerprint`. Scanned # Hives whose entry still carries the removed `certFingerprint`. Scanned
# here, at top level, because that is the only place an assertion about a # here, at top level, because that is the only place an assertion about a
@ -96,7 +96,7 @@ let
# unrepresentable rather than merely detected: a default set is all or # unrepresentable rather than merely detected: a default set is all or
# nothing, and the assertion is then only ever about what an operator # nothing, and the assertion is then only ever about what an operator
# typed. # typed.
queueLocal = deployCfg.nats && deployCfg.authelia && cfg.hiveName != null; queueLocal = deployCfg.nats.enable && deployCfg.authelia.enable && cfg.hiveName != null;
in in
{ {
options.services.hyperhive.swarm.hives = lib.mkOption { options.services.hyperhive.swarm.hives = lib.mkOption {

View file

@ -90,11 +90,11 @@ let
cases = [ cases = [
{ {
name = "a hive that has not opted into all-local runs no swarm controller"; name = "a hive that has not opted into all-local runs no swarm controller";
ok = !bare.services.hyperhive.deploy.controller; ok = !bare.services.hyperhive.deploy.swarm-controller.enable;
} }
{ {
name = "the all-local mode turns the swarm controller on"; name = "the all-local mode turns the swarm controller on";
ok = allLocal.services.hyperhive.deploy.controller; ok = allLocal.services.hyperhive.deploy.swarm-controller.enable;
} }
{ {
# The gateway's per-name issuer choice. If this ever collapses to a # The gateway's per-name issuer choice. If this ever collapses to a

View file

@ -4,7 +4,7 @@ The **swarm-level** daemon. Where `hive-c0re` owns the agents on one host, this
owns what is true *across* hives — so a swarm runs one of them and most hives owns what is true *across* hives — so a swarm runs one of them and most hives
leave it off. leave it off.
Opt-in per host via `services.hyperhive.deploy.controller`, which is Opt-in per host via `services.hyperhive.deploy.swarm-controller.enable`, which is
deliberately **not** derived from `services.hyperhive.enable`: turning it on is deliberately **not** derived from `services.hyperhive.enable`: turning it on is
a statement about swarm topology, not about whether hyperhive is installed. a statement about swarm topology, not about whether hyperhive is installed.

View file

@ -1,6 +1,6 @@
//! Swarm-level controller daemon. Runs as the unprivileged //! Swarm-level controller daemon. Runs as the unprivileged
//! `swarm-controller` user on whichever host the operator flips //! `swarm-controller` user on whichever host the operator flips
//! `services.hyperhive.deploy.controller` on, and serves HTTP over a //! `services.hyperhive.deploy.swarm-controller.enable` on, and serves HTTP over a
//! unix socket that the hive-gateway's nginx proxies to. //! unix socket that the hive-gateway's nginx proxies to.
//! //!
//! Configuration is read-only and loaded once at startup from env vars the //! Configuration is read-only and loaded once at startup from env vars the