Compare commits
2 changed files with 12 additions and 224 deletions
|
|
@ -49,22 +49,6 @@ let
|
||||||
# working login into an intermittent one.
|
# working login into an intermittent one.
|
||||||
forgeSecretPath = "/var/lib/forgejo-oidc/${cfg.sso.clientId}.secret";
|
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`
|
# Forgejo's OAuth2 callback shape. Built from the SAME `ssoSourceName`
|
||||||
# the registration uses, so the redirect URI authelia is told to allow
|
# the registration uses, so the redirect URI authelia is told to allow
|
||||||
# and the one forgejo will actually send cannot drift apart — a
|
# and the one forgejo will actually send cannot drift apart — a
|
||||||
|
|
@ -410,28 +394,6 @@ 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 {
|
config = lib.mkIf config.services.hyperhive.enable {
|
||||||
|
|
@ -969,79 +931,6 @@ in
|
||||||
fi
|
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
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1117,49 +1006,6 @@ 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 {
|
networking.firewall = lib.mkIf cfg.openFirewall {
|
||||||
allowedTCPPorts = [
|
allowedTCPPorts = [
|
||||||
cfg.httpPort
|
cfg.httpPort
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ let
|
||||||
};
|
};
|
||||||
|
|
||||||
natsCfg = config.services.hyperhive.swarm.nats;
|
natsCfg = config.services.hyperhive.swarm.nats;
|
||||||
forgeCfg = config.services.hyperhive.swarm.forge;
|
|
||||||
|
|
||||||
# The controller's own OAuth2 client. It is NOT a hive: the per-hive
|
# The controller's own OAuth2 client. It is NOT a hive: the per-hive
|
||||||
# clients the roster issues belong to hives, and the responder's client
|
# clients the roster issues belong to hives, and the responder's client
|
||||||
|
|
@ -67,22 +66,6 @@ let
|
||||||
SWARM_CONTROLLER_OIDC_CLIENT_SECRET_FILE = "%d/queue-client.secret";
|
SWARM_CONTROLLER_OIDC_CLIENT_SECRET_FILE = "%d/queue-client.secret";
|
||||||
};
|
};
|
||||||
|
|
||||||
# Independent of `queueEnv`/`queueLocal` on purpose — the queue
|
|
||||||
# coordinates' own co-location guard is narrower than it looks (it
|
|
||||||
# silently drops the controller's identity on a host that splits
|
|
||||||
# authelia/NATS from swarm-controller; a known gap tracked separately).
|
|
||||||
# Forge access has nothing to do with whether authelia+NATS happen to be
|
|
||||||
# local, only with whether `cfg.forgeTokenFile` resolves to a real file —
|
|
||||||
# which `forgeTokenFile`'s own default already handles (forge-local vs.
|
|
||||||
# operator-copied). Gating a second time here would just repeat that
|
|
||||||
# option's own logic under a different name.
|
|
||||||
forgeEnv = lib.optionalAttrs (cfg.forgeTokenFile != null) {
|
|
||||||
SWARM_CONTROLLER_FORGE_URL = "https://${forgeCfg.domain}";
|
|
||||||
# Same `%d` shape as the queue secret above — root reads the plaintext
|
|
||||||
# at unit start, the daemon's own user sees a 0400 copy.
|
|
||||||
SWARM_CONTROLLER_FORGE_TOKEN_FILE = "%d/forge-token";
|
|
||||||
};
|
|
||||||
|
|
||||||
# Wrapped rather than documented: every one of these values is derived
|
# Wrapped rather than documented: every one of these values is derived
|
||||||
# from an option this deployment already set, so making the operator
|
# from an option this deployment already set, so making the operator
|
||||||
# re-supply them on the command line would be asking them to repeat the
|
# re-supply them on the command line would be asking them to repeat the
|
||||||
|
|
@ -228,44 +211,6 @@ in
|
||||||
module's own activation condition.
|
module's own activation condition.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
forgeTokenFile = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.str;
|
|
||||||
# `services.hyperhive.swarm.forge` has no `enable` of its own to
|
|
||||||
# check — the module activates on the general
|
|
||||||
# `config.services.hyperhive.enable` instead (see
|
|
||||||
# `hive-forge/default.nix`'s own `config = lib.mkIf
|
|
||||||
# config.services.hyperhive.enable { ... }`), so that's the
|
|
||||||
# condition to match here too. Referencing a `forge.enable` that
|
|
||||||
# doesn't exist threw `attribute 'enable' missing` on every host
|
|
||||||
# that turns swarm-controller on — caught in review, not by
|
|
||||||
# `nix flake check` (nothing in its checked combinations forced
|
|
||||||
# this particular default to actually evaluate).
|
|
||||||
default = if config.services.hyperhive.enable then forgeCfg.hostSwarmControllerTokenFile else null;
|
|
||||||
defaultText = lib.literalExpression ''
|
|
||||||
forge's own `hostSwarmControllerTokenFile` when this host runs
|
|
||||||
hyperhive at all (forge has no separate enable), else null
|
|
||||||
'';
|
|
||||||
example = "/var/lib/secrets/swarm-controller-forge.token";
|
|
||||||
description = ''
|
|
||||||
Path to this host's copy of the swarm-controller's forge access
|
|
||||||
token (see `hive-forge/default.nix`'s
|
|
||||||
`forgejo-swarm-controller-account` + `hive-forge-swarm-controller-token`
|
|
||||||
units, which mint and collect it onto forge's own host).
|
|
||||||
|
|
||||||
Defaults to forge's own delivery path on every host running
|
|
||||||
hyperhive (forge deploys unconditionally alongside it — see
|
|
||||||
`hive-forge/default.nix`, it has no `enable` of its own).
|
|
||||||
Override explicitly if forge's actual token file ends up
|
|
||||||
somewhere else — copy it out of forge's
|
|
||||||
{option}`services.hyperhive.swarm.forge.hostSwarmControllerTokenFile`
|
|
||||||
with whatever secret management this deployment already uses,
|
|
||||||
the same shape `swarm.nix`'s `clientSecretFile` documents for
|
|
||||||
its own cross-host case. `null` means no forge access — the
|
|
||||||
daemon logs that and continues without it, the same
|
|
||||||
graceful-absence shape the queue coordinates already use.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf (config.services.hyperhive.enable && cfg.enable) {
|
config = lib.mkIf (config.services.hyperhive.enable && cfg.enable) {
|
||||||
|
|
@ -306,14 +251,13 @@ in
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
ExecStart = "${cfg.package}/bin/swarm-controller";
|
ExecStart = "${cfg.package}/bin/swarm-controller";
|
||||||
|
|
||||||
# Only the credentials that actually resolve on this host. An
|
# Only when the queue is actually reachable from here. An absent
|
||||||
# absent credential is not a failure for either: the daemon logs
|
# credential is not a failure: the daemon logs that no queue is
|
||||||
# that no queue / no forge is configured and serves its HTTP
|
# configured and serves its HTTP surface, which is the correct
|
||||||
# surface regardless, which is the correct behaviour on a host
|
# behaviour on the hosts that do not run one.
|
||||||
# that doesn't run one.
|
LoadCredential = lib.mkIf queueLocal [
|
||||||
LoadCredential =
|
"queue-client.secret:${autheliaCfg.hostClientSecretDir}/${queueClientId}.secret"
|
||||||
lib.optional queueLocal "queue-client.secret:${autheliaCfg.hostClientSecretDir}/${queueClientId}.secret"
|
];
|
||||||
++ lib.optional (cfg.forgeTokenFile != null) "forge-token:${cfg.forgeTokenFile}";
|
|
||||||
User = "swarm-controller";
|
User = "swarm-controller";
|
||||||
Group = "swarm-controller";
|
Group = "swarm-controller";
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
|
|
@ -351,11 +295,10 @@ in
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
# Queue coordinates (`queueEnv`) and forge coordinates (`forgeEnv`)
|
# Queue coordinates (`queueEnv`) merge in last and are present only
|
||||||
# merge in last, each present only when its own resolution actually
|
# where the queue and its IdP both run. The daemon refuses a PARTIAL
|
||||||
# succeeded on this host. Both refuse a PARTIAL set rather than
|
# set rather than treating it as absent, which is why they are built
|
||||||
# treating it as absent, which is why each is built as one attrset
|
# as one attrset and never assigned individually.
|
||||||
# and never assigned individually.
|
|
||||||
environment = {
|
environment = {
|
||||||
SWARM_CONTROLLER_SOCKET = cfg.socketPath;
|
SWARM_CONTROLLER_SOCKET = cfg.socketPath;
|
||||||
# The swarm's hive directory, JSON-encoded — the full directory
|
# The swarm's hive directory, JSON-encoded — the full directory
|
||||||
|
|
@ -380,8 +323,7 @@ in
|
||||||
# off (the daemon just has nothing to apply it to).
|
# off (the daemon just has nothing to apply it to).
|
||||||
SWARM_CONTROLLER_STALE_AFTER_SECS = toString cfg.staleAfterSeconds;
|
SWARM_CONTROLLER_STALE_AFTER_SECS = toString cfg.staleAfterSeconds;
|
||||||
}
|
}
|
||||||
// queueEnv
|
// queueEnv;
|
||||||
// forgeEnv;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue