From 086fa3bbd30cecf990391502fef3ad849d83fa96 Mon Sep 17 00:00:00 2001 From: Eric Curtin Date: Tue, 18 Nov 2025 09:38:10 +0000 Subject: [PATCH] --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 34ffb941..c3f47816 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 e7485eeb..efb3df31 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 f95bfd56..5c252478 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 4c6249fb..b8fd0b9a 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 df0fb05d..ea112de6 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) + } }