Watch
0
0
Fork
You've already forked hyperhive
0

Compare commits

..
29 changed files with 255 additions and 418 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
OTEL collector via the OpenTelemetry Rust SDK / OTLP HTTP exporter.
- **`swarm-controller/`** — swarm-level daemon, opt-in per host
(`services.hyperhive.deploy.swarm-controller.enable`). Where `hive-c0re` owns
(`services.hyperhive.swarm.controller.enable`). Where `hive-c0re` owns
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
unix socket the gateway's nginx proxies to — ⚠️ **the socket's

View file

@ -14,8 +14,8 @@ Single nginx in front of every hyperhive web surface. Runs on the **host**, next
| `chat.<swarm>/_matrix/*` | `chat.<swarm>` | tuwunel (`8008`) | `matrix.gatewayHost != null` |
| `chat.<swarm>/` | `chat.<swarm>` | fluffychat-web static | `matrix.gui.enable` |
| `chat.<swarm>/config.json` | `chat.<swarm>` | inline JSON (FluffyChat boot config) | `matrix.gui.enable && domain != null` |
| `auth.<swarm>/` | `auth.<swarm>` | authelia (`9091`) | `deploy.authelia` |
| `<swarm>/` | `<swarm>` | swarm-ui dist (static), behind an authelia subrequest | `deploy.swarm-ui` |
| `auth.<swarm>/` | `auth.<swarm>` | authelia (`9091`) | `swarm.authelia.enable` |
| `<swarm>/` | `<swarm>` | swarm-ui dist (static), behind an authelia subrequest | `swarm.ui.enable` |
The authelia vhost is declared only by the host that **runs** authelia, not by every hive that uses it — a client hive knows the swarm's `authelia.url` but must not answer for a name it doesn't serve. Its server name is exactly `swarm.authelia.domain`: authelia validates `authelia_url ⊂ session cookie domain` at startup, so a near-miss is a container that refuses to boot. It carries no `auth_basic` — the login page must not sit behind the login mechanism it replaces — and sets the four `X-Forwarded-{Proto,Host,Uri,For}` headers, since authelia decides by the *original* request rather than the hop it sees.
@ -64,7 +64,7 @@ Each location carries a duplicated `auth_basic` block (separate locations don't
- `<hive-domain>` → `127.0.0.1`
- `forge.<swarm>` → `127.0.0.1` (when forge.behindGateway)
- `chat.<swarm>` → `127.0.0.1` (when matrix.gatewayHost set)
- `auth.<swarm>` → `127.0.0.1` (when deploy.authelia)
- `auth.<swarm>` → `127.0.0.1` (when swarm.authelia.enable)
`lib.unique` de-dupes if any sub-domain happens to equal another entry. Operators with real DNS leave it off.

View file

@ -18,7 +18,7 @@ services.hyperhive.otel = {
`enable` is the single gate. `endpoint` is where telemetry ends up after it
leaves the swarm — optional, because the swarm's own metrics store
(`deploy.victoriametrics`) is a destination in its own right. With both,
(`swarm.victoriametrics.enable`) is a destination in its own right. With both,
telemetry goes to both. See
[`swarm/services.md`](swarm/services.md#metrics-victoriametrics--grafana).
@ -80,7 +80,7 @@ the host is:
| | runs where | receives from | does |
|---|---|---|---|
| **hive tier** — `otel.enable` | every hive with agents | that hive's agents, on the bridge | forwards to the swarm tier. Holds no credential, picks no destination |
| **swarm tier** — `deploy.swarm-otel` | once per swarm | every hive's collector | writes the swarm's store and exports upstream |
| **swarm tier** — `swarm.otel.enable` | once per swarm | every hive's collector | writes the swarm's store and exports upstream |
An all-local host runs both, and needs nothing said about the hop between them.

View file

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

View file

@ -44,7 +44,7 @@ echo "hunter2" | hivectl gateway create-user mara --password-stdin
hivectl gateway list-users
```
### 3 · Swarm SSO (only when `deploy.authelia`)
### 3 · Swarm SSO (only when `swarm.authelia.enable`)
⚠️ **Required to finish the install, not optional.** Authelia treats an
empty user store as a fatal startup error, so until this runs the
@ -70,7 +70,7 @@ than amend — adding the group afterwards is `swarmctl user update mara
Detail, including what the password is and why this stays manual:
[`swarm/sso.md`](swarm/sso.md).
### 4 · Swarm UI (only when `deploy.swarm-ui`, on by default with the controller)
### 4 · Swarm UI (only when `swarm.ui.enable`, on by default with the controller)
Nothing to run — it is served on the swarm apex
(`https://<swarm.domain>/`) as soon as the host rebuilds. Two things

View file

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

View file

@ -34,7 +34,7 @@ One authelia per swarm, in a `swarm-authelia` container, at
provider, differentiated by roles and claims rather than by mechanism —
there is one IdP and one auth path.
- **`deploy.authelia`** — run the container here. Defaults from
- **`swarm.authelia.enable`** — run the container here. Defaults from
`swarm.enableRequiredServices`.
- **`swarm.authelia.url`** — where clients are sent to authenticate.
Present on **every** hive, defaulting to this host's own instance only
@ -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:
```nix
services.hyperhive.deploy.victoriametrics.enable = true;
services.hyperhive.deploy.grafana.enable = false;
services.hyperhive.swarm.victoriametrics.enable = true;
services.hyperhive.swarm.grafana.enable = false;
```
⚠️ **This starts a database that grows for as long as the swarm runs.**

View file

@ -14,7 +14,7 @@ separate `enable` would be a second fact free to disagree with the first.
authelia binds loopback only. The **gateway** on the host running it
publishes it as `auth.<swarm.domain>` — vhost, dnsmasq record and TLS
name all follow `deploy.authelia`, so there is nothing to turn on
name all follow `swarm.authelia.enable`, so there is nothing to turn on
separately. (Details, including why a client hive must not declare that
vhost: [`../gateway.md`](../gateway.md).)

View file

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

View file

@ -12,7 +12,6 @@
{
imports = [
./hyperhive.nix
./deploy.nix
./local-defaults.nix
./hive-c0re
./hive-ci.nix

View file

@ -1,200 +0,0 @@
# "What does THIS host deploy?"
#
# Separated from `services.hyperhive.swarm.*` because those are two
# different kinds of fact and only one of them varies per machine:
#
# swarm.* — swarm-wide truth. The swarm's name, domain, hives, peers,
# CA, and where each service lives. **Identical on every
# host**, byte for byte; a hive needs all of it to be a
# *client* of the swarm.
# deploy.* — this machine's deployment decisions. Necessarily different
# on every host, because that is what a deployment is.
#
# The `enable` toggles used to live under `swarm.*`, which made the
# namespace that is supposed to be identical everywhere carry the one
# thing that must differ.
#
# Flat and named for the thing deployed — `deploy.forgejo`, not
# `deploy.swarmServices.forgejo`: grouping by "swarm service" re-encodes
# the service-side taxonomy into a layer that does not care about it.
#
# ⚠️ 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
# spread across the service modules, so the whole move has a single home
# and a single file to delete when the deprecation window closes — the
# shape ./swarm-peers-removed.nix already uses.
{ lib, config, ... }:
let
deployCfg = config.services.hyperhive.deploy;
in
{
imports = [
# Same type, same meaning, new path — so a rename carries it exactly
# and existing configs keep evaluating with one warning naming both
# paths. Precedent: ./hive-forge/default.nix, ./hive-matrix.nix.
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "grafana" "enable" ]
[ "services" "hyperhive" "deploy" "grafana" "enable" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "victoriametrics" "enable" ]
[ "services" "hyperhive" "deploy" "victoriametrics" "enable" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "victorialogs" "enable" ]
[ "services" "hyperhive" "deploy" "victorialogs" "enable" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "controller" "enable" ]
[ "services" "hyperhive" "deploy" "swarm-controller" "enable" ]
)
(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" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "nats" "enable" ]
[ "services" "hyperhive" "deploy" "nats" "enable" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "otel" "enable" ]
[ "services" "hyperhive" "deploy" "swarm-otel" "enable" ]
)
];
options.services.hyperhive.deploy = {
grafana.enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Run the swarm's metrics UI on this host.
Off by default and not derived from
{option}`services.hyperhive.enable`: a swarm has one Grafana, so
running it is a decision about this host rather than about
whether hyperhive is installed.
'';
};
victoriametrics.enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Run the swarm's metrics store on this host.
Derives from
{option}`services.hyperhive.swarm.enableRequiredServices` together
with {option}`services.hyperhive.deploy.grafana.enable`: a store
with no UI is unreadable and a UI with no store is empty, so there
is no sensible deployment that takes one and not the other from
that switch. Set either directly to run exactly one.
'';
};
victorialogs.enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Run the swarm's log store on this host.
Derives from
{option}`services.hyperhive.swarm.enableRequiredServices` for the
same reason as the metrics pair above: a hive that is not the
service host is a *client* of this store, not a second one.
'';
};
authelia.enable = lib.mkOption {
type = lib.types.bool;
default = false;
example = true;
description = ''
Run the swarm's authelia in a `swarm-authelia` container on this
host. {option}`services.hyperhive.swarm.enableRequiredServices`
turns this on — a swarm has one SSO provider, and that says it
lives here.
With it off, this hive is a *client*:
{option}`services.hyperhive.swarm.authelia.url` still points at
whoever runs it, and no container is created. That asymmetry is
why the two live in different namespaces — every hive needs the
client half, only one runs the server half.
'';
};
swarm-otel.enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Run the **swarm's** telemetry collector on this host.
Derives from
{option}`services.hyperhive.swarm.enableRequiredServices` with the
metrics pair it feeds: a swarm has one of these, and it belongs
wherever the shared services live rather than on every hive.
Named `swarm-otel` rather than `otel` because there are two
collectors and the tier is the whole distinction:
{option}`services.hyperhive.otel.enable` is the **hive-tier** one,
which every hive runs. A bare `deploy.otel` would not say which
it meant. A hive that does not run the swarm collector still runs
its own, and reaches this one by name at
{option}`services.hyperhive.swarm.otel.domain`.
'';
};
nats.enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Run the swarm's message queue in a `swarm-nats` container on this
host. A swarm has one queue, so this belongs on the same host as
the rest of the shared services.
Off by default, and off means *absent*: no container is created
and nothing else in the evaluated config changes.
'';
};
swarm-controller.enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Run the swarm-controller daemon on this host.
Off by default and deliberately not derived from
{option}`services.hyperhive.enable`: a swarm has one controller,
so running it is a decision about this host rather than about
whether hyperhive is installed.
'';
};
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` rather
than from
{option}`services.hyperhive.swarm.enableRequiredServices`: 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.
'';
};
};
}

View file

@ -22,7 +22,6 @@ let
# assertion below turns into an eval failure rather than a discovery
# request to `null/.well-known/...`.
autheliaCfg = config.services.hyperhive.swarm.authelia;
deployCfg = config.services.hyperhive.deploy;
autheliaUrl = autheliaCfg.url;
autheliaDiscoveryUrl = "${toString autheliaUrl}/.well-known/openid-configuration";
@ -30,7 +29,7 @@ let
# authelia, so the secret can be moved without an operator. The other
# two cases (swarm side via swarmctl, remote hive) leave
# `clientSecretFile` to be set explicitly — see docs/swarm/.
ssoLocal = deployCfg.authelia.enable;
ssoLocal = autheliaCfg.enable;
# Where the plaintext lands inside the forge container. Under
# /var/lib rather than /run: the forge may start before the delivery

View file

@ -25,14 +25,13 @@ let
# below turns into an eval failure rather than a discovery request to
# `null/.well-known/…`.
autheliaCfg = config.services.hyperhive.swarm.authelia;
deployCfg = config.services.hyperhive.deploy;
autheliaUrl = autheliaCfg.url;
# The all-local case: this host runs BOTH the homeserver and the swarm's
# authelia, so the secret can be moved without an operator. The other
# two cases (swarm side, remote hive) leave `clientSecretFile` to be set
# explicitly — same split the forge module documents.
ssoLocal = deployCfg.authelia.enable;
ssoLocal = autheliaCfg.enable;
# Where the plaintext lands inside the matrix container. Under /var/lib
# 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
# is exactly why it needed a reviewer rather than a check.
systemd.services.swarm-controller.environment.SWARM_CONTROLLER_OIDC_CA_FILE =
lib.mkIf hyperhiveCfg.deploy.swarm-controller.enable "${cfg.stateDir}/trust-bundle.pem";
lib.mkIf hyperhiveCfg.swarm.controller.enable "${cfg.stateDir}/trust-bundle.pem";
};
}

View file

@ -33,7 +33,7 @@ in
shared services
(`services.hyperhive.swarm.enableRequiredServices`), the swarm
CA (`services.hyperhive.swarm.ca.autoConfigure`), the swarm
controller (`services.hyperhive.deploy.swarm-controller.enable`), and the
controller (`services.hyperhive.swarm.controller.enable`), and the
host's `/etc/hosts` entries for the names this hive serves
(`services.hyperhive.gateway.localHostsEntry`) — with no real DNS
for those names, the operator is browsing them from the same box
@ -80,13 +80,22 @@ in
# reach whoever runs the responder, and minting them here would move
# that hand-off somewhere less visible rather than removing it.
nats.autoGenerateCallout = lib.mkDefault cfg.enableAllLocalDefaults;
# The controller is asserted by the MODE and by nothing else. Its own
# option stays `default = false` precisely because running it is a
# statement about swarm topology — but "this box is the whole
# deployment" IS that statement, and it is the one shape where the
# answer isn't ambiguous. Deriving it from `enableRequiredServices`
# instead would be wrong: a hive in a larger swarm can legitimately
# want the shared services without being the host that controls them.
controller.enable = lib.mkDefault cfg.enableAllLocalDefaults;
# The controller's queue coordinates. Co-location is what makes these
# derivable at all — loopback only reaches the queue when the queue is
# here, and the minted client secret only exists on the host authelia
# ran its first boot on — so they belong to the mode that asserts this
# box is the whole deployment, not to the options' own `default`.
#
# Deriving them from `deploy.nats` / `deploy.authelia`
# Deriving them from `swarm.nats.enable` / `swarm.authelia.enable`
# inside those defaults is the mixing this file exists to prevent: the
# option would be describing a deployment shape instead of describing
# itself, and "what does all-local turn on?" would stop having one
@ -108,19 +117,4 @@ in
lib.mkDefault "${config.services.hyperhive.swarm.authelia.hostClientSecretDir}/swarm-controller.secret"
);
};
# The controller is asserted by the MODE and by nothing else. Its own
# option stays `default = false` precisely because running it is a
# statement about swarm topology — but "this box is the whole
# deployment" IS that statement, and it is the one shape where the
# answer isn't ambiguous. Deriving it from `enableRequiredServices`
# instead would be wrong: a hive in a larger swarm can legitimately
# want the shared services without being the host that controls them.
#
# Sits outside the `swarm` attrset above because it is a `deploy.*`
# option (./deploy.nix): "does THIS host run the controller" is exactly
# the per-host fact `swarm.*` may not carry. The ⚠️ collision note above
# does not apply here — that one is about two definitions of `swarm`
# itself, and this is a different top-level path.
config.services.hyperhive.deploy.swarm-controller.enable = lib.mkDefault cfg.enableAllLocalDefaults;
}

View file

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

View file

@ -42,7 +42,6 @@ let
hyperhiveDomain = hyperhiveCfg.domain;
swarmDomain = hyperhiveCfg.swarm.domain;
uiCfg = hyperhiveCfg.swarm.ui;
deployCfg = hyperhiveCfg.deploy;
forgeCfg = hyperhiveCfg.swarm.forge;
# Group an account must hold to reach operator-only surfaces. Named
@ -345,12 +344,22 @@ let
'';
in
{
# `enable` moved to `services.hyperhive.deploy.authelia.enable` — see
# ./deploy.nix. Whether this host runs the swarm's SSO provider is a
# deployment decision; what stays here is what authelia IS, including
# `url` and the OIDC client registry every hive needs as a *client*
# whether or not it runs the container.
options.services.hyperhive.swarm.authelia = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
example = true;
description = ''
Run the swarm's authelia in a `swarm-authelia` container on this
host. `services.hyperhive.swarm.enableRequiredServices` turns
this on — a swarm has one SSO provider, and that says it lives
here.
With it off, this hive is a *client*: `url` below still points
at whoever runs it, and no container is created.
'';
};
package = lib.mkOption {
type = lib.types.package;
default = pkgs.authelia;
@ -427,7 +436,7 @@ in
url = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = if deployCfg.authelia.enable then "https://${cfg.domain}" else null;
default = if cfg.enable then "https://${cfg.domain}" else null;
defaultText = lib.literalExpression ''if enable then "https://''${domain}" else null'';
example = "https://auth.example.com";
description = ''
@ -467,8 +476,8 @@ in
oidc.hiveIdentities = lib.mkOption {
type = lib.types.bool;
default = deployCfg.nats.enable;
defaultText = lib.literalExpression "services.hyperhive.deploy.nats.enable";
default = hyperhiveCfg.swarm.nats.enable;
defaultText = lib.literalExpression "services.hyperhive.swarm.nats.enable";
description = ''
Mint one machine client per hive in
{option}`services.hyperhive.swarm.hives`, so each hive can
@ -810,7 +819,7 @@ in
bridgeUrl = lib.mkOption {
type = lib.types.nullOr lib.types.str;
readOnly = true;
default = if deployCfg.authelia.enable then "http://127.0.0.1:${toString cfg.bridgePort}" else null;
default = if cfg.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'';
description = ''
Where `swarm-authelia-bridge` answers, **as seen from this
@ -829,7 +838,7 @@ in
};
};
config = lib.mkIf (hyperhiveCfg.enable && deployCfg.authelia.enable) {
config = lib.mkIf (hyperhiveCfg.enable && cfg.enable) {
# The derived half of the client list, declared the same way an
# operator declares one. Everything downstream then reads a single
# uniformly-typed `cfg.oidc.clients` and cannot tell the parts apart —
@ -964,7 +973,7 @@ in
# Authelia's own gateway surface: the vhost that fronts it and the
# name the hive resolver answers for. Both live here rather than in
# the gateway, and both are inside `deployCfg.authelia.enable` — that guard is the
# the gateway, and both are inside `cfg.enable` — that guard is the
# load-bearing part.
#
# ⚠️ Every hive in a swarm knows `authelia.url`, but only the host
@ -1009,9 +1018,7 @@ in
# 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
# 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.swarm-otel.enable
{
services.hyperhive.swarm.otel.scrapeTargets = lib.mkIf config.services.hyperhive.swarm.otel.enable {
authelia = "127.0.0.1:${toString cfg.metricsPort}";
};
@ -1399,7 +1406,7 @@ in
# collector is registered — see `metricsRule` above,
# which is where the reasoning for both halves lives.
lib.optional forgeCfg.behindGateway metricsRule
++ lib.optional deployCfg.swarm-ui.enable {
++ lib.optional uiCfg.enable {
domain = uiCfg.domain;
subject = [ "group:${operatorGroup}" ];
policy = "one_factor";

View file

@ -15,7 +15,6 @@
}:
let
cfg = config.services.hyperhive.swarm.controller;
deployCfg = config.services.hyperhive.deploy;
autheliaCfg = config.services.hyperhive.swarm.authelia;
# What `swarmctl` needs in order to act on authelia from the host.
@ -25,7 +24,7 @@ let
# 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
# nothing reads, which is the failure mode that costs an afternoon.
autheliaEnv = lib.optionalAttrs deployCfg.authelia.enable {
autheliaEnv = lib.optionalAttrs autheliaCfg.enable {
# The CONFIGURED authelia, not whatever is on PATH: the argon2
# parameters baked into a hash have to match the verifier's.
SWARMCTL_AUTHELIA_BIN = "${autheliaCfg.package}/bin/authelia";
@ -120,7 +119,7 @@ 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:<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). `swarm.ui.enable`
# 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.
#
@ -130,7 +129,7 @@ let
# 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 deployCfg.swarm-ui.enable {
webhookEnv = lib.optionalAttrs uiCfg.enable {
SWARM_CONTROLLER_PUBLIC_URL = "https://${uiCfg.domain}";
};
@ -174,14 +173,14 @@ let
# so the two ends agree on exactly one path, `/v1/metrics`, arrived at
# from opposite directions.
#
# Gated on `deployCfg.swarm-otel.enable` alone (a swarm-wide fact, not "enabled
# Gated on `otelSwarmCfg.enable` alone (a swarm-wide fact, not "enabled
# HERE"): the collector is reachable by name wherever it runs, so a
# controller not co-located with it now exports over https instead of
# exporting nothing — the graceful-absence case left is "no swarm-otel
# anywhere in this swarm at all," which `forgeEnv` already models the
# same way.
otelSwarmCfg = config.services.hyperhive.swarm.otel;
otelEnv = lib.optionalAttrs deployCfg.swarm-otel.enable {
otelEnv = lib.optionalAttrs otelSwarmCfg.enable {
OTEL_EXPORTER_OTLP_ENDPOINT = "https://${otelSwarmCfg.domain}/${otelSwarmCfg.producerName}";
# Checked by `swarm-otel.nix`'s `oidc/${producerName}` authenticator
# against exactly this controller's own registered audience (see the
@ -215,7 +214,7 @@ in
name = "swarm-controller";
consumers = [ "swarm-controller" ];
hostUnit = true;
enable = deployCfg.swarm-controller.enable;
enable = cfg.enable;
})
];
@ -239,12 +238,22 @@ in
'';
};
# `enable` moved to `services.hyperhive.deploy.swarm-controller.enable` — see
# ./deploy.nix. `services.hyperhive.enableAllLocalDefaults` still
# asserts it, and that was never an exception to "not derived from
# services.hyperhive.enable": that mode says "this box is the whole
# deployment", which answers the topology question outright, where
# `enable` alone never can. What stays here is what the daemon IS.
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Run the swarm-controller daemon on this host. Off by default and
deliberately not derived from `services.hyperhive.enable`: a swarm
has one controller, so enabling it per hive is a decision about
swarm topology, not about whether hyperhive is installed.
`services.hyperhive.enableAllLocalDefaults` does assert it, and
that is not an exception to the rule above — it is the rule
applied. That mode says "this box is the whole deployment", which
answers the topology question outright, where
`services.hyperhive.enable` alone never can.
'';
};
package = lib.mkOption {
type = lib.types.package;
@ -451,7 +460,7 @@ in
authBridgeUrl = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = if deployCfg.authelia.enable then autheliaCfg.bridgeUrl else null;
default = if autheliaCfg.enable then autheliaCfg.bridgeUrl else null;
defaultText = lib.literalExpression ''
authelia's own `bridgeUrl` when this host also runs
`swarm-authelia`, else null
@ -474,7 +483,7 @@ in
};
};
config = lib.mkIf (config.services.hyperhive.enable && deployCfg.swarm-controller.enable) {
config = lib.mkIf (config.services.hyperhive.enable && cfg.enable) {
# The daemon and the oneshot that mints its credential — the second one
# failing leaves the first running and unable to authenticate anywhere.
services.hyperhive.swarm.otel.journaldUnits = [
@ -499,7 +508,7 @@ in
# 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
# queue's own client declaration.
services.hyperhive.swarm.authelia.oidc.clients = lib.mkIf deployCfg.authelia.enable [
services.hyperhive.swarm.authelia.oidc.clients = lib.mkIf autheliaCfg.enable [
{
id = queueClientId;
description = "HyperHive swarm controller";
@ -539,7 +548,7 @@ in
services.hyperhive.swarm.controller.queue.natsUrl is unset.
It defaults to loopback only when this host also runs the queue
(`services.hyperhive.deploy.nats.enable`). A controller on its
(`services.hyperhive.swarm.nats.enable`). A controller on its
own host has to be told where the queue is.
'';
}

View file

@ -16,7 +16,6 @@
}:
let
cfg = config.services.hyperhive.swarm.grafana;
deployCfg = config.services.hyperhive.deploy;
networkCfg = config.services.hyperhive.network;
hyperhiveCfg = config.services.hyperhive;
gatewayCfg = hyperhiveCfg.gateway;
@ -77,7 +76,7 @@ let
# 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
# forge and matrix modules document.
ssoLocal = deployCfg.grafana.enable && deployCfg.authelia.enable;
ssoLocal = cfg.enable && autheliaCfg.enable;
autheliaUrl = toString autheliaCfg.url;
# Where the plaintext lands inside the container. Under /var/lib rather
@ -107,12 +106,18 @@ let
in
{
# `enable` moved to `services.hyperhive.deploy.grafana.enable` — see
# ./deploy.nix. Whether this host runs the swarm's Grafana is a
# deployment decision, and `swarm.*` has to be identical on every host.
# What stays here is what the service IS: its package, domain, and
# wiring.
options.services.hyperhive.swarm.grafana = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Run the swarm's metrics UI on this host. Off by default and not
derived from {option}`services.hyperhive.enable`: a swarm has one
Grafana, so enabling it is a decision about swarm topology rather
than about whether hyperhive is installed.
'';
};
package = lib.mkOption {
type = lib.types.package;
default = pkgs.grafana;
@ -307,8 +312,8 @@ in
};
};
config = lib.mkIf (hyperhiveCfg.enable && deployCfg.grafana.enable) {
# The gateway name and the quick-link, both inside `deploy.grafana` — that
config = lib.mkIf (hyperhiveCfg.enable && cfg.enable) {
# The gateway name and the quick-link, both inside `cfg.enable` — that
# 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
# hive declaring the vhost would answer for a service it does not have.
@ -408,9 +413,7 @@ in
# Declared here rather than in the collector's module, per that option's
# rule: an entry exists where the service that named it runs.
services.hyperhive.swarm.otel.scrapeTargets =
lib.mkIf config.services.hyperhive.deploy.swarm-otel.enable
{
services.hyperhive.swarm.otel.scrapeTargets = lib.mkIf config.services.hyperhive.swarm.otel.enable {
grafana = "127.0.0.1:${toString cfg.metricsPort}";
};

View file

@ -7,7 +7,6 @@
let
cfg = config.services.hyperhive.swarm.nats;
autheliaCfg = config.services.hyperhive.swarm.authelia;
deployCfg = config.services.hyperhive.deploy;
autheliaUrl = autheliaCfg.url;
networkCfg = config.services.hyperhive.network;
# Read even when the controller runs on a different host: what is needed
@ -49,7 +48,7 @@ let
hostPath = name: "${secretDir}/${name}";
# The responder needs all three credentials. Gating on them rather than
# on `deployCfg.nats.enable` keeps a half-configured hive at "queue up, denying
# on `cfg.enable` keeps a half-configured hive at "queue up, denying
# everyone" instead of "unit crash-looping on a missing file".
#
# In auto mode the seeds are minted on this host before the container
@ -210,9 +209,18 @@ in
# per-service vhost pattern do not apply.
options.services.hyperhive.swarm.nats = {
# `enable` moved to `services.hyperhive.deploy.nats.enable` — see
# ./deploy.nix. What stays here is what the queue IS: its domain,
# ports, accounts and callout wiring.
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Run the swarm's message queue in a `swarm-nats` container on this
host. A swarm has one queue, so this belongs on the same host as
the rest of the shared services.
Off by default, and off means *absent*: no container is created
and nothing else in the evaluated config changes.
'';
};
# ⚠️ Deliberately NO `package` option, unlike this module's siblings.
# `services.nats` upstream does not expose one — it resolves
@ -399,7 +407,7 @@ in
};
};
config = lib.mkIf deployCfg.nats.enable {
config = lib.mkIf cfg.enable {
# The responder as well as the server: a denial reaches the client as a
# timeout, so the server's own log is the only place it is an error.
services.hyperhive.swarm.otel.journaldUnits = [
@ -414,7 +422,7 @@ in
# several layers from "the operator never set the issuer".
assertion = cfg.autoGenerateCallout || cfg.calloutIssuerPublicKey != "";
message = ''
services.hyperhive.deploy.nats.enable requires
services.hyperhive.swarm.nats.enable requires
nats.calloutIssuerPublicKey — the public half of the account
nkey that signs user JWTs for this queue.
@ -431,7 +439,7 @@ in
# somebody connects.
assertion = cfg.autoGenerateCallout || cfg.calloutUserPublicKey != "";
message = ''
services.hyperhive.deploy.nats.enable requires
services.hyperhive.swarm.nats.enable requires
nats.calloutUserPublicKey — the public half of the user nkey
the auth-callout responder authenticates as.
@ -476,7 +484,7 @@ in
{
assertion = autheliaUrl != null;
message = ''
services.hyperhive.deploy.nats.enable requires
services.hyperhive.swarm.nats.enable requires
services.hyperhive.swarm.authelia.url — the queue authenticates
clients by validating tokens that authelia issued.
@ -506,7 +514,7 @@ in
# authorization grant 'client_credentials'`. Introspection — which
# is all the responder needs today — worked throughout, which is why
# nothing was visibly broken while the comment was untrue.
services.hyperhive.swarm.authelia.oidc.clients = lib.mkIf deployCfg.authelia.enable [
services.hyperhive.swarm.authelia.oidc.clients = lib.mkIf autheliaCfg.enable [
{
id = cfg.clientId;
description = "HyperHive swarm queue";
@ -535,9 +543,7 @@ in
# ⚠️ `swarm.otel`, not `hyperhive.otel` — two collectors one word apart,
# and only this one reads `scrapeTargets`. Written in full so the gate
# and the option it gates are visibly the same path.
services.hyperhive.swarm.otel.scrapeTargets =
lib.mkIf config.services.hyperhive.deploy.swarm-otel.enable
{
services.hyperhive.swarm.otel.scrapeTargets = lib.mkIf config.services.hyperhive.swarm.otel.enable {
nats = "127.0.0.1:${toString cfg.metricsPort}";
};
@ -677,7 +683,7 @@ in
# door nobody can open, which is the deliberate interim state.
#
# ⚠️ It is gated on the seeds being configured rather than on
# `deployCfg.nats.enable`, so a half-configured hive gets a running,
# `cfg.enable`, so a half-configured hive gets a running,
# refusing queue instead of a unit that crash-loops on a
# missing file. A queue that denies everyone is a legible
# failure; a restart loop is not.
@ -761,7 +767,7 @@ in
# being up says nothing about whether its in-container secrets unit
# has finished. The wait in the script is what actually closes it;
# this only stops us spinning for the full timeout on every boot.
++ lib.optional deployCfg.authelia.enable "container@${autheliaCfg.machine}.service";
++ lib.optional autheliaCfg.enable "container@${autheliaCfg.machine}.service";
requires = lib.optional cfg.autoGenerateCallout "swarm-nats-callout-keys.service";
serviceConfig = {
Type = "oneshot";

Some files were not shown because too many files have changed in this diff Show more