ostree_ext/container/
unencapsulate.rs1use crate::container::store::LayerProgress;
35
36use super::*;
37use containers_image_proxy::{ImageProxy, OpenedImage};
38use fn_error_context::context;
39use futures_util::{Future, FutureExt};
40use oci_spec::image::{self as oci_image, Digest};
41use std::sync::{Arc, Mutex};
42use tokio::{
43 io::{AsyncBufRead, AsyncRead},
44 sync::watch::{Receiver, Sender},
45};
46use tracing::instrument;
47
48type Progress = tokio::sync::watch::Sender<u64>;
49
50#[pin_project::pin_project]
52#[derive(Debug)]
53pub(crate) struct ProgressReader<T> {
54 #[pin]
55 pub(crate) reader: T,
56 #[pin]
57 pub(crate) progress: Arc<Mutex<Progress>>,
58}
59
60impl<T: AsyncRead> ProgressReader<T> {
61 pub(crate) fn new(reader: T) -> (Self, Receiver<u64>) {
62 let (progress, r) = tokio::sync::watch::channel(1);
63 let progress = Arc::new(Mutex::new(progress));
64 (ProgressReader { reader, progress }, r)
65 }
66}
67
68impl<T: AsyncRead> AsyncRead for ProgressReader<T> {
69 fn poll_read(
70 self: std::pin::Pin<&mut Self>,
71 cx: &mut std::task::Context<'_>,
72 buf: &mut tokio::io::ReadBuf<'_>,
73 ) -> std::task::Poll<std::io::Result<()>> {
74 let this = self.project();
75 let len = buf.filled().len();
76 match this.reader.poll_read(cx, buf) {
77 v @ std::task::Poll::Ready(Ok(_)) => {
78 let progress = this.progress.lock().unwrap();
79 let state = {
80 let mut state = *progress.borrow();
81 let newlen = buf.filled().len();
82 debug_assert!(newlen >= len);
83 let read = (newlen - len) as u64;
84 state += read;
85 state
86 };
87 let _ = progress.send(state);
89 v
90 }
91 o => o,
92 }
93 }
94}
95
96async fn fetch_manifest_impl(
97 proxy: &mut ImageProxy,
98 imgref: &OstreeImageReference,
99) -> Result<(oci_image::ImageManifest, oci_image::Digest)> {
100 let oi = &proxy.open_image(&imgref.imgref.to_string()).await?;
101 let (digest, manifest) = proxy.fetch_manifest(oi).await?;
102 proxy.close_image(oi).await?;
103 Ok((manifest, oci_image::Digest::from_str(digest.as_str())?))
104}
105
106#[context("Fetching manifest")]
108pub async fn fetch_manifest(
109 imgref: &OstreeImageReference,
110) -> Result<(oci_image::ImageManifest, oci_image::Digest)> {
111 let mut proxy = ImageProxy::new().await?;
112 fetch_manifest_impl(&mut proxy, imgref).await
113}
114
115#[context("Fetching manifest and config")]
117pub async fn fetch_manifest_and_config(
118 imgref: &OstreeImageReference,
119) -> Result<(
120 oci_image::ImageManifest,
121 oci_image::Digest,
122 oci_image::ImageConfiguration,
123)> {
124 let proxy = ImageProxy::new().await?;
125 let oi = &proxy.open_image(&imgref.imgref.to_string()).await?;
126 let (digest, manifest) = proxy.fetch_manifest(oi).await?;
127 let digest = oci_image::Digest::from_str(&digest)?;
128 let config = proxy.fetch_config(oi).await?;
129 Ok((manifest, digest, config))
130}
131
132#[derive(Debug)]
134pub struct Import {
135 pub ostree_commit: String,
137 pub image_digest: Digest,
139
140 pub deprecated_warning: Option<String>,
142}
143
144pub(crate) async fn join_fetch<T: std::fmt::Debug>(
161 worker: impl Future<Output = Result<T>>,
162 driver: impl Future<Output = Result<()>>,
163) -> Result<T> {
164 let (worker, driver) = tokio::join!(worker, driver);
165 match (worker, driver) {
166 (Ok(t), Ok(())) => Ok(t),
167 (Err(worker), Err(driver)) => {
168 let text = driver.root_cause().to_string();
169 if text.ends_with("broken pipe") {
170 tracing::trace!("Ignoring broken pipe failure from driver");
171 Err(worker)
172 } else {
173 Err(driver.context(format!("client error: {worker:#}")))
174 }
175 }
176 (Ok(_), Err(driver)) => Err(driver),
177 (Err(worker), Ok(())) => Err(worker),
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn test_join_fetch_preserves_typed_driver_error() {
187 let runtime = tokio::runtime::Builder::new_current_thread()
188 .build()
189 .unwrap();
190 let error = runtime
191 .block_on(join_fetch(
192 async { Err::<(), _>(anyhow!("client read failed")) },
193 async {
194 let error = containers_image_proxy::Error::BlobError(
195 containers_image_proxy::GetBlobError::Retryable("connection reset".into()),
196 );
197 Err::<(), _>(anyhow::Error::from(error))
198 },
199 ))
200 .unwrap_err();
201
202 assert!(error.chain().any(|source| matches!(
203 source.downcast_ref::<containers_image_proxy::Error>(),
204 Some(containers_image_proxy::Error::BlobError(
205 containers_image_proxy::GetBlobError::Retryable(_)
206 ))
207 )));
208 assert!(format!("{error:#}").contains("client read failed"));
209 }
210}
211
212#[context("Importing {}", imgref)]
214#[instrument(level = "debug", skip(repo))]
215pub async fn unencapsulate(repo: &ostree::Repo, imgref: &OstreeImageReference) -> Result<Import> {
216 let importer = super::store::ImageImporter::new(repo, imgref, Default::default()).await?;
217 importer.unencapsulate().await
218}
219
220pub(crate) async fn fetch_layer<'a>(
222 proxy: &'a ImageProxy,
223 img: &OpenedImage,
224 manifest: &oci_image::ImageManifest,
225 layer: &'a oci_image::Descriptor,
226 progress: Option<&'a Sender<Option<store::LayerProgress>>>,
227 layer_info: Option<&Vec<containers_image_proxy::ConvertedLayerInfo>>,
228 transport_src: Transport,
229) -> Result<(
230 Box<dyn AsyncBufRead + Send + Unpin>,
231 impl Future<Output = Result<()>> + 'a,
232 oci_image::MediaType,
233)> {
234 use futures_util::future::Either;
235 tracing::debug!("fetching {}", layer.digest());
236 let layer_index = manifest.layers().iter().position(|x| x == layer).unwrap();
237 let (blob_digest, size);
238 let mut media_type: oci_image::MediaType;
239 match transport_src {
240 Transport::ContainerStorage | Transport::DockerDaemon => {
244 let layer_info = layer_info.ok_or_else(|| {
245 anyhow!("skopeo too old to pull from containers-storage or docker-daemon")
246 })?;
247 let n_layers = layer_info.len();
248 let layer_blob = layer_info.get(layer_index).ok_or_else(|| {
249 anyhow!("blobid position {layer_index} exceeds diffid count {n_layers}")
250 })?;
251 size = layer_blob.size;
252 media_type = layer_blob.media_type.clone();
253
254 if transport_src == Transport::DockerDaemon {
257 if let oci_image::MediaType::Other(t) = &media_type {
258 if t.as_str() == "application/vnd.docker.image.rootfs.diff.tar.gzip" {
259 media_type = oci_image::MediaType::Other(
260 "application/vnd.docker.image.rootfs.diff.tar".to_string(),
261 );
262 }
263 }
264 }
265
266 blob_digest = &layer_blob.digest;
267 }
268 _ => {
269 size = layer.size();
270 media_type = layer.media_type().clone();
271 blob_digest = layer.digest();
272 }
273 };
274
275 let stream = proxy.get_blob_stream(img, blob_digest, size).await?;
276 let (blob, driver) = stream.into_parts();
277 let blob = tokio::io::BufReader::new(blob);
278 let driver = async { driver.await.map_err(Into::into) };
279
280 if let Some(progress) = progress {
281 let (readprogress, mut readwatch) = ProgressReader::new(blob);
282 let readprogress = tokio::io::BufReader::new(readprogress);
283 let readproxy = async move {
284 while let Ok(()) = readwatch.changed().await {
285 let fetched = readwatch.borrow_and_update();
286 let status = LayerProgress {
287 layer_index,
288 fetched: *fetched,
289 total: size,
290 };
291 progress.send_replace(Some(status));
292 }
293 };
294 let reader = Box::new(readprogress);
295 let driver = futures_util::future::join(readproxy, driver).map(|r| r.1);
296 Ok((reader, Either::Left(driver), media_type))
297 } else {
298 Ok((Box::new(blob), Either::Right(driver), media_type))
299 }
300}