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:
Scott Frederick
2020-02-05 17:10:26 -06:00
parent 4add9632de
commit d07062652e
14 changed files with 530 additions and 55 deletions

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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"
}
}

View File

@@ -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" ]
}
}

View File

@@ -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" ]
}
}

View File

@@ -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" ]
}
}