swarm-builder: lock the swarm meta flake on request
A request on $SWARM.builder.lock-meta carries a rendered flake.nix. The builder writes it into a scratch directory, runs `nix flake lock` there with the same forge credential helper its builds use, answers with the flake.lock nix wrote, and removes the directory. It commits nothing; the controller commits the lock beside the flake. Locks are answered in their own loop, like archetype evaluations, so one does not wait behind an hour-long build. The auth callout grants the subject to the builder (subscribe) and the controller (publish) only. Ported from c574a1694 onto the swarm-wire crate. Refs #4857
This commit is contained in:
parent
d331eb394b
commit
de323a72dd
4 changed files with 320 additions and 42 deletions
|
|
@ -8,6 +8,10 @@
|
|||
//! [`swarm_wire::builder::ARCHETYPES_SUBJECT`] with the agent
|
||||
//! archetypes its hyperhive source declares, which the controller stores.
|
||||
//!
|
||||
//! Lock requests on [`swarm_wire::builder::LOCK_SUBJECT`] carry the swarm meta
|
||||
//! flake's `flake.nix`, and the answer is the `flake.lock` `nix` wrote for it in
|
||||
//! a scratch directory. The controller commits it.
|
||||
//!
|
||||
//! The store identity comes from the `BAO_*` environment. The queue client
|
||||
//! secret is read at start and again when authelia refuses it, the forge token
|
||||
//! once per build, both from the store under that identity.
|
||||
|
|
@ -23,8 +27,8 @@ use swarm_queue_client::{ClientSecret, QueueConfig};
|
|||
use swarm_secret_client::forge::Credential;
|
||||
use swarm_secret_client::stored_secret::StoredSecret;
|
||||
use swarm_wire::builder::{
|
||||
ARCHETYPES_DEADLINE, ARCHETYPES_SUBJECT, BUILD_DEADLINE, BuildAgent, QUEUE_GROUP,
|
||||
REPLY_DEADLINE, Reply, SUBJECT,
|
||||
ARCHETYPES_DEADLINE, ARCHETYPES_SUBJECT, BUILD_DEADLINE, BuildAgent, LOCK_DEADLINE,
|
||||
LOCK_SUBJECT, LockMeta, LockReply, QUEUE_GROUP, REPLY_DEADLINE, Reply, SUBJECT,
|
||||
};
|
||||
|
||||
mod archetypes;
|
||||
|
|
@ -71,6 +75,7 @@ async fn main() -> Result<()> {
|
|||
forge_url: forge_url()?,
|
||||
config_org: var("CONFIG_ORG")?,
|
||||
deadline: BUILD_DEADLINE,
|
||||
lock_deadline: LOCK_DEADLINE,
|
||||
});
|
||||
let secret =
|
||||
swarm_secret_client::read_with_startup_retry("reading the queue client secret", || {
|
||||
|
|
@ -109,6 +114,13 @@ async fn main() -> Result<()> {
|
|||
.with_context(|| format!("subscribing to {ARCHETYPES_SUBJECT}"))?;
|
||||
// Outside the build slot below: a list refresh does not wait on a build.
|
||||
tokio::spawn(answer_evaluations(evaluations, queue.clone(), evaluator));
|
||||
let locks = queue
|
||||
.queue_subscribe(LOCK_SUBJECT, QUEUE_GROUP.to_owned())
|
||||
.await
|
||||
.with_context(|| format!("subscribing to {LOCK_SUBJECT}"))?;
|
||||
// Outside the build slot too: a lock builds nothing, so it does not wait
|
||||
// behind an hour-long build.
|
||||
tokio::spawn(answer_locks(locks, queue.clone(), Arc::clone(&builder)));
|
||||
|
||||
// One build at a time. swarm-controller already sends one at a time; this
|
||||
// holds the bound for any request it did not serialise, and `in_slot`
|
||||
|
|
@ -186,6 +198,57 @@ async fn answer_evaluations(
|
|||
tracing::error!("the subscription to {ARCHETYPES_SUBJECT} ended");
|
||||
}
|
||||
|
||||
/// Answer every meta-flake lock request, one at a time, with the lock `nix`
|
||||
/// wrote or why it did not.
|
||||
async fn answer_locks(
|
||||
mut requests: async_nats::Subscriber,
|
||||
queue: async_nats::Client,
|
||||
builder: Arc<nix_build::Builder>,
|
||||
) {
|
||||
while let Some(msg) = requests.next().await {
|
||||
let Some(reply_to) = msg.reply.clone() else {
|
||||
tracing::warn!("a meta-flake lock arrived with no reply subject; dropping it");
|
||||
continue;
|
||||
};
|
||||
let reply = handle_lock(&msg.payload, &builder).await;
|
||||
match serde_json::to_vec(&reply) {
|
||||
Ok(body) => {
|
||||
if let Err(e) = queue.publish(reply_to, body.into()).await {
|
||||
tracing::warn!(error = %e, "publishing a lock's reply failed");
|
||||
}
|
||||
}
|
||||
Err(e) => tracing::warn!(error = %e, "encoding a lock's reply failed"),
|
||||
}
|
||||
}
|
||||
tracing::error!("the subscription to {LOCK_SUBJECT} ended");
|
||||
}
|
||||
|
||||
/// Decode one lock request, lock it and log the lock.
|
||||
async fn handle_lock(payload: &[u8], builder: &nix_build::Builder) -> LockReply {
|
||||
let job: LockMeta = match serde_json::from_slice(payload) {
|
||||
Ok(job) => job,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "an undecodable lock; answering with the error");
|
||||
return LockReply::Failed {
|
||||
error_tail: format!("the builder could not decode the request: {e}"),
|
||||
};
|
||||
}
|
||||
};
|
||||
let reply = match within(TOKEN_DEADLINE, forge_token()).await {
|
||||
Ok(token) => builder.lock(&job, &token).await,
|
||||
Err(e) => LockReply::Failed {
|
||||
error_tail: format!("{e:#}"),
|
||||
},
|
||||
};
|
||||
match &reply {
|
||||
LockReply::Locked { .. } => tracing::info!("swarm meta flake locked"),
|
||||
LockReply::Failed { error_tail } => {
|
||||
tracing::warn!(%error_tail, "swarm meta flake lock failed");
|
||||
}
|
||||
}
|
||||
reply
|
||||
}
|
||||
|
||||
/// `build`'s reply once `job` holds the build slot, or a refusal, without
|
||||
/// running `build`, once it has waited [`MAX_SLOT_WAIT`] since `received`.
|
||||
/// `None` only when the slot is closed.
|
||||
|
|
|
|||
Loading…
Reference in a new issue