diff --git a/src/image.rs b/src/image.rs index 17786ecd..d0995bff 100644 --- a/src/image.rs +++ b/src/image.rs @@ -1195,17 +1195,16 @@ impl Docker { /// /// Get a tarball containing all images and metadata for a repository. /// - /// The root of the resulting tar file will contain the file "mainifest.json". If the export is - /// of an image repository, rather than a signle image, there will also be a `repositories` file + /// The root of the resulting tar file will contain the file "manifest.json". If the export is + /// of an image repository, rather than a single image, there will also be a `repositories` file /// with a JSON description of the exported image repositories. /// Additionally, each layer of all exported images will have a sub directory in the archive /// containing the filesystem of the layer. /// - /// See the [Docker API documentation](https://docs.docker.com/engine/api/v1.40/#operation/ImageCommit) + /// See the [Docker API documentation](https://docs.docker.com/engine/api/v1.40/#operation/ImageGet) /// for more information. /// # Arguments - /// - The `image_name` string can refer to an individual image and tag (e.g. alpine:latest), - /// an individual image by I + /// - The `image_name` string referring to an individual image and tag (e.g. alpine:latest) /// /// # Returns /// - An uncompressed TAR archive @@ -1222,6 +1221,33 @@ impl Docker { self.process_into_body(req) } + /// --- + /// + /// # Export Images + /// + /// Get a tarball containing all images and metadata for several image repositories. Shared + /// layers will be deduplicated. + /// + /// See the [Docker API documentation](https://docs.docker.com/engine/api/v1.40/#tag/Image/operation/ImageGetAll) + /// for more information. + /// # Arguments + /// - The `image_names` Vec of image names. + /// + /// # Returns + /// - An uncompressed TAR archive + pub fn export_images(&self, image_names: &Vec<&str>) -> impl Stream> { + let options: Vec<_> = image_names.iter().map(|name| ("names", name)).collect(); + let req = self.build_request( + "/images/get", + Builder::new() + .method(Method::GET) + .header(CONTENT_TYPE, "application/json"), + Some(options), + Ok(Body::empty()), + ); + self.process_into_body(req) + } + /// --- /// /// # Import Image diff --git a/tests/image_test.rs b/tests/image_test.rs index 83587ba2..93cca553 100644 --- a/tests/image_test.rs +++ b/tests/image_test.rs @@ -574,6 +574,42 @@ async fn export_image_test(docker: Docker) -> Result<(), Error> { Ok(()) } +async fn export_images_test(docker: Docker) -> Result<(), Error> { + // pull from registry + create_image_hello_world(&docker).await?; + + let repo = format!("{}hello-world", registry_http_addr()); + let image = format!("{repo}:linux"); + + docker.tag_image(&image, Some(TagImageOptions { + repo: repo.as_ref(), + tag: "mycopy", + })).await?; + + let copy = format!("{repo}:mycopy"); + let images = vec![image.as_ref(), copy.as_ref()]; + let res = docker.export_images(&images); + + let temp_file = "/tmp/bollard_test_images_export.tar"; + let mut archive_file = File::create(temp_file).unwrap(); + // Shouldn't load the whole file into memory, stream it to disk instead + res.for_each(move |data| { + archive_file.write_all(&data.unwrap()).unwrap(); + archive_file.sync_all().unwrap(); + ready(()) + }) + .await; + + // assert that the file containing the exported archive actually exists + let test_file = File::open(temp_file).unwrap(); + // and metadata can be read + test_file.metadata().unwrap(); + + // And delete it to clean up + remove_file(temp_file).unwrap(); + Ok(()) +} + async fn issue_55_test(docker: Docker) -> Result<(), Error> { let dockerfile = "FROM ubuntu:18.04 RUN apt-get update && \ @@ -753,6 +789,12 @@ fn integration_test_export_image() { connect_to_docker_and_run!(export_image_test); } +#[test] +#[cfg(unix)] +fn integration_test_export_images() { + connect_to_docker_and_run!(export_images_test); +} + #[test] #[cfg(unix)] // Flaky