Each container now runs the journal forwarder the CI runner's, Grafana's and the log store's containers run, pushing to a receiver of its own on the swarm collector (serviceProducers hive-forge and hive-matrix). Each authenticates as a new machine OIDC client of its own (swarm.forge.otel.clientId, swarm.matrix.otel.clientId), registered by a glue file wherever authelia runs, minted by authelia and published to the store by the secret publisher. The forge's and the homeserver's browser login clients are unchanged. The client ids are swarm-forge-collector and swarm-matrix-collector, not hive-<x>-collector, which is the client id a hive named <x>-collector gets. The fetch unit inside each container reads the client secret with the store identity the host already holds for that service. The forge publisher's leaf is handed into the forge container as an nspawn system credential, as the grafana and log store containers do. matrix-ctl's leaf is already bound into the homeserver's container, so its forwarder reads it at those paths, as swarm-matrix-appservice-publish does; with no matrix-ctl identity the homeserver's forwarder is off. Each of the two policies gains one read stanza on its forwarder's client path. The host units that use those identities are unchanged. The two containers drop --link-journal=host, and the host collector stops reading them.
215 lines
9 KiB
Nix
215 lines
9 KiB
Nix
# The swarm's forge as every hive sees it: the names and ports it answers on,
|
||
# the URLs it advertises, and the OIDC client it is registered as. What the
|
||
# host running it decides, and the container itself, are in ./default.nix.
|
||
{
|
||
lib,
|
||
config,
|
||
...
|
||
}:
|
||
let
|
||
cfg = config.services.hyperhive.swarm.forge;
|
||
gatewayCfg = config.services.hyperhive.gateway;
|
||
swarmDomain = config.services.hyperhive.swarm.domain;
|
||
|
||
# Forgejo's name for the login source. Duplicated in ./default.nix, which
|
||
# registers the source under it.
|
||
ssoSourceName = "authelia";
|
||
|
||
# Forgejo's OAuth2 callback shape. `ssoSourceName` and `effectiveRootUrl`
|
||
# must equal their copies in ./default.nix: a mismatch is a rejected login
|
||
# with no error text worth reading.
|
||
ssoRedirectUri = "${effectiveRootUrl}user/oauth2/${ssoSourceName}/callback";
|
||
|
||
# Forgejo's `ROOT_URL`, duplicated from ./default.nix, which documents its
|
||
# shape.
|
||
defaultRootUrl =
|
||
let
|
||
portSuffix = if gatewayCfg.httpsPort == 443 then "" else ":${toString gatewayCfg.httpsPort}";
|
||
in
|
||
"https://${cfg.domain}${portSuffix}/";
|
||
effectiveRootUrl = if cfg.rootUrl != null then cfg.rootUrl else defaultRootUrl;
|
||
in
|
||
{
|
||
# Forge lives under `swarm`, beside the other swarm-global services. One
|
||
# rename for the top-level `services.hyperhive.forge` namespace: the
|
||
# subtree comes with it, so existing hives keep evaluating and get one
|
||
# warning naming both paths.
|
||
imports = [
|
||
(lib.mkRenamedOptionModule
|
||
[ "services" "hyperhive" "forge" ]
|
||
[ "services" "hyperhive" "swarm" "forge" ]
|
||
)
|
||
(lib.mkRemovedOptionModule [ "services" "hyperhive" "swarm" "forge" "sso" "enable" ] ''
|
||
SSO is no longer optional: the forge always registers the swarm's
|
||
authelia as a login source.
|
||
|
||
Removed rather than defaulted to true so a config that turned it
|
||
OFF fails here, where the line is, instead of silently gaining a
|
||
login provider on the next rebuild. Drop the line; if it was
|
||
false, set services.hyperhive.swarm.authelia.url as the assertion
|
||
describes.
|
||
'')
|
||
];
|
||
|
||
# Every swarm needs this forge — it's the canonical store for the meta
|
||
# flake + every agent's config repo (and the `internal/*` repos) — but
|
||
# only one host runs it: `deploy.forgejo.enable`, in ../deploy.nix. What
|
||
# follows is what every hive needs to reach it, wherever it runs.
|
||
options.services.hyperhive.swarm.forge = {
|
||
httpPort = lib.mkOption {
|
||
type = lib.types.port;
|
||
default = 3000;
|
||
description = ''
|
||
TCP port the forge serves HTTP on. Default 3000 sits outside
|
||
hyperhive's claimed ranges (dashboard 7000, every agent in
|
||
8100..8999 via FNV-1a hash). Change this if you already have
|
||
another forgejo bound to 3000.
|
||
'';
|
||
};
|
||
|
||
sshPort = lib.mkOption {
|
||
type = lib.types.port;
|
||
default = 2222;
|
||
description = ''
|
||
TCP port the forge's built-in SSH server listens on. Kept off
|
||
22 so it doesn't clash with the host's openssh. Agents push
|
||
with `ssh -p <sshPort> git@<domain>:<owner>/<repo>.git`.
|
||
'';
|
||
};
|
||
|
||
domain = lib.mkOption {
|
||
type = lib.types.str;
|
||
# Under the SWARM domain, not this hive's: a swarm runs one forge
|
||
# and every hive in it reaches the same host, so the name belongs
|
||
# to the swarm rather than to whichever hive happens to run it.
|
||
#
|
||
# Total on a null swarm domain so the required-domain assertion in
|
||
# hive-network.nix is the thing that fires; see the comment there.
|
||
default = if swarmDomain == null then "forge.invalid" else "forge.${swarmDomain}";
|
||
defaultText = lib.literalExpression ''"forge.''${services.hyperhive.swarm.domain}"'';
|
||
example = "git.example.com";
|
||
description = ''
|
||
Public hostname for the forge. Doubles as both the forgejo
|
||
`DOMAIN` setting (clone URLs forgejo advertises) AND the
|
||
gateway vhost server-name (sub-domain routing — see
|
||
`docs/networking/gateway.md`).
|
||
|
||
Defaults to `forge.''${services.hyperhive.swarm.domain}` — the
|
||
swarm's domain, not this hive's, because a swarm runs **one**
|
||
forge that every hive in it talks to.
|
||
|
||
⚠️ To use the per-hive name `forge.''${services.hyperhive.domain}`
|
||
instead, pin it here. Certificates follow either way: the
|
||
store's `swarm-services` role issues for the configured names,
|
||
not for a fixed tree.
|
||
|
||
Set to a full hostname (`git.example.com`,
|
||
`forge.internal.lan`, etc.) for a bespoke vhost shape — the
|
||
full domain goes here, no separate sub-domain-label option.
|
||
'';
|
||
};
|
||
|
||
publicUrl = lib.mkOption {
|
||
type = lib.types.nullOr lib.types.str;
|
||
default = "https://${cfg.domain}";
|
||
defaultText = lib.literalExpression ''"https://''${domain}"'';
|
||
example = "https://forge.example.com";
|
||
description = ''
|
||
Browser-facing forge URL the dashboard uses to build clickable
|
||
forge links (the H0M3 Forge tile, per-agent-row forge links) —
|
||
sourced into every agent container + hive-c0re as
|
||
`HIVE_FORGE_PUBLIC_URL`.
|
||
|
||
Defaults to `https://''${cfg.domain}`, the gateway vhost. When
|
||
`null`, the dashboard **hides** forge links rather than
|
||
guessing one — see `docs/web-ui/dashboard.md::H0M3 page` for
|
||
the rationale (a link built from the operator's own browser
|
||
hostname + a container port is only an accident away from
|
||
wrong on any deployment that isn't plain localhost).
|
||
'';
|
||
};
|
||
|
||
rootUrl = lib.mkOption {
|
||
type = lib.types.nullOr lib.types.str;
|
||
default = null;
|
||
example = "https://forge.example.com/";
|
||
description = ''
|
||
Override the auto-derived forgejo `ROOT_URL`. When `null`
|
||
(default), `ROOT_URL` is `https://''${cfg.domain}/`. The gateway
|
||
always terminates TLS (self-signed is the implicit floor when no
|
||
`gateway.tls.certDir` / ACME is set), so the forge is always
|
||
advertised over https. A non-canonical `gateway.httpsPort` is
|
||
appended as `:<port>`.
|
||
|
||
Set this only for a genuinely bespoke shape (e.g. an external
|
||
reverse proxy on a different host/path). Must end with `/` per
|
||
forgejo's `ROOT_URL` contract.
|
||
'';
|
||
};
|
||
|
||
# The swarm's authelia is always registered as an OpenID Connect
|
||
# login source here — there is no toggle: a forge without it has no
|
||
# way to log a person in through the swarm's SSO.
|
||
#
|
||
# **Additive, never exclusive.** Forgejo keeps its local password
|
||
# database and gains an extra "sign in with" button; this does not
|
||
# disable local login. Deliberate: an identity provider that can take
|
||
# the forge offline when it hiccups is a worse forge than one with
|
||
# two ways in — which is also what makes always-on safe.
|
||
sso = {
|
||
clientId = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "forgejo";
|
||
description = ''
|
||
OAuth2 client id this forge identifies itself with. Must match
|
||
the `id` of the corresponding entry in
|
||
`services.hyperhive.swarm.authelia.oidc.clients`.
|
||
'';
|
||
};
|
||
redirectUri = lib.mkOption {
|
||
type = lib.types.str;
|
||
readOnly = true;
|
||
default = ssoRedirectUri;
|
||
defaultText = lib.literalExpression ''"''${ROOT_URL}user/oauth2/authelia/callback"'';
|
||
description = ''
|
||
OAuth2 callback authelia sends the browser back to, and the URI
|
||
it matches **exactly**.
|
||
|
||
Read-only: forgejo derives it from its own `ROOT_URL` and the
|
||
login source's name, so it is a fact other modules read rather
|
||
than a knob. The glue that registers this client wherever
|
||
authelia runs reads it from here instead of restating the
|
||
format.
|
||
|
||
⚠️ With {option}`services.hyperhive.swarm.forge.rootUrl` unset,
|
||
`ROOT_URL` follows the gateway's `httpsPort`, which is
|
||
per-host. An authelia host that is not the forge's host renders
|
||
the forge's callback only if the two agree on it; set `rootUrl`
|
||
if they do not.
|
||
'';
|
||
};
|
||
# The secret half is fetched from the swarm secret store on the host
|
||
# that runs the forge: `swarm-bao-forge-oidc`, in ./default.nix.
|
||
};
|
||
|
||
otel.clientId = lib.mkOption {
|
||
type = lib.types.str;
|
||
readOnly = true;
|
||
# Not `hive-forge-collector`: `hive-<name>` is a hive's own client id,
|
||
# and `forge-collector` is a name a hive may take.
|
||
default = "swarm-forge-collector";
|
||
description = ''
|
||
OAuth2 client id the journal forwarder inside the forge's container
|
||
authenticates as, and the audience it asks its token for. Its own,
|
||
not {option}`services.hyperhive.swarm.forge.sso.clientId`, which is
|
||
the browser login's.
|
||
|
||
Read-only because three hosts have to agree on it: authelia
|
||
registers the client (`glue-forge-otel-oidc-client.nix`), the
|
||
swarm collector admits it on the container's receiver
|
||
(`swarm.otel.serviceProducers`), and the secret publisher carries
|
||
its secret to the store.
|
||
'';
|
||
};
|
||
};
|
||
}
|