`swarm-bao-pki` was a `RemainAfterExit` oneshot. Once active, nothing re-ran it: a switch restarts it only when its definition changes, and a `Requires=`/`Wants=` on an active unit is satisfied without running it. On muede-lpt2 the 10:57Z switch that added the builder line did change the unit, so switch-to-configuration stopped it in its stop phase. A start of `container@swarm-bao` 4 s later (before the daemon-reload at 10:57:35Z) pulled it back in through `swarm-bao-certs` with the OLD definition, which had no builder line: 48 ms, nothing missing. The start phase then found it active and did nothing. The 20:14Z switch that enabled the builder left the unit unchanged, so it was never re-run, and `container@swarm-builder` crash-looped on the absent `builder.pem`. The unit now goes inactive after each run. Every switch starts `multi-user.target`, which runs it again post-reload with the current definition, and every unit that wants or requires it runs it on its own start. Each line is idempotent on presence, so an existing leaf is never re-signed. The three consumers started from `machines.target` rather than `multi-user.target` also want it, since `After=` alone does not order against a job in another transaction: the builder's and the review classifier's containers (nspawn `--load-credential`) and `swarm-nats-auth-secrets` (pulled in by the queue's container). Fixes #5284
146 lines
5.9 KiB
Nix
146 lines
5.9 KiB
Nix
# `checks.module-eval-builder` — see ./lib.nix for the shared rationale (why
|
|
# this suite exists, naming convention, "evaluates not executes"). The swarm
|
|
# builder's container, its default and its queue and IdP registrations; its
|
|
# store grants are in ./bao-grants.nix beside every other principal's.
|
|
{
|
|
pkgs,
|
|
lib,
|
|
self,
|
|
nixosSystem,
|
|
shared,
|
|
}:
|
|
let
|
|
inherit
|
|
(import ./lib.nix {
|
|
inherit
|
|
pkgs
|
|
lib
|
|
self
|
|
nixosSystem
|
|
;
|
|
})
|
|
hive
|
|
runGroup
|
|
;
|
|
|
|
bare = shared.bareWithController;
|
|
|
|
# On the store's host, which mints the builder's store identity.
|
|
withBuilder = hive {
|
|
deploy.bao.enable = true;
|
|
deploy.builder.enable = true;
|
|
};
|
|
|
|
servicesHere = shared.swarmServicesWithController;
|
|
# Short-circuits on `assertion`, so only a failing one has its message read.
|
|
refusedWithoutIdentity =
|
|
m:
|
|
lib.any (a: !a.assertion && lib.hasInfix "deploy.builder.enable requires" a.message) m.assertions;
|
|
|
|
natsHere = shared.natsOldPath;
|
|
|
|
autheliaHere = hive {
|
|
deploy.bao.enable = true;
|
|
deploy.authelia.enable = true;
|
|
};
|
|
|
|
container = withBuilder.containers.swarm-builder;
|
|
inner = container.config;
|
|
svc = inner.systemd.services.swarm-builder;
|
|
env = svc.environment;
|
|
|
|
cases = [
|
|
{
|
|
name = "the swarm builder's container exists only where its host enables it";
|
|
ok = !(bare.containers ? swarm-builder) && withBuilder.containers ? swarm-builder;
|
|
}
|
|
{
|
|
# The store's host mints the builder's leaf, so the switch needs no
|
|
# manual identity step: the pair is set and the module's identity
|
|
# assertion, which nothing else reads in this fixture, passes. `nats` is
|
|
# the control that the switch is on.
|
|
name = "hosting the shared services turns the builder on, with its store identity";
|
|
ok =
|
|
servicesHere.services.hyperhive.deploy.builder.enable
|
|
&& servicesHere.containers ? swarm-builder
|
|
&& servicesHere.services.hyperhive.deploy.bao.builderClientCertFile != null
|
|
&& servicesHere.services.hyperhive.deploy.bao.builderClientKeyFile != null
|
|
&& !(refusedWithoutIdentity servicesHere)
|
|
&& servicesHere.services.hyperhive.deploy.bao.enable
|
|
&& servicesHere.services.hyperhive.deploy.nats.enable;
|
|
}
|
|
{
|
|
# A config PR is unreviewed input to an evaluation: the container keeps
|
|
# its own netns, and reaches the host's nix daemon through its socket
|
|
# alone, with no daemon of its own racing the bind.
|
|
name = "the builder's container has its own netns and builds only through the host's daemon socket";
|
|
ok =
|
|
container.privateNetwork
|
|
&& inner.services.hyperhive.swarmContainer.privateNetwork
|
|
&& container.bindMounts."/nix/var/nix/daemon-socket".hostPath == "/nix/var/nix/daemon-socket"
|
|
&& !inner.systemd.services.nix-daemon.enable
|
|
&& !inner.systemd.sockets.nix-daemon.enable
|
|
&& inner.systemd.globalEnvironment.NIX_REMOTE == "daemon";
|
|
}
|
|
{
|
|
# The daemon holds its store leaf, handed in by nspawn without a copy on
|
|
# disk; the forge token is read per build and reaches only `git`.
|
|
name = "the builder loads only its store leaf, runs as a dynamic user and carries no token in its unit";
|
|
ok =
|
|
lib.elem "builder-bao-key" svc.serviceConfig.LoadCredential
|
|
&& env.BAO_CLIENT_KEY == "/run/credentials/swarm-builder.service/builder-bao-key"
|
|
&& env.SWARM_BUILDER_OIDC_CLIENT_ID == "swarm-builder"
|
|
&& lib.elem "--load-credential=builder-bao-key:/var/lib/swarm-bao-pki/builder-key.pem" container.extraFlags
|
|
&& svc.serviceConfig.DynamicUser
|
|
&& !(lib.any (lib.hasSuffix "TOKEN") (lib.attrNames env));
|
|
}
|
|
{
|
|
# The minting unit goes inactive after each run, so a start that pulls it
|
|
# runs it again and mints a leaf that is missing; an active
|
|
# `RemainAfterExit` unit would satisfy the pull without running.
|
|
name = "the builder's container runs the leaf-minting unit before every start";
|
|
ok =
|
|
let
|
|
pki = withBuilder.systemd.services.swarm-bao-pki;
|
|
c = withBuilder.systemd.services."container@swarm-builder";
|
|
in
|
|
pki.serviceConfig.Type == "oneshot"
|
|
&& !(pki.serviceConfig.RemainAfterExit or false)
|
|
&& lib.elem "multi-user.target" pki.wantedBy
|
|
&& lib.elem "swarm-bao-pki.service" c.wants
|
|
&& lib.elem "swarm-bao-pki.service" c.after;
|
|
}
|
|
{
|
|
# The check builds on the sources a hive on this host would render into
|
|
# its meta flake, so a green check means the same base.
|
|
name = "the builder builds on the hyperhive and nixpkgs this host's hives use";
|
|
ok =
|
|
env.SWARM_BUILDER_HYPERHIVE_FLAKE == withBuilder.services.hyperhive.c0re.hyperhiveFlake
|
|
&& env.SWARM_BUILDER_NIXPKGS_FLAKE == withBuilder.services.hyperhive.c0re.nixpkgsFlake
|
|
&& lib.hasSuffix "-check-flake" env.SWARM_BUILDER_CHECK_FLAKE
|
|
&& env.SWARM_BUILDER_CONFIG_ORG == "agent-configs"
|
|
&& env.SWARM_BUILDER_FORGE_URL == "https://${withBuilder.services.hyperhive.swarm.forge.domain}";
|
|
}
|
|
{
|
|
# The responder denies a client id it does not know, and a NATS denial
|
|
# arrives as a timeout.
|
|
name = "the queue's responder grants the builder's client its subject";
|
|
ok = lib.hasInfix "--builder-client swarm-builder" natsHere.containers.swarm-nats.config.systemd.services.swarm-nats-auth.serviceConfig.ExecStart;
|
|
}
|
|
{
|
|
name = "the builder's client is registered as a machine, and the publisher carries its secret";
|
|
ok =
|
|
let
|
|
c = lib.findFirst (
|
|
c: c.id == "swarm-builder"
|
|
) null autheliaHere.services.hyperhive.swarm.authelia.oidc.clients;
|
|
in
|
|
c != null
|
|
&& c.kind == "machine"
|
|
&& lib.hasInfix "secret/swarm/services/swarm-builder/oidc/client" (
|
|
autheliaHere.systemd.services.swarm-secret-publish.script
|
|
);
|
|
}
|
|
];
|
|
in
|
|
runGroup "builder" cases
|