From 20a5369f87d2775ccc9f42d3a2e45ed8952f66b5 Mon Sep 17 00:00:00 2001 From: Eric Curtin Date: Tue, 18 Nov 2025 09:38:10 +0000 Subject: [PATCH 1/2] --transport docker-daemon support So we can do things like: sudo bootc switch --transport docker-daemon localhost/bootc:latest Signed-off-by: Eric Curtin --- crates/lib/src/bootc_composefs/repo.rs | 11 ++++++++++ crates/lib/src/cli.rs | 2 +- crates/ostree-ext/src/container/mod.rs | 6 ++++++ .../ostree-ext/src/container/unencapsulate.rs | 10 ++++++--- crates/ostree-ext/src/generic_decompress.rs | 21 +++++++++++++++++++ 5 files changed, 46 insertions(+), 4 deletions(-) diff --git a/crates/lib/src/bootc_composefs/repo.rs b/crates/lib/src/bootc_composefs/repo.rs index 34ffb9417..c3f478169 100644 --- a/crates/lib/src/bootc_composefs/repo.rs +++ b/crates/lib/src/bootc_composefs/repo.rs @@ -56,12 +56,15 @@ pub(crate) async fn initialize_composefs_repository( /// Ex /// docker://quay.io/some-image /// containers-storage:some-image +/// docker-daemon:some-image-id pub(crate) fn get_imgref(transport: &str, image: &str) -> String { let img = image.strip_prefix(":").unwrap_or(&image); let transport = transport.strip_suffix(":").unwrap_or(&transport); if transport == "registry" { format!("docker://{img}") + } else if transport == "docker-daemon" { + format!("docker-daemon:{img}") } else { format!("{transport}:{img}") } @@ -138,4 +141,12 @@ mod tests { format!("docker://{IMAGE_NAME}") ); } + + #[test] + fn test_get_imgref_docker_daemon_transport() { + assert_eq!( + get_imgref("docker-daemon", IMAGE_NAME), + format!("docker-daemon:{IMAGE_NAME}") + ); + } } diff --git a/crates/lib/src/cli.rs b/crates/lib/src/cli.rs index e7485eebd..efb3df316 100644 --- a/crates/lib/src/cli.rs +++ b/crates/lib/src/cli.rs @@ -123,7 +123,7 @@ pub(crate) struct SwitchOpts { #[clap(long = "soft-reboot")] pub(crate) soft_reboot: Option, - /// The transport; e.g. oci, oci-archive, containers-storage. Defaults to `registry`. + /// The transport; e.g. registry, oci, oci-archive, docker-daemon, containers-storage. Defaults to `registry`. #[clap(long, default_value = "registry")] pub(crate) transport: String, diff --git a/crates/ostree-ext/src/container/mod.rs b/crates/ostree-ext/src/container/mod.rs index f95bfd566..5c252478a 100644 --- a/crates/ostree-ext/src/container/mod.rs +++ b/crates/ostree-ext/src/container/mod.rs @@ -66,6 +66,8 @@ pub enum Transport { ContainerStorage, /// Local directory (`dir:`) Dir, + /// Local Docker daemon (`docker-daemon:`) + DockerDaemon, } /// Combination of a remote image reference and transport. @@ -114,6 +116,7 @@ impl TryFrom<&str> for Transport { Self::DOCKER_ARCHIVE_STR => Self::DockerArchive, Self::CONTAINERS_STORAGE_STR => Self::ContainerStorage, Self::LOCAL_DIRECTORY_STR => Self::Dir, + Self::DOCKER_DAEMON_STR => Self::DockerDaemon, o => return Err(anyhow!("Unknown transport '{}'", o)), }) } @@ -126,6 +129,7 @@ impl Transport { const CONTAINERS_STORAGE_STR: &'static str = "containers-storage"; const LOCAL_DIRECTORY_STR: &'static str = "dir"; const REGISTRY_STR: &'static str = "registry"; + const DOCKER_DAEMON_STR: &'static str = "docker-daemon"; /// Retrieve an identifier that can then be re-parsed from [`Transport::try_from::<&str>`]. pub fn serializable_name(&self) -> &'static str { @@ -136,6 +140,7 @@ impl Transport { Transport::DockerArchive => Self::DOCKER_ARCHIVE_STR, Transport::ContainerStorage => Self::CONTAINERS_STORAGE_STR, Transport::Dir => Self::LOCAL_DIRECTORY_STR, + Transport::DockerDaemon => Self::DOCKER_DAEMON_STR, } } } @@ -258,6 +263,7 @@ impl std::fmt::Display for Transport { Self::OciDir => "oci:", Self::ContainerStorage => "containers-storage:", Self::Dir => "dir:", + Self::DockerDaemon => "docker-daemon:", }; f.write_str(s) } diff --git a/crates/ostree-ext/src/container/unencapsulate.rs b/crates/ostree-ext/src/container/unencapsulate.rs index 4c6249fb6..b8fd0b9a2 100644 --- a/crates/ostree-ext/src/container/unencapsulate.rs +++ b/crates/ostree-ext/src/container/unencapsulate.rs @@ -205,9 +205,13 @@ pub(crate) async fn fetch_layer<'a>( let (blob, driver, size); let media_type: oci_image::MediaType; match transport_src { - Transport::ContainerStorage => { - let layer_info = layer_info - .ok_or_else(|| anyhow!("skopeo too old to pull from containers-storage"))?; + // Both containers-storage and docker-daemon store layers uncompressed in their + // local storage, even though the manifest may indicate they are compressed. + // We need to use the actual media type from layer_info to avoid decompression errors. + Transport::ContainerStorage | Transport::DockerDaemon => { + let layer_info = layer_info.ok_or_else(|| { + anyhow!("skopeo too old to pull from containers-storage or docker-daemon") + })?; let n_layers = layer_info.len(); let layer_blob = layer_info.get(layer_index).ok_or_else(|| { anyhow!("blobid position {layer_index} exceeds diffid count {n_layers}") diff --git a/crates/ostree-ext/src/generic_decompress.rs b/crates/ostree-ext/src/generic_decompress.rs index df0fb05dd..ea112de69 100644 --- a/crates/ostree-ext/src/generic_decompress.rs +++ b/crates/ostree-ext/src/generic_decompress.rs @@ -22,6 +22,11 @@ use crate::oci_spec::image as oci_image; /// TODO: change the skopeo code to shield us from this correctly const DOCKER_TYPE_LAYER_TAR: &str = "application/vnd.docker.image.rootfs.diff.tar"; +/// The Docker MIME type for gzipped layers when stored in docker-daemon. +/// Even though this indicates gzip compression, docker-daemon actually stores +/// the layers uncompressed, so we need to treat this as uncompressed. +const DOCKER_TYPE_LAYER_TAR_GZIP: &str = "application/vnd.docker.image.rootfs.diff.tar.gzip"; + /// Extends the `Read` trait with another method to get mutable access to the inner reader trait ReadWithGetInnerMut: Read + Send + 'static { fn get_inner_mut(&mut self) -> &mut dyn Read; @@ -125,6 +130,9 @@ impl Decompressor { oci_image::MediaType::Other(t) if t.as_str() == DOCKER_TYPE_LAYER_TAR => { Box::new(TransparentDecompressor(src)) } + oci_image::MediaType::Other(t) if t.as_str() == DOCKER_TYPE_LAYER_TAR_GZIP => { + Box::new(TransparentDecompressor(src)) + } o => anyhow::bail!("Unhandled layer type: {}", o), }; Ok(Self { @@ -228,4 +236,17 @@ mod tests { assert_eq!(e.to_string(), "Unknown frame descriptor".to_string()); drop(d) } + + #[test] + fn test_docker_tar_gzip_media_type_uses_transparent_decompressor() { + // Test that the docker-daemon gzip media type is treated as uncompressed + let data = b"test data"; + let media_type = oci_image::MediaType::Other(DOCKER_TYPE_LAYER_TAR_GZIP.to_string()); + let mut d = Decompressor::new(&media_type, &data[..]).unwrap(); + let mut buf = [0u8; 32]; + let n = d.read(&mut buf).unwrap(); + assert_eq!(n, data.len()); + assert_eq!(&buf[..n], data); + drop(d) + } } From a7e33bb2014457dc303735b7e4277d2fe76fe41d Mon Sep 17 00:00:00 2001 From: Eric Curtin Date: Tue, 18 Nov 2025 16:04:42 +0000 Subject: [PATCH 2/2] Move docker .tar.gz handling to unencapsulate.rs To handle this is a different location Signed-off-by: Eric Curtin --- .../ostree-ext/src/container/unencapsulate.rs | 15 ++++++++++++- crates/ostree-ext/src/generic_decompress.rs | 21 ------------------- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/crates/ostree-ext/src/container/unencapsulate.rs b/crates/ostree-ext/src/container/unencapsulate.rs index b8fd0b9a2..c386576d0 100644 --- a/crates/ostree-ext/src/container/unencapsulate.rs +++ b/crates/ostree-ext/src/container/unencapsulate.rs @@ -203,7 +203,7 @@ pub(crate) async fn fetch_layer<'a>( tracing::debug!("fetching {}", layer.digest()); let layer_index = manifest.layers().iter().position(|x| x == layer).unwrap(); let (blob, driver, size); - let media_type: oci_image::MediaType; + let mut media_type: oci_image::MediaType; match transport_src { // Both containers-storage and docker-daemon store layers uncompressed in their // local storage, even though the manifest may indicate they are compressed. @@ -218,6 +218,19 @@ pub(crate) async fn fetch_layer<'a>( })?; size = layer_blob.size; media_type = layer_blob.media_type.clone(); + + // docker-daemon stores layers uncompressed even when the media type + // indicates gzip compression. Translate to the uncompressed variant. + if transport_src == Transport::DockerDaemon { + if let oci_image::MediaType::Other(t) = &media_type { + if t.as_str() == "application/vnd.docker.image.rootfs.diff.tar.gzip" { + media_type = oci_image::MediaType::Other( + "application/vnd.docker.image.rootfs.diff.tar".to_string(), + ); + } + } + } + (blob, driver) = proxy.get_blob(img, &layer_blob.digest, size).await?; } _ => { diff --git a/crates/ostree-ext/src/generic_decompress.rs b/crates/ostree-ext/src/generic_decompress.rs index ea112de69..df0fb05dd 100644 --- a/crates/ostree-ext/src/generic_decompress.rs +++ b/crates/ostree-ext/src/generic_decompress.rs @@ -22,11 +22,6 @@ use crate::oci_spec::image as oci_image; /// TODO: change the skopeo code to shield us from this correctly const DOCKER_TYPE_LAYER_TAR: &str = "application/vnd.docker.image.rootfs.diff.tar"; -/// The Docker MIME type for gzipped layers when stored in docker-daemon. -/// Even though this indicates gzip compression, docker-daemon actually stores -/// the layers uncompressed, so we need to treat this as uncompressed. -const DOCKER_TYPE_LAYER_TAR_GZIP: &str = "application/vnd.docker.image.rootfs.diff.tar.gzip"; - /// Extends the `Read` trait with another method to get mutable access to the inner reader trait ReadWithGetInnerMut: Read + Send + 'static { fn get_inner_mut(&mut self) -> &mut dyn Read; @@ -130,9 +125,6 @@ impl Decompressor { oci_image::MediaType::Other(t) if t.as_str() == DOCKER_TYPE_LAYER_TAR => { Box::new(TransparentDecompressor(src)) } - oci_image::MediaType::Other(t) if t.as_str() == DOCKER_TYPE_LAYER_TAR_GZIP => { - Box::new(TransparentDecompressor(src)) - } o => anyhow::bail!("Unhandled layer type: {}", o), }; Ok(Self { @@ -236,17 +228,4 @@ mod tests { assert_eq!(e.to_string(), "Unknown frame descriptor".to_string()); drop(d) } - - #[test] - fn test_docker_tar_gzip_media_type_uses_transparent_decompressor() { - // Test that the docker-daemon gzip media type is treated as uncompressed - let data = b"test data"; - let media_type = oci_image::MediaType::Other(DOCKER_TYPE_LAYER_TAR_GZIP.to_string()); - let mut d = Decompressor::new(&media_type, &data[..]).unwrap(); - let mut buf = [0u8; 32]; - let n = d.read(&mut buf).unwrap(); - assert_eq!(n, data.len()); - assert_eq!(&buf[..n], data); - drop(d) - } }