swarm-builder: scope the forge token to the forge, one build at a time, opt-in
argus's review on #5177: the git credential helper answered every `get`, so a config PR's own flake inputs could have git send the builder's forge token to any host. The helper is now configured under `credential.<forge-url>.helper` and itself answers only an https request whose host is the forge's; a test runs it as git does and checks that other hosts, http and non-get operations get nothing. Builds are bounded: every `build_agent` node needs the one `builder` jobq resource, so one runs at a time and the rest wait pending, and a newer head cancels its PR's pending build (a running one finishes and writes on its own commit). The builder also holds a one-permit semaphore around each build. `builder` and `review-classifier`, the forge logins swarm-controller owns, join nix/reserved-names.nix, with a test tying the list to both constants. The builder is now opt-in like the CI runner: `allSwarmServices` no longer turns it on, since it runs unreviewed config PRs through the host's nix daemon. mara had not answered atlas's question on the PR.
This commit is contained in:
parent
b68b74d687
commit
cafa120d77
13 changed files with 360 additions and 70 deletions
|
|
@ -48,7 +48,7 @@ async fn main() -> Result<()> {
|
|||
check_flake: var("CHECK_FLAKE")?,
|
||||
hyperhive: var("HYPERHIVE_FLAKE")?,
|
||||
nixpkgs: var("NIXPKGS_FLAKE")?,
|
||||
forge_url: var("FORGE_URL")?.trim_end_matches('/').to_owned(),
|
||||
forge_url: forge_url()?,
|
||||
config_org: var("CONFIG_ORG")?,
|
||||
deadline: BUILD_DEADLINE,
|
||||
});
|
||||
|
|
@ -74,14 +74,22 @@ async fn main() -> Result<()> {
|
|||
.with_context(|| format!("subscribing to {SUBJECT}"))?;
|
||||
tracing::info!(subject = SUBJECT, "builder: awaiting builds");
|
||||
|
||||
// One build at a time. swarm-controller already sends one at a time; this
|
||||
// holds the bound for any request it did not serialise.
|
||||
let slot = Arc::new(tokio::sync::Semaphore::new(1));
|
||||
|
||||
while let Some(msg) = requests.next().await {
|
||||
let Some(reply_to) = msg.reply.clone() else {
|
||||
tracing::warn!("a build arrived with no reply subject; dropping it");
|
||||
continue;
|
||||
};
|
||||
let queue = queue.clone();
|
||||
let slot = Arc::clone(&slot);
|
||||
let builder = Arc::clone(&builder);
|
||||
tokio::spawn(async move {
|
||||
let Ok(_permit) = slot.acquire().await else {
|
||||
return;
|
||||
};
|
||||
let reply = handle(&msg.payload, &builder).await;
|
||||
match serde_json::to_vec(&reply) {
|
||||
Ok(body) => {
|
||||
|
|
@ -188,3 +196,15 @@ async fn client_secret(client_id: &str) -> Result<String> {
|
|||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// The forge's base URL, which must be `https`: the credential helper offers
|
||||
/// the token to `https` requests for its host alone.
|
||||
fn forge_url() -> Result<String> {
|
||||
let name = format!("{PREFIX}_FORGE_URL");
|
||||
let url = std::env::var(&name).with_context(|| format!("{name} is unset"))?;
|
||||
let url = url.trim_end_matches('/');
|
||||
if !url.starts_with("https://") || url["https://".len()..].contains('/') {
|
||||
bail!("{name} is {url:?}, not https://<host>[:port]");
|
||||
}
|
||||
Ok(url.to_owned())
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue