1use anyhow::{Context, Result};
2use oci_client::{
3 Reference,
4 client::{Client, ClientConfig, Config, ImageLayer},
5 secrets::RegistryAuth,
6};
7
8use crate::build::{OciImage, OciLayer};
9use bv_core::lockfile::LayerDescriptor;
10
11fn registry_auth() -> RegistryAuth {
12 if let Ok(token) = std::env::var("GITHUB_TOKEN") {
13 RegistryAuth::Basic("token".into(), token)
14 } else {
15 RegistryAuth::Anonymous
16 }
17}
18
19fn token_auth(token: &str) -> RegistryAuth {
20 RegistryAuth::Basic("token".into(), token.to_string())
21}
22
23pub fn load_from_tarball(path: &std::path::Path) -> Result<OciImage> {
25 use std::io::Read;
26
27 let f =
28 std::fs::File::open(path).with_context(|| format!("open tarball {}", path.display()))?;
29 let mut archive = tar::Archive::new(f);
30
31 let mut manifest_bytes: Option<Vec<u8>> = None;
32 let mut blobs: std::collections::HashMap<String, Vec<u8>> = std::collections::HashMap::new();
33
34 for entry in archive.entries().context("read tar entries")? {
35 let mut entry = entry.context("read tar entry")?;
36 let path_str = entry
37 .path()
38 .context("get entry path")?
39 .to_string_lossy()
40 .into_owned();
41 let mut data = Vec::new();
42 entry.read_to_end(&mut data).context("read entry data")?;
43
44 if path_str == "manifest.json" {
45 manifest_bytes = Some(data);
46 } else if let Some(hex) = path_str.strip_prefix("blobs/sha256/") {
47 blobs.insert(hex.to_string(), data);
48 }
49 }
50
51 let manifest_bytes = manifest_bytes.context("manifest.json not found in tarball")?;
52 let manifest: serde_json::Value =
53 serde_json::from_slice(&manifest_bytes).context("parse manifest.json")?;
54
55 let config_digest = manifest["config"]["digest"]
56 .as_str()
57 .context("manifest.config.digest missing")?;
58 let config_hex = config_digest
59 .strip_prefix("sha256:")
60 .unwrap_or(config_digest);
61 let config = blobs
62 .remove(config_hex)
63 .with_context(|| format!("config blob {config_hex} not found in tarball"))?;
64
65 let layers_json = manifest["layers"]
66 .as_array()
67 .context("manifest.layers missing")?;
68 let mut layers = Vec::new();
69 for layer_json in layers_json {
70 let digest = layer_json["digest"]
71 .as_str()
72 .context("layer.digest missing")?;
73 let media_type = layer_json["mediaType"]
74 .as_str()
75 .context("layer.mediaType missing")?;
76 let size = layer_json["size"].as_u64().context("layer.size missing")?;
77 let hex = digest.strip_prefix("sha256:").unwrap_or(digest);
78 let compressed = blobs
81 .get(hex)
82 .cloned()
83 .with_context(|| format!("layer blob {hex} not found in tarball"))?;
84
85 layers.push(OciLayer {
86 compressed,
87 uncompressed_digest: String::new(),
88 descriptor: LayerDescriptor {
89 digest: digest.to_string(),
90 size,
91 media_type: media_type.to_string(),
92 conda_package: None,
93 },
94 });
95 }
96
97 Ok(OciImage {
98 name: String::new(),
99 version: String::new(),
100 layers,
101 config,
102 })
103}
104
105pub async fn push_authenticated(image: &OciImage, reference: &str, token: &str) -> Result<String> {
110 push_inner(image, reference, token_auth(token)).await
111}
112
113pub async fn push(image: &OciImage, reference: &str) -> Result<String> {
117 push_inner(image, reference, registry_auth()).await
118}
119
120async fn push_inner(image: &OciImage, reference: &str, auth: RegistryAuth) -> Result<String> {
121 let reference: Reference = reference
122 .parse()
123 .with_context(|| format!("parse OCI reference '{reference}'"))?;
124
125 let config = ClientConfig {
126 protocol: oci_client::client::ClientProtocol::HttpsExcept(vec![
127 "localhost".into(),
128 "127.0.0.1".into(),
129 ]),
130 max_concurrent_upload: 1,
133 ..Default::default()
134 };
135
136 let client = Client::new(config);
137
138 let mut delay = std::time::Duration::from_secs(30);
139 let mut last_err: Option<anyhow::Error> = None;
140
141 for attempt in 0..8u32 {
142 if attempt > 0 {
143 eprintln!(
144 " transient error, retrying in {:?} (attempt {}/8)...",
145 delay,
146 attempt + 1
147 );
148 tokio::time::sleep(delay).await;
149 delay = (delay * 2).min(std::time::Duration::from_secs(120));
150 }
151
152 let layers: Vec<ImageLayer> = image
154 .layers
155 .iter()
156 .map(|l| ImageLayer::new(l.compressed.clone(), l.descriptor.media_type.clone(), None))
157 .collect();
158 let oci_config = Config::oci_v1(image.config.clone(), None);
159
160 match client
161 .push(&reference, &layers, oci_config, &auth, None)
162 .await
163 {
164 Ok(resp) => {
165 let digest = resp
169 .manifest_url
170 .rsplit('/')
171 .next()
172 .filter(|s| s.starts_with("sha256:"))
173 .or_else(|| resp.manifest_url.split('@').nth(1))
174 .unwrap_or("unknown")
175 .to_string();
176 return Ok(digest);
177 }
178 Err(e) if is_retriable(&e) => {
179 last_err = Some(anyhow::anyhow!("{e}"));
180 }
181 Err(e) => {
182 return Err(anyhow::anyhow!("{e}"))
183 .with_context(|| format!("push image to '{reference}'"));
184 }
185 }
186 }
187
188 Err(last_err.unwrap()).with_context(|| {
189 format!("push image to '{reference}' (rate limit: all retries exhausted after 8 attempts)")
190 })
191}
192
193fn is_retriable(e: &oci_client::errors::OciDistributionError) -> bool {
194 let combined = format!("{e:?} {e}").to_lowercase();
197 combined.contains("429")
198 || combined.contains("toomanyrequests")
199 || combined.contains("error sending request")
200 || combined.contains("error decoding response body")
201 || combined.contains("connection reset")
202 || combined.contains("connection closed")
203 || combined.contains("timed out")
204}
205
206pub async fn verify(reference: &str, expected_digest: &str) -> Result<()> {
209 let reference: Reference = reference
210 .parse()
211 .with_context(|| format!("parse OCI reference '{reference}'"))?;
212
213 let client = Client::new(ClientConfig::default());
214 let auth = registry_auth();
215
216 let (_manifest, digest) = client
217 .pull_manifest(&reference, &auth)
218 .await
219 .with_context(|| format!("pull manifest for '{reference}'"))?;
220
221 if digest != expected_digest {
222 anyhow::bail!(
223 "digest mismatch for '{reference}': expected {expected_digest} but registry returned {digest}"
224 );
225 }
226
227 Ok(())
228}