Add imagePlatform option for image building

An `imagePlatform` option for the Maven and Gradle image-building
goal/task can be used to specify the os/architecture of any
builder, run, and buildpack images that are pulled during image
building.

Closes gh-40944
This commit is contained in:
Scott Frederick
2024-07-30 12:54:14 -05:00
parent 67bae524b2
commit dfab18c965
45 changed files with 1373 additions and 275 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,6 +20,8 @@ import java.util.stream.IntStream;
import org.junit.jupiter.api.Test;
import org.springframework.boot.buildpack.platform.docker.type.ApiVersion;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;

View File

@@ -39,6 +39,7 @@ import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.boot.buildpack.platform.docker.type.Binding;
import org.springframework.boot.buildpack.platform.docker.type.ImageName;
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.io.Owner;
import org.springframework.boot.buildpack.platform.io.TarArchive;
@@ -392,6 +393,13 @@ class BuildRequestTests {
assertThat(withAppDir.getSecurityOptions()).containsExactly("label=user:USER", "label=role:ROLE");
}
@Test
void withPlatformSetsPlatform() throws Exception {
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));
BuildRequest withAppDir = request.withImagePlatform("linux/arm64");
assertThat(withAppDir.getImagePlatform()).isEqualTo(ImagePlatform.of("linux/arm64"));
}
private void hasExpectedJarContent(TarArchive archive) {
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

View File

@@ -36,6 +36,7 @@ import org.springframework.boot.buildpack.platform.docker.type.ContainerReferenc
import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus;
import org.springframework.boot.buildpack.platform.docker.type.Image;
import org.springframework.boot.buildpack.platform.docker.type.ImageArchive;
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.io.TarArchive;
@@ -86,9 +87,12 @@ class BuilderTests {
DockerApi docker = mockDockerApi();
Image builderImage = loadImage("image.json");
Image runImage = loadImage("run-image.json");
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), isNull()))
.willAnswer(withPulledImage(builderImage));
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)),
any(), isNull()))
.willAnswer(withPulledImage(runImage));
Builder builder = new Builder(BuildLog.to(out), docker, null);
BuildRequest request = getTestRequest();
@@ -97,9 +101,10 @@ class BuilderTests {
assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
ArgumentCaptor<ImageArchive> archive = ArgumentCaptor.forClass(ImageArchive.class);
then(docker.image()).should()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), isNull());
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), isNull());
then(docker.image()).should()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull());
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)),
any(), isNull());
then(docker.image()).should().load(archive.capture(), any());
then(docker.image()).should().remove(archive.getValue().getTag(), true);
then(docker.image()).shouldHaveNoMoreInteractions();
@@ -115,12 +120,12 @@ class BuilderTests {
.withBuilderRegistryTokenAuthentication("builder token")
.withPublishRegistryTokenAuthentication("publish token");
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(),
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(),
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())))
.willAnswer(withPulledImage(builderImage));
given(docker.image()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(),
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())))
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)),
any(), eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())))
.willAnswer(withPulledImage(runImage));
Builder builder = new Builder(BuildLog.to(out), docker, dockerConfiguration);
BuildRequest request = getTestRequest().withPublish(true);
@@ -129,11 +134,11 @@ class BuilderTests {
assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
ArgumentCaptor<ImageArchive> archive = ArgumentCaptor.forClass(ImageArchive.class);
then(docker.image()).should()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(),
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(),
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()));
then(docker.image()).should()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(),
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()));
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)),
any(), eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()));
then(docker.image()).should()
.push(eq(request.getName()), any(),
eq(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()));
@@ -148,9 +153,12 @@ class BuilderTests {
DockerApi docker = mockDockerApi();
Image builderImage = loadImage("image-with-no-run-image-tag.json");
Image runImage = loadImage("run-image.json");
given(docker.image().pull(eq(ImageReference.of("gcr.io/paketo-buildpacks/builder:latest")), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of("gcr.io/paketo-buildpacks/builder:latest")), isNull(), any(), isNull()))
.willAnswer(withPulledImage(builderImage));
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:latest")), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:latest")), eq(ImagePlatform.from(builderImage)),
any(), isNull()))
.willAnswer(withPulledImage(runImage));
Builder builder = new Builder(BuildLog.to(out), docker, null);
BuildRequest request = getTestRequest().withBuilder(ImageReference.of("gcr.io/paketo-buildpacks/builder"));
@@ -168,12 +176,13 @@ class BuilderTests {
DockerApi docker = mockDockerApi();
Image builderImage = loadImage("image-with-run-image-digest.json");
Image runImage = loadImage("run-image.json");
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), isNull()))
.willAnswer(withPulledImage(builderImage));
given(docker.image()
.pull(eq(ImageReference
.of("docker.io/cloudfoundry/run@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d")),
any(), isNull()))
eq(ImagePlatform.from(builderImage)), any(), isNull()))
.willAnswer(withPulledImage(runImage));
Builder builder = new Builder(BuildLog.to(out), docker, null);
BuildRequest request = getTestRequest();
@@ -191,9 +200,12 @@ class BuilderTests {
DockerApi docker = mockDockerApi();
Image builderImage = loadImage("image-with-empty-stack.json");
Image runImage = loadImage("run-image.json");
given(docker.image().pull(eq(ImageReference.of("gcr.io/paketo-buildpacks/builder:latest")), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of("gcr.io/paketo-buildpacks/builder:latest")), isNull(), any(), isNull()))
.willAnswer(withPulledImage(builderImage));
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)),
any(), isNull()))
.willAnswer(withPulledImage(runImage));
Builder builder = new Builder(BuildLog.to(out), docker, null);
BuildRequest request = getTestRequest().withBuilder(ImageReference.of("gcr.io/paketo-buildpacks/builder"));
@@ -211,9 +223,12 @@ class BuilderTests {
DockerApi docker = mockDockerApi();
Image builderImage = loadImage("image.json");
Image runImage = loadImage("run-image.json");
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), isNull()))
.willAnswer(withPulledImage(builderImage));
given(docker.image().pull(eq(ImageReference.of("example.com/custom/run:latest")), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of("example.com/custom/run:latest")), eq(ImagePlatform.from(builderImage)), any(),
isNull()))
.willAnswer(withPulledImage(runImage));
Builder builder = new Builder(BuildLog.to(out), docker, null);
BuildRequest request = getTestRequest().withRunImage(ImageReference.of("example.com/custom/run:latest"));
@@ -231,9 +246,12 @@ class BuilderTests {
DockerApi docker = mockDockerApi();
Image builderImage = loadImage("image.json");
Image runImage = loadImage("run-image.json");
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), isNull()))
.willAnswer(withPulledImage(builderImage));
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)),
any(), isNull()))
.willAnswer(withPulledImage(runImage));
given(docker.image().inspect(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF))))
.willReturn(builderImage);
@@ -247,7 +265,7 @@ class BuilderTests {
ArgumentCaptor<ImageArchive> archive = ArgumentCaptor.forClass(ImageArchive.class);
then(docker.image()).should().load(archive.capture(), any());
then(docker.image()).should().remove(archive.getValue().getTag(), true);
then(docker.image()).should(never()).pull(any(), any());
then(docker.image()).should(never()).pull(any(), any(), any());
then(docker.image()).should(times(2)).inspect(any());
}
@@ -257,9 +275,12 @@ class BuilderTests {
DockerApi docker = mockDockerApi();
Image builderImage = loadImage("image.json");
Image runImage = loadImage("run-image.json");
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), isNull()))
.willAnswer(withPulledImage(builderImage));
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)),
any(), isNull()))
.willAnswer(withPulledImage(runImage));
given(docker.image().inspect(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF))))
.willReturn(builderImage);
@@ -273,7 +294,7 @@ class BuilderTests {
ArgumentCaptor<ImageArchive> archive = ArgumentCaptor.forClass(ImageArchive.class);
then(docker.image()).should().load(archive.capture(), any());
then(docker.image()).should().remove(archive.getValue().getTag(), true);
then(docker.image()).should(times(2)).pull(any(), any(), isNull());
then(docker.image()).should(times(2)).pull(any(), any(), any(), isNull());
then(docker.image()).should(never()).inspect(any());
}
@@ -283,9 +304,12 @@ class BuilderTests {
DockerApi docker = mockDockerApi();
Image builderImage = loadImage("image.json");
Image runImage = loadImage("run-image.json");
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), isNull()))
.willAnswer(withPulledImage(builderImage));
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)),
any(), isNull()))
.willAnswer(withPulledImage(runImage));
given(docker.image().inspect(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF))))
.willThrow(
@@ -304,7 +328,7 @@ class BuilderTests {
then(docker.image()).should().load(archive.capture(), any());
then(docker.image()).should().remove(archive.getValue().getTag(), true);
then(docker.image()).should(times(2)).inspect(any());
then(docker.image()).should(times(2)).pull(any(), any(), isNull());
then(docker.image()).should(times(2)).pull(any(), any(), any(), isNull());
}
@Test
@@ -313,9 +337,12 @@ class BuilderTests {
DockerApi docker = mockDockerApi();
Image builderImage = loadImage("image.json");
Image runImage = loadImage("run-image.json");
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), isNull()))
.willAnswer(withPulledImage(builderImage));
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)),
any(), isNull()))
.willAnswer(withPulledImage(runImage));
Builder builder = new Builder(BuildLog.to(out), docker, null);
BuildRequest request = getTestRequest().withTags(ImageReference.of("my-application:1.2.3"));
@@ -339,12 +366,12 @@ class BuilderTests {
.withBuilderRegistryTokenAuthentication("builder token")
.withPublishRegistryTokenAuthentication("publish token");
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(),
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(),
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())))
.willAnswer(withPulledImage(builderImage));
given(docker.image()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(),
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())))
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)),
any(), eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())))
.willAnswer(withPulledImage(runImage));
Builder builder = new Builder(BuildLog.to(out), docker, dockerConfiguration);
BuildRequest request = getTestRequest().withPublish(true).withTags(ImageReference.of("my-application:1.2.3"));
@@ -354,11 +381,11 @@ class BuilderTests {
assertThat(out.toString()).contains("Successfully created image tag 'docker.io/library/my-application:1.2.3'");
then(docker.image()).should()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(),
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(),
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()));
then(docker.image()).should()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(),
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()));
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)),
any(), eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()));
then(docker.image()).should()
.push(eq(request.getName()), any(),
eq(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()));
@@ -372,15 +399,46 @@ class BuilderTests {
then(docker.image()).shouldHaveNoMoreInteractions();
}
@Test
void buildInvokesBuilderWithPlatform() throws Exception {
TestPrintStream out = new TestPrintStream();
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
DockerApi docker = mockDockerApi(platform);
Image builderImage = loadImage("image-with-platform.json");
Image runImage = loadImage("run-image-with-platform.json");
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), eq(platform), any(), isNull()))
.willAnswer(withPulledImage(builderImage));
given(docker.image()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(platform), any(), isNull()))
.willAnswer(withPulledImage(runImage));
Builder builder = new Builder(BuildLog.to(out), docker, null);
BuildRequest request = getTestRequest().withImagePlatform("linux/arm64/v1");
builder.build(request);
assertThat(out.toString()).contains("Running creator");
assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
ArgumentCaptor<ImageArchive> archive = ArgumentCaptor.forClass(ImageArchive.class);
then(docker.image()).should()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), eq(platform), any(), isNull());
then(docker.image()).should()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(platform), any(), isNull());
then(docker.image()).should().load(archive.capture(), any());
then(docker.image()).should().remove(archive.getValue().getTag(), true);
then(docker.image()).shouldHaveNoMoreInteractions();
}
@Test
void buildWhenStackIdDoesNotMatchThrowsException() throws Exception {
TestPrintStream out = new TestPrintStream();
DockerApi docker = mockDockerApi();
Image builderImage = loadImage("image.json");
Image runImage = loadImage("run-image-with-bad-stack.json");
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), isNull()))
.willAnswer(withPulledImage(builderImage));
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)),
any(), isNull()))
.willAnswer(withPulledImage(runImage));
Builder builder = new Builder(BuildLog.to(out), docker, null);
BuildRequest request = getTestRequest();
@@ -395,9 +453,12 @@ class BuilderTests {
DockerApi docker = mockDockerApiLifecycleError();
Image builderImage = loadImage("image.json");
Image runImage = loadImage("run-image.json");
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), isNull()))
.willAnswer(withPulledImage(builderImage));
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)),
any(), isNull()))
.willAnswer(withPulledImage(runImage));
Builder builder = new Builder(BuildLog.to(out), docker, null);
BuildRequest request = getTestRequest();
@@ -413,7 +474,7 @@ class BuilderTests {
DockerConfiguration dockerConfiguration = new DockerConfiguration()
.withBuilderRegistryTokenAuthentication("builder token");
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(),
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), any(),
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())))
.willAnswer(withPulledImage(builderImage));
Builder builder = new Builder(BuildLog.to(out), docker, dockerConfiguration);
@@ -431,7 +492,7 @@ class BuilderTests {
DockerConfiguration dockerConfiguration = new DockerConfiguration()
.withBuilderRegistryTokenAuthentication("builder token");
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(),
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), any(),
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())))
.willAnswer(withPulledImage(builderImage));
Builder builder = new Builder(BuildLog.to(out), docker, dockerConfiguration);
@@ -447,9 +508,10 @@ class BuilderTests {
DockerApi docker = mockDockerApiLifecycleError();
Image builderImage = loadImage("image.json");
Image runImage = loadImage("run-image.json");
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), any(), isNull()))
.willAnswer(withPulledImage(builderImage));
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull()))
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), any(), isNull()))
.willAnswer(withPulledImage(runImage));
Builder builder = new Builder(BuildLog.to(out), docker, null);
BuildpackReference reference = BuildpackReference.of("urn:cnb:builder:example/buildpack@1.2.3");
@@ -460,9 +522,13 @@ class BuilderTests {
}
private DockerApi mockDockerApi() throws IOException {
return mockDockerApi(null);
}
private DockerApi mockDockerApi(ImagePlatform platform) throws IOException {
ContainerApi containerApi = mock(ContainerApi.class);
ContainerReference reference = ContainerReference.of("container-ref");
given(containerApi.create(any(), any())).willReturn(reference);
given(containerApi.create(any(), eq(platform), any())).willReturn(reference);
given(containerApi.wait(eq(reference))).willReturn(ContainerStatus.of(0, null));
ImageApi imageApi = mock(ImageApi.class);
VolumeApi volumeApi = mock(VolumeApi.class);
@@ -476,7 +542,7 @@ class BuilderTests {
private DockerApi mockDockerApiLifecycleError() throws IOException {
ContainerApi containerApi = mock(ContainerApi.class);
ContainerReference reference = ContainerReference.of("container-ref");
given(containerApi.create(any(), any())).willReturn(reference);
given(containerApi.create(any(), isNull(), any())).willReturn(reference);
given(containerApi.wait(eq(reference))).willReturn(ContainerStatus.of(9, null));
ImageApi imageApi = mock(ImageApi.class);
VolumeApi volumeApi = mock(VolumeApi.class);
@@ -499,7 +565,7 @@ class BuilderTests {
private Answer<Image> withPulledImage(Image image) {
return (invocation) -> {
TotalProgressPullListener listener = invocation.getArgument(1, TotalProgressPullListener.class);
TotalProgressPullListener listener = invocation.getArgument(2, TotalProgressPullListener.class);
listener.onStart();
listener.onFinish();
return image;

View File

@@ -49,6 +49,7 @@ import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig;
import org.springframework.boot.buildpack.platform.docker.type.ContainerContent;
import org.springframework.boot.buildpack.platform.docker.type.ContainerReference;
import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus;
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.docker.type.VolumeName;
import org.springframework.boot.buildpack.platform.io.IOConsumer;
@@ -62,6 +63,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
@@ -92,8 +94,8 @@ class LifecycleTests {
@ParameterizedTest
@BooleanValueSource
void executeExecutesPhases(boolean trustBuilder) throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
createLifecycle(trustBuilder).execute();
if (trustBuilder) {
@@ -111,8 +113,8 @@ class LifecycleTests {
@Test
void executeWithBindingsExecutesPhases() throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
BuildRequest request = getTestRequest(true).withBindings(Binding.of("/host/src/path:/container/dest/path:ro"),
Binding.of("volume-name:/container/volume/path:rw"));
@@ -123,8 +125,8 @@ class LifecycleTests {
@Test
void executeExecutesPhasesWithPlatformApi03() throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
createLifecycle(true, "builder-metadata-platform-api-0.3.json").execute();
assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-platform-api-0.3.json"));
@@ -134,8 +136,8 @@ class LifecycleTests {
@ParameterizedTest
@BooleanValueSource
void executeOnlyUploadsContentOnce(boolean trustBuilder) throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
createLifecycle(trustBuilder).execute();
assertThat(this.content).hasSize(1);
@@ -144,8 +146,8 @@ class LifecycleTests {
@ParameterizedTest
@BooleanValueSource
void executeWhenAlreadyRunThrowsException(boolean trustBuilder) throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
Lifecycle lifecycle = createLifecycle(trustBuilder);
lifecycle.execute();
@@ -156,8 +158,8 @@ class LifecycleTests {
@ParameterizedTest
@BooleanValueSource
void executeWhenBuilderReturnsErrorThrowsException(boolean trustBuilder) throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(9, null));
assertThatExceptionOfType(BuilderException.class).isThrownBy(() -> createLifecycle(trustBuilder).execute())
.withMessage(
@@ -167,8 +169,8 @@ class LifecycleTests {
@ParameterizedTest
@BooleanValueSource
void executeWhenCleanCacheClearsCache(boolean trustBuilder) throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
BuildRequest request = getTestRequest(trustBuilder).withCleanCache(true);
createLifecycle(request).execute();
@@ -188,8 +190,8 @@ class LifecycleTests {
@Test
void executeWhenPlatformApiNotSupportedThrowsException() throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
assertThatIllegalStateException()
.isThrownBy(() -> createLifecycle(true, "builder-metadata-unsupported-api.json").execute())
@@ -198,8 +200,8 @@ class LifecycleTests {
@Test
void executeWhenMultiplePlatformApisNotSupportedThrowsException() throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
assertThatIllegalStateException()
.isThrownBy(() -> createLifecycle(true, "builder-metadata-unsupported-apis.json").execute())
@@ -209,8 +211,8 @@ class LifecycleTests {
@ParameterizedTest
@BooleanValueSource
void executeWhenMultiplePlatformApisSupportedExecutesPhase(boolean trustBuilder) throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
createLifecycle(trustBuilder, "builder-metadata-supported-apis.json").execute();
if (trustBuilder) {
@@ -234,8 +236,8 @@ class LifecycleTests {
@Test
void executeWithNetworkExecutesPhases() throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
BuildRequest request = getTestRequest(true).withNetwork("test");
createLifecycle(request).execute();
@@ -246,8 +248,8 @@ class LifecycleTests {
@ParameterizedTest
@BooleanValueSource
void executeWithCacheVolumeNamesExecutesPhases(boolean trustBuilder) throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
BuildRequest request = getTestRequest(trustBuilder).withBuildWorkspace(Cache.volume("work-volume"))
.withBuildCache(Cache.volume("build-volume"))
@@ -269,8 +271,8 @@ class LifecycleTests {
@ParameterizedTest
@BooleanValueSource
void executeWithCacheBindMountsExecutesPhases(boolean trustBuilder) throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
BuildRequest request = getTestRequest(trustBuilder).withBuildWorkspace(Cache.bind("/tmp/work"))
.withBuildCache(Cache.bind("/tmp/build-cache"))
@@ -292,8 +294,8 @@ class LifecycleTests {
@ParameterizedTest
@BooleanValueSource
void executeWithCreatedDateExecutesPhases(boolean trustBuilder) throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
BuildRequest request = getTestRequest(trustBuilder).withCreatedDate("2020-07-01T12:34:56Z");
createLifecycle(request).execute();
@@ -313,8 +315,8 @@ class LifecycleTests {
@ParameterizedTest
@BooleanValueSource
void executeWithApplicationDirectoryExecutesPhases(boolean trustBuilder) throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
BuildRequest request = getTestRequest(trustBuilder).withApplicationDirectory("/application");
createLifecycle(request).execute();
@@ -334,8 +336,8 @@ class LifecycleTests {
@ParameterizedTest
@BooleanValueSource
void executeWithSecurityOptionsExecutesPhases(boolean trustBuilder) throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
BuildRequest request = getTestRequest(trustBuilder)
.withSecurityOptions(List.of("label=user:USER", "label=role:ROLE"));
@@ -356,8 +358,8 @@ class LifecycleTests {
@ParameterizedTest
@BooleanValueSource
void executeWithDockerHostAndRemoteAddressExecutesPhases(boolean trustBuilder) throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
BuildRequest request = getTestRequest(trustBuilder);
createLifecycle(request, ResolvedDockerHost.from(DockerHostConfiguration.forAddress("tcp://192.168.1.2:2376")))
@@ -378,8 +380,8 @@ class LifecycleTests {
@ParameterizedTest
@BooleanValueSource
void executeWithDockerHostAndLocalAddressExecutesPhases(boolean trustBuilder) throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
BuildRequest request = getTestRequest(trustBuilder);
createLifecycle(request, ResolvedDockerHost.from(DockerHostConfiguration.forAddress("/var/alt.sock")))
@@ -397,6 +399,29 @@ class LifecycleTests {
assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
}
@ParameterizedTest
@BooleanValueSource
void executeWithImagePlatformExecutesPhases(boolean trustBuilder) throws Exception {
given(this.docker.container().create(any(), eq(ImagePlatform.of("linux/arm64"))))
.willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), eq(ImagePlatform.of("linux/arm64")), any()))
.willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
BuildRequest request = getTestRequest(trustBuilder).withImagePlatform("linux/arm64");
createLifecycle(request).execute();
if (trustBuilder) {
assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator.json"));
}
else {
assertPhaseWasRun("analyzer", withExpectedConfig("lifecycle-analyzer.json"));
assertPhaseWasRun("detector", withExpectedConfig("lifecycle-detector.json"));
assertPhaseWasRun("restorer", withExpectedConfig("lifecycle-restorer.json"));
assertPhaseWasRun("builder", withExpectedConfig("lifecycle-builder.json"));
assertPhaseWasRun("exporter", withExpectedConfig("lifecycle-exporter.json"));
}
assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
}
private DockerApi mockDockerApi() {
DockerApi docker = mock(DockerApi.class);
ImageApi imageApi = mock(ImageApi.class);
@@ -458,8 +483,8 @@ class LifecycleTests {
ArrayNode command = getCommand(config);
String name = command.get(0).asText().substring(1).replaceAll("/", "-");
this.configs.put(name, config);
if (invocation.getArguments().length > 1) {
this.content.put(name, invocation.getArgument(1, ContainerContent.class));
if (invocation.getArguments().length > 2) {
this.content.put(name, invocation.getArgument(2, ContainerContent.class));
}
return ContainerReference.of(name);
};

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,6 +28,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.buildpack.platform.docker.LogUpdateEvent;
import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent;
import org.springframework.boot.buildpack.platform.docker.type.Image;
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.docker.type.VolumeName;
import org.springframework.util.FileCopyUtils;
@@ -51,6 +52,7 @@ class PrintStreamBuildLogTests {
BuildRequest request = mock(BuildRequest.class);
ImageReference name = ImageReference.of("my-app:latest");
ImageReference builderImageReference = ImageReference.of("cnb/builder");
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
Image builderImage = mock(Image.class);
given(builderImage.getDigests()).willReturn(Collections.singletonList("00000001"));
ImageReference runImageReference = ImageReference.of("cnb/runner");
@@ -60,11 +62,12 @@ class PrintStreamBuildLogTests {
ImageReference tag = ImageReference.of("my-app:1.0");
given(request.getTags()).willReturn(Collections.singletonList(tag));
log.start(request);
Consumer<TotalProgressEvent> pullBuildImageConsumer = log.pullingImage(builderImageReference,
Consumer<TotalProgressEvent> pullBuildImageConsumer = log.pullingImage(builderImageReference, null,
ImageType.BUILDER);
pullBuildImageConsumer.accept(new TotalProgressEvent(100));
log.pulledImage(builderImage, ImageType.BUILDER);
Consumer<TotalProgressEvent> pullRunImageConsumer = log.pullingImage(runImageReference, ImageType.RUNNER);
Consumer<TotalProgressEvent> pullRunImageConsumer = log.pullingImage(runImageReference, platform,
ImageType.RUNNER);
pullRunImageConsumer.accept(new TotalProgressEvent(100));
log.pulledImage(runImage, ImageType.RUNNER);
log.executingLifecycle(request, LifecycleVersion.parse("0.5"), Cache.volume(VolumeName.of("pack-abc.cache")));

View File

@@ -18,15 +18,19 @@ package org.springframework.boot.buildpack.platform.docker;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.message.BasicHeader;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
@@ -39,15 +43,18 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.buildpack.platform.docker.DockerApi.ContainerApi;
import org.springframework.boot.buildpack.platform.docker.DockerApi.ImageApi;
import org.springframework.boot.buildpack.platform.docker.DockerApi.SystemApi;
import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi;
import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport;
import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport.Response;
import org.springframework.boot.buildpack.platform.docker.type.ApiVersion;
import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig;
import org.springframework.boot.buildpack.platform.docker.type.ContainerContent;
import org.springframework.boot.buildpack.platform.docker.type.ContainerReference;
import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus;
import org.springframework.boot.buildpack.platform.docker.type.Image;
import org.springframework.boot.buildpack.platform.docker.type.ImageArchive;
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.docker.type.VolumeName;
import org.springframework.boot.buildpack.platform.io.Content;
@@ -80,12 +87,18 @@ import static org.mockito.Mockito.times;
@ExtendWith(MockitoExtension.class)
class DockerApiTests {
private static final String API_URL = "/" + DockerApi.API_VERSION;
private static final String API_URL = "/v" + DockerApi.MINIMUM_API_VERSION;
public static final String PING_URL = "/_ping";
private static final String IMAGES_URL = API_URL + "/images";
private static final String IMAGES_1_41_URL = "/v" + ApiVersion.of(1, 41) + "/images";
private static final String CONTAINERS_URL = API_URL + "/containers";
private static final String CONTAINERS_1_41_URL = "/v" + ApiVersion.of(1, 41) + "/containers";
private static final String VOLUMES_URL = API_URL + "/volumes";
@Mock
@@ -124,6 +137,29 @@ class DockerApiTests {
};
}
private Response responseWithHeaders(Header... headers) {
return new Response() {
@Override
public InputStream getContent() {
return null;
}
@Override
public Header getHeader(String name) {
return Arrays.stream(headers)
.filter((header) -> header.getName().equals(name))
.findFirst()
.orElse(null);
}
@Override
public void close() {
}
};
}
@Test
void createDockerApi() {
DockerApi api = new DockerApi();
@@ -154,13 +190,14 @@ class DockerApiTests {
@Test
void pullWhenReferenceIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.api.pull(null, this.pullListener))
assertThatIllegalArgumentException().isThrownBy(() -> this.api.pull(null, null, this.pullListener))
.withMessage("Reference must not be null");
}
@Test
void pullWhenListenerIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.api.pull(ImageReference.of("ubuntu"), null))
assertThatIllegalArgumentException()
.isThrownBy(() -> this.api.pull(ImageReference.of("ubuntu"), null, null))
.withMessage("Listener must not be null");
}
@@ -171,7 +208,7 @@ class DockerApiTests {
URI imageUri = new URI(IMAGES_URL + "/gcr.io/paketo-buildpacks/builder:base/json");
given(http().post(eq(createUri), isNull())).willReturn(responseOf("pull-stream.json"));
given(http().get(imageUri)).willReturn(responseOf("type/image.json"));
Image image = this.api.pull(reference, this.pullListener);
Image image = this.api.pull(reference, null, this.pullListener);
assertThat(image.getLayers()).hasSize(46);
InOrder ordered = inOrder(this.pullListener);
ordered.verify(this.pullListener).onStart();
@@ -186,7 +223,7 @@ class DockerApiTests {
URI imageUri = new URI(IMAGES_URL + "/gcr.io/paketo-buildpacks/builder:base/json");
given(http().post(eq(createUri), eq("auth token"))).willReturn(responseOf("pull-stream.json"));
given(http().get(imageUri)).willReturn(responseOf("type/image.json"));
Image image = this.api.pull(reference, this.pullListener, "auth token");
Image image = this.api.pull(reference, null, this.pullListener, "auth token");
assertThat(image.getLayers()).hasSize(46);
InOrder ordered = inOrder(this.pullListener);
ordered.verify(this.pullListener).onStart();
@@ -194,6 +231,36 @@ class DockerApiTests {
ordered.verify(this.pullListener).onFinish();
}
@Test
void pullWithPlatformPullsImageAndProducesEvents() throws Exception {
ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base");
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
URI createUri = new URI(IMAGES_1_41_URL
+ "/create?fromImage=gcr.io%2Fpaketo-buildpacks%2Fbuilder%3Abase&platform=linux%2Farm64%2Fv1");
URI imageUri = new URI(IMAGES_1_41_URL + "/gcr.io/paketo-buildpacks/builder:base/json");
given(http().head(eq(new URI(PING_URL))))
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "1.41")));
given(http().post(eq(createUri), isNull())).willReturn(responseOf("pull-stream.json"));
given(http().get(imageUri)).willReturn(responseOf("type/image.json"));
Image image = this.api.pull(reference, platform, this.pullListener);
assertThat(image.getLayers()).hasSize(46);
InOrder ordered = inOrder(this.pullListener);
ordered.verify(this.pullListener).onStart();
ordered.verify(this.pullListener, times(595)).onUpdate(any());
ordered.verify(this.pullListener).onFinish();
}
@Test
void pullWithPlatformAndInsufficientApiVersionThrowsException() throws Exception {
ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base");
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
given(http().head(eq(new URI(PING_URL)))).willReturn(responseWithHeaders(
new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, DockerApi.MINIMUM_API_VERSION)));
assertThatIllegalArgumentException().isThrownBy(() -> this.api.pull(reference, platform, this.pullListener))
.withMessageContaining("must be at least 1.41")
.withMessageContaining("current API version is 1.24");
}
@Test
void pushWhenReferenceIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.api.push(null, this.pushListener, null))
@@ -460,7 +527,7 @@ class DockerApiTests {
@Test
void createWhenConfigIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.api.create(null))
assertThatIllegalArgumentException().isThrownBy(() -> this.api.create(null, null))
.withMessage("Config must not be null");
}
@@ -471,7 +538,7 @@ class DockerApiTests {
URI createUri = new URI(CONTAINERS_URL + "/create");
given(http().post(eq(createUri), eq("application/json"), any()))
.willReturn(responseOf("create-container-response.json"));
ContainerReference containerReference = this.api.create(config);
ContainerReference containerReference = this.api.create(config, null);
assertThat(containerReference).hasToString("e90e34656806");
then(http()).should().post(any(), any(), this.writer.capture());
ByteArrayOutputStream out = new ByteArrayOutputStream();
@@ -493,7 +560,7 @@ class DockerApiTests {
.willReturn(responseOf("create-container-response.json"));
URI uploadUri = new URI(CONTAINERS_URL + "/e90e34656806/archive?path=%2F");
given(http().put(eq(uploadUri), eq("application/x-tar"), any())).willReturn(emptyResponse());
ContainerReference containerReference = this.api.create(config, content);
ContainerReference containerReference = this.api.create(config, null, content);
assertThat(containerReference).hasToString("e90e34656806");
then(http()).should().post(any(), any(), this.writer.capture());
ByteArrayOutputStream out = new ByteArrayOutputStream();
@@ -504,6 +571,34 @@ class DockerApiTests {
assertThat(out.toByteArray()).hasSizeGreaterThan(2000);
}
@Test
void createWithPlatformCreatesContainer() throws Exception {
ImageReference imageReference = ImageReference.of("ubuntu:bionic");
ContainerConfig config = ContainerConfig.of(imageReference, (update) -> update.withCommand("/bin/bash"));
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
given(http().head(eq(new URI(PING_URL))))
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "1.41")));
URI createUri = new URI(CONTAINERS_1_41_URL + "/create?platform=linux%2Farm64%2Fv1");
given(http().post(eq(createUri), eq("application/json"), any()))
.willReturn(responseOf("create-container-response.json"));
ContainerReference containerReference = this.api.create(config, platform);
assertThat(containerReference).hasToString("e90e34656806");
then(http()).should().post(any(), any(), this.writer.capture());
ByteArrayOutputStream out = new ByteArrayOutputStream();
this.writer.getValue().accept(out);
assertThat(out.toByteArray()).hasSize(config.toString().length());
}
@Test
void createWithPlatformAndInsufficientApiVersionThrowsException() throws Exception {
ImageReference imageReference = ImageReference.of("ubuntu:bionic");
ContainerConfig config = ContainerConfig.of(imageReference, (update) -> update.withCommand("/bin/bash"));
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
assertThatIllegalArgumentException().isThrownBy(() -> this.api.create(config, platform))
.withMessageContaining("must be at least 1.41")
.withMessageContaining("current API version is 1.24");
}
@Test
void startWhenReferenceIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.api.start(null))
@@ -621,4 +716,42 @@ class DockerApiTests {
}
@Nested
class SystemDockerApiTests {
private SystemApi api;
@BeforeEach
void setup() {
this.api = DockerApiTests.this.dockerApi.system();
}
@Test
void getApiVersionWithVersionHeaderReturnsVersion() throws Exception {
given(http().head(eq(new URI(PING_URL))))
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "1.44")));
assertThat(this.api.getApiVersion()).isEqualTo(ApiVersion.of(1, 44));
}
@Test
void getApiVersionWithEmptyVersionHeaderReturnsDefaultVersion() throws Exception {
given(http().head(eq(new URI(PING_URL))))
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "")));
assertThat(this.api.getApiVersion()).isEqualTo(DockerApi.MINIMUM_API_VERSION);
}
@Test
void getApiVersionWithNoVersionHeaderReturnsDefaultVersion() throws Exception {
given(http().head(eq(new URI(PING_URL)))).willReturn(emptyResponse());
assertThat(this.api.getApiVersion()).isEqualTo(DockerApi.MINIMUM_API_VERSION);
}
@Test
void getApiVersionWithExceptionReturnsDefaultVersion() throws Exception {
given(http().head(eq(new URI(PING_URL)))).willThrow(new IOException("simulated error"));
assertThat(this.api.getApiVersion()).isEqualTo(DockerApi.MINIMUM_API_VERSION);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.build;
package org.springframework.boot.buildpack.platform.docker.type;
import java.util.Arrays;
@@ -22,7 +22,6 @@ import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link ApiVersion}.
@@ -57,18 +56,6 @@ class ApiVersionTests {
assertThat(version.getMinor()).isEqualTo(2);
}
@Test
void assertSupportsWhenSupports() {
ApiVersion.parse("1.2").assertSupports(ApiVersion.parse("1.0"));
}
@Test
void assertSupportsWhenDoesNotSupportThrowsException() {
assertThatIllegalStateException()
.isThrownBy(() -> ApiVersion.parse("1.2").assertSupports(ApiVersion.parse("1.3")))
.withMessage("Detected platform API version '1.3' does not match supported version '1.2'");
}
@Test
void supportsWhenSame() {
assertThat(supports("0.0", "0.0")).isTrue();

View File

@@ -78,7 +78,7 @@ class ImageArchiveTests extends AbstractJsonTests {
}
private void assertExpectedConfig(TarArchiveEntry entry, byte[] content) throws Exception {
assertThat(entry.getName()).isEqualTo("682f8d24b9d9c313d1190a0e955dcb5e65ec9beea40420999839c6f0cbb38382.json");
assertThat(entry.getName()).isEqualTo("416c76dc7f691f91e80516ff039e056f32f996b59af4b1cb8114e6ae8171a374.json");
String actualJson = new String(content, StandardCharsets.UTF_8);
String expectedJson = StreamUtils.copyToString(getContent("image-archive-config.json"), StandardCharsets.UTF_8);
JSONAssert.assertEquals(expectedJson, actualJson, false);

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2012-2024 the original author or authors.
*
* 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
*
* https://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 org.springframework.boot.buildpack.platform.docker.type;
import java.io.IOException;
import org.junit.jupiter.api.Test;
import org.springframework.boot.buildpack.platform.json.AbstractJsonTests;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
class ImagePlatformTests extends AbstractJsonTests {
@Test
void ofWithOsParses() {
ImagePlatform platform = ImagePlatform.of("linux");
assertThat(platform.toString()).isEqualTo("linux");
}
@Test
void ofWithOsAndArchitectureParses() {
ImagePlatform platform = ImagePlatform.of("linux/amd64");
assertThat(platform.toString()).isEqualTo("linux/amd64");
}
@Test
void ofWithOsAndArchitectureAndVariantParses() {
ImagePlatform platform = ImagePlatform.of("linux/amd64/v1");
assertThat(platform.toString()).isEqualTo("linux/amd64/v1");
}
@Test
void ofWithEmptyValueFails() {
assertThatIllegalArgumentException().isThrownBy(() -> ImagePlatform.of(""))
.withMessageContaining("Value must not be empty");
}
@Test
void ofWithTooManySegmentsFails() {
assertThatIllegalArgumentException().isThrownBy(() -> ImagePlatform.of("linux/amd64/v1/extra"))
.withMessageContaining("value 'linux/amd64/v1/extra'");
}
@Test
void fromImageMatchesImage() throws IOException {
ImagePlatform platform = ImagePlatform.from(getImage());
assertThat(platform.toString()).isEqualTo("linux/amd64/v1");
}
private Image getImage() throws IOException {
return Image.of(getContent("image.json"));
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -67,6 +67,18 @@ class ImageTests extends AbstractJsonTests {
assertThat(image.getOs()).isEqualTo("linux");
}
@Test
void getArchitectureReturnsArchitecture() throws Exception {
Image image = getImage();
assertThat(image.getArchitecture()).isEqualTo("amd64");
}
@Test
void getVariantReturnsVariant() throws Exception {
Image image = getImage();
assertThat(image.getVariant()).isEqualTo("v1");
}
@Test
void getCreatedReturnsDate() throws Exception {
Image image = getImage();

View File

@@ -2,7 +2,7 @@ Building image 'docker.io/library/my-app:latest'
> Pulling builder image 'docker.io/cnb/builder' ..................................................
> Pulled builder image '00000001'
> Pulling run image 'docker.io/cnb/runner' ..................................................
> Pulling run image 'docker.io/cnb/runner' for platform 'linux/arm64/v1' ..................................................
> Pulled run image '00000002'
> Executing lifecycle version v0.5.0
> Using build cache volume 'pack-abc.cache'

View File

@@ -0,0 +1,98 @@
{
"Id": "sha256:1332879bc8e38793a45ebe5a750f2a1c35df07ec2aa9c18f694644a9de77359b",
"RepoTags": [
"cloudfoundry/run:base-cnb"
],
"RepoDigests": [
"cloudfoundry/run@sha256:fb5ecb90a42b2067a859aab23fc1f5e9d9c2589d07ba285608879e7baa415aad"
],
"Parent": "",
"Comment": "",
"Created": "2020-03-20T20:18:18.117972538Z",
"Container": "91d1af87c3bb6163cd9c7cb21e6891cd25f5fa3c7417779047776e288c0bc234",
"ContainerConfig": {
"Hostname": "91d1af87c3bb",
"Domainname": "",
"User": "1000:1000",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/sh",
"-c",
"#(nop) ",
"LABEL io.buildpacks.stack.id=io.buildpacks.stacks.bionic"
],
"ArgsEscaped": true,
"Image": "sha256:fbe314bcb23f15a2a09603b6620acd67c332fd08fbf2a7bc3db8fb2f5078d994",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": {
"io.buildpacks.stack.id": "io.buildpacks.stacks.bionic"
}
},
"DockerVersion": "18.09.6",
"Author": "",
"Config": {
"Hostname": "",
"Domainname": "",
"User": "1000:1000",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/bash"
],
"ArgsEscaped": true,
"Image": "sha256:fbe314bcb23f15a2a09603b6620acd67c332fd08fbf2a7bc3db8fb2f5078d994",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": {
"io.buildpacks.stack.id": "io.buildpacks.stacks.bionic"
}
},
"Architecture": "arm64",
"Os": "linux",
"Variant": "v1",
"Size": 71248531,
"VirtualSize": 71248531,
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/17f0a4530fbc3e2982f9dc8feb8c8ddc124473bdd50130dae20856ac597d82dd/diff:/var/lib/docker/overlay2/73dfd4e2075fccb239b3d5e9b33b32b8e410bdc3cd5a620b41346f44cc5c51f7/diff:/var/lib/docker/overlay2/b3924ed7c91730f6714d33c455db888604b59ab093033b3f59ac16ecdd777987/diff:/var/lib/docker/overlay2/e36a32cd0ab20b216a8db1a8a166b17464399e4d587d22504088a7a6ef0a68a4/diff:/var/lib/docker/overlay2/3334e94fe191333b65f571912c0fcfbbf31aeb090a2fb9b4cfdbc32a37c0fe5f/diff",
"MergedDir": "/var/lib/docker/overlay2/8d3f9e3c00bc5072f8051ec7884500ca394f2331d8bcc9452f68d04531f50f82/merged",
"UpperDir": "/var/lib/docker/overlay2/8d3f9e3c00bc5072f8051ec7884500ca394f2331d8bcc9452f68d04531f50f82/diff",
"WorkDir": "/var/lib/docker/overlay2/8d3f9e3c00bc5072f8051ec7884500ca394f2331d8bcc9452f68d04531f50f82/work"
},
"Name": "overlay2"
},
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:c8be1b8f4d60d99c281fc2db75e0f56df42a83ad2f0b091621ce19357e19d853",
"sha256:977183d4e9995d9cd5ffdfc0f29e911ec9de777bcb0f507895daa1068477f76f",
"sha256:6597da2e2e52f4d438ad49a14ca79324f130a9ea08745505aa174a8db51cb79d",
"sha256:16542a8fc3be1bfaff6ed1daa7922e7c3b47b6c3a8d98b7fca58b9517bb99b75",
"sha256:c1daeb79beb276c7441d9a1d7281433e9a7edb9f652b8996ecc62b51e88a47b2",
"sha256:eb195d29dc1aa6e4239f00e7868deebc5ac12bebe76104e0b774c1ef29ca78e3"
]
},
"Metadata": {
"LastTagTime": "0001-01-01T00:00:00Z"
}
}

View File

@@ -1,5 +1,5 @@
{
"config": {
"Config": {
"Hostname": "",
"Domainname": "",
"User": "vcap",
@@ -25,8 +25,8 @@
"io.buildpacks.stack.id": "org.cloudfoundry.stacks.cflinuxfs3"
}
},
"created": "1980-01-01T00:00:01Z",
"history": [
"Created": "1980-01-01T00:00:01Z",
"History": [
{
},
@@ -169,8 +169,10 @@
}
],
"os": "linux",
"rootfs": {
"Architecture": "amd64",
"Os": "linux",
"Variant": "v1",
"RootFS": {
"diff_ids": [
"sha256:733a8e5ce32984099ef675fce04730f6e2a6dcfdf5bd292fea01a8f936265342",
"sha256:7755b972f0b4f49de73ef5114fb3ba9c69d80f217e80da99f56f0d0a5dcb3d70",

View File

@@ -1,6 +1,6 @@
[
{
"Config": "682f8d24b9d9c313d1190a0e955dcb5e65ec9beea40420999839c6f0cbb38382.json",
"Config": "416c76dc7f691f91e80516ff039e056f32f996b59af4b1cb8114e6ae8171a374.json",
"Layers": [
"blank_0",
"blank_1",

View File

@@ -72,9 +72,10 @@
"io.buildpacks.stack.id": "org.cloudfoundry.stacks.cflinuxfs3"
}
},
"Architecture": "amd64",
"Os": "linux",
"Size": 1559461360,
"Architecture": "amd64",
"Variant": "v1",
"Size": 1559461360,
"VirtualSize": 1559461360,
"GraphDriver": {
"Data": {