Compare commits
29 changed files with 255 additions and 418 deletions
|
|
@ -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.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
|
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
|
||||||
|
|
|
||||||
|
|
@ -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>/_matrix/*` | `chat.<swarm>` | tuwunel (`8008`) | `matrix.gatewayHost != null` |
|
||||||
| `chat.<swarm>/` | `chat.<swarm>` | fluffychat-web static | `matrix.gui.enable` |
|
| `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` |
|
| `chat.<swarm>/config.json` | `chat.<swarm>` | inline JSON (FluffyChat boot config) | `matrix.gui.enable && domain != null` |
|
||||||
| `auth.<swarm>/` | `auth.<swarm>` | authelia (`9091`) | `deploy.authelia` |
|
| `auth.<swarm>/` | `auth.<swarm>` | authelia (`9091`) | `swarm.authelia.enable` |
|
||||||
| `<swarm>/` | `<swarm>` | swarm-ui dist (static), behind an authelia subrequest | `deploy.swarm-ui` |
|
| `<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.
|
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`
|
- `<hive-domain>` → `127.0.0.1`
|
||||||
- `forge.<swarm>` → `127.0.0.1` (when forge.behindGateway)
|
- `forge.<swarm>` → `127.0.0.1` (when forge.behindGateway)
|
||||||
- `chat.<swarm>` → `127.0.0.1` (when matrix.gatewayHost set)
|
- `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.
|
`lib.unique` de-dupes if any sub-domain happens to equal another entry. Operators with real DNS leave it off.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ services.hyperhive.otel = {
|
||||||
|
|
||||||
`enable` is the single gate. `endpoint` is where telemetry ends up after it
|
`enable` is the single gate. `endpoint` is where telemetry ends up after it
|
||||||
leaves the swarm — optional, because the swarm's own metrics store
|
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
|
telemetry goes to both. See
|
||||||
[`swarm/services.md`](swarm/services.md#metrics-victoriametrics--grafana).
|
[`swarm/services.md`](swarm/services.md#metrics-victoriametrics--grafana).
|
||||||
|
|
||||||
|
|
@ -80,7 +80,7 @@ the host is:
|
||||||
| | runs where | receives from | does |
|
| | 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 |
|
| **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.
|
An all-local host runs both, and needs nothing said about the hop between them.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.swarm-controller.enable`. systemd `StateDirectory=`,
|
`services.hyperhive.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
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ echo "hunter2" | hivectl gateway create-user mara --password-stdin
|
||||||
hivectl gateway list-users
|
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
|
⚠️ **Required to finish the install, not optional.** Authelia treats an
|
||||||
empty user store as a fatal startup error, so until this runs the
|
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:
|
Detail, including what the password is and why this stays manual:
|
||||||
[`swarm/sso.md`](swarm/sso.md).
|
[`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
|
Nothing to run — it is served on the swarm apex
|
||||||
(`https://<swarm.domain>/`) as soon as the host rebuilds. Two things
|
(`https://<swarm.domain>/`) as soon as the host rebuilds. Two things
|
||||||
|
|
|
||||||
|
|
@ -276,7 +276,7 @@ migrating agent keeps one unbroken incremental chain. See
|
||||||
|
|
||||||
## Swarm controller
|
## 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
|
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
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ One authelia per swarm, in a `swarm-authelia` container, at
|
||||||
provider, differentiated by roles and claims rather than by mechanism —
|
provider, differentiated by roles and claims rather than by mechanism —
|
||||||
there is one IdP and one auth path.
|
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.enableRequiredServices`.
|
||||||
- **`swarm.authelia.url`** — where clients are sent to authenticate.
|
- **`swarm.authelia.url`** — where clients are sent to authenticate.
|
||||||
Present on **every** hive, defaulting to this host's own instance only
|
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:
|
empty. To run one without the other, set it directly:
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
services.hyperhive.deploy.victoriametrics.enable = true;
|
services.hyperhive.swarm.victoriametrics.enable = true;
|
||||||
services.hyperhive.deploy.grafana.enable = false;
|
services.hyperhive.swarm.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.**
|
||||||
|
|
|
||||||
|
|
@ -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
|
authelia binds loopback only. The **gateway** on the host running it
|
||||||
publishes it as `auth.<swarm.domain>` — vhost, dnsmasq record and TLS
|
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
|
separately. (Details, including why a client hive must not declare that
|
||||||
vhost: [`../gateway.md`](../gateway.md).)
|
vhost: [`../gateway.md`](../gateway.md).)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ answers for one host. This one is the view *across* hives.
|
||||||
## Enabling
|
## Enabling
|
||||||
|
|
||||||
```nix
|
```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`:
|
Derived from the controller rather than from `enableRequiredServices`:
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
./hyperhive.nix
|
./hyperhive.nix
|
||||||
./deploy.nix
|
|
||||||
./local-defaults.nix
|
./local-defaults.nix
|
||||||
./hive-c0re
|
./hive-c0re
|
||||||
./hive-ci.nix
|
./hive-ci.nix
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -22,7 +22,6 @@ let
|
||||||
# assertion below turns into an eval failure rather than a discovery
|
# assertion below turns into an eval failure rather than a discovery
|
||||||
# request to `null/.well-known/...`.
|
# request to `null/.well-known/...`.
|
||||||
autheliaCfg = config.services.hyperhive.swarm.authelia;
|
autheliaCfg = config.services.hyperhive.swarm.authelia;
|
||||||
deployCfg = config.services.hyperhive.deploy;
|
|
||||||
autheliaUrl = autheliaCfg.url;
|
autheliaUrl = autheliaCfg.url;
|
||||||
autheliaDiscoveryUrl = "${toString autheliaUrl}/.well-known/openid-configuration";
|
autheliaDiscoveryUrl = "${toString autheliaUrl}/.well-known/openid-configuration";
|
||||||
|
|
||||||
|
|
@ -30,7 +29,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.enable;
|
ssoLocal = autheliaCfg.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
|
||||||
|
|
|
||||||
|
|
@ -25,14 +25,13 @@ let
|
||||||
# below turns into an eval failure rather than a discovery request to
|
# below turns into an eval failure rather than a discovery request to
|
||||||
# `null/.well-known/…`.
|
# `null/.well-known/…`.
|
||||||
autheliaCfg = config.services.hyperhive.swarm.authelia;
|
autheliaCfg = config.services.hyperhive.swarm.authelia;
|
||||||
deployCfg = config.services.hyperhive.deploy;
|
|
||||||
autheliaUrl = autheliaCfg.url;
|
autheliaUrl = autheliaCfg.url;
|
||||||
|
|
||||||
# The all-local case: this host runs BOTH the homeserver and the swarm's
|
# 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
|
# 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.enable;
|
ssoLocal = autheliaCfg.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
|
||||||
|
|
|
||||||
|
|
@ -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.swarm-controller.enable "${cfg.stateDir}/trust-bundle.pem";
|
lib.mkIf hyperhiveCfg.swarm.controller.enable "${cfg.stateDir}/trust-bundle.pem";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.swarm-controller.enable`), and the
|
controller (`services.hyperhive.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
|
||||||
|
|
@ -80,13 +80,22 @@ in
|
||||||
# reach whoever runs the responder, and minting them here would move
|
# reach whoever runs the responder, and minting them here would move
|
||||||
# that hand-off somewhere less visible rather than removing it.
|
# that hand-off somewhere less visible rather than removing it.
|
||||||
nats.autoGenerateCallout = lib.mkDefault cfg.enableAllLocalDefaults;
|
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
|
# The controller's queue coordinates. Co-location is what makes these
|
||||||
# derivable at all — loopback only reaches the queue when the queue is
|
# derivable at all — loopback only reaches the queue when the queue is
|
||||||
# here, and the minted client secret only exists on the host authelia
|
# 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
|
# 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`.
|
# 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
|
# inside those defaults is the mixing this file exists to prevent: the
|
||||||
# option would be describing a deployment shape instead of describing
|
# option would be describing a deployment shape instead of describing
|
||||||
# itself, and "what does all-local turn on?" would stop having one
|
# 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"
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.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
|
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.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
|
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.enable`) is the
|
({option}`services.hyperhive.swarm.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.
|
||||||
'';
|
'';
|
||||||
|
|
@ -232,7 +232,7 @@ in
|
||||||
type = lib.types.nullOr lib.types.str;
|
type = lib.types.nullOr lib.types.str;
|
||||||
default =
|
default =
|
||||||
if
|
if
|
||||||
config.services.hyperhive.deploy.authelia.enable && config.services.hyperhive.hiveName != null
|
config.services.hyperhive.swarm.authelia.enable && config.services.hyperhive.hiveName != null
|
||||||
then
|
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"
|
||||||
|
|
@ -284,7 +284,6 @@ in
|
||||||
let
|
let
|
||||||
otel = config.services.hyperhive.otel;
|
otel = config.services.hyperhive.otel;
|
||||||
autheliaCfg = config.services.hyperhive.swarm.authelia;
|
autheliaCfg = config.services.hyperhive.swarm.authelia;
|
||||||
deployCfg = config.services.hyperhive.deploy;
|
|
||||||
swarmOtelCfg = config.services.hyperhive.swarm.otel;
|
swarmOtelCfg = config.services.hyperhive.swarm.otel;
|
||||||
hiveName = config.services.hyperhive.hiveName;
|
hiveName = config.services.hyperhive.hiveName;
|
||||||
listen = "${config.services.hyperhive.network.bridgeIp}:${toString otel.collector.port}";
|
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
|
# 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.enable "container@${autheliaCfg.machine}.service";
|
after = lib.optional autheliaCfg.enable "container@${autheliaCfg.machine}.service";
|
||||||
requires = lib.optional deployCfg.authelia.enable "container@${autheliaCfg.machine}.service";
|
requires = lib.optional autheliaCfg.enable "container@${autheliaCfg.machine}.service";
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
Type = "oneshot";
|
Type = "oneshot";
|
||||||
RemainAfterExit = true;
|
RemainAfterExit = true;
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,6 @@ let
|
||||||
hyperhiveDomain = hyperhiveCfg.domain;
|
hyperhiveDomain = hyperhiveCfg.domain;
|
||||||
swarmDomain = hyperhiveCfg.swarm.domain;
|
swarmDomain = hyperhiveCfg.swarm.domain;
|
||||||
uiCfg = hyperhiveCfg.swarm.ui;
|
uiCfg = hyperhiveCfg.swarm.ui;
|
||||||
deployCfg = hyperhiveCfg.deploy;
|
|
||||||
forgeCfg = hyperhiveCfg.swarm.forge;
|
forgeCfg = hyperhiveCfg.swarm.forge;
|
||||||
|
|
||||||
# Group an account must hold to reach operator-only surfaces. Named
|
# Group an account must hold to reach operator-only surfaces. Named
|
||||||
|
|
@ -345,12 +344,22 @@ let
|
||||||
'';
|
'';
|
||||||
in
|
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 = {
|
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 {
|
package = lib.mkOption {
|
||||||
type = lib.types.package;
|
type = lib.types.package;
|
||||||
default = pkgs.authelia;
|
default = pkgs.authelia;
|
||||||
|
|
@ -427,7 +436,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.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'';
|
defaultText = lib.literalExpression ''if enable then "https://''${domain}" else null'';
|
||||||
example = "https://auth.example.com";
|
example = "https://auth.example.com";
|
||||||
description = ''
|
description = ''
|
||||||
|
|
@ -467,8 +476,8 @@ in
|
||||||
|
|
||||||
oidc.hiveIdentities = lib.mkOption {
|
oidc.hiveIdentities = lib.mkOption {
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
default = deployCfg.nats.enable;
|
default = hyperhiveCfg.swarm.nats.enable;
|
||||||
defaultText = lib.literalExpression "services.hyperhive.deploy.nats.enable";
|
defaultText = lib.literalExpression "services.hyperhive.swarm.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 +819,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.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'';
|
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 +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
|
# 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 +973,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.enable` — that guard is the
|
# the gateway, and both are inside `cfg.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,11 +1018,9 @@ 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 =
|
services.hyperhive.swarm.otel.scrapeTargets = lib.mkIf config.services.hyperhive.swarm.otel.enable {
|
||||||
lib.mkIf config.services.hyperhive.deploy.swarm-otel.enable
|
authelia = "127.0.0.1:${toString cfg.metricsPort}";
|
||||||
{
|
};
|
||||||
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 —
|
||||||
|
|
@ -1399,7 +1406,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.enable {
|
++ lib.optional uiCfg.enable {
|
||||||
domain = uiCfg.domain;
|
domain = uiCfg.domain;
|
||||||
subject = [ "group:${operatorGroup}" ];
|
subject = [ "group:${operatorGroup}" ];
|
||||||
policy = "one_factor";
|
policy = "one_factor";
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.services.hyperhive.swarm.controller;
|
cfg = config.services.hyperhive.swarm.controller;
|
||||||
deployCfg = config.services.hyperhive.deploy;
|
|
||||||
autheliaCfg = config.services.hyperhive.swarm.authelia;
|
autheliaCfg = config.services.hyperhive.swarm.authelia;
|
||||||
|
|
||||||
# What `swarmctl` needs in order to act on authelia from the host.
|
# 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
|
# 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.enable {
|
autheliaEnv = lib.optionalAttrs autheliaCfg.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";
|
||||||
|
|
@ -120,7 +119,7 @@ let
|
||||||
# ⚠️ Correct ONLY while the vhost and this daemon share a host, and they do
|
# ⚠️ Correct ONLY while the vhost and this daemon share a host, and they do
|
||||||
# by construction: the UI's `/api/` location proxies
|
# by construction: the UI's `/api/` location proxies
|
||||||
# `http://unix:<socketPath>`, a path that resolves nowhere else, and this
|
# `http://unix:<socketPath>`, a path that resolves nowhere else, and this
|
||||||
# daemon binds no TCP address at all (see the header). `deploy.swarm-ui`
|
# daemon binds no TCP address at all (see the header). `swarm.ui.enable`
|
||||||
# therefore is not a guess about *some* host publishing the endpoint — it is
|
# therefore is not a guess about *some* host publishing the endpoint — it is
|
||||||
# the flag that declares that vhost, on the box holding the socket.
|
# the 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
|
# the moment to add an explicit `publicUrl` option — not before, because
|
||||||
# until then there is exactly one derivable answer and an option would only
|
# until then there is exactly one derivable answer and an option would only
|
||||||
# be a second place to get it wrong.
|
# be a second place to get it wrong.
|
||||||
webhookEnv = lib.optionalAttrs deployCfg.swarm-ui.enable {
|
webhookEnv = lib.optionalAttrs uiCfg.enable {
|
||||||
SWARM_CONTROLLER_PUBLIC_URL = "https://${uiCfg.domain}";
|
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
|
# so the two ends agree on exactly one path, `/v1/metrics`, arrived at
|
||||||
# from opposite directions.
|
# 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
|
# 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.swarm-otel.enable {
|
otelEnv = lib.optionalAttrs otelSwarmCfg.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 +214,7 @@ in
|
||||||
name = "swarm-controller";
|
name = "swarm-controller";
|
||||||
consumers = [ "swarm-controller" ];
|
consumers = [ "swarm-controller" ];
|
||||||
hostUnit = true;
|
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
|
enable = lib.mkOption {
|
||||||
# ./deploy.nix. `services.hyperhive.enableAllLocalDefaults` still
|
type = lib.types.bool;
|
||||||
# asserts it, and that was never an exception to "not derived from
|
default = false;
|
||||||
# services.hyperhive.enable": that mode says "this box is the whole
|
description = ''
|
||||||
# deployment", which answers the topology question outright, where
|
Run the swarm-controller daemon on this host. Off by default and
|
||||||
# `enable` alone never can. What stays here is what the daemon IS.
|
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 {
|
package = lib.mkOption {
|
||||||
type = lib.types.package;
|
type = lib.types.package;
|
||||||
|
|
@ -451,7 +460,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.enable then autheliaCfg.bridgeUrl else null;
|
default = if autheliaCfg.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 +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
|
# 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 +508,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.enable [
|
services.hyperhive.swarm.authelia.oidc.clients = lib.mkIf autheliaCfg.enable [
|
||||||
{
|
{
|
||||||
id = queueClientId;
|
id = queueClientId;
|
||||||
description = "HyperHive swarm controller";
|
description = "HyperHive swarm controller";
|
||||||
|
|
@ -539,7 +548,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.enable`). A controller on its
|
(`services.hyperhive.swarm.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.
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.services.hyperhive.swarm.grafana;
|
cfg = config.services.hyperhive.swarm.grafana;
|
||||||
deployCfg = config.services.hyperhive.deploy;
|
|
||||||
networkCfg = config.services.hyperhive.network;
|
networkCfg = config.services.hyperhive.network;
|
||||||
hyperhiveCfg = config.services.hyperhive;
|
hyperhiveCfg = config.services.hyperhive;
|
||||||
gatewayCfg = hyperhiveCfg.gateway;
|
gatewayCfg = hyperhiveCfg.gateway;
|
||||||
|
|
@ -77,7 +76,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.enable && deployCfg.authelia.enable;
|
ssoLocal = cfg.enable && autheliaCfg.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,12 +106,18 @@ let
|
||||||
|
|
||||||
in
|
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 = {
|
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 {
|
package = lib.mkOption {
|
||||||
type = lib.types.package;
|
type = lib.types.package;
|
||||||
default = pkgs.grafana;
|
default = pkgs.grafana;
|
||||||
|
|
@ -307,8 +312,8 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf (hyperhiveCfg.enable && deployCfg.grafana.enable) {
|
config = lib.mkIf (hyperhiveCfg.enable && cfg.enable) {
|
||||||
# The gateway name and the quick-link, both inside `deploy.grafana` — that
|
# 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
|
# 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
|
||||||
# hive declaring the vhost would answer for a service it does not have.
|
# hive declaring the vhost would answer for a service it does not have.
|
||||||
|
|
@ -408,11 +413,9 @@ 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 =
|
services.hyperhive.swarm.otel.scrapeTargets = lib.mkIf config.services.hyperhive.swarm.otel.enable {
|
||||||
lib.mkIf config.services.hyperhive.deploy.swarm-otel.enable
|
grafana = "127.0.0.1:${toString cfg.metricsPort}";
|
||||||
{
|
};
|
||||||
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.
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@
|
||||||
let
|
let
|
||||||
cfg = config.services.hyperhive.swarm.nats;
|
cfg = config.services.hyperhive.swarm.nats;
|
||||||
autheliaCfg = config.services.hyperhive.swarm.authelia;
|
autheliaCfg = config.services.hyperhive.swarm.authelia;
|
||||||
deployCfg = config.services.hyperhive.deploy;
|
|
||||||
autheliaUrl = autheliaCfg.url;
|
autheliaUrl = autheliaCfg.url;
|
||||||
networkCfg = config.services.hyperhive.network;
|
networkCfg = config.services.hyperhive.network;
|
||||||
# Read even when the controller runs on a different host: what is needed
|
# Read even when the controller runs on a different host: what is needed
|
||||||
|
|
@ -49,7 +48,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.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".
|
# 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,9 +209,18 @@ 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.enable` — see
|
enable = lib.mkOption {
|
||||||
# ./deploy.nix. What stays here is what the queue IS: its domain,
|
type = lib.types.bool;
|
||||||
# ports, accounts and callout wiring.
|
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.
|
# ⚠️ Deliberately NO `package` option, unlike this module's siblings.
|
||||||
# `services.nats` upstream does not expose one — it resolves
|
# `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
|
# 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 +422,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.enable requires
|
services.hyperhive.swarm.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 +439,7 @@ in
|
||||||
# somebody connects.
|
# somebody connects.
|
||||||
assertion = cfg.autoGenerateCallout || cfg.calloutUserPublicKey != "";
|
assertion = cfg.autoGenerateCallout || cfg.calloutUserPublicKey != "";
|
||||||
message = ''
|
message = ''
|
||||||
services.hyperhive.deploy.nats.enable requires
|
services.hyperhive.swarm.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 +484,7 @@ in
|
||||||
{
|
{
|
||||||
assertion = autheliaUrl != null;
|
assertion = autheliaUrl != null;
|
||||||
message = ''
|
message = ''
|
||||||
services.hyperhive.deploy.nats.enable requires
|
services.hyperhive.swarm.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 +514,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.enable [
|
services.hyperhive.swarm.authelia.oidc.clients = lib.mkIf autheliaCfg.enable [
|
||||||
{
|
{
|
||||||
id = cfg.clientId;
|
id = cfg.clientId;
|
||||||
description = "HyperHive swarm queue";
|
description = "HyperHive swarm queue";
|
||||||
|
|
@ -535,11 +543,9 @@ 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 =
|
services.hyperhive.swarm.otel.scrapeTargets = lib.mkIf config.services.hyperhive.swarm.otel.enable {
|
||||||
lib.mkIf config.services.hyperhive.deploy.swarm-otel.enable
|
nats = "127.0.0.1:${toString cfg.metricsPort}";
|
||||||
{
|
};
|
||||||
nats = "127.0.0.1:${toString cfg.metricsPort}";
|
|
||||||
};
|
|
||||||
|
|
||||||
containers.swarm-nats = {
|
containers.swarm-nats = {
|
||||||
autoStart = true;
|
autoStart = true;
|
||||||
|
|
@ -677,7 +683,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.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
|
# 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.
|
||||||
|
|
@ -761,7 +767,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.enable "container@${autheliaCfg.machine}.service";
|
++ lib.optional autheliaCfg.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";
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue