runner_core/packs/resolver/
mod.rs

1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4
5use anyhow::{Result, anyhow, bail};
6use tempfile::TempPath;
7
8mod azblob;
9mod fs;
10mod gcs;
11mod http;
12mod oci;
13mod s3;
14
15pub use azblob::AzBlobResolver;
16pub use fs::FsResolver;
17pub use gcs::GcsResolver;
18pub use http::HttpResolver;
19pub use oci::OciResolver;
20pub use s3::S3Resolver;
21
22/// Response from a resolver indicating where the artifact was stored.
23pub struct FetchResponse {
24    location: FetchLocation,
25}
26
27impl FetchResponse {
28    pub fn from_path(path: PathBuf) -> Self {
29        Self {
30            location: FetchLocation::Permanent(path),
31        }
32    }
33
34    pub fn from_temp(path: TempPath) -> Self {
35        Self {
36            location: FetchLocation::Temporary(path),
37        }
38    }
39
40    pub fn path(&self) -> &Path {
41        match &self.location {
42            FetchLocation::Permanent(path) => path,
43            FetchLocation::Temporary(path) => path.as_ref(),
44        }
45    }
46}
47
48enum FetchLocation {
49    Permanent(PathBuf),
50    Temporary(TempPath),
51}
52
53pub trait PackResolver: Send + Sync {
54    fn scheme(&self) -> &'static str;
55    fn fetch(&self, locator: &str) -> Result<FetchResponse>;
56}
57
58#[derive(Default)]
59pub struct ResolverRegistry {
60    resolvers: HashMap<String, Arc<dyn PackResolver>>,
61}
62
63impl ResolverRegistry {
64    pub fn register(&mut self, resolver: impl PackResolver + 'static) {
65        self.resolvers
66            .insert(resolver.scheme().to_string(), Arc::new(resolver));
67    }
68
69    pub fn register_builtin(&mut self, fs_root: PathBuf) -> Result<()> {
70        self.register(FsResolver::new(fs_root));
71        self.register(HttpResolver::new("http")?);
72        self.register(HttpResolver::new("https")?);
73        self.register(OciResolver::new()?);
74        self.register(S3Resolver::new()?);
75        self.register(GcsResolver::new()?);
76        self.register(AzBlobResolver::new()?);
77        Ok(())
78    }
79
80    pub fn fetch(&self, reference: &str) -> Result<FetchResponse> {
81        let parsed = ParsedReference::parse(reference)?;
82        let resolver = self
83            .resolvers
84            .get(&parsed.scheme)
85            .ok_or_else(|| anyhow!("no resolver registered for scheme `{}`", parsed.scheme))?;
86        resolver.fetch(&parsed.locator)
87    }
88}
89
90struct ParsedReference {
91    scheme: String,
92    locator: String,
93}
94
95impl ParsedReference {
96    fn parse(input: &str) -> Result<Self> {
97        let (scheme_part, rest) = input
98            .split_once("://")
99            .ok_or_else(|| anyhow!("reference `{input}` is missing a URI scheme"))?;
100        if rest.is_empty() {
101            bail!("reference `{input}` is missing a locator");
102        }
103        if let Some((logical, actual)) = scheme_part.split_once('+') {
104            let locator = format!("{actual}://{rest}");
105            return Ok(Self {
106                scheme: logical.to_ascii_lowercase(),
107                locator,
108            });
109        }
110        Ok(Self {
111            scheme: scheme_part.to_ascii_lowercase(),
112            locator: input.to_string(),
113        })
114    }
115}