Skip to content

Commit

Permalink
fix: allow pushing images with different arch/os to docker daemon (#4268
Browse files Browse the repository at this point in the history
)

* fix: allow pushing images with different arch/os to docker daemon; fix warning logging
  • Loading branch information
mpeddada1 authored Jun 25, 2024
1 parent b06b001 commit ff15988
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 88 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@

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;
Expand Down Expand Up @@ -60,6 +57,8 @@ public class JibIntegrationTest {

@Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();

private String imageToDelete;

private final String dockerHost =
System.getenv("DOCKER_IP") != null ? System.getenv("DOCKER_IP") : "localhost";

Expand Down Expand Up @@ -104,8 +103,11 @@ public void setUp() {
}

@After
public void tearDown() {
public void tearDown() throws IOException, InterruptedException {
System.clearProperty("sendCredentialsOverHttp");
if (imageToDelete != null) {
new Command("docker", "rmi", imageToDelete).run();
}
}

@Test
Expand All @@ -122,6 +124,7 @@ public void testBasic_helloWorld()
Assert.assertEquals("Hello World\n", pullAndRunBuiltImage(toImage));
Assert.assertEquals(
"Hello World\n", pullAndRunBuiltImage(toImage + "@" + jibContainer.getDigest()));
imageToDelete = toImage;
}

@Test
Expand All @@ -138,20 +141,21 @@ public void testBasic_dockerDaemonBaseImage()
Assert.assertEquals("Hello World\n", pullAndRunBuiltImage(toImage));
Assert.assertEquals(
"Hello World\n", pullAndRunBuiltImage(toImage + "@" + jibContainer.getDigest()));
imageToDelete = toImage;
}

@Test
public void testBasic_dockerDaemonBaseImageToDockerDaemon()
throws IOException, InterruptedException, InvalidImageReferenceException, ExecutionException,
RegistryException, CacheDirectoryCreationException {
String toImage = dockerHost + ":5000/docker-to-docker";
Jib.from(DockerDaemonImage.named(dockerHost + ":5000/busybox"))
.setEntrypoint("echo", "Hello World")
.containerize(
Containerizer.to(DockerDaemonImage.named(dockerHost + ":5000/docker-to-docker")));
.containerize(Containerizer.to(DockerDaemonImage.named(toImage)));

String output =
new Command("docker", "run", "--rm", dockerHost + ":5000/docker-to-docker").run();
String output = new Command("docker", "run", "--rm", toImage).run();
Assert.assertEquals("Hello World\n", output);
imageToDelete = toImage;
}

@Test
Expand All @@ -171,6 +175,7 @@ public void testBasic_tarBaseImage_dockerSavedCommand()
Assert.assertEquals("Hello World\n", pullAndRunBuiltImage(toImage));
Assert.assertEquals(
"Hello World\n", pullAndRunBuiltImage(toImage + "@" + jibContainer.getDigest()));
imageToDelete = toImage;
}

@Test
Expand Down Expand Up @@ -233,6 +238,7 @@ public void testBasic_tarBaseImage_jibImageToDockerDaemon()
Assert.assertEquals("Hello World\n", pullAndRunBuiltImage(toImage));
Assert.assertEquals(
"Hello World\n", pullAndRunBuiltImage(toImage + "@" + jibContainer.getDigest()));
imageToDelete = toImage;
}

