swarm-controller reads its forge token from the store, not a host file
The controller's forge token never entered bao: the forge minted it in its container, hive-forge-swarm-controller-token copied it to a host file, and the controller LoadCredential'ed that file. forgeTokenFile defaulted to that path only where the forge ran, so a controller on any other host had no forge access unless an operator copied the file by hand. Now the forge's host publishes it and the controller reads it: - swarm-bao-forge-publisher (replaces hive-forge-swarm-controller-token) waits for the in-container token, logs in with its own forge-publisher leaf and writes it to swarm/controller/swarm-controller/forge/token. It runs on every forge container start, the old unit's trigger. - New principal swarm-forge-publisher: leaf minted by glue-bao-tls.nix, policy create/update on that one path, written by swarm-bao-forge-publisher-policy, CN reserved as a hive name. - The controller policy gains read on that path. swarm-controller reads it at start under its own certificate with the OIDC secret's retry, and runs without forge access if it still cannot. - forgeTokenFile, its LoadCredential and the forgeEnv gate are gone; SWARM_CONTROLLER_FORGE_URL is always set. hostSwarmControllerTokenFile is removed, and a tmpfiles rule deletes the old host copy on the forge host. The in-container file stays: the mint unit's only-once guard reads it, and it is the publisher's source. Refs #4998
This commit is contained in:
parent
c433bdef92
commit
121c4678aa
18 changed files with 580 additions and 277 deletions
|
|
@ -313,8 +313,8 @@ async fn run_swarm_node(
|
|||
},
|
||||
SwarmNodeKind::CreateRepo { agent } => match deps.forge {
|
||||
None => Outcome::Failed(
|
||||
"no forge configured on this host (SWARM_CONTROLLER_FORGE_URL / \
|
||||
SWARM_CONTROLLER_FORGE_TOKEN_FILE unset)"
|
||||
"no forge access here (SWARM_CONTROLLER_FORGE_URL unset, or its token \
|
||||
not in the swarm secret store)"
|
||||
.to_owned(),
|
||||
),
|
||||
Some(client) => match client.create_repo(&agent).await {
|
||||
|
|
@ -327,8 +327,8 @@ async fn run_swarm_node(
|
|||
},
|
||||
SwarmNodeKind::CreateForgeUser { agent } => match deps.forge {
|
||||
None => Outcome::Failed(
|
||||
"no forge configured on this host (SWARM_CONTROLLER_FORGE_URL / \
|
||||
SWARM_CONTROLLER_FORGE_TOKEN_FILE unset)"
|
||||
"no forge access here (SWARM_CONTROLLER_FORGE_URL unset, or its token \
|
||||
not in the swarm secret store)"
|
||||
.to_owned(),
|
||||
),
|
||||
Some(client) => match client.ensure_agent_user(&agent).await {
|
||||
|
|
@ -338,8 +338,8 @@ async fn run_swarm_node(
|
|||
},
|
||||
SwarmNodeKind::AddRepoMember { agent } => match deps.forge {
|
||||
None => Outcome::Failed(
|
||||
"no forge configured on this host (SWARM_CONTROLLER_FORGE_URL / \
|
||||
SWARM_CONTROLLER_FORGE_TOKEN_FILE unset)"
|
||||
"no forge access here (SWARM_CONTROLLER_FORGE_URL unset, or its token \
|
||||
not in the swarm secret store)"
|
||||
.to_owned(),
|
||||
),
|
||||
Some(client) => match client.add_repo_member(&agent, &agent).await {
|
||||
|
|
@ -350,8 +350,8 @@ async fn run_swarm_node(
|
|||
SwarmNodeKind::GrantDocsRead { agent } => grant_docs_read(deps.forge, &agent).await,
|
||||
SwarmNodeKind::InitAgentConfigRepo { agent } => match deps.forge {
|
||||
None => Outcome::Failed(
|
||||
"no forge configured on this host (SWARM_CONTROLLER_FORGE_URL / \
|
||||
SWARM_CONTROLLER_FORGE_TOKEN_FILE unset)"
|
||||
"no forge access here (SWARM_CONTROLLER_FORGE_URL unset, or its token \
|
||||
not in the swarm secret store)"
|
||||
.to_owned(),
|
||||
),
|
||||
Some(client) => match client.seed_agent_config(&agent, &agent).await {
|
||||
|
|
@ -411,8 +411,8 @@ async fn mint_forge_token(
|
|||
|
||||
let Some(client) = forge else {
|
||||
return Outcome::Failed(
|
||||
"no forge configured on this host (SWARM_CONTROLLER_FORGE_URL / \
|
||||
SWARM_CONTROLLER_FORGE_TOKEN_FILE unset), so no agent forge token can be minted"
|
||||
"no forge access here (SWARM_CONTROLLER_FORGE_URL unset, or its token \
|
||||
not in the swarm secret store), so no agent forge token can be minted"
|
||||
.to_owned(),
|
||||
);
|
||||
};
|
||||
|
|
@ -431,8 +431,8 @@ async fn grant_docs_read(
|
|||
|
||||
let Some(client) = forge else {
|
||||
return Outcome::Failed(
|
||||
"no forge configured on this host (SWARM_CONTROLLER_FORGE_URL / \
|
||||
SWARM_CONTROLLER_FORGE_TOKEN_FILE unset), so no docs grant can be made"
|
||||
"no forge access here (SWARM_CONTROLLER_FORGE_URL unset, or its token \
|
||||
not in the swarm secret store), so no docs grant can be made"
|
||||
.to_owned(),
|
||||
);
|
||||
};
|
||||
|
|
@ -461,8 +461,8 @@ async fn mint_hive_forge_token(
|
|||
|
||||
let Some(client) = forge else {
|
||||
return Outcome::Failed(
|
||||
"no forge configured on this host (SWARM_CONTROLLER_FORGE_URL / \
|
||||
SWARM_CONTROLLER_FORGE_TOKEN_FILE unset), so no hive forge token can be minted"
|
||||
"no forge access here (SWARM_CONTROLLER_FORGE_URL unset, or its token \
|
||||
not in the swarm secret store), so no hive forge token can be minted"
|
||||
.to_owned(),
|
||||
);
|
||||
};
|
||||
|
|
@ -3017,13 +3017,14 @@ async fn main() -> Result<()> {
|
|||
None
|
||||
}
|
||||
};
|
||||
// Same shape again: a controller with no forge configured still serves
|
||||
// Same shape again: a controller with no forge access still serves
|
||||
// everything else, and the forge-shaped node kinds give an honest
|
||||
// per-job failure rather than this fn refusing to start.
|
||||
let forge_client = match forge::Client::from_env() {
|
||||
// per-job failure rather than this fn refusing to start. A token missing
|
||||
// from the store lands here too, after `from_env`'s retries.
|
||||
let forge_client = match forge::Client::from_env().await {
|
||||
Ok(client) => client.map(Arc::new),
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %format!("{e:#}"), "forge misconfigured; repo provisioning is off");
|
||||
tracing::warn!(error = %format!("{e:#}"), "no forge access; repo provisioning is off");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
|
@ -3250,7 +3251,7 @@ mod tests {
|
|||
/// interesting behavior is in `run_swarm_node`'s executor arm, which
|
||||
/// this exercises directly).
|
||||
///
|
||||
/// Deliberately offline: with both forge env vars unset,
|
||||
/// Deliberately offline: with the forge URL unset,
|
||||
/// `forge::Client::from_env` returns `Ok(None)` (see that module's doc
|
||||
/// comment), so this exercises the whole claim → run → complete path
|
||||
/// through `hive_jobq::scheduler::Scheduler` without a real forge
|
||||
|
|
@ -3259,7 +3260,7 @@ mod tests {
|
|||
/// actual forge instance and isn't something a unit test in this crate
|
||||
/// can reach.
|
||||
///
|
||||
/// SAFETY: single-threaded mutation of the two `forge` env vars this
|
||||
/// SAFETY: single-threaded mutation of the `forge` env var this
|
||||
/// test itself owns, restored before returning — no other test in this
|
||||
/// crate reads them.
|
||||
/// One roster, one agent name, two hives — the accepted one and a
|
||||
|
|
@ -4177,7 +4178,6 @@ mod tests {
|
|||
async fn create_repo_node_runs_end_to_end_and_fails_without_forge_configured() {
|
||||
unsafe {
|
||||
std::env::remove_var("SWARM_CONTROLLER_FORGE_URL");
|
||||
std::env::remove_var("SWARM_CONTROLLER_FORGE_TOKEN_FILE");
|
||||
}
|
||||
|
||||
let mut sched = hive_jobq::scheduler::Scheduler::new(
|
||||
|
|
@ -4231,7 +4231,6 @@ mod tests {
|
|||
async fn create_forge_user_node_runs_end_to_end_and_fails_without_forge_configured() {
|
||||
unsafe {
|
||||
std::env::remove_var("SWARM_CONTROLLER_FORGE_URL");
|
||||
std::env::remove_var("SWARM_CONTROLLER_FORGE_TOKEN_FILE");
|
||||
}
|
||||
|
||||
let mut sched = hive_jobq::scheduler::Scheduler::new(
|
||||
|
|
|
|||
Loading…
Reference in a new issue