swarm-controller: the forge-token backfill creates a missing forge user
An agent with a hive-agent-* store identity but no forge user was observed as NoForgeUser and dropped by plan(), so it never got a token. plan() now keeps it, and queue_forge_token_mints inserts CreateForgeUser ahead of MintAgentForgeToken with after_ok, the edge declare_agent_job already uses. ensure_agent_user folds an existing user into success, so the extra node is a no-op for agents that have one. Refs #3782
This commit is contained in:
parent
abc942cff3
commit
22f0acfd6d
2 changed files with 112 additions and 30 deletions
|
|
@ -126,21 +126,30 @@ pub fn classify(stored: Option<&forge::Credential>, listed: &[AccessToken]) -> D
|
|||
pub enum Observed {
|
||||
/// Both reads worked, and this is what [`classify`] made of them.
|
||||
Decided(Decision),
|
||||
/// The forge has no user by this agent's name. Creating one is agent
|
||||
/// creation's job, not this module's.
|
||||
/// The forge has no user by this agent's name. Planned like a mint: the
|
||||
/// queued job creates the user first (`CreateForgeUser`, idempotent), so
|
||||
/// an agent that holds a store identity but was never given a forge
|
||||
/// account still ends up with both.
|
||||
NoForgeUser,
|
||||
/// A read failed. Nothing is known, so nothing is done.
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// The agents a pass mints for.
|
||||
/// The agents a pass mints for: a [`Decision::Mint`], or an agent with no
|
||||
/// forge user yet ([`Observed::NoForgeUser`]), whose job creates the user
|
||||
/// before it mints.
|
||||
///
|
||||
/// Only [`Decision::Mint`]. [`Observed::Unknown`] in particular never mints: a
|
||||
/// forge or store outage must not turn into a rotation of every agent's token.
|
||||
/// [`Observed::Unknown`] in particular never mints: a forge or store outage
|
||||
/// must not turn into a rotation of every agent's token.
|
||||
pub fn plan(observed: &[(String, Observed)]) -> Vec<String> {
|
||||
observed
|
||||
.iter()
|
||||
.filter(|(_, o)| matches!(o, Observed::Decided(Decision::Mint(_))))
|
||||
.filter(|(_, o)| {
|
||||
matches!(
|
||||
o,
|
||||
Observed::Decided(Decision::Mint(_)) | Observed::NoForgeUser
|
||||
)
|
||||
})
|
||||
.map(|(agent, _)| agent.clone())
|
||||
.collect()
|
||||
}
|
||||
|
|
@ -191,7 +200,10 @@ impl Client {
|
|||
.await
|
||||
.with_context(|| format!("reading {path}"))?;
|
||||
let Some(listed) = self.list_agent_tokens(agent).await? else {
|
||||
bail!("the forge has no user {agent:?}, so there is nothing to mint a token for");
|
||||
bail!(
|
||||
"the forge has no user {agent:?} even after this job's create_forge_user \
|
||||
node, so there is nothing to mint a token for"
|
||||
);
|
||||
};
|
||||
let reason = match classify(stored.as_ref(), &listed) {
|
||||
Decision::Keep => {
|
||||
|
|
@ -299,7 +311,10 @@ impl Client {
|
|||
for agent in policy::agents_from_role_names(&roles) {
|
||||
let o = self.observe_agent(&store, &agent).await;
|
||||
if o == Observed::NoForgeUser {
|
||||
tracing::debug!(agent, "agent forge token: no forge user; skipped");
|
||||
tracing::info!(
|
||||
agent,
|
||||
"agent forge token: no forge user; creating one first"
|
||||
);
|
||||
}
|
||||
observed.push((agent, o));
|
||||
}
|
||||
|
|
@ -497,7 +512,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn only_a_mint_decision_is_planned() {
|
||||
fn a_mint_or_a_missing_user_is_planned() {
|
||||
let observed = [
|
||||
("a".to_owned(), Observed::Decided(Decision::Keep)),
|
||||
(
|
||||
|
|
@ -512,7 +527,16 @@ mod tests {
|
|||
Observed::Decided(Decision::Mint(MintReason::ScopeMismatch)),
|
||||
),
|
||||
];
|
||||
assert_eq!(plan(&observed), ["b", "f"]);
|
||||
assert_eq!(plan(&observed), ["b", "d", "f"]);
|
||||
}
|
||||
|
||||
/// An agent with a store identity and no forge user used to be skipped
|
||||
/// on every pass, forever. It is planned now; the job it gets creates
|
||||
/// the user before it mints (see `queue_forge_token_mints`).
|
||||
#[test]
|
||||
fn a_missing_forge_user_is_planned() {
|
||||
let observed = [("ruth".to_owned(), Observed::NoForgeUser)];
|
||||
assert_eq!(plan(&observed), ["ruth"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in a new issue