Skip to main content

greentic_bundle/catalog/
client.rs

1use std::path::Path;
2
3use anyhow::{Result, bail};
4use greentic_distributor_client::oci_packs::DefaultRegistryClient;
5use greentic_distributor_client::{OciPackFetcher, PackFetchOptions};
6use tokio::runtime::Runtime;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct FetchedCatalog {
10    pub resolved_ref: String,
11    pub digest: String,
12    pub bytes: Vec<u8>,
13}
14
15pub trait CatalogArtifactClient {
16    fn fetch_catalog(&self, root: &Path, reference: &str) -> Result<FetchedCatalog>;
17}
18
19#[derive(Debug, Default, Clone, Copy)]
20pub struct CacheOnlyCatalogClient;
21
22impl CatalogArtifactClient for CacheOnlyCatalogClient {
23    fn fetch_catalog(&self, _root: &Path, reference: &str) -> Result<FetchedCatalog> {
24        bail!(
25            "catalog {reference} is not cached locally and this build does not yet provide a remote fetch backend; seed the workspace-local cache first or rerun with a local file:// catalog"
26        )
27    }
28}
29
30#[derive(Debug, Default, Clone, Copy)]
31pub struct DistributorCatalogClient;
32
33impl CatalogArtifactClient for DistributorCatalogClient {
34    fn fetch_catalog(&self, root: &Path, reference: &str) -> Result<FetchedCatalog> {
35        let mapped = map_remote_catalog_reference(reference)?;
36        let fetcher: OciPackFetcher<DefaultRegistryClient> =
37            OciPackFetcher::new(PackFetchOptions {
38                allow_tags: true,
39                offline: false,
40                cache_dir: root
41                    .join(crate::catalog::CACHE_ROOT_DIR)
42                    .join("distributor"),
43                ..PackFetchOptions::default()
44            });
45        let runtime = Runtime::new()?;
46        let resolved = runtime.block_on(fetcher.fetch_pack_to_cache(&mapped.oci_reference))?;
47        let bytes = std::fs::read(&resolved.path)?;
48        Ok(FetchedCatalog {
49            resolved_ref: format!("oci://{}", mapped.oci_reference),
50            digest: resolved.resolved_digest,
51            bytes,
52        })
53    }
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct RemoteCatalogRef {
58    pub oci_reference: String,
59    pub source_kind: RemoteCatalogSourceKind,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum RemoteCatalogSourceKind {
64    Oci,
65    GhcrWellKnown,
66}
67
68pub fn map_remote_catalog_reference(reference: &str) -> Result<RemoteCatalogRef> {
69    if let Some(raw) = reference.strip_prefix("oci://") {
70        let trimmed = raw.trim();
71        if trimmed.is_empty() {
72            bail!("catalog reference {reference} is missing an OCI path");
73        }
74        return Ok(RemoteCatalogRef {
75            oci_reference: trimmed.to_string(),
76            source_kind: RemoteCatalogSourceKind::Oci,
77        });
78    }
79
80    if let Some(raw) = reference.strip_prefix("ghcr://") {
81        let trimmed = raw.trim().trim_start_matches('/');
82        if trimmed.is_empty() {
83            bail!("catalog reference {reference} is missing a GHCR path");
84        }
85        return Ok(RemoteCatalogRef {
86            oci_reference: format!("ghcr.io/greenticai/{}", default_ghcr_tag(trimmed)),
87            source_kind: RemoteCatalogSourceKind::GhcrWellKnown,
88        });
89    }
90
91    bail!(
92        "catalog {reference} is not a supported remote catalog ref; use file://, oci://<registry>/<repo>[:tag|@sha256:...], or ghcr://<path>[:tag|@sha256:...] for ghcr.io/greenticai"
93    )
94}
95
96fn default_ghcr_tag(reference: &str) -> String {
97    if reference.contains('@')
98        || reference
99            .rsplit('/')
100            .next()
101            .unwrap_or_default()
102            .contains(':')
103    {
104        reference.to_string()
105    } else {
106        format!("{reference}:latest")
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::{RemoteCatalogSourceKind, map_remote_catalog_reference};
113
114    #[test]
115    fn maps_ghcr_shortcut_to_greenticai_namespace() {
116        let mapped = map_remote_catalog_reference("ghcr://catalogs/well-known").expect("mapped");
117        assert_eq!(
118            mapped.oci_reference,
119            "ghcr.io/greenticai/catalogs/well-known:latest"
120        );
121        assert_eq!(mapped.source_kind, RemoteCatalogSourceKind::GhcrWellKnown);
122    }
123
124    #[test]
125    fn preserves_explicit_digest_for_ghcr_shortcut() {
126        let mapped = map_remote_catalog_reference("ghcr://catalogs/well-known@sha256:abc123")
127            .expect("mapped");
128        assert_eq!(
129            mapped.oci_reference,
130            "ghcr.io/greenticai/catalogs/well-known@sha256:abc123"
131        );
132    }
133
134    #[test]
135    fn preserves_raw_oci_reference() {
136        let mapped =
137            map_remote_catalog_reference("oci://ghcr.io/example/catalogs/demo:1").expect("mapped");
138        assert_eq!(mapped.oci_reference, "ghcr.io/example/catalogs/demo:1");
139        assert_eq!(mapped.source_kind, RemoteCatalogSourceKind::Oci);
140    }
141}