diff --git a/config/checkstyle/copyright-java.header b/config/checkstyle/copyright-java.header index d15ad5912e..e60211cae3 100644 --- a/config/checkstyle/copyright-java.header +++ b/config/checkstyle/copyright-java.header @@ -1,5 +1,5 @@ ^/\*$ -^ \* Copyright 20(17|18|19|20|21|22|23) Google LLC\.$ +^ \* Copyright 20(17|18|19|20|21|22|23|24) Google LLC\.$ ^ \*$ ^ \* Licensed under the Apache License, Version 2\.0 \(the "License"\); you may not$ ^ \* use this file except in compliance with the License\. You may obtain a copy of$ diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java index a9916708c3..4e49d1e12d 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java @@ -16,6 +16,9 @@ package com.google.cloud.tools.jib.api; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + import com.google.cloud.tools.jib.Command; import com.google.cloud.tools.jib.api.buildplan.Platform; import com.google.cloud.tools.jib.blob.Blobs; @@ -304,6 +307,65 @@ public void testScratch_multiPlatform() Assert.assertEquals("windows", platform2.getOs()); } + @Test + public void testBasic_jibImageToDockerDaemon() + throws IOException, InterruptedException, InvalidImageReferenceException, ExecutionException, + RegistryException, CacheDirectoryCreationException { + Jib.from(DockerDaemonImage.named(dockerHost + ":5000/busybox")) + .setEntrypoint("echo", "Hello World") + .containerize( + Containerizer.to(DockerDaemonImage.named(dockerHost + ":5000/docker-to-docker"))); + + String output = + new Command("docker", "run", "--rm", dockerHost + ":5000/docker-to-docker").run(); + Assert.assertEquals("Hello World\n", output); + } + + @Test + public void testBasicMultiPlatform_toDockerDaemon() + throws IOException, InterruptedException, ExecutionException, RegistryException, + CacheDirectoryCreationException, InvalidImageReferenceException { + Jib.from( + RegistryImage.named( + "busybox@sha256:4f47c01fa91355af2865ac10fef5bf6ec9c7f42ad2321377c21e844427972977")) + .setPlatforms( + ImmutableSet.of(new Platform("arm64", "linux"), new Platform("amd64", "linux"))) + .setEntrypoint("echo", "Hello World") + .containerize( + Containerizer.to( + DockerDaemonImage.named(dockerHost + ":5000/docker-daemon-multi-platform")) + .setAllowInsecureRegistries(true)); + + String output = + new Command("docker", "run", "--rm", dockerHost + ":5000/docker-daemon-multi-platform") + .run(); + Assert.assertEquals("Hello World\n", output); + } + + @Test + public void testBasicMultiPlatform_toDockerDaemon_noMatchingImage() { + ExecutionException exception = + assertThrows( + ExecutionException.class, + () -> + Jib.from( + RegistryImage.named( + "busybox@sha256:4f47c01fa91355af2865ac10fef5bf6ec9c7f42ad2321377c21e844427972977")) + .setPlatforms( + ImmutableSet.of( + new Platform("s390x", "linux"), new Platform("arm", "linux"))) + .setEntrypoint("echo", "Hello World") + .containerize( + Containerizer.to( + DockerDaemonImage.named( + dockerHost + ":5000/docker-daemon-multi-platform")) + .setAllowInsecureRegistries(true))); + assertThat(exception) + .hasCauseThat() + .hasMessageThat() + .startsWith("The configured platforms don't match the Docker Engine's OS and architecture"); + } + @Test public void testDistroless_ociManifest() throws IOException, InterruptedException, ExecutionException, RegistryException, diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerClient.java index 50ce2f463f..7e08a79495 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerClient.java @@ -69,4 +69,15 @@ void save(ImageReference imageReference, Path outputPath, Consumer written * @throws InterruptedException if the {@code docker inspect} process was interrupted */ ImageDetails inspect(ImageReference imageReference) throws IOException, InterruptedException; + + /** + * Gets docker info details of local docker installation. + * + * @return docker info details. + * @throws IOException if an I/O exception occurs or {@code docker info} failed + * @throws InterruptedException if the {@code docker info} process was interrupted + */ + default DockerInfoDetails info() throws IOException, InterruptedException { + return new DockerInfoDetails(); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerInfoDetails.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerInfoDetails.java new file mode 100644 index 0000000000..fa486c40f9 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerInfoDetails.java @@ -0,0 +1,40 @@ +/* + * Copyright 2024 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.api; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.cloud.tools.jib.json.JsonTemplate; + +/** Contains docker info details outputted by {@code docker info}. */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class DockerInfoDetails implements JsonTemplate { + + @JsonProperty("OSType") + private String osType = ""; + + @JsonProperty("Architecture") + private String architecture = ""; + + public String getOsType() { + return osType; + } + + public String getArchitecture() { + return architecture; + } +} diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index 46c63bfc9d..be760a13eb 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java @@ -18,6 +18,7 @@ import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.api.DockerClient; +import com.google.cloud.tools.jib.api.DockerInfoDetails; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.steps.LocalBaseImageSteps.LocalImage; @@ -52,6 +53,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.function.Consumer; +import java.util.logging.Logger; import java.util.stream.Collectors; import javax.annotation.Nullable; @@ -64,6 +66,8 @@ */ public class StepsRunner { + private static final Logger LOGGER = Logger.getLogger(StepsRunner.class.getName()); + /** Holds the individual step results. */ private static class StepResults { @@ -413,7 +417,8 @@ private void buildAndCacheApplicationLayers( BuildAndCacheApplicationLayerStep.makeList(buildContext, progressDispatcherFactory)); } - private void buildImages(ProgressEventDispatcher.Factory progressDispatcherFactory) { + @VisibleForTesting + void buildImages(ProgressEventDispatcher.Factory progressDispatcherFactory) { results.baseImagesAndBuiltImages = executorService.submit( () -> { @@ -616,13 +621,17 @@ private void loadDocker( results.buildResult = executorService.submit( () -> { - Verify.verify( - results.baseImagesAndBuiltImages.get().size() == 1, - "multi-platform image building not supported when pushing to Docker engine"); - Image builtImage = - results.baseImagesAndBuiltImages.get().values().iterator().next().get(); + DockerInfoDetails dockerInfoDetails = dockerClient.info(); + String osType = dockerInfoDetails.getOsType(); + String architecture = normalizeArchitecture(dockerInfoDetails.getArchitecture()); + Optional builtImage = fetchBuiltImageForLocalBuild(osType, architecture); + Preconditions.checkState( + builtImage.isPresent(), + String.format( + "The configured platforms don't match the Docker Engine's OS and architecture (%s/%s)", + osType, architecture)); return new LoadDockerStep( - buildContext, progressDispatcherFactory, dockerClient, builtImage) + buildContext, progressDispatcherFactory, dockerClient, builtImage.get()) .call(); }); } @@ -647,4 +656,34 @@ private void writeTarFile( private List> scheduleCallables(ImmutableList> callables) { return callables.stream().map(executorService::submit).collect(Collectors.toList()); } + + @VisibleForTesting + String normalizeArchitecture(String architecture) { + // Create mapping based on https://docs.docker.com/engine/install/#supported-platforms + if (architecture.equals("x86_64")) { + return "amd64"; + } else if (architecture.equals("aarch64")) { + return "arm64"; + } + return architecture; + } + + @VisibleForTesting + Optional fetchBuiltImageForLocalBuild(String osType, String architecture) + throws InterruptedException, ExecutionException { + if (results.baseImagesAndBuiltImages.get().size() > 1) { + LOGGER.warning( + String.format( + "Detected multi-platform configuration, only building the one that matches the local Docker Engine's os and architecture (%s/%s)", + osType, architecture)); + } + for (Map.Entry> imageEntry : + results.baseImagesAndBuiltImages.get().entrySet()) { + Image image = imageEntry.getValue().get(); + if (image.getArchitecture().equals(architecture) && image.getOs().equals(osType)) { + return Optional.of(image); + } + } + return Optional.empty(); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/CliDockerClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/CliDockerClient.java index 7c9c23cd9f..1842a79de3 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/docker/CliDockerClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/docker/CliDockerClient.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.api.DockerClient; +import com.google.cloud.tools.jib.api.DockerInfoDetails; import com.google.cloud.tools.jib.api.ImageDetails; import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.http.NotifyingOutputStream; @@ -184,6 +185,17 @@ public boolean supported(Map parameters) { return true; } + @Override + public DockerInfoDetails info() throws IOException, InterruptedException { + // Runs 'docker info'. + Process infoProcess = docker("info", "-f", "{{json .}}"); + if (infoProcess.waitFor() != 0) { + throw new IOException( + "'docker info' command failed with error: " + getStderrOutput(infoProcess)); + } + return JsonTemplateMapper.readJson(infoProcess.getInputStream(), DockerInfoDetails.class); + } + @Override public String load(ImageTarball imageTarball, Consumer writtenByteCountListener) throws InterruptedException, IOException { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/StepsRunnerTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/StepsRunnerTest.java index e8a60de2d5..9436c7df4a 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/StepsRunnerTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/StepsRunnerTest.java @@ -16,6 +16,9 @@ package com.google.cloud.tools.jib.builder.steps; +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; + import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.builder.ProgressEventDispatcher; import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImagesAndRegistryClient; @@ -26,6 +29,7 @@ import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.cloud.tools.jib.registry.ManifestAndDigest; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.util.concurrent.ForwardingExecutorService; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -83,6 +87,10 @@ protected ExecutorService delegate() { @Mock private ProgressEventDispatcher.Factory progressDispatcherFactory; @Mock private ProgressEventDispatcher progressDispatcher; @Mock private ExecutorService executorService; + @Mock private Image builtArm64AndLinuxImage; + @Mock private Image builtAmd64AndWindowsImage; + @Mock private Image baseImage1; + @Mock private Image baseImage2; private StepsRunner stepsRunner; @@ -90,14 +98,14 @@ protected ExecutorService delegate() { public void setup() { stepsRunner = new StepsRunner(new MockListeningExecutorService(), buildContext); - Mockito.when(progressDispatcherFactory.create(Mockito.anyString(), Mockito.anyLong())) + when(progressDispatcherFactory.create(Mockito.anyString(), Mockito.anyLong())) .thenReturn(progressDispatcher); } @Test public void testObtainBaseImageLayers_skipObtainingDuplicateLayers() throws DigestException, InterruptedException, ExecutionException { - Mockito.when(executorService.submit(Mockito.any(PullBaseImageStep.class))) + when(executorService.submit(Mockito.any(PullBaseImageStep.class))) .thenReturn(Futures.immediateFuture(new ImagesAndRegistryClient(null, null))); // Pretend that a thread pulling base images returned some (meaningless) result. stepsRunner.pullBaseImages(progressDispatcherFactory); @@ -118,7 +126,7 @@ public void testObtainBaseImageLayers_skipObtainingDuplicateLayers() PreparedLayer preparedLayer1 = Mockito.mock(PreparedLayer.class); PreparedLayer preparedLayer2 = Mockito.mock(PreparedLayer.class); PreparedLayer preparedLayer3 = Mockito.mock(PreparedLayer.class); - Mockito.when(executorService.submit(Mockito.any(ObtainBaseImageLayerStep.class))) + when(executorService.submit(Mockito.any(ObtainBaseImageLayerStep.class))) .thenReturn(Futures.immediateFuture(preparedLayer1)) .thenReturn(Futures.immediateFuture(preparedLayer2)) .thenReturn(Futures.immediateFuture(preparedLayer3)); @@ -127,7 +135,7 @@ public void testObtainBaseImageLayers_skipObtainingDuplicateLayers() // 1. Should schedule two threads to obtain new layers. Image image = Mockito.mock(Image.class); - Mockito.when(image.getLayers()).thenReturn(ImmutableList.of(layer1, layer2)); + when(image.getLayers()).thenReturn(ImmutableList.of(layer1, layer2)); stepsRunner.obtainBaseImageLayers(image, true, preparedLayersCache, progressDispatcherFactory); Assert.assertEquals(2, preparedLayersCache.size()); // two new layers cached @@ -141,7 +149,7 @@ public void testObtainBaseImageLayers_skipObtainingDuplicateLayers() Assert.assertEquals(preparedLayer2, preparedLayersCache.get(digest2).get()); // 3. Another image with one duplicate layer. - Mockito.when(image.getLayers()).thenReturn(ImmutableList.of(layer3, layer2)); + when(image.getLayers()).thenReturn(ImmutableList.of(layer3, layer2)); stepsRunner.obtainBaseImageLayers(image, true, preparedLayersCache, progressDispatcherFactory); Assert.assertEquals(3, preparedLayersCache.size()); // one new layer cached Assert.assertEquals(preparedLayer1, preparedLayersCache.get(digest1).get()); @@ -156,7 +164,7 @@ public void testObtainBaseImageLayers_skipObtainingDuplicateLayers() @Test public void testIsImagePushed_skipExistingEnabledAndManifestPresent() { Optional> manifestResult = Mockito.mock(Optional.class); - Mockito.when(manifestResult.isPresent()).thenReturn(true); + when(manifestResult.isPresent()).thenReturn(true); System.setProperty(JibSystemProperties.SKIP_EXISTING_IMAGES, "true"); Assert.assertFalse(stepsRunner.isImagePushed(manifestResult)); @@ -174,8 +182,86 @@ public void testIsImagePushed_skipExistingImageDisabledAndManifestPresent() { public void testIsImagePushed_skipExistingImageEnabledAndManifestNotPresent() { Optional> manifestResult = Mockito.mock(Optional.class); System.setProperty(JibSystemProperties.SKIP_EXISTING_IMAGES, "true"); - Mockito.when(manifestResult.isPresent()).thenReturn(false); + when(manifestResult.isPresent()).thenReturn(false); Assert.assertTrue(stepsRunner.isImagePushed(manifestResult)); } + + @Test + public void testFetchBuildImageForLocalBuild_matchingOsAndArch() + throws ExecutionException, InterruptedException { + when(builtArm64AndLinuxImage.getArchitecture()).thenReturn("arm64"); + when(builtAmd64AndWindowsImage.getArchitecture()).thenReturn("amd64"); + when(builtAmd64AndWindowsImage.getOs()).thenReturn("windows"); + when(executorService.submit(Mockito.any(Callable.class))) + .thenReturn( + Futures.immediateFuture( + ImmutableMap.of( + baseImage1, + Futures.immediateFuture(builtArm64AndLinuxImage), + baseImage2, + Futures.immediateFuture(builtAmd64AndWindowsImage)))); + stepsRunner.buildImages(progressDispatcherFactory); + + Optional expectedImage = stepsRunner.fetchBuiltImageForLocalBuild("windows", "amd64"); + + assertThat(expectedImage.get().getOs()).isEqualTo("windows"); + assertThat(expectedImage.get().getArchitecture()).isEqualTo("amd64"); + } + + @Test + public void testFetchBuildImageForLocalBuild_differentOs() + throws ExecutionException, InterruptedException { + when(builtArm64AndLinuxImage.getArchitecture()).thenReturn("arm64"); + when(builtArm64AndLinuxImage.getOs()).thenReturn("linux"); + when(builtAmd64AndWindowsImage.getArchitecture()).thenReturn("amd64"); + when(executorService.submit(Mockito.any(Callable.class))) + .thenReturn( + Futures.immediateFuture( + ImmutableMap.of( + baseImage1, + Futures.immediateFuture(builtArm64AndLinuxImage), + baseImage2, + Futures.immediateFuture(builtAmd64AndWindowsImage)))); + stepsRunner.buildImages(progressDispatcherFactory); + + Optional expectedImage = stepsRunner.fetchBuiltImageForLocalBuild("os", "arm64"); + + assertThat(expectedImage.isPresent()).isFalse(); + } + + @Test + public void testFetchBuildImageForLocalBuild_differentArch() + throws ExecutionException, InterruptedException { + when(builtArm64AndLinuxImage.getArchitecture()).thenReturn("arm64"); + when(builtAmd64AndWindowsImage.getArchitecture()).thenReturn("amd64"); + when(executorService.submit(Mockito.any(Callable.class))) + .thenReturn( + Futures.immediateFuture( + ImmutableMap.of( + baseImage1, + Futures.immediateFuture(builtArm64AndLinuxImage), + baseImage2, + Futures.immediateFuture(builtAmd64AndWindowsImage)))); + stepsRunner.buildImages(progressDispatcherFactory); + + Optional expectedImage = stepsRunner.fetchBuiltImageForLocalBuild("linux", "arch"); + + assertThat(expectedImage.isPresent()).isFalse(); + } + + @Test + public void testNormalizeArchitecture_aarch64() { + assertThat(stepsRunner.normalizeArchitecture("aarch64")).isEqualTo("arm64"); + } + + @Test + public void testNormalizeArchitecture_x86_64() { + assertThat(stepsRunner.normalizeArchitecture("x86_64")).isEqualTo("amd64"); + } + + @Test + public void testNormalizeArchitecture_arm() { + assertThat(stepsRunner.normalizeArchitecture("arm")).isEqualTo("arm"); + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/CliDockerClientTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/CliDockerClientTest.java index 3e7cca5b4a..a88539f8c0 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/docker/CliDockerClientTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/docker/CliDockerClientTest.java @@ -16,8 +16,12 @@ package com.google.cloud.tools.jib.docker; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.api.DockerClient; +import com.google.cloud.tools.jib.api.DockerInfoDetails; import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.docker.CliDockerClient.DockerImageDetails; import com.google.cloud.tools.jib.image.ImageTarball; @@ -83,6 +87,60 @@ public void testIsDockerInstalled_pass() throws URISyntaxException { Paths.get(Resources.getResource("core/docker/emptyFile").toURI()))); } + @Test + public void testInfo() throws InterruptedException, IOException { + String dockerInfoJson = "{ \"OSType\": \"windows\"," + "\"Architecture\": \"arm64\"}"; + DockerClient testDockerClient = + new CliDockerClient( + subcommand -> { + assertThat(subcommand).containsExactly("info", "-f", "{{json .}}"); + return mockProcessBuilder; + }); + // Simulates stdout. + Mockito.when(mockProcess.getInputStream()) + .thenReturn(new ByteArrayInputStream(dockerInfoJson.getBytes())); + + DockerInfoDetails infoDetails = testDockerClient.info(); + assertThat(infoDetails.getArchitecture()).isEqualTo("arm64"); + assertThat(infoDetails.getOsType()).isEqualTo("windows"); + } + + @Test + public void testInfo_fail() throws InterruptedException { + DockerClient testDockerClient = + new CliDockerClient( + subcommand -> { + assertThat(subcommand).containsExactly("info", "-f", "{{json .}}"); + return mockProcessBuilder; + }); + Mockito.when(mockProcess.waitFor()).thenReturn(1); + Mockito.when(mockProcess.getErrorStream()) + .thenReturn(new ByteArrayInputStream("error".getBytes(StandardCharsets.UTF_8))); + + IOException exception = assertThrows(IOException.class, testDockerClient::info); + assertThat(exception) + .hasMessageThat() + .contains("'docker info' command failed with error: error"); + } + + @Test + public void testInfo_returnsUnknownKeys() throws InterruptedException, IOException { + String dockerInfoJson = "{ \"unknownOS\": \"windows\"," + "\"unknownArchitecture\": \"arm64\"}"; + DockerClient testDockerClient = + new CliDockerClient( + subcommand -> { + assertThat(subcommand).containsExactly("info", "-f", "{{json .}}"); + return mockProcessBuilder; + }); + Mockito.when(mockProcess.waitFor()).thenReturn(0); + Mockito.when(mockProcess.getInputStream()) + .thenReturn(new ByteArrayInputStream(dockerInfoJson.getBytes())); + + DockerInfoDetails infoDetails = testDockerClient.info(); + assertThat(infoDetails.getArchitecture()).isEmpty(); + assertThat(infoDetails.getOsType()).isEmpty(); + } + @Test public void testLoad() throws IOException, InterruptedException { DockerClient testDockerClient = diff --git a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java index 7c53878dc5..faf32e883e 100644 --- a/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java +++ b/jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java @@ -592,4 +592,14 @@ public void testCredHelperConfiguration() simpleTestProject, targetImage, "build-cred-helper.gradle")) .isEqualTo("Hello, world. \n1970-01-01T00:00:01Z\n"); } + + @Test + public void testToDockerDaemon_multiPlatform() + throws DigestException, IOException, InterruptedException { + String targetImage = "multiplatform:gradle" + System.nanoTime(); + assertThat( + JibRunHelper.buildToDockerDaemonAndRun( + simpleTestProject, targetImage, "build-multi-platform.gradle")) + .isEqualTo("Hello, world. \n1970-01-01T00:00:01Z\n"); + } } diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-multi-platform.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-multi-platform.gradle new file mode 100644 index 0000000000..5235164884 --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-multi-platform.gradle @@ -0,0 +1,34 @@ +plugins { + id 'java' + id 'com.google.cloud.tools.jib' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + implementation files('libs/dependency-1.0.0.jar') +} + +jib { + from { + image = 'eclipse-temurin:11' + platforms { + platform { + architecture = 'amd64' + os = 'linux' + } + platform { + architecture = 'arm64' + os = 'linux' + } + } + } + to { + image = System.getProperty('_TARGET_IMAGE') + } +} diff --git a/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java b/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java index 7d3be1d8c5..d41781d223 100644 --- a/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java +++ b/jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java @@ -275,4 +275,14 @@ public void testCredHelperConfigurationComplex() "Hello, world. \n1970-01-01T00:00:01Z\n", new Command("docker", "run", "--rm", targetImage).run()); } + + @Test + public void testMultiPlatform() + throws DigestException, VerificationException, IOException, InterruptedException { + String targetImage = "multiplatformproject:maven" + System.nanoTime(); + buildToDockerDaemon(simpleTestProject, targetImage, "pom-multiplatform-build.xml"); + Assert.assertEquals( + "Hello, world. \n1970-01-01T00:00:01Z\n", + new Command("docker", "run", "--rm", targetImage).run()); + } } diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-multiplatform-build.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-multiplatform-build.xml index 76a5966111..3269081f05 100644 --- a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-multiplatform-build.xml +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-multiplatform-build.xml @@ -41,7 +41,7 @@ ${jib-maven-plugin.version} - busybox@sha256:4f47c01fa91355af2865ac10fef5bf6ec9c7f42ad2321377c21e844427972977 + eclipse-temurin:11 arm64