swarm-controller: provision + collect its own forge account token

This commit is contained in:
damocles 2026-08-16 19:16:41 +02:00
commit c1e46b378d
2 changed files with 213 additions and 12 deletions

View file

@ -49,6 +49,22 @@ let
# working login into an intermittent one.
forgeSecretPath = "/var/lib/forgejo-oidc/${cfg.sso.clientId}.secret";
# The swarm-controller's forge account. Same name as
# `swarm-controller.nix`'s existing `queueClientId` — the account is
# provisioned to *match* that identity, not invented independently —
# "swarm controller having one identity with stuff derived from it is
# the right shape" was the swarm-level design call this account
# follows. A constant, not an option:
# nothing here makes the name configurable without also updating
# `swarm-controller.nix`'s own constant, so a shared option would
# invite the two to drift rather than prevent it.
swarmControllerForgeUser = "swarm-controller";
# Where the minted token lands inside the forge container — under
# forgejo's own state dir for the same "survives a reboot" reason as
# `forgeSecretPath` above.
swarmControllerTokenPath = "/var/lib/forgejo/swarm-controller-token";
# Forgejo's OAuth2 callback shape. Built from the SAME `ssoSourceName`
# the registration uses, so the redirect URI authelia is told to allow
# and the one forgejo will actually send cannot drift apart — a
@ -394,6 +410,28 @@ in
'';
};
};
hostSwarmControllerTokenFile = lib.mkOption {
type = lib.types.str;
default = "/var/lib/hyperhive-forge/swarm-controller.token";
description = ''
Host path where this forge deposits the freshly-minted forge
access token for the swarm's `swarm-controller` account (see
`systemd.services.forgejo-swarm-controller-account` inside the
forge container, and `hive-forge-swarm-controller-token` on the
host, which copies the token out).
Same role for this token as
`services.hyperhive.swarm.authelia.hostClientSecretDir` plays
for the OIDC secret: a **host**-local path (not inside any
container), read directly by `swarm-controller.nix`'s
`LoadCredential` when the controller runs on this same host.
On any other host the token has to get there somehow copy it
out of this path with whatever secret management this
deployment already uses, the same shape `swarm.nix`'s own
`clientSecretFile` documents for the analogous cross-host case.
'';
};
};
config = lib.mkIf config.services.hyperhive.enable {
@ -931,6 +969,79 @@ in
fi
'';
};
# Provision the swarm-controller's forge account + access token.
# Unconditional (not gated on any SSO/co-location option): forge
# is a swarm-wide singleton, so this account exists wherever
# forge does, regardless of which host (if any) actually runs
# swarm-controller — the design requirement was explicit that
# this must work even when forge and swarm-controller don't
# share a host.
systemd.services.forgejo-swarm-controller-account = {
description = "provision the swarm-controller's forge account + access token";
after = [ "forgejo.service" ];
requires = [ "forgejo.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
User = "forgejo";
Group = "forgejo";
SyslogIdentifier = "forgejo-swarm-controller-account";
};
# `FORGEJO_CUSTOM`, same reason as `forgejo-sso-source` above:
# every `forgejo admin` invocation needs it to find the app.ini
# the upstream module wrote, not just the auth-source verb.
environment = {
FORGEJO_CUSTOM = "/var/lib/forgejo/custom";
};
path = [
cfg.package
pkgs.coreutils
pkgs.gnugrep
];
script = ''
set -euo pipefail
# Idempotent by query, same reasoning as the SSO unit: assert
# the effect, not the command's exit code.
if ! forgejo admin user list | grep -qE "[[:space:]]${swarmControllerForgeUser}[[:space:]]"; then
forgejo admin user create \
--username ${lib.escapeShellArg swarmControllerForgeUser} \
--email ${lib.escapeShellArg "${swarmControllerForgeUser}@hyperhive.local"} \
--random-password --must-change-password=false
echo "created forge account ${swarmControllerForgeUser}"
fi
if ! forgejo admin user list | grep -qE "[[:space:]]${swarmControllerForgeUser}[[:space:]]"; then
echo "forge account ${swarmControllerForgeUser} absent after creation" >&2
exit 1
fi
token_path=${lib.escapeShellArg swarmControllerTokenPath}
# Token minting is idempotent by the token FILE's existence,
# not a separate stamp — same reasoning `hive-forge-oidc-secret`
# (below) uses for its own delivery: the file is both the
# record of "already done" and the thing that has to survive,
# so a state wipe that deletes it correctly triggers a
# re-mint instead of being silently masked by a stamp that
# outlived what it claims exists.
if [ ! -s "$token_path" ]; then
out=$(forgejo admin user generate-access-token \
--username ${lib.escapeShellArg swarmControllerForgeUser} \
--token-name swarm-controller-boot \
--scopes "write:repository,write:organization,write:issue,read:user")
token=$(printf '%s' "$out" | grep -oE '[0-9a-f]{32,}' | head -n1)
if [ -z "$token" ]; then
echo "no token-shaped word in forgejo's generate-access-token output" >&2
exit 1
fi
umask 0177
printf '%s' "$token" > "$token_path"
echo "minted forge access token for ${swarmControllerForgeUser}"
fi
'';
};
};
};
@ -1006,6 +1117,49 @@ in
'';
};
# The reverse direction of the unit above: collect the
# swarm-controller's freshly-minted forge token OUT of the container
# onto a host path other units/hosts can read. Same "only the host can
# see both trees" reasoning, same ordering constraint — this can only
# depend on the container being up (`container@hive-forge.service`),
# not on the specific in-container oneshot that mints the token
# (containers run their own systemd instance, invisible to this one by
# unit name) — so it polls the same bounded way
# `hive-forge-oidc-secret` does while waiting on authelia above.
#
# Unconditional, like the in-container unit it collects from: forge is
# a swarm-wide singleton, so the token always gets minted and always
# gets collected here, regardless of whether swarm-controller runs on
# this host, another host, or nowhere in this swarm at all.
systemd.services.hive-forge-swarm-controller-token = {
description = "collect the swarm-controller's forge token onto the host";
after = [ "container@hive-forge.service" ];
wantedBy = [ "container@hive-forge.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
SyslogIdentifier = "hive-forge-swarm-controller-token";
};
path = [ pkgs.coreutils ];
script = ''
set -euo pipefail
src=${lib.escapeShellArg "/var/lib/nixos-containers/hive-forge${swarmControllerTokenPath}"}
dst=${lib.escapeShellArg cfg.hostSwarmControllerTokenFile}
for _ in $(seq 1 60); do
[ -s "$src" ] && break
sleep 2
done
if [ ! -s "$src" ]; then
echo "forge has not minted $src after 120s" >&2
exit 1
fi
install -D -m 0400 "$src" "$dst"
'';
};
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [
cfg.httpPort