@Test
Expand Down Expand Up @@ -311,59 +317,14 @@ public void testScratch_multiPlatform()
public void testBasic_jibImageToDockerDaemon()
throws IOException, InterruptedException, InvalidImageReferenceException, ExecutionException,
RegistryException, CacheDirectoryCreationException {
String toImage = dockerHost + ":5000/docker-to-docker";
Jib.from(DockerDaemonImage.named(dockerHost + ":5000/busybox"))
.setEntrypoint("echo", "Hello World")
.containerize(
Containerizer.to(DockerDaemonImage.named(dockerHost + ":5000/docker-to-docker")));
.containerize(Containerizer.to(DockerDaemonImage.named(toImage)));

String output =
new Command("docker", "run", "--rm", dockerHost + ":5000/docker-to-docker").run();
String output = new Command("docker", "run", "--rm", toImage).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");
imageToDelete = toImage;
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* 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 static com.google.common.truth.Truth.assertThat;

import com.google.cloud.tools.jib.Command;
import com.google.cloud.tools.jib.api.buildplan.Platform;
import com.google.cloud.tools.jib.registry.LocalRegistry;
import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
import org.junit.After;
import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Test;

public class JibMultiPlatformIntegrationTest {

@ClassRule public static final LocalRegistry localRegistry = new LocalRegistry(5000);

private final String dockerHost =
System.getenv("DOCKER_IP") != null ? System.getenv("DOCKER_IP") : "localhost";
private String imageToDelete;

@After
public void tearDown() throws IOException, InterruptedException {
System.clearProperty("sendCredentialsOverHttp");
if (imageToDelete != null) {
new Command("docker", "rmi", imageToDelete).run();
}
}

@Test
public void testBasic_jibImageToDockerDaemon_arm64()
throws IOException, InterruptedException, InvalidImageReferenceException, ExecutionException,
RegistryException, CacheDirectoryCreationException {
// Use arm64v8/busybox as base image.
String toImage = dockerHost + ":5000/docker-daemon-mismatched-arch";
Jib.from(
RegistryImage.named(
"busybox@sha256:eb427d855f82782c110b48b9a398556c629ce4951ae252c6f6751a136e194668"))
.containerize(Containerizer.to(DockerDaemonImage.named(toImage)));
String os =
new Command("docker", "inspect", toImage, "--format", "{{.Os}}").run().replace("\n", "");
String architecture =
new Command("docker", "inspect", toImage, "--format", "{{.Architecture}}")
.run()
.replace("\n", "");
assertThat(os).isEqualTo("linux");
assertThat(architecture).isEqualTo("arm64");
imageToDelete = toImage;
}

@Test
public void testBasicMultiPlatform_toDockerDaemon_pickFirstPlatformWhenNoMatchingImage()
throws IOException, InterruptedException, InvalidImageReferenceException,
CacheDirectoryCreationException, ExecutionException, RegistryException {
String toImage = dockerHost + ":5000/docker-daemon-multi-plat-mismatched-configs";
Jib.from(
RegistryImage.named(
"busybox@sha256:4f47c01fa91355af2865ac10fef5bf6ec9c7f42ad2321377c21e844427972977"))
.setPlatforms(ImmutableSet.of(new Platform("s390x", "linux"), new Platform("arm", "linux")))
.containerize(
Containerizer.to(DockerDaemonImage.named(toImage)).setAllowInsecureRegistries(true));
String os =
new Command("docker", "inspect", toImage, "--format", "{{.Os}}").run().replace("\n", "");
String architecture =
new Command("docker", "inspect", toImage, "--format", "{{.Architecture}}")
.run()
.replace("\n", "");
assertThat(os).isEqualTo("linux");
assertThat(architecture).isEqualTo("s390x");
imageToDelete = toImage;
}

@Test
public void testBasicMultiPlatform_toDockerDaemon()
throws IOException, InterruptedException, ExecutionException, RegistryException,
CacheDirectoryCreationException, InvalidImageReferenceException {
String toImage = dockerHost + ":5000/docker-daemon-multi-platform";
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(toImage)).setAllowInsecureRegistries(true));

String output = new Command("docker", "run", "--rm", toImage).run();
Assert.assertEquals("Hello World\n", output);
imageToDelete = toImage;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@
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.LogEvent;
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;
import com.google.cloud.tools.jib.builder.steps.PullBaseImageStep.ImagesAndRegistryClient;
import com.google.cloud.tools.jib.configuration.BuildContext;
import com.google.cloud.tools.jib.configuration.ImageConfiguration;
import com.google.cloud.tools.jib.event.EventHandlers;
import com.google.cloud.tools.jib.filesystem.TempDirectoryProvider;
import com.google.cloud.tools.jib.global.JibSystemProperties;
import com.google.cloud.tools.jib.image.Image;
Expand Down Expand Up @@ -53,7 +55,6 @@
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;

Expand All @@ -66,8 +67,6 @@
*/
public class StepsRunner {

private static final Logger LOGGER = Logger.getLogger(StepsRunner.class.getName());

/** Holds the individual step results. */
private static class StepResults {

Expand Down Expand Up @@ -624,14 +623,11 @@ private void loadDocker(
DockerInfoDetails dockerInfoDetails = dockerClient.info();
String osType = dockerInfoDetails.getOsType();
String architecture = normalizeArchitecture(dockerInfoDetails.getArchitecture());
Optional<Image> 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));
Image builtImage =
fetchBuiltImageForLocalBuild(
osType, architecture, buildContext.getEventHandlers());
return new LoadDockerStep(
buildContext, progressDispatcherFactory, dockerClient, builtImage.get())
buildContext, progressDispatcherFactory, dockerClient, builtImage)
.call();
});
}
Expand Down Expand Up @@ -669,21 +665,24 @@ String normalizeArchitecture(String architecture) {
}

@VisibleForTesting
Optional<Image> fetchBuiltImageForLocalBuild(String osType, String architecture)
Image fetchBuiltImageForLocalBuild(
String osType, String architecture, EventHandlers eventHandlers)
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<Image, Future<Image>> imageEntry :
results.baseImagesAndBuiltImages.get().entrySet()) {
Image image = imageEntry.getValue().get();
if (image.getArchitecture().equals(architecture) && image.getOs().equals(osType)) {
return Optional.of(image);
eventHandlers.dispatch(
LogEvent.warn(
String.format(
"Detected multi-platform configuration, only building image that matches the local Docker Engine's os and architecture (%s/%s) or "
+ "the first platform specified",
osType, architecture)));
for (Map.Entry<Image, Future<Image>> imageEntry :
results.baseImagesAndBuiltImages.get().entrySet()) {
Image image = imageEntry.getValue().get();
if (image.getArchitecture().equals(architecture) && image.getOs().equals(osType)) {
return image;
}
}
}
return Optional.empty();
return results.baseImagesAndBuiltImages.get().values().iterator().next().get();
}
}
Loading

0 comments on commit ff15988

Please sign in to comment.