Skip to content

Commit ec44330

Browse files
authored
feat: determine platform details from local docker installation for jibDockerBuild (#4249)
* feat: determine platform from local docker env when building to docker daemon
1 parent 8b13f0d commit ec44330

File tree

12 files changed

+378
-16
lines changed

12 files changed

+378
-16
lines changed

config/checkstyle/copyright-java.header

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
^/\*$
2-
^ \* Copyright 20(17|18|19|20|21|22|23) Google LLC\.$
2+
^ \* Copyright 20(17|18|19|20|21|22|23|24) Google LLC\.$
33
^ \*$
44
^ \* Licensed under the Apache License, Version 2\.0 \(the "License"\); you may not$
55
^ \* use this file except in compliance with the License\. You may obtain a copy of$

jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java

+62
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717
package com.google.cloud.tools.jib.api;
1818

19+
import static com.google.common.truth.Truth.assertThat;
20+
import static org.junit.Assert.assertThrows;
21+
1922
import com.google.cloud.tools.jib.Command;
2023
import com.google.cloud.tools.jib.api.buildplan.Platform;
2124
import com.google.cloud.tools.jib.blob.Blobs;
@@ -304,6 +307,65 @@ public void testScratch_multiPlatform()
304307
Assert.assertEquals("windows", platform2.getOs());
305308
}
306309

310+
@Test
311+
public void testBasic_jibImageToDockerDaemon()
312+
throws IOException, InterruptedException, InvalidImageReferenceException, ExecutionException,
313+
RegistryException, CacheDirectoryCreationException {
314+
Jib.from(DockerDaemonImage.named(dockerHost + ":5000/busybox"))
315+
.setEntrypoint("echo", "Hello World")
316+
.containerize(
317+
Containerizer.to(DockerDaemonImage.named(dockerHost + ":5000/docker-to-docker")));
318+
319+
String output =
320+
new Command("docker", "run", "--rm", dockerHost + ":5000/docker-to-docker").run();
321+
Assert.assertEquals("Hello World\n", output);
322+
}
323+
324+
@Test
325+
public void testBasicMultiPlatform_toDockerDaemon()
326+
throws IOException, InterruptedException, ExecutionException, RegistryException,
327+
CacheDirectoryCreationException, InvalidImageReferenceException {
328+
Jib.from(
329+
RegistryImage.named(
330+
"busybox@sha256:4f47c01fa91355af2865ac10fef5bf6ec9c7f42ad2321377c21e844427972977"))
331+
.setPlatforms(
332+
ImmutableSet.of(new Platform("arm64", "linux"), new Platform("amd64", "linux")))
333+
.setEntrypoint("echo", "Hello World")
334+
.containerize(
335+
Containerizer.to(
336+
DockerDaemonImage.named(dockerHost + ":5000/docker-daemon-multi-platform"))
337+
.setAllowInsecureRegistries(true));
338+
339+
String output =
340+
new Command("docker", "run", "--rm", dockerHost + ":5000/docker-daemon-multi-platform")
341+
.run();
342+
Assert.assertEquals("Hello World\n", output);
343+
}
344+
345+
@Test
346+
public void testBasicMultiPlatform_toDockerDaemon_noMatchingImage() {
347+
ExecutionException exception =
348+
assertThrows(
349+
ExecutionException.class,
350+
() ->
351+
Jib.from(
352+
RegistryImage.named(
353+
"busybox@sha256:4f47c01fa91355af2865ac10fef5bf6ec9c7f42ad2321377c21e844427972977"))
354+
.setPlatforms(
355+
ImmutableSet.of(
356+
new Platform("s390x", "linux"), new Platform("arm", "linux")))
357+
.setEntrypoint("echo", "Hello World")
358+
.containerize(
359+
Containerizer.to(
360+
DockerDaemonImage.named(
361+
dockerHost + ":5000/docker-daemon-multi-platform"))
362+
.setAllowInsecureRegistries(true)));
363+
assertThat(exception)
364+
.hasCauseThat()
365+
.hasMessageThat()
366+
.startsWith("The configured platforms don't match the Docker Engine's OS and architecture");
367+
}
368+
307369
@Test
308370
public void testDistroless_ociManifest()
309371
throws IOException, InterruptedException, ExecutionException, RegistryException,

jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerClient.java

