//! mirror crawl triggers: after a generation or provisioning, poke knotmirror/gitmirror //! instead of waiting for the network to notice. //! //! the static knot serves no event stream, so mirrors can't discover updates by subscription. //! instead, knot-static POSTs the configured admin endpoints. knotmirror's admin server is the //! known-good target: //! //! POST /api/triggerRepoResync form: repo= //! //! gitmirror (knotmirror v2) speaks XRPC with an in-flux internal interface; //! add its NSID when it settles. use anyhow::Context as _; use bytes::Bytes; use knot_runtime::{HttpLimits, HttpRequest, HttpTransport, ReqwestHttp}; use knot_types::RepoDid; use tracing::warn; #[derive(Debug, Clone)] pub struct MirrorTrigger { pub url: String, pub auth_token: Option, } pub fn triggers_from_config(config: &crate::config::ValidatedConfig) -> Vec { let Some(mirror) = &config.mirror else { return Vec::new(); }; let auth_token = mirror .auth_token_env .as_ref() .and_then(|env| std::env::var(env).ok()) .filter(|token| !token.is_empty()); mirror .resync_urls .iter() .map(|url| MirrorTrigger { url: url.clone(), auth_token: auth_token.clone(), }) .collect() } /// fire-and-log: trigger all mirrors across all changed repos concurrently. pub async fn trigger_all(triggers: &[MirrorTrigger], repos: &[RepoDid]) { if triggers.is_empty() || repos.is_empty() { return; } let futures = repos.iter().flat_map(|repo_did| { triggers.iter().map(move |trigger| async move { match post_resync(trigger, repo_did).await { Ok(status) if status.is_success() => {} Ok(status) => warn!( "mirror resync trigger returned {status} for {repo_did} at {}", trigger.url ), Err(error) => warn!( "mirror resync trigger failed for {repo_did} at {}: {error:#}", trigger.url ), } }) }); futures::future::join_all(futures).await; } /// fire-and-log: mirror availability must never fail a generation. pub async fn trigger_repo_resync(triggers: &[MirrorTrigger], repo_did: &RepoDid) { trigger_all(triggers, std::slice::from_ref(repo_did)).await; } async fn post_resync( trigger: &MirrorTrigger, repo_did: &RepoDid, ) -> anyhow::Result { let transport = ReqwestHttp::new(HttpLimits::default()).context("init http transport")?; let url = url::Url::parse(&trigger.url).context("parse mirror resync url")?; let mut request = HttpRequest::post(url, Bytes::from(format!("repo={repo_did}"))); request.headers.insert( http::header::CONTENT_TYPE, http::header::HeaderValue::from_static("application/x-www-form-urlencoded"), ); if let Some(token) = &trigger.auth_token { request.headers.insert( http::header::AUTHORIZATION, http::header::HeaderValue::from_str(&format!("Bearer {token}")) .context("mirror auth token is not a valid header value")?, ); } transport .execute(request) .await .map(|response| response.status) .context("mirror resync request") }