Watch
0
0
Fork
You've already forked hyperhive
0

swarmctl: re-mint an existing agent's store identity

Agent creation at swarm level is event-driven and nothing sweeps for
agents missing a credential, so an agent created before a credential
joined the mint never receives one -- nothing comes back around to it.
Without a way to re-run the mint by hand, the only route to giving an
existing agent its queue credential would be to delete and recreate the
agent.

POST /api/agents/{name}/identity enqueues the same MintAgentIdentity
node POST /api/agents declares, rather than writing inline: a second
code path that mints an identity is a second place for the four strings
that have to agree to disagree. swarmctl agent mint-identity is the
operator end, the same POST-and-print-the-node-id shape agent create
already has.

--hive is required on both ends. Neither the CLI nor the controller
keeps a roster of which agent runs where, and the credentials this mints
name a hive, so a default would be a guess that hands an agent subjects
on a hive it does not run on.

Documents the backfill as a runbook step, and fills in the renewal cell
the credential matrix requires for the new row.
This commit is contained in:
atlas 2026-09-21 18:36:57 +02:00 • committed by mara
commit 1442168715
6 changed files with 332 additions and 37 deletions

View file

@ -168,6 +168,23 @@ enum AgentVerb {
/// No approval gate guards this: running this binary already means
/// being root on the controller's host.
Create(AgentCreateArgs),
/// Queue a re-mint of an existing agent's identity at the swarm's
/// secret store.
///
/// **The backfill verb.** Agent creation is event-driven and nothing at
/// swarm level sweeps for agents that are missing a credential, so an
/// agent created before a credential joined the mint never receives one.
/// This re-runs the mint for one agent that already exists.
///
/// ⚠️ **It re-mints the agent's store certificate**, which that agent
/// picks up the next time its container boots. The agent's queue secret
/// is left exactly as it is if it already has one, so running this
/// against an already-migrated agent does not disturb its queue
/// connection.
///
/// Queues and returns, the same way `agent create` does — watch the
/// swarm UI's job view for the outcome.
MintIdentity(AgentMintIdentityArgs),
}
#[derive(Args)]
@ -194,6 +211,29 @@ struct AgentCreateArgs {
controller_socket: Option<PathBuf>,
}
#[derive(Args)]
struct AgentMintIdentityArgs {
/// Name of an agent that already exists.
name: String,
/// The hive that agent runs on.
///
/// Required, and deliberately not defaulted: the credentials this mints
/// name a hive, and neither this CLI nor the controller keeps a roster of
/// which agent is on which hive. Naming the wrong one gives the agent an
/// identity scoped to a hive it does not run on. The controller checks
/// the value against the swarm's hive roster and names the known hives if
/// it misses.
#[arg(long, value_name = "HIVE")]
hive: String,
/// swarm-controller's unix socket.
///
/// Supplied by the nix module that installs this binary, from the same
/// `socketPath` option the daemon binds; falls back to
/// `SWARM_CONTROLLER_SOCKET`.
#[arg(long, value_name = "PATH")]
controller_socket: Option<PathBuf>,
}
#[derive(Subcommand)]
enum UserVerb {
/// Add a user, generating a password for them.
@ -262,6 +302,13 @@ fn main() -> Result<()> {
let socket = path_from(args.controller_socket, "SWARM_CONTROLLER_SOCKET")?;
agent::create(&socket, &args.name, &args.hive)
}
// Same socket-resolution reasoning as `Create` above.
Verb::Agent {
command: AgentVerb::MintIdentity(args),
} => {
let socket = path_from(args.controller_socket, "SWARM_CONTROLLER_SOCKET")?;
agent::mint_identity(&socket, &args.name, &args.hive)
}
// Resolved lazily, inside the one arm that actually touches the
// deployment env vars — see the `MarkdownDocs` doc comment above
// for why an unconditional resolve up front would be wrong.
@ -621,6 +668,36 @@ mod tests {
assert!(args.controller_socket.is_none());
}
/// The backfill verb takes the same two names as `create`, and `--hive`
/// is required on it for the same reason: it is an address nobody can
/// infer.
#[test]
fn the_backfill_verb_takes_an_agent_and_a_hive() {
let cli = Cli::try_parse_from([
"swarmctl",
"agent",
"mint-identity",
"scribe",
"--hive",
"alpha",
])
.expect("the minimal form parses");
let Verb::Agent {
command: AgentVerb::MintIdentity(args),
} = cli.command
else {
panic!("expected `agent mint-identity`");
};
assert_eq!(args.name, "scribe");
assert_eq!(args.hive, "alpha");
assert!(args.controller_socket.is_none());
assert!(
Cli::try_parse_from(["swarmctl", "agent", "mint-identity", "scribe"]).is_err(),
"an omitted hive must not be defaulted"
);
}
#[test]
fn parses_authelia_hash_output() {
let out = "Random Password: hunter2\nDigest: $argon2id$v=19$m=65536$abc\n";