use std::sync::Arc;
use traits::{ArtifactEntry, ArtifactFormat, ArtifactId, ArchiveInfo, RepositoryBackendTrait};
pub struct ProxyBackend {
primary: Arc<dyn RepositoryBackendTrait>,
upstreams: Vec<Arc<dyn RepositoryBackendTrait>>,
}
impl ProxyBackend {
pub fn new(
primary: Arc<dyn RepositoryBackendTrait>,
upstreams: Vec<Arc<dyn RepositoryBackendTrait>>,
) -> Self {
Self { primary, upstreams }
}
}
impl RepositoryBackendTrait for ProxyBackend {
fn name(&self) -> &str {
self.primary.name()
}
fn format(&self) -> ArtifactFormat {
self.primary.format()
}
fn is_writable(&self) -> bool {
self.primary.is_writable()
}
fn has_archive(&self) -> bool {
self.primary.has_archive()
}
fn fetch(&self, id: &ArtifactId) -> anyhow::Result<Option<Vec<u8>>> {
if let Some(data) = self.primary.fetch(id)? {
return Ok(Some(data));
}
for upstream in &self.upstreams {
if let Some(data) = upstream.fetch(id)? {
log::debug!(
"proxy: cache miss on {}, served from upstream {}",
self.primary.name(),
upstream.name()
);
return Ok(Some(data));
}
}
Ok(None)
}
fn put(&self, id: &ArtifactId, data: &[u8]) -> anyhow::Result<()> {
self.primary.put(id, data)
}
fn handle_http2_request(
&self,
method: &str,
suburl: &str,
body: &[u8],
) -> anyhow::Result<(u16, Vec<(String, String)>, Vec<u8>)> {
let resp = self.primary.handle_http2_request(method, suburl, body)?;
if resp.0 != 404 {
return Ok(resp);
}
for upstream in &self.upstreams {
let up_resp = upstream.handle_http2_request(method, suburl, body)?;
if up_resp.0 != 404 {
log::debug!(
"proxy: HTTP {} {} — primary 404, served from upstream {}",
method,
suburl,
upstream.name()
);
return Ok(up_resp);
}
}
Ok(resp)
}
fn list(
&self,
name_filter: Option<&str>,
limit: usize,
) -> anyhow::Result<Vec<ArtifactEntry>> {
self.primary.list(name_filter, limit)
}
fn archive_files(&self, prefix: Option<&str>) -> anyhow::Result<Vec<String>> {
self.primary.archive_files(prefix)
}
fn archive_info(&self) -> anyhow::Result<ArchiveInfo> {
self.primary.archive_info()
}
}
#[cfg(test)]
mod tests {
use super::*;
use traits::ArtifactFormat;
#[cfg(feature = "testmatrix")]
fn fstatus(component: &str, check: &str, ok: bool, detail: &str) {
nornir_testmatrix::functional_status(component, check, ok, detail);
}
struct StubBackend {
name: String,
data: std::collections::HashMap<String, Vec<u8>>,
http_status: u16,
http_body: Vec<u8>,
writable: bool,
}
impl StubBackend {
fn always_200(name: &str, body: &[u8]) -> Arc<Self> {
Arc::new(Self {
name: name.into(),
data: Default::default(),
http_status: 200,
http_body: body.to_vec(),
writable: false,
})
}
fn always_404(name: &str) -> Arc<Self> {
Arc::new(Self {
name: name.into(),
data: Default::default(),
http_status: 404,
http_body: b"Not found".to_vec(),
writable: false,
})
}
fn with_artifact(name: &str, crate_name: &str, version: &str, body: &[u8]) -> Arc<Self> {
let mut data = std::collections::HashMap::new();
data.insert(format!("{}/{}", crate_name, version), body.to_vec());
Arc::new(Self {
name: name.into(),
data,
http_status: 404,
http_body: b"Not found".to_vec(),
writable: false,
})
}
}
impl RepositoryBackendTrait for StubBackend {
fn name(&self) -> &str { &self.name }
fn format(&self) -> ArtifactFormat { ArtifactFormat::Rust }
fn is_writable(&self) -> bool { self.writable }
fn fetch(&self, id: &ArtifactId) -> anyhow::Result<Option<Vec<u8>>> {
let key = format!("{}/{}", id.name, id.version);
Ok(self.data.get(&key).cloned())
}
fn put(&self, _: &ArtifactId, _: &[u8]) -> anyhow::Result<()> {
anyhow::bail!("read-only stub")
}
fn handle_http2_request(
&self,
_method: &str,
_suburl: &str,
_body: &[u8],
) -> anyhow::Result<(u16, Vec<(String, String)>, Vec<u8>)> {
Ok((self.http_status, Vec::new(), self.http_body.clone()))
}
}
fn artifact(name: &str, version: &str) -> ArtifactId {
ArtifactId { namespace: None, name: name.into(), version: version.into() }
}
#[test]
fn fetch_returns_primary_hit_without_consulting_upstreams() {
let primary = StubBackend::with_artifact("primary", "tokio", "1.0", b"tokio-bytes");
let upstream = StubBackend::with_artifact("upstream", "tokio", "1.0", b"upstream-bytes");
let proxy = ProxyBackend::new(primary, vec![upstream]);
let result = proxy.fetch(&artifact("tokio", "1.0")).unwrap();
assert_eq!(result.as_deref(), Some(b"tokio-bytes".as_ref()), "primary hit must win");
#[cfg(feature = "testmatrix")]
fstatus(
"proxy",
"fetch_primary_hit_wins",
result.as_deref() == Some(b"tokio-bytes".as_ref()),
&format!("primary hit served {:?} (upstream not consulted)", result.as_deref().map(<[u8]>::len)),
);
}
#[test]
fn fetch_falls_through_to_upstream_on_primary_miss() {
let primary = StubBackend::always_404("primary");
let upstream = StubBackend::with_artifact("upstream", "serde", "1.0", b"serde-bytes");
let proxy = ProxyBackend::new(primary, vec![upstream]);
let result = proxy.fetch(&artifact("serde", "1.0")).unwrap();
assert_eq!(result.as_deref(), Some(b"serde-bytes".as_ref()), "upstream must serve on primary miss");
#[cfg(feature = "testmatrix")]
fstatus(
"proxy",
"fetch_falls_through_to_upstream",
result.as_deref() == Some(b"serde-bytes".as_ref()),
"primary 404 -> upstream served the bytes",
);
}
#[test]
fn fetch_returns_none_when_all_miss() {
let primary = StubBackend::always_404("primary");
let upstream = StubBackend::always_404("upstream");
let proxy = ProxyBackend::new(primary, vec![upstream]);
let result = proxy.fetch(&artifact("missing", "1.0")).unwrap();
assert!(result.is_none());
#[cfg(feature = "testmatrix")]
fstatus(
"proxy",
"fetch_none_when_all_miss",
result.is_none(),
"primary + upstream both 404 -> None",
);
}
#[test]
fn http_returns_primary_on_non_404() {
let primary = StubBackend::always_200("primary", b"primary-body");
let upstream = StubBackend::always_200("upstream", b"upstream-body");
let proxy = ProxyBackend::new(primary, vec![upstream]);
let (status, _, body) = proxy.handle_http2_request("GET", "/repo/index/config.json", b"").unwrap();
assert_eq!(status, 200);
assert_eq!(body, b"primary-body");
#[cfg(feature = "testmatrix")]
fstatus(
"proxy",
"http_returns_primary_on_non_404",
status == 200 && body == b"primary-body",
&format!("status={status} body=primary-body (upstream skipped)"),
);
}
#[test]
fn http_falls_through_to_upstream_on_primary_404() {
let primary = StubBackend::always_404("primary");
let upstream = StubBackend::always_200("upstream", b"upstream-body");
let proxy = ProxyBackend::new(primary, vec![upstream]);
let (status, _, body) = proxy.handle_http2_request("GET", "/repo/index/se/rd/serde", b"").unwrap();
assert_eq!(status, 200);
assert_eq!(body, b"upstream-body");
#[cfg(feature = "testmatrix")]
fstatus(
"proxy",
"http_falls_through_to_upstream_on_404",
status == 200 && body == b"upstream-body",
&format!("primary 404 -> upstream status={status} body=upstream-body"),
);
}
#[test]
fn http_returns_404_when_all_404() {
let primary = StubBackend::always_404("primary");
let upstream1 = StubBackend::always_404("upstream1");
let upstream2 = StubBackend::always_404("upstream2");
let proxy = ProxyBackend::new(primary, vec![upstream1, upstream2]);
let (status, _, _) = proxy.handle_http2_request("GET", "/x", b"").unwrap();
assert_eq!(status, 404);
#[cfg(feature = "testmatrix")]
fstatus(
"proxy",
"http_404_when_all_404",
status == 404,
&format!("primary + 2 upstreams all 404 -> status {status}"),
);
}
#[test]
fn proxy_name_and_format_delegate_to_primary() {
let primary = StubBackend::always_404("my-primary-repo");
let proxy = ProxyBackend::new(primary, vec![]);
assert_eq!(proxy.name(), "my-primary-repo");
assert_eq!(proxy.format(), ArtifactFormat::Rust);
#[cfg(feature = "testmatrix")]
fstatus(
"proxy",
"name_and_format_delegate_to_primary",
proxy.name() == "my-primary-repo" && proxy.format() == ArtifactFormat::Rust,
&format!("name={} format={:?}", proxy.name(), proxy.format()),
);
}
}