nix: split module-eval into per-subsystem checks
The single module-eval derivation forced ~62 full nixosSystem fixtures live at once to compute its cases list: 10.6GB peak RSS / 5m25s to evaluate, by far the dominant cost in nix flake check. Splits it into 21 independent checks.module-eval-* derivations (1-7 fixtures each) sharing builders/helpers via module-eval/lib.nix, so no single derivation needs more than a handful of fixtures live at once. A few cases spanning two clusters carry a small duplicated fixture rather than threading shared state through lib.nix.
This commit is contained in:
parent
69b70a9c6f
commit
dc418a5223
24 changed files with 3760 additions and 2997 deletions
169
nix/module-eval/agent-queue-bao.nix
Normal file
169
nix/module-eval/agent-queue-bao.nix
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
# `checks.module-eval-agent-queue-bao` — see ./lib.nix for the shared
|
||||
# rationale (why this suite exists, naming convention, "evaluates
|
||||
# not executes").
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
self,
|
||||
nixosSystem,
|
||||
}:
|
||||
let
|
||||
inherit
|
||||
(import ./lib.nix {
|
||||
inherit
|
||||
pkgs
|
||||
lib
|
||||
self
|
||||
nixosSystem
|
||||
;
|
||||
})
|
||||
agent
|
||||
agentWith
|
||||
runGroup
|
||||
agentHarness
|
||||
;
|
||||
|
||||
# The agent side of the swarm queue. Both coordinates set is the only state
|
||||
# in which the harness unit declares a credential at all, so the pair and
|
||||
# the empty fixture beside it are the two arms worth having.
|
||||
agentQueue = agent {
|
||||
queue.natsUrl = "nats://10.42.0.1:4222";
|
||||
queue.tokenEndpoint = "https://auth.t.local/api/oidc/token";
|
||||
};
|
||||
|
||||
agentNoQueue = agent { };
|
||||
|
||||
# The agent side of the swarm secret store. The address is the whole switch —
|
||||
# it is both what generates the login check and what that check points at —
|
||||
# so it and the empty fixture beside it are the two arms worth having.
|
||||
agentBao = agentWith { services.hyperhive.agent.bao.addr = "https://bao.t.local:8200"; };
|
||||
|
||||
agentNoBao = agentWith { };
|
||||
|
||||
agentBaoIdentity = machine: machine.systemd.services.hive-agent-bao-identity;
|
||||
cases = [
|
||||
{
|
||||
# Both ids or neither: the secret authenticates nobody without the id it
|
||||
# belongs to, and the harness refuses to treat one of the two as a queue.
|
||||
name = "an agent with queue coordinates imports both halves of its credential";
|
||||
ok =
|
||||
let
|
||||
c = (agentHarness agentQueue).serviceConfig.LoadCredential;
|
||||
in
|
||||
builtins.elem "hive-queue-agent-secret" c && builtins.elem "hive-queue-agent-client-id" c;
|
||||
}
|
||||
{
|
||||
# `%d` and not a path under the state dir: the host file is `0600`
|
||||
# root-owned, so the only copy this unprivileged unit can open is the
|
||||
# one systemd puts in its own credentials directory.
|
||||
name = "the harness reads its queue credential out of the credentials directory";
|
||||
ok =
|
||||
let
|
||||
e = (agentHarness agentQueue).environment;
|
||||
in
|
||||
e.HIVE_AGENT_OIDC_CLIENT_SECRET_FILE == "%d/hive-queue-agent-secret"
|
||||
&& e.HIVE_AGENT_OIDC_CLIENT_ID_FILE == "%d/hive-queue-agent-client-id";
|
||||
}
|
||||
{
|
||||
# An agent built before its hive was handed the queue's address. It
|
||||
# must declare nothing rather than name a credential that never
|
||||
# arrives — and the harness then reports "no queue coordinates"
|
||||
# instead of a half-set environment.
|
||||
name = "an agent with no queue coordinates declares no credential";
|
||||
ok =
|
||||
let
|
||||
u = agentHarness agentNoQueue;
|
||||
in
|
||||
!(u.serviceConfig ? LoadCredential)
|
||||
&& !(u.environment ? HIVE_AGENT_OIDC_CLIENT_SECRET_FILE)
|
||||
&& !(u.environment ? HIVE_AGENT_OIDC_CLIENT_ID_FILE);
|
||||
}
|
||||
{
|
||||
# The three ids `hive_c0re::lifecycle::agent_identity` forwards under.
|
||||
# Neither end can discover the other's spelling, and a mismatch is a
|
||||
# credential that is simply not there — which this unit then reports as
|
||||
# a hive that delivered nothing.
|
||||
name = "an agent with the store enabled imports every half of its identity";
|
||||
ok =
|
||||
let
|
||||
c = (agentBaoIdentity agentBao).serviceConfig.LoadCredential;
|
||||
in
|
||||
builtins.elem "hive-agent-bao-cert" c
|
||||
&& builtins.elem "hive-agent-bao-key" c
|
||||
&& builtins.elem "hive-agent-bao-server-ca" c;
|
||||
}
|
||||
{
|
||||
# `%d` and not a path under the agent's state dir, for the reason the
|
||||
# queue arm above gives: the host file is `0600` to the hive daemon, so
|
||||
# the only copy this unprivileged unit can open is the one systemd puts
|
||||
# in its own credentials directory. The address is the option's value
|
||||
# rather than a literal that agrees with it today.
|
||||
name = "the identity check presents its certificate out of the credentials directory";
|
||||
ok =
|
||||
let
|
||||
e = (agentBaoIdentity agentBao).environment;
|
||||
in
|
||||
e.BAO_CLIENT_CERT == "%d/hive-agent-bao-cert"
|
||||
&& e.BAO_CLIENT_KEY == "%d/hive-agent-bao-key"
|
||||
&& e.BAO_ADDR == agentBao.services.hyperhive.agent.bao.addr;
|
||||
}
|
||||
{
|
||||
# Same 403-not-a-miss reason as the hive-side readers: the path
|
||||
# `swarm_secret_client::mtls::identity_path` builds is the one this
|
||||
# agent's own policy stanza covers, and a path outside it is refused
|
||||
# however correct it looks. Built from the agent's own name rather than
|
||||
# from a literal, because the name is what makes it this agent's path
|
||||
# and not some other agent's.
|
||||
name = "the identity check reads the agent's own path";
|
||||
ok =
|
||||
let
|
||||
m = agentBao;
|
||||
name = m.services.hyperhive.agent.user.name;
|
||||
in
|
||||
lib.hasInfix "secret/swarm/agents/${name}/bao-mtls" (agentBaoIdentity m).script;
|
||||
}
|
||||
{
|
||||
# The whole point of the unit, and the thing a quieter default would
|
||||
# undo: every arm of the check ends the unit non-zero, so an agent that
|
||||
# cannot authenticate as itself says so at boot instead of at whichever
|
||||
# pull needed the store first.
|
||||
name = "the identity check fails the unit rather than degrading";
|
||||
ok =
|
||||
let
|
||||
u = agentBaoIdentity agentBao;
|
||||
in
|
||||
lib.hasInfix "exit 1" u.script
|
||||
&& !(lib.hasInfix "exit 0" u.script)
|
||||
&& u.serviceConfig.Restart == "on-failure";
|
||||
}
|
||||
{
|
||||
# Nothing about the identity may be printed, and the read-back is where
|
||||
# that could slip: `bao kv get` on this path answers with certificate
|
||||
# material, and the object beside it is a private key. The check needs
|
||||
# only whether the read succeeded.
|
||||
#
|
||||
# The path goes through `lib.escapeShellArg` here for the same reason the
|
||||
# module passes it through one — that helper decides whether an argument
|
||||
# needs quotes at all, and this one (only `[a-z0-9/-]`) comes back bare.
|
||||
# Spelling the quotes in by hand asserts a rendering nixpkgs chooses
|
||||
# rather than the redirect this property is about.
|
||||
name = "the identity check discards what it reads back";
|
||||
ok =
|
||||
let
|
||||
m = agentBao;
|
||||
name = m.services.hyperhive.agent.user.name;
|
||||
arg = lib.escapeShellArg "secret/swarm/agents/${name}/bao-mtls";
|
||||
in
|
||||
lib.hasInfix "bao kv get -field=cert ${arg} >/dev/null" (agentBaoIdentity m).script;
|
||||
}
|
||||
{
|
||||
# The absence arm, and what makes the four above able to fail. An agent
|
||||
# whose swarm never minted an identity has nothing to log in with, and a
|
||||
# failed unit at every boot would be the loudest possible statement
|
||||
# about a deployment that never asked for one.
|
||||
name = "an agent told no store address runs no identity check";
|
||||
ok = !(agentNoBao.systemd.services ? hive-agent-bao-identity);
|
||||
}
|
||||
];
|
||||
in
|
||||
runGroup "agent-queue-bao" cases
|
||||
Loading…
Reference in a new issue