swarm-controller: provision + collect its own forge account token
This commit is contained in:
parent
b03ae55786
commit
c1e46b378d
2 changed files with 213 additions and 12 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue