agents: pull the forge token from bao; drop tea-login
forge-token.nix fetches swarm/agents/<agent>/forge-token under the agent's own store identity into /run/hive-agent-forge-token/token, and re-fetches on a timer so a rotation lands. hive-forge, the git credential helper, hive-forge-notify, forge-avatar-sync and the web UI read that file first and fall back to <state>/forge-token. tea-login is deleted: it copied the token into ~/.config/tea, which docs/swarm/credentials.md forbids for a store secret. hive-forge covers the same verbs. swarmctl gains agent mint-forge-token. Refs #3782
This commit is contained in:
parent
52c8c0b0de
commit
dd32a395f7
16 changed files with 501 additions and 156 deletions
|
|
@ -1,5 +1,5 @@
|
|||
//! `swarmctl agent create` and `swarmctl agent mint-identity` — queue work
|
||||
//! on the swarm-controller's job graph.
|
||||
//! `swarmctl agent create`, `swarmctl agent mint-identity` and `swarmctl agent
|
||||
//! mint-forge-token` — queue work on the swarm-controller's job graph.
|
||||
//!
|
||||
//! Each POSTs and returns as soon as the work is *inserted*. Both verbs
|
||||
//! print the queued node id and stop: creation's last node only *publishes*
|
||||
|
|
@ -64,6 +64,12 @@ struct MintIdentityResponse {
|
|||
node_id: u64,
|
||||
}
|
||||
|
||||
/// Success body of `POST /api/agents/{name}/forge-token`, which takes no body.
|
||||
#[derive(Deserialize)]
|
||||
struct MintForgeTokenResponse {
|
||||
node_id: u64,
|
||||
}
|
||||
|
||||
/// Run `swarmctl agent create`.
|
||||
///
|
||||
/// Synchronous on purpose: every other verb in this crate is, and this is
|
||||
|
|
@ -137,6 +143,33 @@ pub(crate) fn mint_identity(socket: &Path, name: &str, hive: &str) -> Result<()>
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Run `swarmctl agent mint-forge-token`.
|
||||
///
|
||||
/// Synchronous for the same reason [`create`] is, and built on the same
|
||||
/// round trip. No `--hive`: the token's store path has no hive in it.
|
||||
pub(crate) fn mint_forge_token(socket: &Path, name: &str) -> Result<()> {
|
||||
let name = parse_ident(name, "agent name")?;
|
||||
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_io()
|
||||
.build()
|
||||
.context("starting a tokio runtime for the controller request")?;
|
||||
let resp: MintForgeTokenResponse = rt.block_on(post(
|
||||
socket,
|
||||
&format!("/api/agents/{name}/forge-token"),
|
||||
&serde_json::json!({}),
|
||||
"mint-forge-token",
|
||||
))?;
|
||||
|
||||
println!("queued: job node {}", resp.node_id);
|
||||
println!(
|
||||
"agent {name:?}'s forge token will be checked once the job graph runs, and minted \
|
||||
only if it is missing or stale; `swarmctl` does not wait for it. The agent picks a \
|
||||
new token up on its next fetch"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// One `POST /api/agents` round trip over the controller's unix socket.
|
||||
async fn post_create(socket: &Path, name: &str, hive: &str) -> Result<CreateAgentResponse> {
|
||||
post(
|
||||
|
|
|
|||
|
|
@ -184,6 +184,15 @@ enum AgentVerb {
|
|||
/// Queues and returns, the same way `agent create` does — watch the
|
||||
/// swarm UI's job view for the outcome.
|
||||
MintIdentity(AgentMintIdentityArgs),
|
||||
/// Check one agent's forge token, and mint it if it's missing or stale.
|
||||
///
|
||||
/// swarm-controller does this for every agent with a store identity at
|
||||
/// start and every five minutes; this is for when waiting isn't an
|
||||
/// option. It leaves a current token alone.
|
||||
///
|
||||
/// Queues and returns, the same way `agent create` does — watch the
|
||||
/// swarm UI's job view for the outcome.
|
||||
MintForgeToken(AgentMintForgeTokenArgs),
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
|
|
@ -210,6 +219,19 @@ struct AgentCreateArgs {
|
|||
controller_socket: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
struct AgentMintForgeTokenArgs {
|
||||
/// Name of an agent that already exists.
|
||||
name: 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(Args)]
|
||||
struct AgentMintIdentityArgs {
|
||||
/// Name of an agent that already exists.
|
||||
|
|
@ -308,6 +330,13 @@ fn main() -> Result<()> {
|
|||
let socket = path_from(args.controller_socket, "SWARM_CONTROLLER_SOCKET")?;
|
||||
agent::mint_identity(&socket, &args.name, &args.hive)
|
||||
}
|
||||
// Same socket-resolution reasoning as `Create` above.
|
||||
Verb::Agent {
|
||||
command: AgentVerb::MintForgeToken(args),
|
||||
} => {
|
||||
let socket = path_from(args.controller_socket, "SWARM_CONTROLLER_SOCKET")?;
|
||||
agent::mint_forge_token(&socket, &args.name)
|
||||
}
|
||||
// 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.
|
||||
|
|
@ -697,6 +726,31 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_forge_token_verb_takes_an_agent_and_no_hive() {
|
||||
let cli = Cli::try_parse_from(["swarmctl", "agent", "mint-forge-token", "scribe"])
|
||||
.expect("the minimal form parses");
|
||||
let Verb::Agent {
|
||||
command: AgentVerb::MintForgeToken(args),
|
||||
} = cli.command
|
||||
else {
|
||||
panic!("expected `agent mint-forge-token`");
|
||||
};
|
||||
assert_eq!(args.name, "scribe");
|
||||
assert!(
|
||||
Cli::try_parse_from([
|
||||
"swarmctl",
|
||||
"agent",
|
||||
"mint-forge-token",
|
||||
"scribe",
|
||||
"--hive",
|
||||
"a"
|
||||
])
|
||||
.is_err(),
|
||||
"the token has no hive, so the verb must not take one"
|
||||
);
|
||||
}
|
||||
|
||||
#[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