diff --git a/docker-java-api/src/main/java/com/github/dockerjava/api/command/SaveImageCmd.java b/docker-java-api/src/main/java/com/github/dockerjava/api/command/SaveImageCmd.java index aee9011cb..e80191267 100644 --- a/docker-java-api/src/main/java/com/github/dockerjava/api/command/SaveImageCmd.java +++ b/docker-java-api/src/main/java/com/github/dockerjava/api/command/SaveImageCmd.java @@ -1,6 +1,7 @@ package com.github.dockerjava.api.command; import java.io.InputStream; +import java.util.List; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; @@ -28,7 +29,21 @@ public interface SaveImageCmd extends SyncDockerCmd { SaveImageCmd withTag(String tag); /** - * Its the responsibility of the caller to consume and/or close the {@link InputStream} to prevent connection leaks. + * Confines the saved image to the specified platform or platforms (if invoked repeatedly). + * @since {@link com.github.dockerjava.core.RemoteApiVersion#VERSION_1_48} for one platform and + * {@link com.github.dockerjava.core.RemoteApiVersion#VERSION_1_52} for multiple platforms. + * @return this + */ + SaveImageCmd withPlatform(String platform); + + /** + * Gets the platforms that were added by {@link #withPlatform(String)}. + * @return platforms the saved image should be confined to. + */ + List getPlatforms(); + + /** + * It's the responsibility of the caller to consume and/or close the {@link InputStream} to prevent connection leaks. * * @throws NotFoundException * No such image diff --git a/docker-java-api/src/main/java/com/github/dockerjava/api/command/SaveImagesCmd.java b/docker-java-api/src/main/java/com/github/dockerjava/api/command/SaveImagesCmd.java index 1dd504434..146585d8b 100644 --- a/docker-java-api/src/main/java/com/github/dockerjava/api/command/SaveImagesCmd.java +++ b/docker-java-api/src/main/java/com/github/dockerjava/api/command/SaveImagesCmd.java @@ -34,6 +34,19 @@ interface TaggedImage { */ List getImages(); + /** + * Confines the saved image to the specified platform or platforms (if invoked repeatedly). + * @since {@link com.github.dockerjava.core.RemoteApiVersion#VERSION_1_48} for one platform and. + * @return this + */ + SaveImagesCmd withPlatform(String platform); + + /** + * Gets the platform that was set by {@link #withPlatform(String)}. + * @return platform the saved images should be confined to. + */ + String getPlatform(); + /** * Its the responsibility of the caller to consume and/or close the {@link InputStream} to prevent connection leaks. * diff --git a/docker-java-core/src/main/java/com/github/dockerjava/core/RemoteApiVersion.java b/docker-java-core/src/main/java/com/github/dockerjava/core/RemoteApiVersion.java index 373a67332..2f22739ad 100644 --- a/docker-java-core/src/main/java/com/github/dockerjava/core/RemoteApiVersion.java +++ b/docker-java-core/src/main/java/com/github/dockerjava/core/RemoteApiVersion.java @@ -95,6 +95,8 @@ public class RemoteApiVersion implements Serializable { public static final RemoteApiVersion VERSION_1_42 = RemoteApiVersion.create(1, 42); public static final RemoteApiVersion VERSION_1_43 = RemoteApiVersion.create(1, 43); public static final RemoteApiVersion VERSION_1_44 = RemoteApiVersion.create(1, 44); + public static final RemoteApiVersion VERSION_1_48 = RemoteApiVersion.create(1, 48); + public static final RemoteApiVersion VERSION_1_52 = RemoteApiVersion.create(1, 52); /** diff --git a/docker-java-core/src/main/java/com/github/dockerjava/core/command/SaveImageCmdImpl.java b/docker-java-core/src/main/java/com/github/dockerjava/core/command/SaveImageCmdImpl.java index 0ec72bcc5..0dcc5662e 100644 --- a/docker-java-core/src/main/java/com/github/dockerjava/core/command/SaveImageCmdImpl.java +++ b/docker-java-core/src/main/java/com/github/dockerjava/core/command/SaveImageCmdImpl.java @@ -1,16 +1,20 @@ package com.github.dockerjava.core.command; import java.io.InputStream; +import java.util.List; import java.util.Objects; import com.github.dockerjava.api.command.SaveImageCmd; import com.github.dockerjava.api.exception.NotFoundException; +import com.google.common.collect.ImmutableList; public class SaveImageCmdImpl extends AbstrDockerCmd implements SaveImageCmd { private String name; private String tag; + private final ImmutableList.Builder platforms = ImmutableList.builder(); + public SaveImageCmdImpl(SaveImageCmd.Exec exec, String name) { super(exec); withName(name); @@ -54,4 +58,16 @@ public SaveImageCmd withTag(String tag) { public InputStream exec() throws NotFoundException { return super.exec(); } + + @Override + public SaveImageCmd withPlatform(String platform) { + platforms.add(platform); + return this; + } + + @Override + public List getPlatforms() { + return platforms.build(); + } + } diff --git a/docker-java-core/src/main/java/com/github/dockerjava/core/command/SaveImagesCmdImpl.java b/docker-java-core/src/main/java/com/github/dockerjava/core/command/SaveImagesCmdImpl.java index 43e11f609..3913540a3 100644 --- a/docker-java-core/src/main/java/com/github/dockerjava/core/command/SaveImagesCmdImpl.java +++ b/docker-java-core/src/main/java/com/github/dockerjava/core/command/SaveImagesCmdImpl.java @@ -31,6 +31,8 @@ public String toString() { } } + private String platform; + private final ImmutableList.Builder taggedImages = ImmutableList.builder(); public SaveImagesCmdImpl(final SaveImagesCmd.Exec exec) { @@ -43,7 +45,16 @@ public SaveImagesCmd withImage(@Nonnull final String name, @Nonnull final String return this; } + @Override + public SaveImagesCmd withPlatform(String platform) { + this.platform = platform; + return this; + } + @Override + public String getPlatform() { + return platform; + } @Override public List getImages() { diff --git a/docker-java-core/src/main/java/com/github/dockerjava/core/exec/SaveImageCmdExec.java b/docker-java-core/src/main/java/com/github/dockerjava/core/exec/SaveImageCmdExec.java index 94001bd5c..d151a1de8 100644 --- a/docker-java-core/src/main/java/com/github/dockerjava/core/exec/SaveImageCmdExec.java +++ b/docker-java-core/src/main/java/com/github/dockerjava/core/exec/SaveImageCmdExec.java @@ -4,11 +4,14 @@ import com.github.dockerjava.core.DockerClientConfig; import com.github.dockerjava.core.MediaType; import com.github.dockerjava.core.WebTarget; +import com.github.dockerjava.core.util.PlatformUtil; import com.google.common.base.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.InputStream; +import java.util.HashSet; +import java.util.Set; public class SaveImageCmdExec extends AbstrSyncDockerCmdExec implements SaveImageCmd.Exec { private static final Logger LOGGER = LoggerFactory.getLogger(SaveImageCmdExec.class); @@ -28,6 +31,13 @@ protected InputStream execute(SaveImageCmd command) { WebTarget webResource = getBaseResource(). path("/images/" + name + "/get"); + Set platforms = new HashSet<>(command.getPlatforms()); + if (!platforms.isEmpty()) { + for (String platform : platforms) { + webResource = webResource.queryParamsJsonMap("platform", PlatformUtil.platformMap(platform)); + } + } + LOGGER.trace("GET: {}", webResource); return webResource.request().accept(MediaType.APPLICATION_JSON).get(); } diff --git a/docker-java-core/src/main/java/com/github/dockerjava/core/exec/SaveImagesCmdExec.java b/docker-java-core/src/main/java/com/github/dockerjava/core/exec/SaveImagesCmdExec.java index a1bb47c05..36fc8034d 100644 --- a/docker-java-core/src/main/java/com/github/dockerjava/core/exec/SaveImagesCmdExec.java +++ b/docker-java-core/src/main/java/com/github/dockerjava/core/exec/SaveImagesCmdExec.java @@ -4,6 +4,7 @@ import com.github.dockerjava.core.DockerClientConfig; import com.github.dockerjava.core.MediaType; import com.github.dockerjava.core.WebTarget; +import com.github.dockerjava.core.util.PlatformUtil; import com.google.common.collect.ImmutableSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,10 +30,14 @@ protected InputStream execute(SaveImagesCmd command) { for (SaveImagesCmd.TaggedImage image : images) { queryParamSet.add(image.asString()); } - final WebTarget webResource = getBaseResource() + WebTarget webResource = getBaseResource() .path("/images/get") .queryParamsSet("names", queryParamSet.build()); + if (command.getPlatform() != null) { + webResource = webResource.queryParamsJsonMap("platform", PlatformUtil.platformMap(command.getPlatform())); + } + LOGGER.trace("GET: {}", webResource); return webResource.request().accept(MediaType.APPLICATION_JSON).get(); } diff --git a/docker-java-core/src/main/java/com/github/dockerjava/core/util/PlatformUtil.java b/docker-java-core/src/main/java/com/github/dockerjava/core/util/PlatformUtil.java new file mode 100644 index 000000000..6fe042f2e --- /dev/null +++ b/docker-java-core/src/main/java/com/github/dockerjava/core/util/PlatformUtil.java @@ -0,0 +1,32 @@ +package com.github.dockerjava.core.util; + +import java.util.HashMap; +import java.util.Map; + +public class PlatformUtil { + + private PlatformUtil() { + } + + public static Map platformMap(String platform) { + + HashMap platformMap = new HashMap<>(); + if (platform == null) { + return platformMap; + } + String[] r = platform.split("/"); + if (r.length > 0) { + platformMap.put("os", r[0]); + } + if (r.length > 1) { + platformMap.put("architecture", r[1]); + } + if (r.length > 2) { + platformMap.put("variant", r[2]); + } + + return platformMap; + + } + +} diff --git a/docker-java/pom.xml b/docker-java/pom.xml index 3cfd7f255..835447584 100644 --- a/docker-java/pom.xml +++ b/docker-java/pom.xml @@ -138,6 +138,13 @@ test + + org.apache.commons + commons-compress + ${apache-compress.version} + test + + org.projectlombok lombok diff --git a/docker-java/src/test/java/com/github/dockerjava/cmd/CustomCommandIT.java b/docker-java/src/test/java/com/github/dockerjava/cmd/CustomCommandIT.java index bf273a98c..13931b41f 100644 --- a/docker-java/src/test/java/com/github/dockerjava/cmd/CustomCommandIT.java +++ b/docker-java/src/test/java/com/github/dockerjava/cmd/CustomCommandIT.java @@ -17,7 +17,7 @@ public class CustomCommandIT extends CmdIT { @Test public void testCustomCommand() throws Exception { - DockerHttpClient httpClient = CmdIT.createDockerHttpClient(DockerRule.config(null)); + DockerHttpClient httpClient = CmdIT.createDockerHttpClient(DockerRule.config(null).build()); Assume.assumeNotNull(httpClient); diff --git a/docker-java/src/test/java/com/github/dockerjava/cmd/SaveImageCmdIT.java b/docker-java/src/test/java/com/github/dockerjava/cmd/SaveImageCmdIT.java index cb5a4666c..3a0a04a53 100644 --- a/docker-java/src/test/java/com/github/dockerjava/cmd/SaveImageCmdIT.java +++ b/docker-java/src/test/java/com/github/dockerjava/cmd/SaveImageCmdIT.java @@ -1,13 +1,27 @@ package com.github.dockerjava.cmd; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.core.DockerRule; +import com.github.dockerjava.core.RemoteApiVersion; +import lombok.SneakyThrows; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.io.IOUtils; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; import java.io.InputStream; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import static com.github.dockerjava.utils.TestUtils.getVersion; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; public class SaveImageCmdIT extends CmdIT { @@ -29,6 +43,83 @@ public void saveImage() throws Exception { ) { assertThat(image2.read(), not(-1)); } + + ObjectMapper objectMapper = new ObjectMapper(); + + if (getVersion(dockerRule.getClient()).isGreaterOrEqual(RemoteApiVersion.VERSION_1_48)) { + + try (DockerClient c = createDockerClient(DockerRule.config(null).withApiVersion(RemoteApiVersion.VERSION_1_48).build())) { + + c.pullImageCmd("busybox").withTag("latest").withPlatform("linux/arm64").start().awaitCompletion(); + + Map tar; + + try (InputStream inputStream = c.saveImageCmd("busybox").withTag("latest").withPlatform("linux/arm64").exec()) { + tar = loadTar(inputStream); + } + + List> list = + objectMapper.readValue(tar.get("manifest.json"), new TypeReference>>() {}); + + assertThat(list.size(), is(1)); + + Map config = objectMapper.readValue(tar.get(list.get(0).get("Config").toString()), new TypeReference>() {}); + + assertThat(config.get("architecture"), is("arm64")); + assertThat(config.get("os"), is("linux")); + + } + + } + + // $TODO: test multi-platform save, but it requires containerd image store + } + @SneakyThrows + private Map loadTar(InputStream data) { + Map out = new LinkedHashMap<>(); + + try (TarArchiveInputStream tin = new TarArchiveInputStream(data)) { + TarArchiveEntry e; + while ((e = tin.getNextEntry()) != null) { + if (e.isDirectory()) { + continue; + } + String name = e.getName(); + long sizeL = e.getSize(); + if (sizeL < 0) { + throw new IOException("Negative size for tar entry: " + name + " size=" + sizeL); + } + if (sizeL > Integer.MAX_VALUE) { + throw new IOException("Tar entry too large to load into memory: " + name + " size=" + sizeL); + } + int size = (int) sizeL; + + // Read exactly this entry's bytes. + byte[] bytes = readFully(tin, size); + out.put(name, bytes); + + // TarArchiveInputStream aligns to 512-byte blocks internally; no manual skip needed. + } + } + + return out; + + } + + private static byte[] readFully(InputStream in, int size) throws IOException { + byte[] buf = new byte[size]; + int off = 0; + while (off < size) { + int r = in.read(buf, off, size - off); + if (r < 0) { + throw new IOException("Unexpected EOF while reading tar entry (" + off + "/" + size + " bytes)"); + } + off += r; + } + return buf; + } + + } diff --git a/docker-java/src/test/java/com/github/dockerjava/core/DockerRule.java b/docker-java/src/test/java/com/github/dockerjava/core/DockerRule.java index af606a5b1..53947b424 100644 --- a/docker-java/src/test/java/com/github/dockerjava/core/DockerRule.java +++ b/docker-java/src/test/java/com/github/dockerjava/core/DockerRule.java @@ -37,7 +37,7 @@ public class DockerRule extends ExternalResource { private final Set createdVolumeNames = new HashSet<>(); - private final DefaultDockerClientConfig config = config(); + private final DefaultDockerClientConfig config = config().build(); public DockerClient newClient() { DockerClientImpl dockerClient = CmdIT.createDockerClient(config); @@ -168,19 +168,19 @@ protected void after() { } } - private static DefaultDockerClientConfig config() { + private static DefaultDockerClientConfig.Builder config() { return config(null); } - public static DefaultDockerClientConfig config(String password) { + public static DefaultDockerClientConfig.Builder config(String password) { DefaultDockerClientConfig.Builder builder = DefaultDockerClientConfig.createDefaultConfigBuilder() .withApiVersion(RemoteApiVersion.VERSION_1_44) .withRegistryUrl("https://index.docker.io/v1/"); if (password != null) { - builder = builder.withRegistryPassword(password); + builder.withRegistryPassword(password); } - return builder.build(); + return builder; } public String buildImage(File baseDir) throws Exception { diff --git a/docker-java/src/test/java/com/github/dockerjava/junit/PrivateRegistryRule.java b/docker-java/src/test/java/com/github/dockerjava/junit/PrivateRegistryRule.java index 327bfc941..49ceba06e 100644 --- a/docker-java/src/test/java/com/github/dockerjava/junit/PrivateRegistryRule.java +++ b/docker-java/src/test/java/com/github/dockerjava/junit/PrivateRegistryRule.java @@ -29,7 +29,7 @@ public class PrivateRegistryRule extends ExternalResource { private String containerId; public PrivateRegistryRule() { - this.dockerClient = CmdIT.createDockerClient(DockerRule.config(null)); + this.dockerClient = CmdIT.createDockerClient(DockerRule.config(null).build()); } public AuthConfig getAuthConfig() { diff --git a/pom.xml b/pom.xml index 72add3980..97aedfbc8 100644 --- a/pom.xml +++ b/pom.xml @@ -74,7 +74,8 @@ 2.2 1.8 2.3.3 - 3.3.0 + 3.3.0 + 1.28.0 3.0.2