greentic_deployer/cli/
bundle_fetch.rs1use std::path::PathBuf;
23
24use greentic_distributor_client::oci_packs::DefaultRegistryClient;
25use greentic_distributor_client::{OciPackFetcher, PackFetchOptions};
26
27use super::OpError;
28
29const OCI_SCHEME: &str = "oci://";
31
32pub fn fetch_bundle_uri_to_local(reference: &str) -> Result<PathBuf, OpError> {
43 let trimmed = reference.trim();
44 if trimmed.is_empty() {
45 return Err(OpError::InvalidArgument(
46 "bundle_source_uri cannot be empty".to_string(),
47 ));
48 }
49 let oci_ref = trimmed.strip_prefix(OCI_SCHEME).ok_or_else(|| {
50 OpError::NotYetImplemented(format!(
51 "apply can only fetch `oci://` bundle_source_uri references; `{trimmed}` is \
52 unsupported — declare a local `bundle_path` instead"
53 ))
54 })?;
55 if oci_ref.is_empty() {
56 return Err(OpError::InvalidArgument(format!(
57 "bundle_source_uri `{trimmed}` has no registry reference after `{OCI_SCHEME}`"
58 )));
59 }
60 fetch_oci_to_cache(oci_ref)
61}
62
63fn fetch_oci_to_cache(oci_ref: &str) -> Result<PathBuf, OpError> {
70 std::thread::scope(|scope| {
71 let handle = scope.spawn(|| -> Result<PathBuf, OpError> {
72 let rt = tokio::runtime::Builder::new_current_thread()
73 .enable_all()
74 .build()
75 .map_err(|source| OpError::Fetch(format!("build oci fetch runtime: {source}")))?;
76 let opts = PackFetchOptions {
79 allow_tags: true,
80 offline: false,
81 ..PackFetchOptions::default()
82 };
83 let fetcher: OciPackFetcher<DefaultRegistryClient> = OciPackFetcher::new(opts);
84 rt.block_on(fetcher.fetch_pack_to_cache(oci_ref))
85 .map(|resolved| resolved.path)
86 .map_err(|source| OpError::Fetch(format!("oci pull `{oci_ref}`: {source}")))
87 });
88 match handle.join() {
89 Ok(result) => result,
90 Err(_) => Err(OpError::Fetch("oci fetch thread panicked".to_string())),
91 }
92 })
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn empty_reference_is_invalid_argument() {
101 let err = fetch_bundle_uri_to_local(" ").unwrap_err();
102 assert_eq!(err.kind(), "invalid-argument");
103 }
104
105 #[test]
106 fn oci_scheme_with_no_target_is_invalid_argument() {
107 let err = fetch_bundle_uri_to_local("oci://").unwrap_err();
108 assert_eq!(err.kind(), "invalid-argument");
109 }
110
111 #[test]
112 fn local_path_reference_is_not_yet_implemented() {
113 let err = fetch_bundle_uri_to_local("/srv/bundles/webchat-bot.gtbundle").unwrap_err();
114 assert_eq!(err.kind(), "not-yet-implemented");
115 }
116
117 #[test]
118 fn repo_scheme_is_not_yet_implemented() {
119 let err = fetch_bundle_uri_to_local("repo://acme/webchat-bot:v1").unwrap_err();
120 assert_eq!(err.kind(), "not-yet-implemented");
121 }
122
123 #[test]
124 fn store_scheme_is_not_yet_implemented() {
125 let err = fetch_bundle_uri_to_local("store://acme/webchat-bot:v1").unwrap_err();
126 assert_eq!(err.kind(), "not-yet-implemented");
127 }
128
129 #[test]
130 fn https_scheme_is_not_yet_implemented() {
131 let err =
132 fetch_bundle_uri_to_local("https://example.com/webchat-bot.gtbundle").unwrap_err();
133 assert_eq!(err.kind(), "not-yet-implemented");
134 }
135
136 #[test]
141 #[ignore = "network: pulls the public demo bundle from ghcr"]
142 fn live_oci_pull_returns_a_readable_archive() {
143 let path = fetch_bundle_uri_to_local(
144 "oci://ghcr.io/greenticai/greentic-demo-bundles/webchat-bot:v1",
145 )
146 .expect("pull public demo bundle");
147 assert!(
148 path.is_file(),
149 "fetched bundle should be a file: {}",
150 path.display()
151 );
152 let len = std::fs::metadata(&path).expect("stat fetched bundle").len();
153 assert!(len > 0, "fetched bundle should be non-empty");
154 }
155}