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:
parent
ffd5018b18
commit
1442168715
6 changed files with 332 additions and 37 deletions
|
|
@ -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";
|
||||
|
|
|
|||
Loading…
Reference in a new issue