swarm-controller re-issues an agent's store leaf at half its validity, but the running container kept presenting the copy its hive delivered at start, so a re-issued leaf reached the agent only on its next restart. hive-agent-bao-identity now seeds a live copy of the pair in its own runtime directory from the delivered credentials, and re-runs every 10 minutes: it logs in with the live leaf, reads its own swarm/agents/<agent>/bao-mtls (a read render_agent already grants), and when the leaf changed, logs in with the new pair and swaps it in only if the store accepts it. Every unit in the container that logs in to the store presents the live pair: hive-agent (queue-identity.nix), hive-matrix-daemon, hive-subagent-daemon, hive-agent-forge-token, hive-agent-forge-accounts and hive-agent-github-token. They keep loading the server CA as a credential. The long-running daemons are ordered after the refresher so the live pair exists when they start. It renews only while the held leaf is still valid: the login that reads the new leaf is made with the current one. Refs #5017
221 lines
8.4 KiB
Nix
221 lines
8.4 KiB
Nix
# `checks.module-eval-agent-forge-bao` — see ./lib.nix for the shared
|
|
# rationale (why this suite exists, naming convention, "evaluates
|
|
# not executes").
|
|
#
|
|
# The agent side of the swarm-minted forge token: ../agent-modules/forge-token.nix
|
|
# fetches it, and ../agent-modules/forge.nix's readers find it.
|
|
{
|
|
pkgs,
|
|
lib,
|
|
self,
|
|
nixosSystem,
|
|
}:
|
|
let
|
|
inherit
|
|
(import ./lib.nix {
|
|
inherit
|
|
pkgs
|
|
lib
|
|
self
|
|
nixosSystem
|
|
;
|
|
})
|
|
agentWith
|
|
runGroup
|
|
;
|
|
|
|
forgeUrl = "http://forge.t.local";
|
|
baoAddr = "https://bao.t.local:8200";
|
|
|
|
# A forge and a store: the fetch exists and every reader points at it.
|
|
agentForgeBao = agentWith {
|
|
services.hyperhive.agent.bao.addr = baoAddr;
|
|
services.hyperhive.agent.forge.url = forgeUrl;
|
|
services.hyperhive.agent.icon = pkgs.emptyFile;
|
|
};
|
|
|
|
# A forge and no store: the absence arm, and what makes the cases above able
|
|
# to fail.
|
|
agentForgeNoBao = agentWith {
|
|
services.hyperhive.agent.forge.url = forgeUrl;
|
|
services.hyperhive.agent.icon = pkgs.emptyFile;
|
|
};
|
|
|
|
fetchUnit = machine: machine.systemd.services.hive-agent-forge-token;
|
|
accountsUnit = machine: machine.systemd.services.hive-agent-forge-accounts;
|
|
tokenFile = machine: machine.services.hyperhive.agent.forge.tokenFile;
|
|
in
|
|
let
|
|
cases = [
|
|
{
|
|
# The whole switch is the store address, as for the queue credential.
|
|
name = "an agent with a store address fetches its forge token";
|
|
ok =
|
|
agentForgeBao.systemd.services ? hive-agent-forge-token
|
|
&& agentForgeBao.systemd.timers ? hive-agent-forge-token;
|
|
}
|
|
{
|
|
name = "an agent told no store address fetches no forge token";
|
|
ok =
|
|
!(agentForgeNoBao.systemd.services ? hive-agent-forge-token)
|
|
&& !(agentForgeNoBao.systemd.timers ? hive-agent-forge-token)
|
|
&& !(agentForgeNoBao.systemd.globalEnvironment ? HIVE_FORGE_TOKEN_FILE);
|
|
}
|
|
{
|
|
# The nix half of `swarm_secret_client::forge::agent_token_path` plus the
|
|
# mount. Spelled out: the Rust test pins `swarm/agents/atlas/forge-token`,
|
|
# and a drift between the two is a fetch that 404s forever and says
|
|
# "not minted yet".
|
|
name = "the fetch reads the agent's own forge-token path";
|
|
ok =
|
|
let
|
|
name = agentForgeBao.services.hyperhive.agent.user.name;
|
|
in
|
|
lib.hasInfix "secret/swarm/agents/${name}/forge-token" (fetchUnit agentForgeBao).script;
|
|
}
|
|
{
|
|
# One store identity per agent: the fetch presents the same certificate
|
|
# the identity check keeps current. Every `BAO_*` value is an address or
|
|
# a path, never a value.
|
|
name = "the fetch presents the agent's own store identity, by path";
|
|
ok =
|
|
let
|
|
u = fetchUnit agentForgeBao;
|
|
e = u.environment;
|
|
in
|
|
e.BAO_ADDR == baoAddr
|
|
&& e.BAO_CLIENT_CERT == "/run/hive-agent-bao-identity/cert"
|
|
&& e.BAO_CLIENT_KEY == "/run/hive-agent-bao-identity/key";
|
|
}
|
|
{
|
|
# The field is the secret. It goes to a file by redirect and is never
|
|
# echoed.
|
|
name = "the fetch writes the token by redirect and never echoes it";
|
|
ok =
|
|
let
|
|
s = (fetchUnit agentForgeBao).script;
|
|
in
|
|
lib.hasInfix "-field=value" s
|
|
&& lib.hasInfix "> ${lib.escapeShellArg "/run/hive-agent-forge-token/token.new"}" s
|
|
&& !(lib.hasInfix "echo \"$(bao" s)
|
|
&& !(lib.hasInfix "echo $(bao" s);
|
|
}
|
|
{
|
|
# A timer can only start an inactive unit, so the fetch must not stay
|
|
# active; the directory, and the token in it, has to outlive each run.
|
|
name = "the fetch can be re-run by its timer without losing the token";
|
|
ok =
|
|
let
|
|
c = (fetchUnit agentForgeBao).serviceConfig;
|
|
in
|
|
!c.RemainAfterExit
|
|
&& c.RuntimeDirectory == "hive-agent-forge-token"
|
|
&& c.RuntimeDirectoryPreserve == "yes"
|
|
&& c.UMask == "0377";
|
|
}
|
|
{
|
|
# Rename in only on a change: a reader never sees half a token, and the
|
|
# avatar watcher does not fire on every timer tick.
|
|
name = "the fetch swaps the token in by rename, only when it changed";
|
|
ok =
|
|
let
|
|
s = (fetchUnit agentForgeBao).script;
|
|
in
|
|
lib.hasInfix "cmp -s" s && lib.hasInfix "mv -f" s;
|
|
}
|
|
{
|
|
# Every unit and the bash-task runner find the token through this. A
|
|
# path, never the value.
|
|
name = "the fetched token's path is published to every unit";
|
|
ok =
|
|
agentForgeBao.systemd.globalEnvironment.HIVE_FORGE_TOKEN_FILE == tokenFile agentForgeBao
|
|
&& tokenFile agentForgeBao == "/run/hive-agent-forge-token/token";
|
|
}
|
|
{
|
|
# The avatar sync has to re-fire when the swarm's token lands, which is
|
|
# the file the fetch writes, not the state-dir file nothing writes any
|
|
# more.
|
|
name = "the avatar watcher follows the fetched token";
|
|
ok =
|
|
agentForgeBao.systemd.paths.forge-avatar-sync.pathConfig.PathChanged == tokenFile agentForgeBao
|
|
&& builtins.elem "hive-agent-forge-token.service" agentForgeBao.systemd.services.forge-avatar-sync.after;
|
|
}
|
|
{
|
|
# Both readers take the fetched token first and fall back to the state
|
|
# file, which is still the only copy for an agent with no store identity.
|
|
name = "the avatar sync reads the fetched token before the state file";
|
|
ok =
|
|
let
|
|
s = agentForgeBao.systemd.services.forge-avatar-sync.script;
|
|
name = agentForgeBao.services.hyperhive.agent.user.name;
|
|
fetched = lib.escapeShellArg (tokenFile agentForgeBao);
|
|
state = lib.escapeShellArg "/agents/${name}/state/forge-token";
|
|
in
|
|
lib.hasInfix "for f in ${fetched} ${state}; do" s;
|
|
}
|
|
{
|
|
# tea-login copied the token into ~/.config/tea/config.yml, which
|
|
# docs/swarm/credentials.md forbids for a store secret. Gone, with the
|
|
# package it configured.
|
|
name = "no tea-login unit and no tea package";
|
|
ok =
|
|
!(agentForgeBao.systemd.services ? tea-login)
|
|
&& !(agentForgeNoBao.systemd.services ? tea-login)
|
|
&& !(builtins.elem pkgs.tea agentForgeBao.environment.systemPackages);
|
|
}
|
|
{
|
|
name = "an agent with a store address fetches its external forge accounts, and one without does not";
|
|
ok =
|
|
agentForgeBao.systemd.services ? hive-agent-forge-accounts
|
|
&& agentForgeBao.systemd.timers ? hive-agent-forge-accounts
|
|
&& !(agentForgeNoBao.systemd.services ? hive-agent-forge-accounts)
|
|
&& !(agentForgeNoBao.systemd.timers ? hive-agent-forge-accounts);
|
|
}
|
|
{
|
|
# The nix half of `swarm_secret_client::forge::{accounts_dir,account_path}`,
|
|
# and the two file names `hive-forge -f` reads.
|
|
name = "the account fetch lists the agent's own accounts and reads each into hive-forge's files";
|
|
ok =
|
|
let
|
|
name = agentForgeBao.services.hyperhive.agent.user.name;
|
|
s = (accountsUnit agentForgeBao).script;
|
|
in
|
|
lib.hasInfix "bao kv list -format=json secret/swarm/agents/${name}/forge >" s
|
|
&& !(lib.hasInfix "/index/" s)
|
|
&& lib.hasInfix "path=\"secret/swarm/agents/${name}/forge/$label\"" s
|
|
&& lib.hasInfix "/agents/${name}/state" s
|
|
&& lib.hasInfix "/forge-$label-token" s
|
|
&& lib.hasInfix "/forge-$label.json" s
|
|
&& lib.hasInfix "{base_url: (.data.data.url | strings)}" s;
|
|
}
|
|
{
|
|
# The agent user owns its state dir (./user.nix), and the files keep
|
|
# the `0600` they have always had.
|
|
name = "the account fetch runs as the agent, with its own store identity";
|
|
ok =
|
|
let
|
|
u = accountsUnit agentForgeBao;
|
|
name = agentForgeBao.services.hyperhive.agent.user.name;
|
|
in
|
|
u.serviceConfig.User == name
|
|
&& u.serviceConfig.UMask == "0077"
|
|
&& u.environment.BAO_CLIENT_CERT == "/run/hive-agent-bao-identity/cert";
|
|
}
|
|
{
|
|
# Files a hive wrote keep working until the operator re-links them.
|
|
name = "the account fetch never deletes a state-dir file it did not stage";
|
|
ok =
|
|
let
|
|
s = (accountsUnit agentForgeBao).script;
|
|
in
|
|
!(lib.hasInfix "rm -f \"$token\"" s)
|
|
&& !(lib.hasInfix "rm -f \"$sidecar\"" s)
|
|
&& !(lib.hasInfix "forge-*" s);
|
|
}
|
|
{
|
|
name = "the account fetch re-runs every two minutes";
|
|
ok = agentForgeBao.systemd.timers.hive-agent-forge-accounts.timerConfig.OnUnitInactiveSec == "2min";
|
|
}
|
|
];
|
|
in
|
|
runGroup "agent-forge-bao" cases
|