Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ci-runner: load the registration token in a register unit of its own

systemd hands a unit's credentials to every process of that unit, and a
host job is a process of the runner daemon, so the `LoadCredential=` on
gitea-runner-swarm still let any workflow read the registration token
from `$CREDENTIALS_DIRECTORY`.

swarm-ci-runner-register.service, a oneshot under the daemon's dynamic
user and state dir, now holds the only `LoadCredential=` and runs the
precond and `act_runner register` (only when `.runner` is missing). The
daemon unit has no credential, no `EnvironmentFile` and no `TOKEN`; its
pre-start step starts the register unit, so a missing token fails the
daemon's start and `Restart=on-failure` retries it.

The precond compares `.runner`'s labels in the form the daemon rewrites
them to (`nix` -> `nix:host`); compared verbatim, a scheme-less label
re-registered the runner on every start.

tmpfiles removes the unused `registration-token.env` and upstream's
`.token-hash`/`.labels` markers.
This commit is contained in:
atlas 2026-10-05 16:33:24 +02:00
commit 8ffceea8e3
4 changed files with 184 additions and 100 deletions

View file

@ -47,10 +47,14 @@ let
};
# The runner alone: no hive, no forge, no store, its store identity placed
# by hand.
# by hand. `nix` is a label without a scheme.
ciAlone = hive {
deploy.hive-controller.enable = false;
deploy.ci-runner.enable = true;
deploy.ci-runner.labels = [
"hive-ci:host"
"nix"
];
deploy.bao.ciRunnerClientCertFile = "/etc/pki/bao-ci-runner.pem";
deploy.bao.ciRunnerClientKeyFile = "/etc/pki/bao-ci-runner-key.pem";
};
@ -82,7 +86,8 @@ let
forgeSettings = forgeHere.containers.hive-forge.config.services.forgejo.settings;
runnerService = ciAlone.containers.swarm-ci-runner.config.systemd.services.gitea-runner-swarm;
registerStep = lib.findFirst lib.isDerivation null runnerService.serviceConfig.ExecStartPre;
registerService =
ciAlone.containers.swarm-ci-runner.config.systemd.services.swarm-ci-runner-register;
cases = [
{
@ -154,9 +159,10 @@ let
ok = refusedOver ciNoIdentity "deploy.bao.ciRunnerClientCertFile";
}
{
# A host job inherits the daemon's whole environment, and a token there
# lets any workflow register runners against the forge.
name = "the CI runner daemon carries no registration token in its environment";
# systemd hands a credential to every process of the unit that loads
# it, and a host job is one of the daemon's processes and inherits its
# environment; a token in either lets any workflow register runners.
name = "the CI runner daemon can read no registration token";
ok =
let
sc = runnerService.serviceConfig;
@ -164,26 +170,48 @@ let
!(runnerService.environment ? TOKEN)
&& lib.toList (sc.EnvironmentFile or [ ]) == [ ]
&& !(lib.any (lib.hasPrefix "TOKEN=") (lib.toList (sc.Environment or [ ])))
&& sc.LoadCredential == [ "registration-token:/var/lib/swarm-ci-runner/registration-token" ];
&& !(sc ? LoadCredential)
&& !(sc ? LoadCredentialEncrypted)
&& !(sc ? SetCredential);
}
{
# The only reader of the credential, and the only registration path.
name = "the CI runner registers from its credential, and only without a .runner";
name = "the CI register unit alone loads the registration token, and runs before the daemon";
ok =
let
text = registerStep.text;
beforeRegister = lib.head (lib.splitString "register --no-interactive" text);
sc = registerService.serviceConfig;
in
registerStep != null
sc.LoadCredential == [ "registration-token:/var/lib/swarm-ci-runner/registration-token" ]
&& sc.Type == "oneshot"
&& sc.User == runnerService.serviceConfig.User
&& sc.DynamicUser
&& sc.StateDirectory == runnerService.serviceConfig.StateDirectory
&& lib.any (
s: lib.hasSuffix "/bin/systemctl start swarm-ci-runner-register.service" s
) runnerService.serviceConfig.ExecStartPre
&& !(lib.any (
s: lib.hasInfix "gitea-register-runner" (toString s)
) runnerService.serviceConfig.ExecStartPre)
&& lib.hasInfix "if [ -e .runner ]; then\n exit 0\nfi" beforeRegister
) runnerService.serviceConfig.ExecStartPre);
}
{
name = "the CI register unit registers from its credential, and only without a .runner";
ok =
let
text = registerService.script;
beforeRegister = lib.head (lib.splitString "register --no-interactive" text);
in
lib.hasInfix "if [ -e .runner ]; then\n exit 0\nfi" beforeRegister
&& lib.hasInfix ''token="$(< "$CREDENTIALS_DIRECTORY/registration-token")"'' beforeRegister
&& lib.hasInfix ''--token "$token"'' text
&& !(lib.hasInfix "export" text)
&& !(lib.hasInfix "TOKEN" text);
}
{
# The daemon rewrites `.runner`'s labels to this form on start; compared
# against the configured strings verbatim, a scheme-less label would
# discard `.runner` and register a new runner on every start.
name = "the CI runner compares .runner's labels in the form the daemon writes them";
ok = registerService.environment.SWARM_CI_RUNNER_LABELS == "hive-ci:host,nix:host";
}
{
# A removed option evaluates to an assertion naming the replacement.
name = "a CI option under deploy.forgejo names its replacement under deploy.ci-runner";