use std::path::PathBuf;
use greentic_distributor_client::oci_packs::DefaultRegistryClient;
use greentic_distributor_client::{OciPackFetcher, PackFetchOptions};
use super::OpError;
const OCI_SCHEME: &str = "oci://";
pub fn fetch_bundle_uri_to_local(reference: &str) -> Result<PathBuf, OpError> {
let trimmed = reference.trim();
if trimmed.is_empty() {
return Err(OpError::InvalidArgument(
"bundle_source_uri cannot be empty".to_string(),
));
}
let oci_ref = trimmed.strip_prefix(OCI_SCHEME).ok_or_else(|| {
OpError::NotYetImplemented(format!(
"apply can only fetch `oci://` bundle_source_uri references; `{trimmed}` is \
unsupported — declare a local `bundle_path` instead"
))
})?;
if oci_ref.is_empty() {
return Err(OpError::InvalidArgument(format!(
"bundle_source_uri `{trimmed}` has no registry reference after `{OCI_SCHEME}`"
)));
}
fetch_oci_to_cache(oci_ref)
}
fn fetch_oci_to_cache(oci_ref: &str) -> Result<PathBuf, OpError> {
std::thread::scope(|scope| {
let handle = scope.spawn(|| -> Result<PathBuf, OpError> {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|source| OpError::Fetch(format!("build oci fetch runtime: {source}")))?;
let opts = PackFetchOptions {
allow_tags: true,
offline: false,
..PackFetchOptions::default()
};
let fetcher: OciPackFetcher<DefaultRegistryClient> = OciPackFetcher::new(opts);
rt.block_on(fetcher.fetch_pack_to_cache(oci_ref))
.map(|resolved| resolved.path)
.map_err(|source| OpError::Fetch(format!("oci pull `{oci_ref}`: {source}")))
});
match handle.join() {
Ok(result) => result,
Err(_) => Err(OpError::Fetch("oci fetch thread panicked".to_string())),
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_reference_is_invalid_argument() {
let err = fetch_bundle_uri_to_local(" ").unwrap_err();
assert_eq!(err.kind(), "invalid-argument");
}
#[test]
fn oci_scheme_with_no_target_is_invalid_argument() {
let err = fetch_bundle_uri_to_local("oci://").unwrap_err();
assert_eq!(err.kind(), "invalid-argument");
}
#[test]
fn local_path_reference_is_not_yet_implemented() {
let err = fetch_bundle_uri_to_local("/srv/bundles/webchat-bot.gtbundle").unwrap_err();
assert_eq!(err.kind(), "not-yet-implemented");
}
#[test]
fn repo_scheme_is_not_yet_implemented() {
let err = fetch_bundle_uri_to_local("repo://acme/webchat-bot:v1").unwrap_err();
assert_eq!(err.kind(), "not-yet-implemented");
}
#[test]
fn store_scheme_is_not_yet_implemented() {
let err = fetch_bundle_uri_to_local("store://acme/webchat-bot:v1").unwrap_err();
assert_eq!(err.kind(), "not-yet-implemented");
}
#[test]
fn https_scheme_is_not_yet_implemented() {
let err =
fetch_bundle_uri_to_local("https://example.com/webchat-bot.gtbundle").unwrap_err();
assert_eq!(err.kind(), "not-yet-implemented");
}
#[test]
#[ignore = "network: pulls the public demo bundle from ghcr"]
fn live_oci_pull_returns_a_readable_archive() {
let path = fetch_bundle_uri_to_local(
"oci://ghcr.io/greenticai/greentic-demo-bundles/webchat-bot:v1",
)
.expect("pull public demo bundle");
assert!(
path.is_file(),
"fetched bundle should be a file: {}",
path.display()
);
let len = std::fs::metadata(&path).expect("stat fetched bundle").len();
assert!(len > 0, "fetched bundle should be non-empty");
}
}