Watch
0
0
Fork
You've already forked hyperhive
0

Compare commits

..
14 changed files with 253 additions and 401 deletions

View file

@ -3,10 +3,9 @@
Private Forgejo instance running in a nixos-container, used as the
swarm's persistent code-collaboration surface (issues, PRs, reviews,
attachments). Configured via `services.hyperhive.swarm.forge.*` (what the
forge IS to every hive — names, ports, URLs) plus
forge IS to every hive — its package, names, ports, URLs) plus
`services.hyperhive.deploy.forgejo.*` (what the host running it decides —
which build it runs, how it's served, what it mirrors, where its
host-local secrets sit).
how it's served, what it mirrors, where its host-local secrets sit).
Container
shape, ROOT_URL / sub-domain routing, and operator-vs-in-cluster URL
handling live in [`docs/networking/gateway.md`](../networking/gateway.md); this file owns the

View file

@ -625,7 +625,7 @@ existing in the page DOM.
A static matrix web client (default `pkgs.fluffychat-web` rebuilt
with `--base-href /matrix/`, swappable via
`services.hyperhive.deploy.matrix.gui.package`) served by the hive-gateway
`services.hyperhive.swarm.matrix.gui.package`) served by the hive-gateway
nginx container at `/matrix/` when
`services.hyperhive.deploy.matrix.gui.enable` is on (defaults to
`matrix.enable`). c0re signals availability via the

View file

@ -142,19 +142,19 @@
agentBaseToplevel = lib.mkDefault self.packages.x86_64-linux.agent-base-toplevel;
managerToplevel = lib.mkDefault self.packages.x86_64-linux.ruth-toplevel;
};
services.hyperhive.deploy.swarm-controller.package =
services.hyperhive.swarm.controller.package =
lib.mkDefault
self.packages.${pkgs.stdenv.hostPlatform.system}.swarm-controller;
services.hyperhive.deploy.swarm-controller.swarmctlPackage =
services.hyperhive.swarm.controller.swarmctlPackage =
lib.mkDefault
self.packages.${pkgs.stdenv.hostPlatform.system}.swarmctl;
services.hyperhive.deploy.swarm-ui.package =
services.hyperhive.swarm.ui.package =
lib.mkDefault
self.packages.${pkgs.stdenv.hostPlatform.system}.swarm-ui;
services.hyperhive.deploy.nats.authPackage =
services.hyperhive.swarm.nats.authPackage =
lib.mkDefault
self.packages.${pkgs.stdenv.hostPlatform.system}.swarm-nats-auth;
services.hyperhive.deploy.authelia.bridgePackage =
services.hyperhive.swarm.authelia.bridgePackage =
lib.mkDefault
self.packages.${pkgs.stdenv.hostPlatform.system}.swarm-authelia-bridge;
services.hyperhive.gateway.swaggerUiTheme =

View file

@ -164,10 +164,9 @@ in
)
# The queue's callout identity. What stays under `swarm.nats` is what the
# queue IS to every hive — the ports it answers on and the client id it is
# registered under; these five are what the host running it decides. The
# responder package is one of them now, further down with the other
# `*.package` moves.
# queue IS to every hive — the ports it answers on, the client id it is
# registered under, the responder package; these five are what the host
# running it decides.
#
# ⚠️ The two PUBLIC keys move with their seeds. A public key looks
# swarm-wide, and the user key genuinely is published to every client that
@ -196,10 +195,9 @@ in
)
# The rest of the forge split. What stays under `swarm.forge` is what the
# forge IS from any hive's point of view — the names and ports it answers
# on, the URLs it advertises, the client id it is registered under; these
# five are what the host running it decides. Its package moved as well,
# further down with the other `*.package` moves. `sso` splits
# forge IS from any hive's point of view — its package, the names and ports
# it answers on, the URLs it advertises, the client id it is registered
# under; these five are what the host running it decides. `sso` splits
# because its two halves are different facts: the client id must match the
# entry in authelia's register, the secret is a path on this machine.
#
@ -229,10 +227,9 @@ in
)
# The homeserver, split the same way. `swarm.matrix` keeps what it IS to
# every hive — the name it answers to, the ports and URLs it is reached
# on, the client id it is registered under. These six are what the host
# running it decides; its two packages moved as well, further down with
# the other `*.package` moves. ⚠️ `enable` is NOT here: it was renamed
# every hive — its package, the name it answers to, the ports and URLs it
# is reached on, the client id it is registered under. These six are what
# the host running it decides. ⚠️ `enable` is NOT here: it was renamed
# earlier and its shim lives in ./hive-matrix.nix, so that file is the one
# to check before assuming this table is the whole matrix story.
(lib.mkRenamedOptionModule
@ -262,9 +259,8 @@ in
# Retention is read only where the container is defined, so it is a
# decision of the host running the store rather than something the swarm
# agrees on. The two stores keep their domain and port in `swarm.*`,
# because a client hive needs those to reach them; `package` followed
# `enable` and `retentionPeriod` across in a later slice.
# agrees on. The two stores keep everything else — package, domain, port
# — in `swarm.*`, because a client hive needs those to reach them.
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "victoriametrics" "retentionPeriod" ]
[ "services" "hyperhive" "deploy" "victoriametrics" "retentionPeriod" ]
@ -339,68 +335,6 @@ in
[ "services" "hyperhive" "swarm" "wireguard" "persistentKeepalive" ]
[ "services" "hyperhive" "deploy" "wireguard" "persistentKeepalive" ]
)
# Which build a service runs is a decision of the host that runs it, so
# every `*.package` follows its `enable` across. The split these replace
# was the sharpest case: `gui.enable` had already moved while
# `gui.package` stayed, so one client's two halves sat in two namespaces
# and ./hive-matrix.nix said so in a comment above the option.
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "matrix" "package" ]
[ "services" "hyperhive" "deploy" "matrix" "package" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "matrix" "gui" "package" ]
[ "services" "hyperhive" "deploy" "matrix" "gui" "package" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "grafana" "package" ]
[ "services" "hyperhive" "deploy" "grafana" "package" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "victorialogs" "package" ]
[ "services" "hyperhive" "deploy" "victorialogs" "package" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "victoriametrics" "package" ]
[ "services" "hyperhive" "deploy" "victoriametrics" "package" ]
)
# With this one `services.hyperhive.swarm.ui` holds nothing at all — it had
# exactly two options and both have moved, so the namespace now exists only
# as this entry and the `enable` one above it.
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "ui" "package" ]
[ "services" "hyperhive" "deploy" "swarm-ui" "package" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "nats" "authPackage" ]
[ "services" "hyperhive" "deploy" "nats" "authPackage" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "authelia" "package" ]
[ "services" "hyperhive" "deploy" "authelia" "package" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "authelia" "bridgePackage" ]
[ "services" "hyperhive" "deploy" "authelia" "bridgePackage" ]
)
# ⚠️ `swarm-controller`, not `hive-controller` — see the note in
# ./swarm-controller.nix. Both namespaces exist, so the wrong target here
# lands on a live option in hive-c0re's tree instead of failing.
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "controller" "package" ]
[ "services" "hyperhive" "deploy" "swarm-controller" "package" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "controller" "swarmctlPackage" ]
[ "services" "hyperhive" "deploy" "swarm-controller" "swarmctlPackage" ]
)
# ⚠️ `deploy.forgejo`, not `deploy.forge` — the deploy-side name differs
# from the swarm-side one here, as it does for the controller and the UI.
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "forge" "package" ]
[ "services" "hyperhive" "deploy" "forgejo" "package" ]
)
];
# ⚠️ `deploy.forgejo` is declared in ./hive-ci.nix, not here, and it is the

