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

@ -34,6 +34,7 @@ let
};
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
# clients the roster issues belong to hives, and the responder's client
@ -66,6 +67,22 @@ let
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
# 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
@ -211,6 +228,33 @@ in
module's own activation condition.
'';
};
forgeTokenFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = if forgeCfg.enable then forgeCfg.hostSwarmControllerTokenFile else null;
defaultText = lib.literalExpression ''
forge's own `hostSwarmControllerTokenFile` when this host runs
forge, 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 only when forge runs on
this same host. On any other host the token has to get here
somehow 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
the analogous cross-host case. `null` (the default when forge
isn't local and nothing else was set) 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) {
@ -251,13 +295,14 @@ in
serviceConfig = {
ExecStart = "${cfg.package}/bin/swarm-controller";
# Only when the queue is actually reachable from here. An absent
# credential is not a failure: the daemon logs that no queue is
# configured and serves its HTTP surface, which is the correct
# behaviour on the hosts that do not run one.
LoadCredential = lib.mkIf queueLocal [
"queue-client.secret:${autheliaCfg.hostClientSecretDir}/${queueClientId}.secret"
];
# Only the credentials that actually resolve on this host. An
# absent credential is not a failure for either: the daemon logs
# that no queue / no forge is configured and serves its HTTP
# surface regardless, which is the correct behaviour on a host
# that doesn't run one.
LoadCredential =
lib.optional queueLocal "queue-client.secret:${autheliaCfg.hostClientSecretDir}/${queueClientId}.secret"
++ lib.optional (cfg.forgeTokenFile != null) "forge-token:${cfg.forgeTokenFile}";
User = "swarm-controller";
Group = "swarm-controller";
Restart = "on-failure";
@ -295,10 +340,11 @@ in
];
};
# Queue coordinates (`queueEnv`) merge in last and are present only
# where the queue and its IdP both run. The daemon refuses a PARTIAL
# set rather than treating it as absent, which is why they are built
# as one attrset and never assigned individually.
# Queue coordinates (`queueEnv`) and forge coordinates (`forgeEnv`)
# merge in last, each present only when its own resolution actually
# succeeded on this host. Both refuse a PARTIAL set rather than
# treating it as absent, which is why each is built as one attrset
# and never assigned individually.
environment = {
SWARM_CONTROLLER_SOCKET = cfg.socketPath;
# The swarm's hive directory, JSON-encoded — the full directory
@ -323,7 +369,8 @@ in
# off (the daemon just has nothing to apply it to).
SWARM_CONTROLLER_STALE_AFTER_SECS = toString cfg.staleAfterSeconds;
}
// queueEnv;
// queueEnv
// forgeEnv;
};
};
}