diff --git a/bobbin/crates/bobbin/src/config.rs b/bobbin/crates/bobbin/src/config.rs index 6ba0fb79b..00c2be3ca 100644 --- a/bobbin/crates/bobbin/src/config.rs +++ b/bobbin/crates/bobbin/src/config.rs @@ -30,6 +30,7 @@ const KNOWN_KEYS: &[&str] = &[ "knot.allow_private", "knot.require_https", "mirror.url", + "mirror_v2.url", "log.format", "log.filter", ]; @@ -54,6 +55,7 @@ const KNOWN_ENVS: &[&str] = &[ "BOBBIN_KNOT_ALLOW_PRIVATE", "BOBBIN_KNOT_REQUIRE_HTTPS", "BOBBIN_MIRROR_URL", + "BOBBIN_MIRROR_V2_URL", "BOBBIN_LOG_FORMAT", "BOBBIN_LOG", ]; @@ -87,6 +89,9 @@ pub struct BobbinConfig { #[config(nested)] pub mirror: MirrorConfig, + #[config(nested)] + pub mirror_v2: MirrorV2Config, + #[config(nested)] pub log: LogConfig, } @@ -264,6 +269,13 @@ pub struct MirrorConfig { pub url: Option, } +#[derive(Debug, Config)] +pub struct MirrorV2Config { + /// Origin of a v2 mirror (gitmirror) XRPC server. + #[config(env = "BOBBIN_MIRROR_V2_URL")] + pub url: Option, +} + #[derive(Debug, Config)] pub struct LogConfig { /// Log emitter format. `text` produces human-readable output for local diff --git a/bobbin/crates/bobbin/src/main.rs b/bobbin/crates/bobbin/src/main.rs index f56a1792f..c1f682077 100644 --- a/bobbin/crates/bobbin/src/main.rs +++ b/bobbin/crates/bobbin/src/main.rs @@ -245,6 +245,22 @@ async fn run(cfg: BobbinConfig) -> anyhow::Result<()> { ), None => tracing::info!("we will forward git reads to knots, since mirror.url is unset"), } + let mirror_v2 = cfg + .mirror_v2 + .url + .as_ref() + .map(|url| MirrorProxy::new(url, clock.clone(), hasher.clone()).map(Arc::new)) + .transpose() + .context("mirror_v2.url")?; + match mirror_v2.as_ref() { + Some(m) => tracing::info!( + mirror_v2 = %m.host().url(), + "we will forward few sh.tangled.git.* to the v2 knotmirror", + ), + None => tracing::info!( + "some sh.tangled.git.* methods will fail, since mirror_v2.url is unset", + ), + } let search_heap = usize::try_from(search_heap_cap) .with_context(|| format!("search heap {search_heap_cap} exceeds usize"))?; let search = Arc::new(SearchIndex::new(search_heap, clock.clone())?); @@ -371,6 +387,7 @@ async fn run(cfg: BobbinConfig) -> anyhow::Result<()> { .with_identity(identity) .with_limiter(limiter) .with_mirror(mirror) + .with_mirror_v2(mirror_v2) .with_proxies(trusted_proxies); let app = router(state); diff --git a/bobbin/crates/knot-proxy/src/mirror.rs b/bobbin/crates/knot-proxy/src/mirror.rs index 23f5b8728..96ac9234e 100644 --- a/bobbin/crates/knot-proxy/src/mirror.rs +++ b/bobbin/crates/knot-proxy/src/mirror.rs @@ -124,6 +124,15 @@ impl MirrorProxy { &self.host } + pub async fn forward_raw( + &self, + nsid: &Nsid, + query: &[(&str, &str)], + headers: HeaderMap, + ) -> Result { + self.proxy.forward(&self.host, nsid, query, headers).await + } + pub async fn forward>( &self, nsid: &MirrorNsid, @@ -137,9 +146,7 @@ impl MirrorProxy { .filter(|(k, _)| *k != REPO_PARAM) .chain(std::iter::once((REPO_PARAM, repo.as_ref()))) .collect(); - self.proxy - .forward(&self.host, &nsid.0, &keyed, headers) - .await + self.forward_raw(&nsid.0, &keyed, headers).await } } diff --git a/bobbin/crates/xrpc/src/lib.rs b/bobbin/crates/xrpc/src/lib.rs index 771db3dc6..05c9f3e2c 100644 --- a/bobbin/crates/xrpc/src/lib.rs +++ b/bobbin/crates/xrpc/src/lib.rs @@ -136,6 +136,7 @@ pub struct AppState { pub coverage: Arc, pub knots: Arc, pub mirror: Option>, + pub mirror_v2: Option>, pub search: Arc, pub resolver: Arc, pub identity: Arc, @@ -173,6 +174,7 @@ impl AppState { coverage, knots, mirror: None, + mirror_v2: None, search, resolver, identity, @@ -193,6 +195,11 @@ impl AppState { self } + pub fn with_mirror_v2(mut self, mirror_v2: Option>) -> Self { + self.mirror_v2 = mirror_v2; + self + } + pub fn with_identity(mut self, identity: Arc) -> Self { self.identity = identity; self @@ -528,6 +535,13 @@ const KNOT_PROXIED_NSIDS: &[&str] = &[ "sh.tangled.knot.listKeys", ]; +const MIRROR_V2_PROXIED_NSIDS: &[&str] = &[ + "sh.tangled.git.temp2.listCommits", + "sh.tangled.git.temp2.getDiff", + "sh.tangled.git.temp2.getInterdiff", + "sh.tangled.git.temp2.mergeCheck", +]; + const PASSTHROUGH_HEADERS: &[&HeaderName] = &[ &CONTENT_TYPE, &CONTENT_LENGTH, @@ -552,7 +566,12 @@ type ProxyParams = Vec<(String, String)>; fn knot_proxied_routes() -> Router { let with_repo = register_proxied(Router::new(), REPO_PROXIED_NSIDS, proxy_repo_handler); - register_proxied(with_repo, KNOT_PROXIED_NSIDS, proxy_knot_handler) + let with_knot = register_proxied(with_repo, KNOT_PROXIED_NSIDS, proxy_knot_handler); + register_proxied( + with_knot, + MIRROR_V2_PROXIED_NSIDS, + proxy_mirror_v2_handler, + ) } fn register_proxied( @@ -3083,3 +3102,32 @@ async fn proxy_knot_handler( let forward: Vec<(&str, &str)> = rest.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect(); dispatch_knot(&state, &nsid, &host, &forward, allowed).await } + +/// Hand the request to the v2 mirror unchanged. Deliberately shorter than its siblings: the origin +/// is operator-configured rather than resolved per-repo, and the mirror answers the same nsid we +/// were called on, so there is nothing to extract, rewrite, or validate. +/// +/// Modelled on [`dispatch_knot`], not [`dispatch_mirror`]: there is no knot to fall back to, so a +/// 4xx streams straight through and the mirror's own lexicon errors reach the client intact. +async fn proxy_mirror_v2_handler( + state: AppState, + headers: HeaderMap, + socket: SocketPeer, + params: ProxyParams, + nsid: Nsid, +) -> Result { + let mirror_v2 = state + .mirror_v2 + .as_ref() + .ok_or_else(|| XrpcError::UpstreamUnavailable("mirror_v2.url is unset".into()))?; + let allowed = filter_request_headers(&headers, socket, &state.client_address); + let forward: Vec<(&str, &str)> = params + .iter() + .map(|(k, v)| (k.as_str(), v.as_str())) + .collect(); + mirror_v2 + .forward_raw(&nsid, &forward, allowed) + .await + .map(upstream_to_axum) + .map_err(map_proxy_error) +} diff --git a/bobbin/example.toml b/bobbin/example.toml index 081dc5c02..e337a4223 100644 --- a/bobbin/example.toml +++ b/bobbin/example.toml @@ -166,6 +166,10 @@ # Can also be specified via environment variable `BOBBIN_MIRROR_URL`. #url = +[mirror_v2] +# Can also be specified via environment variable `BOBBIN_MIRROR_V2_URL`. +#url = + [log] # Log emitter format. `text` produces human-readable output for local # development. `json` emits one structured object per line for log diff --git a/docker-compose.yml b/docker-compose.yml index 09c5967ce..fb015cfa9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -507,6 +507,7 @@ services: BOBBIN_SLINGSHOT_URL: http://hydrant:3000 BOBBIN_KNOT_ALLOW_PRIVATE: "true" BOBBIN_KNOT_REQUIRE_HTTPS: "false" + BOBBIN_MIRROR_V2_URL: http://gitmirror:9001 BOBBIN_LOG: info volumes: - .:/src:cached