diff --git a/config/checkstyle/copyright-java.header b/config/checkstyle/copyright-java.header index d15ad5912ea..e60211cae38 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/examples/dropwizard/pom.xml b/examples/dropwizard/pom.xml index ab09f99b0a8..1a0780ecde6 100644 --- a/examples/dropwizard/pom.xml +++ b/examples/dropwizard/pom.xml @@ -26,7 +26,7 @@ 1.5.0 /app - 3.4.2 + 3.4.3 diff --git a/examples/helloworld/build.gradle b/examples/helloworld/build.gradle index c6c2225278f..5ffb87042c0 100644 --- a/examples/helloworld/build.gradle +++ b/examples/helloworld/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '3.4.2' + id 'com.google.cloud.tools.jib' version '3.4.3' } sourceCompatibility = 1.8 diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index 924c67a1364..b5a5bf3479c 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 3.4.2 + 3.4.3 3.8.0 diff --git a/examples/java-agent/build.gradle b/examples/java-agent/build.gradle index ae272cdffde..1f24cc34581 100644 --- a/examples/java-agent/build.gradle +++ b/examples/java-agent/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.google.cloud.tools.jib' version '3.4.2' + id 'com.google.cloud.tools.jib' version '3.4.3' id 'de.undercouch.download' version '4.0.0' id 'com.gorylenko.gradle-git-properties' version '2.2.0' } diff --git a/examples/java-agent/pom.xml b/examples/java-agent/pom.xml index 7fc43504e53..a4d476488b2 100644 --- a/examples/java-agent/pom.xml +++ b/examples/java-agent/pom.xml @@ -9,7 +9,7 @@ UTF-8 - 3.4.2 + 3.4.3 3.8.0 1.4.2 3.0.1 diff --git a/examples/ktor/build.gradle.kts b/examples/ktor/build.gradle.kts index 963c3fd4188..128daeab3cc 100644 --- a/examples/ktor/build.gradle.kts +++ b/examples/ktor/build.gradle.kts @@ -1,7 +1,7 @@ plugins { application kotlin("jvm") version "1.3.10" - id("com.google.cloud.tools.jib") version "3.4.2" + id("com.google.cloud.tools.jib") version "3.4.3" } group = "example" diff --git a/examples/micronaut/build.gradle b/examples/micronaut/build.gradle index c0f0842bbdd..fe153ba0a98 100644 --- a/examples/micronaut/build.gradle +++ b/examples/micronaut/build.gradle @@ -2,7 +2,7 @@ plugins { id "groovy" id "com.github.johnrengelman.shadow" version "5.2.0" id "application" - id 'com.google.cloud.tools.jib' version '3.4.2' + id 'com.google.cloud.tools.jib' version '3.4.3' } version "0.1" diff --git a/examples/multi-module/build.gradle b/examples/multi-module/build.gradle index ea07fc421f0..f79edd79874 100644 --- a/examples/multi-module/build.gradle +++ b/examples/multi-module/build.gradle @@ -2,5 +2,5 @@ plugins { id 'org.springframework.boot' version '2.0.3.RELEASE' apply false id 'io.spring.dependency-management' version '1.0.6.RELEASE' apply false - id 'com.google.cloud.tools.jib' version '3.4.2' apply false + id 'com.google.cloud.tools.jib' version '3.4.3' apply false } diff --git a/examples/multi-module/pom.xml b/examples/multi-module/pom.xml index 135e9a8c5e3..38351f0b50d 100644 --- a/examples/multi-module/pom.xml +++ b/examples/multi-module/pom.xml @@ -41,7 +41,7 @@ com.google.cloud.tools jib-maven-plugin - 3.4.2 + 3.4.3 diff --git a/examples/spring-boot/build.gradle b/examples/spring-boot/build.gradle index 5ce5d12ef15..fbd2a775c9a 100644 --- a/examples/spring-boot/build.gradle +++ b/examples/spring-boot/build.gradle @@ -4,7 +4,7 @@ plugins { id 'idea' id 'org.springframework.boot' version '2.1.6.RELEASE' id 'io.spring.dependency-management' version '1.0.6.RELEASE' - id 'com.google.cloud.tools.jib' version '3.4.2' + id 'com.google.cloud.tools.jib' version '3.4.3' } repositories { diff --git a/examples/spring-boot/pom.xml b/examples/spring-boot/pom.xml index 6cf906c0da1..e59baa83776 100644 --- a/examples/spring-boot/pom.xml +++ b/examples/spring-boot/pom.xml @@ -29,7 +29,7 @@ com.google.cloud.tools jib-maven-plugin - 3.4.2 + 3.4.3 diff --git a/examples/vertx/build.gradle b/examples/vertx/build.gradle index 956df52ea2f..02717a203b4 100644 --- a/examples/vertx/build.gradle +++ b/examples/vertx/build.gradle @@ -1,6 +1,6 @@ plugins { id 'io.vertx.vertx-plugin' version '0.1.0' - id 'com.google.cloud.tools.jib' version '3.4.2' + id 'com.google.cloud.tools.jib' version '3.4.3' } repositories { diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index b0f87625669..ab9f84acfd2 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -9,6 +9,12 @@ All notable changes to this project will be documented in this file. ### Fixed +## 0.27.1 + +### Fixed +- fix: When building to the local docker daemon with multiple platforms configured, Jib will now automatically select the image that matches the OS type and architecture of the local Docker environment. ([#4249](https://github.com/GoogleContainerTools/jib/pull/4249)) + + ## 0.27.0 ### Changed diff --git a/jib-core/README.md b/jib-core/README.md index cc6baf2d245..82637ecd91a 100644 --- a/jib-core/README.md +++ b/jib-core/README.md @@ -22,7 +22,7 @@ Add Jib Core as a dependency using Maven: com.google.cloud.tools jib-core - 0.27.0 + 0.27.1 ``` @@ -30,7 +30,7 @@ Add Jib Core as a dependency using Gradle: ```groovy dependencies { - compile 'com.google.cloud.tools:jib-core:0.27.0' + compile 'com.google.cloud.tools:jib-core:0.27.1' } ``` diff --git a/jib-core/gradle.properties b/jib-core/gradle.properties index 86ae6f81f55..67f1f13bde4 100644 --- a/jib-core/gradle.properties +++ b/jib-core/gradle.properties @@ -1 +1 @@ -version = 0.27.1-SNAPSHOT +version = 0.27.2-SNAPSHOT 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 a9916708c35..4e49d1e12d3 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 50ce2f463fc..7e08a794950 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 00000000000..fa486c40f95 --- /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 46c63bfc9de..be760a13eb8 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 7c9c23cd9fa..1842a79de3e 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 e8a60de2d57..9436c7df4a0 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 3e7cca5b4ac..a88539f8c05 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/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index c91b85fee8b..1d519865818 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -9,6 +9,11 @@ All notable changes to this project will be documented in this file. ### Fixed +## 3.4.3 + +### Fixed +- fix: When building to the local docker daemon with multiple platforms configured, Jib will now automatically select the image that matches the OS type and architecture of the local Docker environment. ([#4249](https://github.com/GoogleContainerTools/jib/pull/4249)) + ## 3.4.2 ### Changed diff --git a/jib-gradle-plugin/README.md b/jib-gradle-plugin/README.md index 6bdf8aca965..5c4001018d3 100644 --- a/jib-gradle-plugin/README.md +++ b/jib-gradle-plugin/README.md @@ -53,7 +53,7 @@ In your Gradle Java project, add the plugin to your `build.gradle`: ```groovy plugins { - id 'com.google.cloud.tools.jib' version '3.4.2' + id 'com.google.cloud.tools.jib' version '3.4.3' } ``` diff --git a/jib-gradle-plugin/gradle.properties b/jib-gradle-plugin/gradle.properties index 22c5f533eed..882261a1db3 100644 --- a/jib-gradle-plugin/gradle.properties +++ b/jib-gradle-plugin/gradle.properties @@ -1 +1 @@ -version = 3.4.3-SNAPSHOT +version = 3.4.4-SNAPSHOT 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 7c53878dc50..faf32e883ed 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 00000000000..52351648848 --- /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/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 77a8f260457..2f4ca92c14e 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -9,6 +9,11 @@ All notable changes to this project will be documented in this file. ### Fixed +## 3.4.3 + +### Fixed +- fix: When building to the local docker daemon with multiple platforms configured, Jib will now automatically select the image that matches the OS type and architecture of the local Docker environment. ([#4249](https://github.com/GoogleContainerTools/jib/pull/4249)) + ## 3.4.2 ### Changed diff --git a/jib-maven-plugin/README.md b/jib-maven-plugin/README.md index 37d347355aa..11046482ff9 100644 --- a/jib-maven-plugin/README.md +++ b/jib-maven-plugin/README.md @@ -48,7 +48,7 @@ For information about the project, see the [Jib project README](../README.md). You can containerize your application easily with one command: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:3.4.1:build -Dimage= +mvn compile com.google.cloud.tools:jib-maven-plugin:3.4.3:build -Dimage= ``` This builds and pushes a container image for your application to a container registry. *If you encounter authentication issues, see [Authentication Methods](#authentication-methods).* @@ -56,7 +56,7 @@ This builds and pushes a container image for your application to a container reg To build to a Docker daemon, use: ```shell -mvn compile com.google.cloud.tools:jib-maven-plugin:3.4.1:dockerBuild +mvn compile com.google.cloud.tools:jib-maven-plugin:3.4.3:dockerBuild ``` If you would like to set up Jib as part of your Maven build, follow the guide below. @@ -74,7 +74,7 @@ In your Maven Java project, add the plugin to your `pom.xml`: com.google.cloud.tools jib-maven-plugin - 3.4.1 + 3.4.3 myimage diff --git a/jib-maven-plugin/gradle.properties b/jib-maven-plugin/gradle.properties index 22c5f533eed..882261a1db3 100644 --- a/jib-maven-plugin/gradle.properties +++ b/jib-maven-plugin/gradle.properties @@ -1 +1 @@ -version = 3.4.3-SNAPSHOT +version = 3.4.4-SNAPSHOT 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 7d3be1d8c51..d41781d223e 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 76a5966111d..3269081f05d 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