Add support for CNB platform API v0.2
Cloud Native Buildpacks platform API version 0.2 introduced two breaking changes: the order of invoking the restore and analyze phases was reversed, and the cache phase was removed in favor of distributing caching across other phases. This commit adds support for Cloud Native Buildpacks builders that implement platform API version 0.2, while maintaining compatibility with builders that implement Lifecycle version platform API version 0.1. Closes gh-19829
This commit is contained in:
@@ -26,6 +26,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
* Tests for {@link ApiVersion}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class ApiVersionTests {
|
||||
|
||||
@@ -63,7 +64,7 @@ class ApiVersionTests {
|
||||
void assertSupportsWhenDoesNotSupportThrowsException() {
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> ApiVersion.parse("1.2").assertSupports(ApiVersion.parse("1.3")))
|
||||
.withMessage("Version 'v1.3' is not supported by this version ('v1.2')");
|
||||
.withMessage("Detected platform API version 'v1.3' does not match supported version 'v1.2'");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2012-2020 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.build;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
* Tests for {@link ApiVersions}.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class ApiVersionsTests {
|
||||
|
||||
@Test
|
||||
void assertSupportsWhenAllSupports() {
|
||||
ApiVersions.parse("1.1", "2.2").assertSupports(ApiVersion.parse("1.0"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void assertSupportsWhenDoesNoneSupportedThrowsException() {
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> ApiVersions.parse("1.1", "1.2").assertSupports(ApiVersion.parse("1.3")))
|
||||
.withMessage("Detected platform API version 'v1.3' is not included in supported versions 'v1.1,v1.2'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toStringReturnsString() {
|
||||
assertThat(ApiVersions.parse("1.1", "2.2", "3.3").toString()).isEqualTo("v1.1,v2.2,v3.3");
|
||||
}
|
||||
|
||||
@Test
|
||||
void equalsAndHashCode() {
|
||||
ApiVersions v12a = ApiVersions.parse("1.2", "2.3");
|
||||
ApiVersions v12b = ApiVersions.parse("1.2", "2.3");
|
||||
ApiVersions v13 = ApiVersions.parse("1.3", "2.4");
|
||||
assertThat(v12a.hashCode()).isEqualTo(v12b.hashCode());
|
||||
assertThat(v12a).isEqualTo(v12a).isEqualTo(v12b).isNotEqualTo(v13);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -60,6 +60,7 @@ import static org.mockito.Mockito.verify;
|
||||
* Tests for {@link Lifecycle}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class LifecycleTests {
|
||||
|
||||
@@ -67,52 +68,52 @@ class LifecycleTests {
|
||||
|
||||
private DockerApi docker;
|
||||
|
||||
private Lifecycle lifecycle;
|
||||
|
||||
private Map<String, ContainerConfig> configs = new LinkedHashMap<>();
|
||||
|
||||
private Map<String, ContainerContent> content = new LinkedHashMap<>();
|
||||
|
||||
@BeforeEach
|
||||
void setup() throws Exception {
|
||||
void setup() {
|
||||
this.out = new TestPrintStream();
|
||||
this.docker = mockDockerApi();
|
||||
BuildRequest request = getTestRequest();
|
||||
this.lifecycle = createLifecycle(request);
|
||||
}
|
||||
|
||||
private DockerApi mockDockerApi() {
|
||||
DockerApi docker = mock(DockerApi.class);
|
||||
ImageApi imageApi = mock(ImageApi.class);
|
||||
ContainerApi containerApi = mock(ContainerApi.class);
|
||||
VolumeApi volumeApi = mock(VolumeApi.class);
|
||||
given(docker.image()).willReturn(imageApi);
|
||||
given(docker.container()).willReturn(containerApi);
|
||||
given(docker.volume()).willReturn(volumeApi);
|
||||
return docker;
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeExecutesPhases() throws Exception {
|
||||
void executeExecutesPhasesWithCacherPhase() throws Exception {
|
||||
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
|
||||
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
|
||||
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
|
||||
this.lifecycle.execute();
|
||||
createLifecycle("0.5").execute();
|
||||
assertPhaseWasRun("detector", withExpectedConfig("lifecycle-detector.json"));
|
||||
assertPhaseWasRun("restorer", withExpectedConfig("lifecycle-restorer.json"));
|
||||
assertPhaseWasRun("analyzer", withExpectedConfig("lifecycle-analyzer.json"));
|
||||
assertPhaseWasRun("restorer", withExpectedConfig("lifecycle-restorer-0.1.json"));
|
||||
assertPhaseWasRun("analyzer", withExpectedConfig("lifecycle-analyzer-0.1.json"));
|
||||
assertPhaseWasRun("builder", withExpectedConfig("lifecycle-builder.json"));
|
||||
assertPhaseWasRun("exporter", withExpectedConfig("lifecycle-exporter.json"));
|
||||
assertPhaseWasRun("exporter", withExpectedConfig("lifecycle-exporter-0.1.json"));
|
||||
assertPhaseWasRun("cacher", withExpectedConfig("lifecycle-cacher.json"));
|
||||
assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeExecutesPhasesWithEmbeddedCaching() throws Exception {
|
||||
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
|
||||
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
|
||||
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
|
||||
createLifecycle("0.6").execute();
|
||||
assertPhaseWasRun("detector", withExpectedConfig("lifecycle-detector.json"));
|
||||
assertPhaseWasRun("analyzer", withExpectedConfig("lifecycle-analyzer-0.2.json"));
|
||||
assertPhaseWasRun("restorer", withExpectedConfig("lifecycle-restorer-0.2.json"));
|
||||
assertPhaseWasRun("builder", withExpectedConfig("lifecycle-builder.json"));
|
||||
assertPhaseWasRun("exporter", withExpectedConfig("lifecycle-exporter-0.2.json"));
|
||||
assertPhaseWasNotRun("cacher");
|
||||
assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeOnlyUploadsContentOnce() throws Exception {
|
||||
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
|
||||
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
|
||||
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
|
||||
this.lifecycle.execute();
|
||||
createLifecycle().execute();
|
||||
assertThat(this.content).hasSize(1);
|
||||
}
|
||||
|
||||
@@ -121,8 +122,9 @@ class LifecycleTests {
|
||||
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
|
||||
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
|
||||
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
|
||||
this.lifecycle.execute();
|
||||
assertThatIllegalStateException().isThrownBy(this.lifecycle::execute)
|
||||
Lifecycle lifecycle = createLifecycle();
|
||||
lifecycle.execute();
|
||||
assertThatIllegalStateException().isThrownBy(lifecycle::execute)
|
||||
.withMessage("Lifecycle has already been executed");
|
||||
}
|
||||
|
||||
@@ -131,7 +133,7 @@ class LifecycleTests {
|
||||
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
|
||||
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
|
||||
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(9, null));
|
||||
assertThatExceptionOfType(BuilderException.class).isThrownBy(() -> this.lifecycle.execute())
|
||||
assertThatExceptionOfType(BuilderException.class).isThrownBy(() -> createLifecycle().execute())
|
||||
.withMessage("Builder lifecycle 'detector' failed with status code 9");
|
||||
}
|
||||
|
||||
@@ -148,27 +150,50 @@ class LifecycleTests {
|
||||
|
||||
@Test
|
||||
void closeClearsVolumes() throws Exception {
|
||||
this.lifecycle.close();
|
||||
createLifecycle().close();
|
||||
verify(this.docker.volume()).delete(VolumeName.of("pack-layers-aaaaaaaaaa"), true);
|
||||
verify(this.docker.volume()).delete(VolumeName.of("pack-app-aaaaaaaaaa"), true);
|
||||
}
|
||||
|
||||
private DockerApi mockDockerApi() {
|
||||
DockerApi docker = mock(DockerApi.class);
|
||||
ImageApi imageApi = mock(ImageApi.class);
|
||||
ContainerApi containerApi = mock(ContainerApi.class);
|
||||
VolumeApi volumeApi = mock(VolumeApi.class);
|
||||
given(docker.image()).willReturn(imageApi);
|
||||
given(docker.container()).willReturn(containerApi);
|
||||
given(docker.volume()).willReturn(volumeApi);
|
||||
return docker;
|
||||
}
|
||||
|
||||
private BuildRequest getTestRequest() {
|
||||
TarArchive content = mock(TarArchive.class);
|
||||
ImageReference name = ImageReference.of("my-application");
|
||||
BuildRequest request = BuildRequest.of(name, (owner) -> content);
|
||||
return request;
|
||||
return BuildRequest.of(name, (owner) -> content);
|
||||
}
|
||||
|
||||
private Lifecycle createLifecycle() throws IOException {
|
||||
return createLifecycle("0.6");
|
||||
}
|
||||
|
||||
private Lifecycle createLifecycle(String version) throws IOException {
|
||||
return createLifecycle(getTestRequest(), version);
|
||||
}
|
||||
|
||||
private Lifecycle createLifecycle(BuildRequest request) throws IOException {
|
||||
EphemeralBuilder builder = mockEphemeralBuilder();
|
||||
return createLifecycle(request, "0.6");
|
||||
}
|
||||
|
||||
private Lifecycle createLifecycle(BuildRequest request, String version) throws IOException {
|
||||
EphemeralBuilder builder = mockEphemeralBuilder((version != null) ? version : "0.5");
|
||||
return new TestLifecycle(BuildLog.to(this.out), this.docker, request, ImageReference.of("cloudfoundry/run"),
|
||||
builder);
|
||||
}
|
||||
|
||||
private EphemeralBuilder mockEphemeralBuilder() throws IOException {
|
||||
private EphemeralBuilder mockEphemeralBuilder(String version) throws IOException {
|
||||
EphemeralBuilder builder = mock(EphemeralBuilder.class);
|
||||
byte[] metadataContent = FileCopyUtils.copyToByteArray(getClass().getResourceAsStream("builder-metadata.json"));
|
||||
byte[] metadataContent = FileCopyUtils
|
||||
.copyToByteArray(getClass().getResourceAsStream("builder-metadata-version-" + version + ".json"));
|
||||
BuilderMetadata metadata = BuilderMetadata.fromJson(new String(metadataContent, StandardCharsets.UTF_8));
|
||||
given(builder.getName()).willReturn(ImageReference.of("pack.local/ephemeral-builder"));
|
||||
given(builder.getBuilderMetadata()).willReturn(metadata);
|
||||
@@ -201,6 +226,11 @@ class LifecycleTests {
|
||||
configConsumer.accept(this.configs.get(containerReference.toString()));
|
||||
}
|
||||
|
||||
private void assertPhaseWasNotRun(String name) {
|
||||
ContainerReference containerReference = ContainerReference.of("lifecycle-" + name);
|
||||
assertThat(this.configs.get(containerReference.toString())).isNull();
|
||||
}
|
||||
|
||||
private IOConsumer<ContainerConfig> withExpectedConfig(String name) {
|
||||
return (config) -> {
|
||||
InputStream in = getClass().getResourceAsStream(name);
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
{
|
||||
"description": "Ubuntu bionic base image with buildpacks for Java, NodeJS and Golang",
|
||||
"buildpacks": [
|
||||
{
|
||||
"id": "org.cloudfoundry.debug",
|
||||
"version": "v1.0.106",
|
||||
"latest": true
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.go",
|
||||
"version": "v0.0.2",
|
||||
"latest": true
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.springautoreconfiguration",
|
||||
"version": "v1.0.113",
|
||||
"latest": true
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.buildsystem",
|
||||
"version": "v1.0.133",
|
||||
"latest": true
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.procfile",
|
||||
"version": "v1.0.41",
|
||||
"latest": true
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.nodejs",
|
||||
"version": "v0.0.5",
|
||||
"latest": true
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.distzip",
|
||||
"version": "v1.0.101",
|
||||
"latest": true
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.jdbc",
|
||||
"version": "v1.0.107",
|
||||
"latest": true
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.azureapplicationinsights",
|
||||
"version": "v1.0.105",
|
||||
"latest": true
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.springboot",
|
||||
"version": "v1.0.112",
|
||||
"latest": true
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.openjdk",
|
||||
"version": "v1.0.58",
|
||||
"latest": true
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.tomcat",
|
||||
"version": "v1.1.24",
|
||||
"latest": true
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.googlestackdriver",
|
||||
"version": "v1.0.54",
|
||||
"latest": true
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.jmx",
|
||||
"version": "v1.0.107",
|
||||
"latest": true
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.archiveexpanding",
|
||||
"version": "v1.0.99",
|
||||
"latest": true
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.jvmapplication",
|
||||
"version": "v1.0.82",
|
||||
"latest": true
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.dep",
|
||||
"version": "0.0.64",
|
||||
"latest": true
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.go-compiler",
|
||||
"version": "0.0.55",
|
||||
"latest": true
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.go-mod",
|
||||
"version": "0.0.58",
|
||||
"latest": true
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.node-engine",
|
||||
"version": "0.0.102",
|
||||
"latest": true
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.npm",
|
||||
"version": "0.0.63",
|
||||
"latest": true
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.yarn",
|
||||
"version": "0.0.69",
|
||||
"latest": true
|
||||
}
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"buildpacks": [
|
||||
{
|
||||
"id": "org.cloudfoundry.archiveexpanding",
|
||||
"version": "v1.0.99",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.openjdk",
|
||||
"version": "v1.0.58"
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.buildsystem",
|
||||
"version": "v1.0.133",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.jvmapplication",
|
||||
"version": "v1.0.82"
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.tomcat",
|
||||
"version": "v1.1.24",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.springboot",
|
||||
"version": "v1.0.112",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.distzip",
|
||||
"version": "v1.0.101",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.procfile",
|
||||
"version": "v1.0.41",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.azureapplicationinsights",
|
||||
"version": "v1.0.105",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.debug",
|
||||
"version": "v1.0.106",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.googlestackdriver",
|
||||
"version": "v1.0.54",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.jdbc",
|
||||
"version": "v1.0.107",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.jmx",
|
||||
"version": "v1.0.107",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"id": "org.cloudfoundry.springautoreconfiguration",
|
||||
"version": "v1.0.113",
|
||||
"optional": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"buildpacks": [
|
||||
{
|
||||
"id": "org.cloudfoundry.nodejs",
|
||||
"version": "v0.0.5"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"buildpacks": [
|
||||
{
|
||||
"id": "org.cloudfoundry.go",
|
||||
"version": "v0.0.2"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"stack": {
|
||||
"runImage": {
|
||||
"image": "cloudfoundry/run:base-cnb",
|
||||
"mirrors": null
|
||||
}
|
||||
},
|
||||
"lifecycle": {
|
||||
"version": "0.6.0",
|
||||
"api": {
|
||||
"buildpack": "0.2",
|
||||
"platform": "0.2"
|
||||
}
|
||||
},
|
||||
"createdBy": {
|
||||
"name": "Pack CLI",
|
||||
"version": "dev-2019-11-19-22:34:59"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"User" : "root",
|
||||
"Image" : "pack.local/ephemeral-builder",
|
||||
"Cmd" : [ "/lifecycle/analyzer", "-daemon", "-layers", "/layers", "-cache-dir", "/cache", "docker.io/library/my-application:latest" ],
|
||||
"Labels" : {
|
||||
"author" : "spring-boot"
|
||||
},
|
||||
"HostConfig" : {
|
||||
"Binds" : [ "/var/run/docker.sock:/var/run/docker.sock", "pack-layers-aaaaaaaaaa:/layers", "pack-app-aaaaaaaaaa:/workspace", "pack-cache-b35197ac41ea.build:/cache" ]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"User" : "root",
|
||||
"Image" : "pack.local/ephemeral-builder",
|
||||
"Cmd" : [ "/lifecycle/exporter", "-image", "docker.io/cloudfoundry/run", "-layers", "/layers", "-app", "/workspace", "-daemon", "-launch-cache", "/launch-cache", "-cache-dir", "/cache", "docker.io/library/my-application:latest" ],
|
||||
"Labels" : {
|
||||
"author" : "spring-boot"
|
||||
},
|
||||
"HostConfig" : {
|
||||
"Binds" : [ "/var/run/docker.sock:/var/run/docker.sock", "pack-layers-aaaaaaaaaa:/layers", "pack-app-aaaaaaaaaa:/workspace", "pack-cache-b35197ac41ea.launch:/launch-cache", "pack-cache-b35197ac41ea.build:/cache" ]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"User" : "root",
|
||||
"Image" : "pack.local/ephemeral-builder",
|
||||
"Cmd" : [ "/lifecycle/restorer", "-cache-dir", "/cache", "-layers", "/layers" ],
|
||||
"Labels" : {
|
||||
"author" : "spring-boot"
|
||||
},
|
||||
"HostConfig" : {
|
||||
"Binds" : [ "/var/run/docker.sock:/var/run/docker.sock", "pack-layers-aaaaaaaaaa:/layers", "pack-app-aaaaaaaaaa:/workspace", "pack-cache-b35197ac41ea.build:/cache" ]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user