View file

@ -232,6 +232,21 @@ in
'';
};
package = lib.mkOption {
type = lib.types.package;
default = pkgs.forgejo;
defaultText = lib.literalExpression "pkgs.forgejo";
description = ''
Forgejo package to run inside the container. Defaults to
`pkgs.forgejo` (the latest release line) rather than the
nixpkgs-module default of `pkgs.forgejo-lts`, because LTS
lags far behind on schema and the DB easily ends up "newer
than the binary" if the operator ever ran a non-LTS forgejo
against the same state dir. Override to `pkgs.forgejo-lts`
if you actively want the slower release train.
'';
};
rootUrl = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
@ -279,28 +294,13 @@ in
};
};
# What stays above is what the forge IS from any hive's point of view: the
# names and ports it answers on, the URLs it advertises, and the client id
# it is registered under. What lives here is what the host running it
# decides — which build it runs, how it is served, what it mirrors, and
# where its host-local secrets sit. Same rule as ./swarm-victorialogs.nix,
# and the renames are in ./deploy.nix with the rest.
# What stays above is what the forge IS from any hive's point of view: its
# package, the names and ports it answers on, the URLs it advertises, and
# the client id it is registered under. What lives here is what the host
# running it decides — how it is served, what it mirrors, and where its
# host-local secrets sit. Same rule as ./swarm-victorialogs.nix, and the
# renames are in ./deploy.nix with the rest.
options.services.hyperhive.deploy.forgejo = {
package = lib.mkOption {
type = lib.types.package;
default = pkgs.forgejo;
defaultText = lib.literalExpression "pkgs.forgejo";
description = ''
Forgejo package to run inside the container. Defaults to
`pkgs.forgejo` (the latest release line) rather than the
nixpkgs-module default of `pkgs.forgejo-lts`, because LTS
lags far behind on schema and the DB easily ends up "newer
than the binary" if the operator ever ran a non-LTS forgejo
against the same state dir. Override to `pkgs.forgejo-lts`
if you actively want the slower release train.
'';
};
behindGateway = lib.mkOption {
type = lib.types.bool;
default = config.services.hyperhive.enable;
@ -675,7 +675,7 @@ in
# the nix store — no separate hive-forge container rebuild needed,
# and no persistent-state directory involved.
staticRootWithTheme = pkgs.runCommand "forgejo-static-with-theme" { } ''
cp -r --no-preserve=mode,ownership ${deployCfg.forgejo.package.data}/. $out/
cp -r --no-preserve=mode,ownership ${cfg.package.data}/. $out/
mkdir -p $out/public/assets/css
cp ${./theme-catppuccin-vibec0re.css} \
$out/public/assets/css/theme-catppuccin-vibec0re.css
@ -747,7 +747,7 @@ in
services.forgejo = {
enable = true;
package = deployCfg.forgejo.package;
package = cfg.package;
database.type = "sqlite3";
lfs.enable = true;
settings = {
@ -1036,7 +1036,7 @@ in
FORGEJO_CUSTOM = "/var/lib/forgejo/custom";
};
path = [
deployCfg.forgejo.package
cfg.package
pkgs.coreutils
pkgs.gnugrep
];
@ -1126,7 +1126,7 @@ in
FORGEJO_CUSTOM = "/var/lib/forgejo/custom";
};
path = [
deployCfg.forgejo.package
cfg.package
pkgs.coreutils
pkgs.gnugrep
];

View file

@ -172,9 +172,8 @@ in
[ "services" "hyperhive" "swarm" "matrix" ]
)
# "does THIS host run it" is a per-host decision, and `swarm.*` is the
# namespace every host in the swarm agrees on. What stays under
# `swarm.matrix` is the homeserver's identity as every hive sees it —
# its server name, its addresses, its federation policy.
# namespace every host in the swarm agrees on. The rest of the matrix
# settings genuinely are swarm-wide and stay where they are.
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "matrix" "enable" ]
[ "services" "hyperhive" "deploy" "matrix" "enable" ]
@ -192,6 +191,17 @@ in
];
options.services.hyperhive.swarm.matrix = {
package = lib.mkOption {
type = lib.types.package;
default = pkgs.matrix-tuwunel;
defaultText = lib.literalExpression "pkgs.matrix-tuwunel";
description = ''
matrix-tuwunel package to run inside the container. Defaults
to nixpkgs's `pkgs.matrix-tuwunel`. Override to pin a
specific upstream if you need an unreleased feature.
'';
};
serverName = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
@ -328,6 +338,26 @@ in
'';
};
gui = {
# Whether this host serves it is `deploy.matrix.gui.enable`.
package = lib.mkOption {
type = lib.types.package;
default = fluffychat-web-fixed;
defaultText = lib.literalMD ''
`pkgs.fluffychat-web` with a `postInstall` patch that adds
the three files `flutter341.buildFlutterApplication` skips.
'';
description = ''
Static web client dist served at `gatewayHost`'s vhost
(`chat.<swarm-domain>` by default). Override
to swap fluffychat for hydrogen-web, cinny, element-web, or
an out-of-tree dist — any replacement is mounted at the
sub-domain root with the upstream-default `<base href "/">`,
no sub-path gymnastics needed.
'';
};
};
# This homeserver always delegates login to the swarm's authelia, as
# an OIDC relying party — matrix SSO (`m.login.sso`), offered
# alongside password login. No toggle: a homeserver in a swarm is a
@ -368,24 +398,13 @@ in
};
# What stays above is what the homeserver IS from any hive's point of view:
# the name it answers to, the ports and URLs it is reached on, and the client
# id it is registered under. What lives here is what the host running it
# decides — which build it runs, whether it is exposed, which peers it trusts,
# how large a request it accepts, and where its host-local secrets sit. Same rule as
# its package, the name it answers to, the ports and URLs it is reached on,
# and the client id it is registered under. What lives here is what the host
# running it decides — whether it is exposed, which peers it trusts, how large
# a request it accepts, and where its host-local secrets sit. Same rule as
# ./swarm-victorialogs.nix; `enable` already lives in ./deploy.nix, which also
# carries the renames.
options.services.hyperhive.deploy.matrix = {
package = lib.mkOption {
type = lib.types.package;
default = pkgs.matrix-tuwunel;
defaultText = lib.literalExpression "pkgs.matrix-tuwunel";
description = ''
matrix-tuwunel package to run inside the container. Defaults
to nixpkgs's `pkgs.matrix-tuwunel`. Override to pin a
specific upstream if you need an unreleased feature.
'';
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
@ -490,28 +509,11 @@ in
off, the dashboard's `M4TR1X →` tab is hidden. See
`docs/networking/gateway.md` for the discovery flow that lets clients
auto-find the sub-domain. The client build itself is
`deploy.matrix.gui.package` — which client, as opposed to whether
`swarm.matrix.gui.package` — which client, as opposed to whether
this host serves it.
'';
};
gui.package = lib.mkOption {
type = lib.types.package;
default = fluffychat-web-fixed;
defaultText = lib.literalMD ''
`pkgs.fluffychat-web` with a `postInstall` patch that adds
the three files `flutter341.buildFlutterApplication` skips.
'';
description = ''
Static web client dist served at `gatewayHost`'s vhost
(`chat.<swarm-domain>` by default). Override
to swap fluffychat for hydrogen-web, cinny, element-web, or
an out-of-tree dist — any replacement is mounted at the
sub-domain root with the upstream-default `<base href "/">`,
no sub-path gymnastics needed.
'';
};
sso.clientSecretFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
@ -632,7 +634,7 @@ in
# fluffychat at sub-domain root, SPA-fallback via the
# Accept-header `$matrix_spa_target` map above.
"/" = {
alias = "${deployCfg.matrix.gui.package}/";
alias = "${cfg.gui.package}/";
extraConfig = ''
try_files $uri $uri/ $matrix_spa_target =404;
'';
@ -997,7 +999,7 @@ in
services.matrix-tuwunel = {
enable = true;
package = deployCfg.matrix.package;
package = cfg.package;
settings.global = {
server_name = effectiveServerName;
# `address` + `port` are upstream `listOf` — wrap singles.
@ -1082,7 +1084,7 @@ in
"oidc_client_secret:${toString deployCfg.matrix.sso.clientSecretFile}"
];
environment.systemPackages = [ deployCfg.matrix.package ];
environment.systemPackages = [ cfg.package ];
};
};

View file

@ -320,12 +320,22 @@ let
'';
in
{
# `enable` and both packages moved to `services.hyperhive.deploy.authelia`
# — see ./deploy.nix. Whether this host runs the swarm's SSO provider, and
# which build it runs, are deployment decisions; 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.
# `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 = {
package = lib.mkOption {
type = lib.types.package;
default = pkgs.authelia;
defaultText = lib.literalExpression "pkgs.authelia";
description = ''
authelia package to run in the container. Defaults to
nixpkgs's; override to pin a specific upstream.
'';
};
port = lib.mkOption {
type = lib.types.port;
default = 9091;
@ -689,6 +699,17 @@ in
'';
};
bridgePackage = lib.mkOption {
type = lib.types.package;
defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarm-authelia-bridge";
description = ''
`swarm-authelia-bridge` package — the only process allowed to
write `usersFile`. Wired by default from this flake's own
package set (see `flake.nix`); override to run a different
build.
'';
};
bridgePort = lib.mkOption {
type = lib.types.port;
default = 9092;
@ -729,37 +750,16 @@ in
# What stays above is what authelia IS to every hive: where it answers
# (`url`), the OIDC register every service checks itself against, its port.
# What the host running the container decides is here — which two builds it
# runs, and three filesystem paths that only exist on the machine running
# `swarm-authelia`. A hive that does not run it has nothing at any of those
# paths. `enable` already lives in ./deploy.nix, which also carries the
# renames.
# What the host running the container decides is here, and all three of
# these are the same kind of thing — a filesystem path that only exists on
# the machine that runs `swarm-authelia`. A hive that does not run it has
# nothing at any of them. `enable` already lives in ./deploy.nix, which
# also carries the renames.
#
# ⚠️ `usersFile` is a path *inside* the container and still belongs here:
# a path's scope is the scope of the filesystem it names, and that
# filesystem is this host's container root.
options.services.hyperhive.deploy.authelia = {
package = lib.mkOption {
type = lib.types.package;
default = pkgs.authelia;
defaultText = lib.literalExpression "pkgs.authelia";
description = ''
authelia package to run in the container. Defaults to
nixpkgs's; override to pin a specific upstream.
'';
};
bridgePackage = lib.mkOption {
type = lib.types.package;
defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarm-authelia-bridge";
description = ''
`swarm-authelia-bridge` package — the only process allowed to
write `usersFile`. Wired by default from this flake's own
package set (see `flake.nix`); override to run a different
build.
'';
};
usersFile = lib.mkOption {
type = lib.types.str;
default = "${stateDir}/users.yml";
@ -1111,7 +1111,7 @@ in
# password for the users file. Without it the container runs
# authelia and cannot invoke it: the unit's ExecStart resolves
# through the store path, and nothing puts the CLI on PATH.
environment.systemPackages = [ deployCfg.authelia.package ];
environment.systemPackages = [ cfg.package ];
# This container shares the host netns, so its own
# firewall.service would rewrite the HOST ruleset at every
@ -1140,16 +1140,16 @@ in
wantedBy = [ "multi-user.target" ];
before = [ "${unitName}.service" ];
requiredBy = [ "${unitName}.service" ];
# `deployCfg.authelia.package` is here for its CLI, not its
# daemon: the client secrets are minted with `authelia crypto
# hash generate`, which is the only way to produce a digest in
# `cfg.package` is here for its CLI, not its daemon: the
# client secrets are minted with `authelia crypto hash
# generate`, which is the only way to produce a digest in
# the exact form authelia will later verify.
path = [
pkgs.coreutils
]
++ lib.optionals oidcEnabled [
pkgs.openssl
deployCfg.authelia.package
cfg.package
];
serviceConfig = {
Type = "oneshot";
@ -1216,7 +1216,7 @@ in
"${unitName}.service"
];
serviceConfig = {
ExecStart = "${deployCfg.authelia.bridgePackage}/bin/swarm-authelia-bridge";
ExecStart = "${cfg.bridgePackage}/bin/swarm-authelia-bridge";
User = unitName;
Group = unitName;
Restart = "on-failure";
@ -1235,7 +1235,7 @@ in
# argon2 parameters baked into a hash have to match the
# verifier's — same reasoning as `swarmctl`'s own
# `SWARMCTL_AUTHELIA_BIN`.
SWARM_AUTHELIA_BRIDGE_AUTHELIA_BIN = "${deployCfg.authelia.package}/bin/authelia";
SWARM_AUTHELIA_BRIDGE_AUTHELIA_BIN = "${cfg.package}/bin/authelia";
# By name through the gateway, not loopback. A loopback
# literal encodes "authelia is in my netns" at the call site,
# and authelia's OIDC endpoints are https-only in effect —
@ -1255,7 +1255,7 @@ in
services.authelia.instances.${instance} = {
enable = true;
package = deployCfg.authelia.package;
package = cfg.package;
# Merged at RUNTIME alongside the nix-generated config, which
# is the whole reason the client digests can exist at all:

View file

@ -28,7 +28,7 @@ let
autheliaEnv = lib.optionalAttrs deployCfg.authelia.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 = "${deployCfg.authelia.package}/bin/authelia";
SWARMCTL_AUTHELIA_BIN = "${autheliaCfg.package}/bin/authelia";
SWARMCTL_AUTHELIA_USERS_FILE = deployCfg.authelia.hostUsersFile;
};
@ -196,7 +196,7 @@ let
# config back to it — and to get it wrong the day one of them changes.
swarmctlConfigured = pkgs.symlinkJoin {
name = "swarmctl-configured";
paths = [ deployCfg.swarm-controller.swarmctlPackage ];
paths = [ cfg.swarmctlPackage ];
nativeBuildInputs = [ pkgs.makeWrapper ];
postBuild = ''
wrapProgram $out/bin/swarmctl ${
@ -243,8 +243,30 @@ in
# 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. Both packages moved to that same
# `deploy.swarm-controller` block. What stays here is what the daemon IS.
# `enable` alone never can. What stays here is what the daemon IS.
package = lib.mkOption {
type = lib.types.package;
defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarm-controller";
description = ''
swarm-controller package. Wired by default from this flake's own
package set (see `flake.nix`); override to run a different build.
'';
};
swarmctlPackage = lib.mkOption {
type = lib.types.package;
defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarmctl";
description = ''
`swarmctl` package — the swarm operator's CLI, installed on this
host alongside the daemon and wrapped with the paths it needs.
A separate option from `package` rather than a second binary in
the same derivation: the CLI runs as root and acts directly,
the daemon runs unprivileged and serves a socket, and pinning one
without the other is a thing an operator may legitimately want.
'';
};
staleAfterSeconds = lib.mkOption {
type = lib.types.ints.positive;
@ -349,39 +371,16 @@ in
};
# What stays above is what the controller IS to the swarm: the quick-links
# it publishes, the client id it is registered under, and how stale a
# snapshot may read. What the host running it decides is here — which two
# builds it runs, where its socket lives, and the three credentials it reads
# off local disk. `enable` already lives in ./deploy.nix, which also carries
# the renames.
# it publishes, the client id it is registered under, how stale a snapshot
# may read, and its packages. What the host running it decides is here —
# where its socket lives, and the three credentials it reads off local
# disk. `enable` already lives in ./deploy.nix, which also carries the
# renames.
#
# ⚠️ `deploy.swarm-controller`, NOT `deploy.hive-controller`. Both exist:
# the latter is hive-c0re's namespace, where `tls.*` lives. A definition on
# the wrong one lands on a live, unrelated option rather than failing.
options.services.hyperhive.deploy.swarm-controller = {
package = lib.mkOption {
type = lib.types.package;
defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarm-controller";
description = ''
swarm-controller package. Wired by default from this flake's own
package set (see `flake.nix`); override to run a different build.
'';
};
swarmctlPackage = lib.mkOption {
type = lib.types.package;
defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarmctl";
description = ''
`swarmctl` package — the swarm operator's CLI, installed on this
host alongside the daemon and wrapped with the paths it needs.
A separate option from `package` rather than a second binary in
the same derivation: the CLI runs as root and acts directly,
the daemon runs unprivileged and serves a socket, and pinning one
without the other is a thing an operator may legitimately want.
'';
};
socketPath = lib.mkOption {
type = lib.types.str;
default = "/run/swarm-controller/controller.sock";
@ -592,7 +591,7 @@ in
after = [ "network.target" ];
serviceConfig = {
ExecStart = "${deployCfg.swarm-controller.package}/bin/swarm-controller";
ExecStart = "${cfg.package}/bin/swarm-controller";
# The two differ on purpose. The queue credential is
# unconditional — the assertions above make its path a value that

View file

@ -111,13 +111,20 @@ 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 to every hive: the name it is
# served under, the port its `/metrics` is re-served on, and the OIDC
# client it is registered as. What the host running it decides — which
# build it runs, where its datasources point, which plugins are in its
# store path, and the socket directory it shares with nginx — is below,
# under `deploy.grafana`.
# What stays here is what the service IS to every hive: its package, the
# name it is served under, the port its `/metrics` is re-served on, and
# the OIDC client it is registered as. What the host running it decides —
# where its datasources point, which plugins are in its store path, and
# the socket directory it shares with nginx — is below, under
# `deploy.grafana`.
options.services.hyperhive.swarm.grafana = {
package = lib.mkOption {
type = lib.types.package;
default = pkgs.grafana;
defaultText = lib.literalExpression "pkgs.grafana";
description = "Grafana package to run.";
};
machine = lib.mkOption {
type = lib.types.str;
readOnly = true;
@ -210,22 +217,15 @@ in
};
# What stays above is what the service IS to every hive. What the host
# running it decides is here: which build it runs, where its datasources
# point, which plugins sit in its store path, and the directory it shares a
# socket with nginx through. `enable` already lives in ./deploy.nix, which
# also carries the renames.
# running it decides is here: where its datasources point, which plugins sit
# in its store path, and the directory it shares a socket with nginx
# through. `enable` already lives in ./deploy.nix, which also carries the
# renames.
#
# ⚠️ Both datasource URLs are wiring and still move. A URL's scope is the
# scope of what it ADDRESSES, not the fact that it is a URL: both stores
# bind loopback, so these can only ever mean "on this host".
options.services.hyperhive.deploy.grafana = {
package = lib.mkOption {
type = lib.types.package;
default = pkgs.grafana;
defaultText = lib.literalExpression "pkgs.grafana";
description = "Grafana package to run.";
};
socketDir = lib.mkOption {
type = lib.types.str;
default = "/run/swarm-grafana";
@ -624,7 +624,7 @@ in
services.grafana = {
enable = true;
package = deployCfg.grafana.package;
package = cfg.package;
# Passed through unconditionally, empty default included.
# Upstream distinguishes `null` from `[ ]`, and both differences

View file

@ -217,14 +217,12 @@ in
# ./deploy.nix. What stays here is what the queue IS: its domain,
# ports, accounts and callout wiring.
# ⚠️ Deliberately NO `package` option for the NATS server, anywhere —
# not here and not under `deploy.nats` either, where every other
# service's build now lives. `services.nats` upstream does not expose
# one; it resolves `pkgs.nats-server` itself, so an option would be
# ignored or need an overlay to mean anything, and an option that does
# not control what it names is worse than its absence. Pin the build
# with `nixpkgs.overlays` if you need to. (`deploy.nats.authPackage`
# is a different thing: the callout responder, which IS ours.)
# ⚠️ Deliberately NO `package` option, unlike this module's siblings.
# `services.nats` upstream does not expose one — it resolves
# `pkgs.nats-server` itself — so an option here would either be
# ignored or need an overlay to mean anything, and an option that
# does not control what it names is worse than its absence. Pin the
# build with `nixpkgs.overlays` if you need to.
port = lib.mkOption {
type = lib.types.port;
@ -286,19 +284,6 @@ in
'';
};
};
# What stays above is what the queue IS to every hive: the ports it answers
# on and the client id it is registered under. What the host running it
# decides is below — which responder build it runs, whether it mints its own
# keypairs, and where the seeds sit. `enable` already lives in ./deploy.nix,
# which also carries the renames.
#
# ⚠️ The two PUBLIC keys move with their seeds rather than staying: peers
# receive the user key over the wire when they connect (docs/swarm/secrets.md),
# they never configure it, and splitting a keypair across two namespaces is
# worse than either placement.
options.services.hyperhive.deploy.nats = {
authPackage = lib.mkOption {
type = lib.types.package;
defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarm-nats-auth";
@ -306,15 +291,25 @@ in
The auth-callout responder package.
⚠️ Named `authPackage`, not `package`, on purpose: this module
deliberately has **no** `package` option for the NATS server
itself, because upstream's `services.nats` resolves
`pkgs.nats-server` on its own — see the note above
`options.services.hyperhive.swarm.nats`. A bare `package` here
would read as "the NATS package" and mean something else
entirely.
deliberately has **no** `package` option for the server itself
(see the note above — upstream's `services.nats` resolves
`pkgs.nats-server` on its own), so a bare `package` here would
read as "the NATS package" and mean something else entirely.
'';
};
};
# What stays above is what the queue IS to every hive: the ports it answers
# on, the client id it is registered under, the responder package. The
# callout identity below is what the host running it decides — whether it
# mints its own keypairs, and where the seeds sit. `enable` already lives in
# ./deploy.nix, which also carries the renames.
#
# ⚠️ The two PUBLIC keys move with their seeds rather than staying: peers
# receive the user key over the wire when they connect (docs/swarm/secrets.md),
# they never configure it, and splitting a keypair across two namespaces is
# worse than either placement.
options.services.hyperhive.deploy.nats = {
autoGenerateCallout = lib.mkOption {
type = lib.types.bool;
default = false;
@ -713,7 +708,7 @@ in
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = lib.concatStringsSep " " [
"${deployCfg.nats.authPackage}/bin/swarm-nats-auth"
"${cfg.authPackage}/bin/swarm-nats-auth"
"--nats-url nats://127.0.0.1:${toString cfg.port}"
"--user-seed-file \${CREDENTIALS_DIRECTORY}/callout-user.seed"
"--issuer-seed-file \${CREDENTIALS_DIRECTORY}/issuer.seed"

View file

@ -13,6 +13,7 @@
...
}:
let
cfg = config.services.hyperhive.swarm.ui;
deployCfg = config.services.hyperhive.deploy;
gatewayCfg = config.services.hyperhive.gateway;
autheliaCfg = config.services.hyperhive.swarm.authelia;
@ -21,11 +22,11 @@ let
# this UI was left out of that overlay entirely (an operator with a
# stylix-themed host saw the dashboard in their own colours but this
# UI still on the default Catppuccin palette), because nothing here
# ever served a themed `colors.css` in place of `deployCfg.swarm-ui.package`'s own.
# ever served a themed `colors.css` in place of `cfg.package`'s own.
# Detection + CSS-generation is shared (`./stylix-theme.nix`).
#
# No package-copy derivation: this vhost has exactly one location
# serving `deployCfg.swarm-ui.package` (unlike hive-c0re's `servedFrontend`, which
# serving `cfg.package` (unlike hive-c0re's `servedFrontend`, which
# backs both the dashboard root AND every per-agent gateway route, so
# a single swapped tree covers both) — an `= /static/colors.css`
# exact-match location overriding just that one file, same idiom
@ -78,10 +79,8 @@ in
'')
];
# ⚠️ `services.hyperhive.swarm.ui` no longer exists as a namespace. `enable`
# moved in an earlier slice and `package` in this one, and those were its
# only two options — so both paths survive purely as renames in ./deploy.nix.
options.services.hyperhive.deploy.swarm-ui.package = lib.mkOption {
options.services.hyperhive.swarm.ui = {
package = lib.mkOption {
type = lib.types.package;
defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarm-ui";
description = ''
@ -90,12 +89,13 @@ in
controller's own API.
Wired by default from this flake's own package set (see
`flake.nix`), the same way `deploy.swarm-controller.package` is. There
`flake.nix`), the same way `swarm.controller.package` is. There
is deliberately **no overlay** in this project, so a
`pkgs.swarm-ui` default here would name an attribute that does
not exist on any real deployment.
'';
};
};
config = lib.mkIf (config.services.hyperhive.enable && deployCfg.swarm-ui.enable) {
assertions = [
@ -178,7 +178,7 @@ in
extraConfig = gatewayCfg.lib.securityHeaders;
locations = {
"/" = {
root = "${deployCfg.swarm-ui.package}";
root = "${cfg.package}";
extraConfig = ''
${swarmAuthRequest}
# SPA: any path the bundle routes client-side is served the
@ -189,7 +189,7 @@ in
}
// lib.optionalAttrs (stylixThemeColors != null) {
# Stylix theming (see the `let` block above): overrides just
# this one file from `deployCfg.swarm-ui.package`'s own static root, rather
# this one file from `cfg.package`'s own static root, rather
# than copying the whole tree to change one file inside it —
# nginx resolves the more specific `=` exact match over the
# `/` prefix root above, so this simply doesn't exist (falling

View file

@ -56,11 +56,17 @@ let
'';
in
{
# What stays here is what the store IS from any hive's point of view: the
# name it answers on and the port. `enable`, `package` and
# `retentionPeriod` are decisions of the host that runs it and live under
# `deploy.*`.
# What stays here is what the store IS from any hive's point of view: its
# package, the name it answers on, the port. `enable` and `retentionPeriod`
# are decisions of the host that runs it and live under `deploy.*`.
options.services.hyperhive.swarm.victorialogs = {
package = lib.mkOption {
type = lib.types.package;
default = pkgs.victorialogs;
defaultText = lib.literalExpression "pkgs.victorialogs";
description = "VictoriaLogs package to run.";
};
machine = lib.mkOption {
type = lib.types.str;
readOnly = true;
@ -110,13 +116,6 @@ in
# and a hive that is a *client* of the log store never consults it. That
# makes it a `deploy.*` value by the same rule as the seal on the secret
# store — options on the auto-deployed service itself.
options.services.hyperhive.deploy.victorialogs.package = lib.mkOption {
type = lib.types.package;
default = pkgs.victorialogs;
defaultText = lib.literalExpression "pkgs.victorialogs";
description = "VictoriaLogs package to run.";
};
options.services.hyperhive.deploy.victorialogs.retentionPeriod = lib.mkOption {
type = lib.types.str;
default = "30d";
@ -260,7 +259,7 @@ in
services.victorialogs = {
enable = true;
package = deployCfg.victorialogs.package;
package = cfg.package;
# ⚠️ PINNED TO LOOPBACK for the same reason the metrics store is,
# and it matters more here: upstream's default listens on every

View file

@ -30,11 +30,17 @@ let
domainBase = if swarmDomain == null then "invalid" else swarmDomain;
in
{
# What stays here is what the store IS from any hive's point of view: the
# name it answers on and the port. `enable`, `package` and
# `retentionPeriod` are decisions of the host that runs it and live under
# `deploy.*`.
# What stays here is what the store IS from any hive's point of view: its
# package, the name it answers on, the port. `enable` and `retentionPeriod`
# are decisions of the host that runs it and live under `deploy.*`.
options.services.hyperhive.swarm.victoriametrics = {
package = lib.mkOption {
type = lib.types.package;
default = pkgs.victoriametrics;
defaultText = lib.literalExpression "pkgs.victoriametrics";
description = "VictoriaMetrics package to run.";
};
machine = lib.mkOption {
type = lib.types.str;
readOnly = true;
@ -73,13 +79,6 @@ in
# Retention is a property of the store this host runs, not something the
# swarm has to agree on: it is read only where the container is defined,
# and a hive that is a *client* of the metrics store never consults it.
options.services.hyperhive.deploy.victoriametrics.package = lib.mkOption {
type = lib.types.package;
default = pkgs.victoriametrics;
defaultText = lib.literalExpression "pkgs.victoriametrics";
description = "VictoriaMetrics package to run.";
};
options.services.hyperhive.deploy.victoriametrics.retentionPeriod = lib.mkOption {
type = lib.types.str;
default = "5y";
@ -214,7 +213,7 @@ in
services.victoriametrics = {
enable = true;
package = deployCfg.victoriametrics.package;
package = cfg.package;
retentionPeriod = deployCfg.victoriametrics.retentionPeriod;
# ⚠️ PINNED TO LOOPBACK, and this is a correction rather than a

View file

@ -16,7 +16,7 @@
#
# ⚠️ **This evaluates, it does not execute** — a command line, a request or
# a certificate needs something that *runs* it. A case needing a rendered
# file must stub what it drags in (`deploy.swarm-ui.package = pkgs.emptyDirectory`).
# file must stub what it drags in (`swarm.ui.package = pkgs.emptyDirectory`).
{
pkgs,
lib,
@ -72,12 +72,11 @@ let
swarm.wireguard.privateKeyFile = "/etc/wireguard/hive.key";
};
# Same shape for the forge, which SPLIT rather than moving whole: the six
# Same shape for the forge, which SPLIT rather than moving whole: the five
# host-side options are set here through their pre-rename paths while the
# rest of `swarm.forge` stays put. All six are defined so that dropping any
# rest of `swarm.forge` stays put. All five are defined so that dropping any
# single shim entry fails the eval, not just the two the assertion reads.
forgeOldPath = hive {
swarm.forge.package = pkgs.emptyDirectory;
swarm.forge.behindGateway = true;
swarm.forge.openFirewall = true;
swarm.forge.hostSwarmControllerTokenFile = "/etc/forge/sc.token";
@ -90,12 +89,11 @@ let
];
};
# The homeserver's turn to split. All eight host-side options are set through
# The homeserver's turn to split. All six host-side options are set through
# their pre-rename paths — including `gui.enable`, whose value is deliberately
# the opposite of its default so the definition has to actually land, and both
# packages, stubbed to a derivation neither option defaults to for the same
# reason. All eight so that dropping any single shim entry fails the eval, not
# just the ones the assertions read.
# the opposite of its default so the definition has to actually land. All six
# so that dropping any single shim entry fails the eval, not just the two the
# assertion reads.
matrixOldPath = hive {
deploy.matrix.enable = true;
swarm.matrix.openFirewall = true;
@ -103,33 +101,26 @@ let
swarm.matrix.maxRequestSize = 31457280;
swarm.matrix.registrationTokenFile = "/etc/matrix/register.token";
swarm.matrix.gui.enable = false;
swarm.matrix.package = pkgs.emptyDirectory;
swarm.matrix.gui.package = pkgs.emptyDirectory;
swarm.matrix.sso.clientSecretFile = "/etc/matrix/oidc.secret";
};
# The queue's callout identity, fourth split slice. `autoGenerateCallout` is
# left FALSE on purpose: that is what makes the seed paths the thing deciding
# `responderConfigured`, so the assertion below is about the seeds rather
# than about the auto-mint branch. Every one of the seven old paths is
# defined — `enable` included, which is why it is spelled the old way here
# while the fixture below uses the new one — so dropping any single nats
# shim fails the eval, not just the arms read.
# than about the auto-mint branch. All five old paths are defined, so
# dropping any single shim entry fails the eval, not just the arms read.
natsOldPath = hive {
swarm.nats.enable = true;
deploy.nats.enable = true;
swarm.nats.autoGenerateCallout = false;
swarm.nats.calloutUserPublicKey = "UTESTUSERPUBKEYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
swarm.nats.calloutIssuerPublicKey = "ATESTISSUERPUBKEYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
swarm.nats.calloutUserSeedFile = "/run/secrets/nats-user.seed";
swarm.nats.calloutIssuerSeedFile = "/run/secrets/nats-issuer.seed";
swarm.nats.authPackage = pkgs.emptyDirectory;
};
# Seventh split slice, plus slice 10's two authelia packages. Of slice 7's
# movers only `usersFile` has a rename entry — the other two are `readOnly`,
# and a rename module contributes a definition, which a read-only option
# refuses; see ./host-modules/deploy.nix. `package` and `bridgePackage` are
# ordinary options, so they do carry one. The two arms
# Seventh split slice. Only `usersFile` has a rename entry: the other two
# movers are `readOnly`, and a rename module contributes a definition, which
# a read-only option refuses — see ./host-modules/deploy.nix. So the two arms
# below have different jobs. `usersFile` tests the rename; the nats one tests
# that a reader repointed to the new namespace still renders the derived
# path, which is the failure this slice could actually have shipped — seven
@ -138,8 +129,6 @@ let
deploy.authelia.enable = true;
deploy.nats.enable = true;
swarm.authelia.usersFile = "/var/lib/test-authelia/users.yml";
swarm.authelia.package = pkgs.emptyDirectory;
swarm.authelia.bridgePackage = pkgs.emptyDirectory;
swarm.nats.autoGenerateCallout = false;
swarm.nats.calloutUserPublicKey = "UTESTUSERPUBKEYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
swarm.nats.calloutIssuerPublicKey = "ATESTISSUERPUBKEYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
@ -154,47 +143,20 @@ let
deploy.matrix.maxRequestSize = 99000000;
};
# `enable` is spelled the OLD way like everything else here, so all seven
# of the controller's shims are exercised rather than six.
controllerOldPath = hive {
swarm.controller.enable = true;
swarm.controller.package = pkgs.emptyDirectory;
swarm.controller.swarmctlPackage = pkgs.emptyDirectory;
deploy.swarm-controller.enable = true;
swarm.controller.socketPath = "/run/test-ctrl/ctrl.sock";
swarm.controller.forgeTokenFile = "/run/secrets/ctrl-forge.token";
swarm.controller.authBridgeUrl = "http://127.0.0.1:19097";
swarm.controller.queue.clientSecretFile = "/run/secrets/ctrl-queue.secret";
};
# The swarm UI, whose namespace this slice empties: `swarm.ui` had exactly
# two options and both have moved, so BOTH old paths are set here and the
# whole namespace now lives or dies by its two rename entries.
uiOldPath = hive {
swarm.ui.enable = true;
swarm.ui.package = pkgs.emptyDirectory;
};
# The two stores, which had NO old-path fixture at all until this slice —
# their `enable` and `retentionPeriod` shims have been uncovered since they
# landed, which is precisely the "a missing shim reads as a clean tree"
# failure ./module-eval.nix's wireguard fixture was written to catch. All six
# old paths are set, so dropping any single entry fails the eval.
storesOldPath = hive {
swarm.victoriametrics.enable = true;
swarm.victorialogs.enable = true;
swarm.victoriametrics.retentionPeriod = "3d";
swarm.victorialogs.retentionPeriod = "5d";
swarm.victoriametrics.package = pkgs.emptyDirectory;
swarm.victorialogs.package = pkgs.emptyDirectory;
};
grafanaOldPath = hive {
deploy.grafana.enable = true;
swarm.grafana.socketDir = "/run/test-grafana-sock";
swarm.grafana.datasourceUrl = "http://127.0.0.1:19999";
swarm.grafana.logsDatasourceUrl = "http://127.0.0.1:19998";
swarm.grafana.plugins = [ ];
swarm.grafana.package = pkgs.emptyDirectory;
};
# The first slice to leave options on BOTH sides of the split, so the
@ -510,43 +472,6 @@ let
# host-side half depends on: no store here, so no bind mount and no unit.
# Without it a hive that merely names a token would drag the store's
# container config into its evaluation.
# Reads the vhost's rendered `root`, not the option: the UI is served
# straight out of a store path, so a shim that resolves but stops
# reaching the module would leave nginx pointing at the default build.
name = "a config written against the pre-rename swarm-ui paths still serves the operator's build";
#
# ⚠️ `unsafeDiscardStringContext` on both sides, load-bearing rather than
# tidy: interpolating a derivation carries string *context*, and this
# suite renders its results into a `buildCommand` that may not reference
# store paths. Comparing the paths as plain text is the intent — the case
# asks "does nginx point HERE", not "depend on what it points at".
ok =
let
want = builtins.unsafeDiscardStringContext "${pkgs.emptyDirectory}";
# `or ""` is NOT enough: nginx's `locations.<l>.root` is `nullOr`, so
# a vhost that HAS the attribute set to null skips the default and
# reaches the coercion. Filter nulls, then discard context.
roots = map builtins.unsafeDiscardStringContext (
lib.filter (r: r != null) (
map (v: v.locations."/".root or null) (builtins.attrValues uiOldPath.services.nginx.virtualHosts)
)
);
in
builtins.elem want roots;
}
{
# Reads the package the CONTAINER renders, not the option: a shim that
# resolves but stops reaching the module would leave the store running
# nixpkgs' default while the operator's override read back fine.
name = "a config written against the pre-rename store paths still picks the operator's package";
ok =
storesOldPath.containers.swarm-victorialogs.config.services.victorialogs.package
== pkgs.emptyDirectory
&&
storesOldPath.containers.swarm-victoriametrics.config.services.victoriametrics.package
== pkgs.emptyDirectory;
}
{
name = "a bootstrap token on a host that runs no store grants nothing";
ok = !(baoGrantNoStore.systemd.services ? swarm-bao-bootstrap-dir);
}