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..1d4dd2cc35 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("arm64", "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/builder/steps/StepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java index a1dbea1762..3679e59316 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 @@ -39,7 +39,6 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; -import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; @@ -67,7 +66,7 @@ */ public class StepsRunner { - Logger logger = Logger.getLogger(StepResults.class.getName()); + private static final Logger LOGGER = Logger.getLogger(StepsRunner.class.getName()); /** Holds the individual step results. */ private static class StepResults { @@ -622,9 +621,17 @@ private void loadDocker( results.buildResult = executorService.submit( () -> { - Image builtImage = fetchBaseImageForLocalBuild(dockerClient); + DockerInfoDetails dockerInfoDetails = dockerClient.info(); + String osType = dockerInfoDetails.getOsType(); + String architecture = computeArchitecture(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(); }); } @@ -659,18 +666,19 @@ private String computeArchitecture(String architecture) { return architecture; } - private Image fetchBaseImageForLocalBuild(DockerClient dockerClient) - throws IOException, InterruptedException, ExecutionException { - DockerInfoDetails dockerInfoDetails = dockerClient.info(); - String osType = dockerInfoDetails.getOsType(); - String dockerArchitecture = computeArchitecture(dockerInfoDetails.getArchitecture()); + private Optional fetchBuiltImageForLocalBuild(String osType, String architecture) + throws InterruptedException, ExecutionException { + if (results.baseImagesAndBuiltImages.get().size() > 1) { + LOGGER.warning( + "Detected multi-platform configuration, only building the one that matches the local Docker Engine's os and architecture"); + } for (Map.Entry> imageEntry : results.baseImagesAndBuiltImages.get().entrySet()) { Image image = imageEntry.getValue().get(); - if (image.getArchitecture().equals(dockerArchitecture) && image.getOs().equals(osType)) { - return image; + if (image.getArchitecture().equals(architecture) && image.getOs().equals(osType)) { + return Optional.of(image); } } - return results.baseImagesAndBuiltImages.get().values().iterator().next().get(); + return Optional.empty(); } } 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..ac9b17ffa9 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,42 @@ 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 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..6a5159da61 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 @@ -39,6 +39,7 @@ import org.gradle.testkit.runner.UnexpectedBuildFailure; import org.hamcrest.CoreMatchers; import org.hamcrest.MatcherAssert; +import org.junit.Assert; import org.junit.Before; import org.junit.ClassRule; import org.junit.Rule; @@ -592,4 +593,28 @@ 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"); + } + + @Test + public void testToDockerDaemon_multiPlatform_invalid() { + String targetImage = "multiplatform:gradle" + System.nanoTime(); + UnexpectedBuildFailure exception = + assertThrows( + UnexpectedBuildFailure.class, + () -> + JibRunHelper.buildToDockerDaemonAndRun( + simpleTestProject, targetImage, "build-multi-platform-invalid.gradle")); + assertThat(exception) + .hasMessageThat() + .contains("The configured platforms don't match the Docker Engine's OS and architecture"); + } } diff --git a/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-multi-platform-invalid.gradle b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-multi-platform-invalid.gradle new file mode 100644 index 0000000000..fc85ed7b4a --- /dev/null +++ b/jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-multi-platform-invalid.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 = 'busybox@sha256:4f47c01fa91355af2865ac10fef5bf6ec9c7f42ad2321377c21e844427972977' + platforms { + platform { + architecture = 'arm' + os = 'linux' + } + platform { + architecture = 's390x' + os = 'linux' + } + } + } + to { + image = System.getProperty('_TARGET_IMAGE') + } +} 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..b3ae633ca9 --- /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:8-jdk-focal' + 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..d7c84abb77 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 @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.maven; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import com.google.cloud.tools.jib.Command; import java.io.IOException; @@ -275,4 +276,29 @@ 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()); + } + + @Test + public void testMultiPlatform_invalidPlatforms() + throws DigestException, VerificationException, IOException, InterruptedException { + String targetImage = "multiplatformproject:maven" + System.nanoTime(); + VerificationException exception = + assertThrows( + VerificationException.class, + () -> + buildToDockerDaemon( + simpleTestProject, targetImage, "pom-multiplatform-invalid-platforms.xml")); + assertThat(exception) + .hasMessageThat() + .contains("The configured platforms don't match the Docker Engine's OS and architecture"); + } } 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 diff --git a/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-multiplatform-invalid-platforms.xml b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-multiplatform-invalid-platforms.xml new file mode 100644 index 0000000000..32a2a81c17 --- /dev/null +++ b/jib-maven-plugin/src/test/resources/maven/projects/simple/pom-multiplatform-invalid-platforms.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + com.test + my-artifact-id + 1 + + + UTF-8 + UTF-8 + @@PluginVersion@@ + + + + + com.test + dependency + 1.0.0 + system + ${project.basedir}/libs/dependency-1.0.0.jar + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + 1.8 + 1.8 + + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + busybox@sha256:4f47c01fa91355af2865ac10fef5bf6ec9c7f42ad2321377c21e844427972977 + + + arm + linux + + + s390x + linux + + + + + ${_TARGET_IMAGE} + + latest + another + + + + + + +