+11
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,15 @@ void save(ImageReference imageReference, Path outputPath, Consumer<Long> written
6969
* @throws InterruptedException if the {@code docker inspect} process was interrupted
7070
*/
7171
ImageDetails inspect(ImageReference imageReference) throws IOException, InterruptedException;
72+
73+
/**
74+
* Gets docker info details of local docker installation.
75+
*
76+
* @return docker info details.
77+
* @throws IOException if an I/O exception occurs or {@code docker info} failed
78+
* @throws InterruptedException if the {@code docker info} process was interrupted
79+
*/
80+
default DockerInfoDetails info() throws IOException, InterruptedException {
81+
return new DockerInfoDetails();
82+
}
7283
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2024 Google LLC.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
17+
package com.google.cloud.tools.jib.api;
18+
19+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
20+
import com.fasterxml.jackson.annotation.JsonProperty;
21+
import com.google.cloud.tools.jib.json.JsonTemplate;
22+
23+
/** Contains docker info details outputted by {@code docker info}. */
24+
@JsonIgnoreProperties(ignoreUnknown = true)
25+
public class DockerInfoDetails implements JsonTemplate {
26+
27+
@JsonProperty("OSType")
28+
private String osType = "";
29+
30+
@JsonProperty("Architecture")
31+
private String architecture = "";
32+
33+
public String getOsType() {
34+
return osType;
35+
}
36+
37+
public String getArchitecture() {
38+
return architecture;
39+
}
40+
}

jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java

+46-7
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import com.google.cloud.tools.jib.api.DescriptorDigest;
2020
import com.google.cloud.tools.jib.api.DockerClient;
21+
import com.google.cloud.tools.jib.api.DockerInfoDetails;
2122
import com.google.cloud.tools.jib.blob.BlobDescriptor;
2223
import com.google.cloud.tools.jib.builder.ProgressEventDispatcher;
2324
import com.google.cloud.tools.jib.builder.steps.LocalBaseImageSteps.LocalImage;
@@ -52,6 +53,7 @@
5253
import java.util.concurrent.ExecutorService;
5354
import java.util.concurrent.Future;
5455
import java.util.function.Consumer;
56+
import java.util.logging.Logger;
5557
import java.util.stream.Collectors;
5658
import javax.annotation.Nullable;
5759

@@ -64,6 +66,8 @@
6466
*/
6567
public class StepsRunner {
6668

69+
private static final Logger LOGGER = Logger.getLogger(StepsRunner.class.getName());
70+
6771
/** Holds the individual step results. */
6872
private static class StepResults {
6973

@@ -413,7 +417,8 @@ private void buildAndCacheApplicationLayers(
413417
BuildAndCacheApplicationLayerStep.makeList(buildContext, progressDispatcherFactory));
414418
}
415419

416-
private void buildImages(ProgressEventDispatcher.Factory progressDispatcherFactory) {
420+
@VisibleForTesting
421+
void buildImages(ProgressEventDispatcher.Factory progressDispatcherFactory) {
417422
results.baseImagesAndBuiltImages =
418423
executorService.submit(
419424
() -> {
@@ -616,13 +621,17 @@ private void loadDocker(
616621
results.buildResult =
617622
executorService.submit(
618623
() -> {
619-
Verify.verify(
620-
results.baseImagesAndBuiltImages.get().size() == 1,
621-
"multi-platform image building not supported when pushing to Docker engine");
622-
Image builtImage =
623-
results.baseImagesAndBuiltImages.get().values().iterator().next().get();
624+
DockerInfoDetails dockerInfoDetails = dockerClient.info();
625+
String osType = dockerInfoDetails.getOsType();
626+
String architecture = normalizeArchitecture(dockerInfoDetails.getArchitecture());
627+
Optional<Image> builtImage = fetchBuiltImageForLocalBuild(osType, architecture);
628+
Preconditions.checkState(
629+
builtImage.isPresent(),
630+
String.format(
631+
"The configured platforms don't match the Docker Engine's OS and architecture (%s/%s)",
632+
osType, architecture));
624633
return new LoadDockerStep(
625-
buildContext, progressDispatcherFactory, dockerClient, builtImage)
634+
buildContext, progressDispatcherFactory, dockerClient, builtImage.get())
626635
.call();
627636
});
628637
}
@@ -647,4 +656,34 @@ private void writeTarFile(
647656
private <E> List<Future<E>> scheduleCallables(ImmutableList<? extends Callable<E>> callables) {
648657
return callables.stream().map(executorService::submit).collect(Collectors.toList());
649658
}
659+
660+
@VisibleForTesting
661+
String normalizeArchitecture(String architecture) {
662+
// Create mapping based on https://docs.docker.com/engine/install/#supported-platforms
663+
if (architecture.equals("x86_64")) {
664+
return "amd64";
665+
} else if (architecture.equals("aarch64")) {
666+
return "arm64";
667+
}
668+
return architecture;
669+
}
670+
671+
@VisibleForTesting
672+
Optional<Image> fetchBuiltImageForLocalBuild(String osType, String architecture)
673+
throws InterruptedException, ExecutionException {
674+
if (results.baseImagesAndBuiltImages.get().size() > 1) {
675+
LOGGER.warning(
676+
String.format(
677+
"Detected multi-platform configuration, only building the one that matches the local Docker Engine's os and architecture (%s/%s)",
678+
osType, architecture));
679+
}
680+
for (Map.Entry<Image, Future<Image>> imageEntry :
681+
results.baseImagesAndBuiltImages.get().entrySet()) {
682+
Image image = imageEntry.getValue().get();
683+
if (image.getArchitecture().equals(architecture) && image.getOs().equals(osType)) {
684+
return Optional.of(image);
685+
}
686+
}
687+
return Optional.empty();
688+
}
650689
}

jib-core/src/main/java/com/google/cloud/tools/jib/docker/CliDockerClient.java

+12
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.fasterxml.jackson.annotation.JsonProperty;
2121
import com.google.cloud.tools.jib.api.DescriptorDigest;
2222
import com.google.cloud.tools.jib.api.DockerClient;
23+
import com.google.cloud.tools.jib.api.DockerInfoDetails;
2324
import com.google.cloud.tools.jib.api.ImageDetails;
2425
import com.google.cloud.tools.jib.api.ImageReference;
2526
import com.google.cloud.tools.jib.http.NotifyingOutputStream;
@@ -184,6 +185,17 @@ public boolean supported(Map<String, String> parameters) {
184185
return true;
185186
}
186187

188+
@Override
189+
public DockerInfoDetails info() throws IOException, InterruptedException {
190+
// Runs 'docker info'.
191+
Process infoProcess = docker("info", "-f", "{{json .}}");
192+
if (infoProcess.waitFor() != 0) {
193+
throw new IOException(
194+
"'docker info' command failed with error: " + getStderrOutput(infoProcess));
195+
}
196+
return JsonTemplateMapper.readJson(infoProcess.getInputStream(), DockerInfoDetails.class);
197+
}
198+
187199
@Override
188200
public String load(ImageTarball imageTarball, Consumer<Long> writtenByteCountListener)
189201
throws InterruptedException, IOException {

0 commit comments

Comments
 (0)