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
pull/20058/head
Scott Frederick 5 years ago
parent 4add9632de
commit d07062652e

@ -22,16 +22,14 @@ import java.util.regex.Pattern;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
* API Version number comprised a major and minor value. * API Version number comprised of a major and minor value.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Scott Frederick
*/ */
final class ApiVersion { final class ApiVersion {
/** private static final ApiVersion PLATFORM_0_1 = new ApiVersion(0, 1);
* The platform API version supported by this release.
*/
static final ApiVersion PLATFORM = new ApiVersion(0, 1);
private static final Pattern PATTERN = Pattern.compile("^v?(\\d+)\\.(\\d*)$"); private static final Pattern PATTERN = Pattern.compile("^v?(\\d+)\\.(\\d*)$");
@ -68,7 +66,7 @@ final class ApiVersion {
void assertSupports(ApiVersion other) { void assertSupports(ApiVersion other) {
if (!supports(other)) { if (!supports(other)) {
throw new IllegalStateException( throw new IllegalStateException(
"Version '" + other + "' is not supported by this version ('" + this + "')"); "Detected platform API version '" + other + "' does not match supported version '" + this + "'");
} }
} }
@ -77,7 +75,7 @@ final class ApiVersion {
* the same version number. A 1.x or higher release matches when the versions have the * the same version number. A 1.x or higher release matches when the versions have the
* same major version and a minor that is equal or greater. * same major version and a minor that is equal or greater.
* @param other the version to check against * @param other the version to check against
* @return of the specified API is supported * @return if the specified API is supported
* @see #assertSupports(ApiVersion) * @see #assertSupports(ApiVersion)
*/ */
boolean supports(ApiVersion other) { boolean supports(ApiVersion other) {
@ -90,6 +88,25 @@ final class ApiVersion {
return this.minor >= other.minor; return this.minor >= other.minor;
} }
/**
* Returns a value indicating whether the API version has a separate cache phase, or
* caching is distributed across other stages.
* @return {@code true} if the API has a separate cache phase, {@code false} otherwise
*/
boolean hasCachePhase() {
return supports(PLATFORM_0_1);
}
/**
* Returns a value indicating whether the API version requires that the analyze phase
* must follow the restore phase, or if the reverse order is required.
* @return {@code true} if the analyze phase follows restore, {@code false} if restore
* follows analyze
*/
boolean analyzeFollowsRestore() {
return supports(PLATFORM_0_1);
}
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (this == obj) { if (this == obj) {
@ -132,4 +149,8 @@ final class ApiVersion {
} }
} }
static ApiVersion of(int major, int minor) {
return new ApiVersion(major, minor);
}
} }

@ -0,0 +1,91 @@
/*
* 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 java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.util.StringUtils;
/**
* A set of API Version numbers comprised of major and minor values.
*
* @author Scott Frederick
*/
final class ApiVersions {
/**
* The platform API versions supported by this release.
*/
static final ApiVersions SUPPORTED_PLATFORMS = new ApiVersions(ApiVersion.of(0, 1), ApiVersion.of(0, 2));
private final ApiVersion[] apiVersions;
private ApiVersions(ApiVersion... versions) {
this.apiVersions = versions;
}
/**
* Assert that the specified version is supported by these API versions.
* @param other the version to check against
*/
void assertSupports(ApiVersion other) {
for (ApiVersion apiVersion : this.apiVersions) {
if (apiVersion.supports(other)) {
return;
}
}
throw new IllegalStateException(
"Detected platform API version '" + other + "' is not included in supported versions '" + this + "'");
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
ApiVersions other = (ApiVersions) obj;
return Arrays.equals(this.apiVersions, other.apiVersions);
}
@Override
public int hashCode() {
return Arrays.hashCode(this.apiVersions);
}
@Override
public String toString() {
return StringUtils.arrayToCommaDelimitedString(this.apiVersions);
}
/**
* Factory method to parse strings into an {@link ApiVersions} instance.
* @param values the values to parse.
* @return the corresponding {@link ApiVersions}
* @throws IllegalArgumentException if any values could not be parsed
*/
static ApiVersions parse(String... values) {
List<ApiVersion> versions = Arrays.stream(values).map(ApiVersion::parse).collect(Collectors.toList());
return new ApiVersions(versions.toArray(new ApiVersion[] {}));
}
}

@ -30,7 +30,6 @@ import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.docker.type.VolumeName; import org.springframework.boot.buildpack.platform.docker.type.VolumeName;
import org.springframework.boot.buildpack.platform.io.TarArchive; import org.springframework.boot.buildpack.platform.io.TarArchive;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/** /**
* A buildpack lifecycle used to run the build {@link Phase phases} needed to package an * A buildpack lifecycle used to run the build {@link Phase phases} needed to package an
@ -40,7 +39,7 @@ import org.springframework.util.StringUtils;
*/ */
class Lifecycle implements Closeable { class Lifecycle implements Closeable {
private static final LifecycleVersion LOGGING_SUPPORTED_VERSION = LifecycleVersion.parse("0.0.5"); private static final LifecycleVersion LOGGING_MINIMUM_VERSION = LifecycleVersion.parse("0.0.5");
private final BuildLog log; private final BuildLog log;
@ -52,7 +51,9 @@ class Lifecycle implements Closeable {
private final EphemeralBuilder builder; private final EphemeralBuilder builder;
private final LifecycleVersion version; private final LifecycleVersion lifecycleVersion;
private final ApiVersion platformVersion;
private final VolumeName layersVolume; private final VolumeName layersVolume;
@ -76,17 +77,18 @@ class Lifecycle implements Closeable {
*/ */
Lifecycle(BuildLog log, DockerApi docker, BuildRequest request, ImageReference runImageReference, Lifecycle(BuildLog log, DockerApi docker, BuildRequest request, ImageReference runImageReference,
EphemeralBuilder builder) { EphemeralBuilder builder) {
checkPlatformVersion(builder);
this.log = log; this.log = log;
this.docker = docker; this.docker = docker;
this.request = request; this.request = request;
this.runImageReference = runImageReference; this.runImageReference = runImageReference;
this.builder = builder; this.builder = builder;
this.version = LifecycleVersion.parse(builder.getBuilderMetadata().getLifecycle().getVersion()); this.lifecycleVersion = LifecycleVersion.parse(builder.getBuilderMetadata().getLifecycle().getVersion());
this.platformVersion = ApiVersion.parse(builder.getBuilderMetadata().getLifecycle().getApi().getPlatform());
this.layersVolume = createRandomVolumeName("pack-layers-"); this.layersVolume = createRandomVolumeName("pack-layers-");
this.applicationVolume = createRandomVolumeName("pack-app-"); this.applicationVolume = createRandomVolumeName("pack-app-");
this.buildCacheVolume = createCacheVolumeName(request, ".build"); this.buildCacheVolume = createCacheVolumeName(request, ".build");
this.launchCacheVolume = createCacheVolumeName(request, ".launch"); this.launchCacheVolume = createCacheVolumeName(request, ".launch");
checkPlatformVersion(this.platformVersion);
} }
protected VolumeName createRandomVolumeName(String prefix) { protected VolumeName createRandomVolumeName(String prefix) {
@ -97,11 +99,8 @@ class Lifecycle implements Closeable {
return VolumeName.basedOn(request.getName(), ImageReference::toLegacyString, "pack-cache-", suffix, 6); return VolumeName.basedOn(request.getName(), ImageReference::toLegacyString, "pack-cache-", suffix, 6);
} }
private void checkPlatformVersion(EphemeralBuilder ephemeralBuilder) { private void checkPlatformVersion(ApiVersion platformVersion) {
String platformVersion = ephemeralBuilder.getBuilderMetadata().getLifecycle().getApi().getPlatform(); ApiVersions.SUPPORTED_PLATFORMS.assertSupports(platformVersion);
if (StringUtils.hasText(platformVersion)) {
ApiVersion.PLATFORM.assertSupports(ApiVersion.parse(platformVersion));
}
} }
/** /**
@ -111,16 +110,24 @@ class Lifecycle implements Closeable {
void execute() throws IOException { void execute() throws IOException {
Assert.state(!this.executed, "Lifecycle has already been executed"); Assert.state(!this.executed, "Lifecycle has already been executed");
this.executed = true; this.executed = true;
this.log.executingLifecycle(this.request, this.version, this.buildCacheVolume); this.log.executingLifecycle(this.request, this.lifecycleVersion, this.buildCacheVolume);
if (this.request.isCleanCache()) { if (this.request.isCleanCache()) {
deleteVolume(this.buildCacheVolume); deleteVolume(this.buildCacheVolume);
} }
run(detectPhase()); run(detectPhase());
run(restorePhase()); if (this.platformVersion.analyzeFollowsRestore()) {
run(analyzePhase()); run(restorePhase());
run(analyzePhase());
}
else {
run(analyzePhase());
run(restorePhase());
}
run(buildPhase()); run(buildPhase());
run(exportPhase()); run(exportPhase());
run(cachePhase()); if (this.platformVersion.hasCachePhase()) {
run(cachePhase());
}
this.log.executedLifecycle(this.request); this.log.executedLifecycle(this.request);
} }
@ -133,9 +140,10 @@ class Lifecycle implements Closeable {
} }
private Phase restorePhase() { private Phase restorePhase() {
String cacheDirArg = this.platformVersion.hasCachePhase() ? "-path" : "-cache-dir";
Phase phase = createPhase("restorer"); Phase phase = createPhase("restorer");
phase.withDaemonAccess(); phase.withDaemonAccess();
phase.withArgs("-path", Folder.CACHE); phase.withArgs(cacheDirArg, Folder.CACHE);
phase.withArgs("-layers", Folder.LAYERS); phase.withArgs("-layers", Folder.LAYERS);
phase.withLogLevelArg(); phase.withLogLevelArg();
phase.withBinds(this.buildCacheVolume, Folder.CACHE); phase.withBinds(this.buildCacheVolume, Folder.CACHE);
@ -151,7 +159,13 @@ class Lifecycle implements Closeable {
} }
phase.withArgs("-daemon"); phase.withArgs("-daemon");
phase.withArgs("-layers", Folder.LAYERS); phase.withArgs("-layers", Folder.LAYERS);
if (!this.platformVersion.hasCachePhase()) {
phase.withArgs("-cache-dir", Folder.CACHE);
}
phase.withArgs(this.request.getName()); phase.withArgs(this.request.getName());
if (!this.platformVersion.hasCachePhase()) {
phase.withBinds(this.buildCacheVolume, Folder.CACHE);
}
return phase; return phase;
} }
@ -172,8 +186,14 @@ class Lifecycle implements Closeable {
phase.withArgs("-app", Folder.APPLICATION); phase.withArgs("-app", Folder.APPLICATION);
phase.withArgs("-daemon"); phase.withArgs("-daemon");
phase.withArgs("-launch-cache", Folder.LAUNCH_CACHE); phase.withArgs("-launch-cache", Folder.LAUNCH_CACHE);
if (!this.platformVersion.hasCachePhase()) {
phase.withArgs("-cache-dir", Folder.CACHE);
}
phase.withArgs(this.request.getName()); phase.withArgs(this.request.getName());
phase.withBinds(this.launchCacheVolume, Folder.LAUNCH_CACHE); phase.withBinds(this.launchCacheVolume, Folder.LAUNCH_CACHE);
if (!this.platformVersion.hasCachePhase()) {
phase.withBinds(this.buildCacheVolume, Folder.CACHE);
}
return phase; return phase;
} }
@ -189,7 +209,7 @@ class Lifecycle implements Closeable {
private Phase createPhase(String name) { private Phase createPhase(String name) {
boolean verboseLogging = this.request.isVerboseLogging() boolean verboseLogging = this.request.isVerboseLogging()
&& this.version.isEqualOrGreaterThan(LOGGING_SUPPORTED_VERSION); && this.lifecycleVersion.isEqualOrGreaterThan(LOGGING_MINIMUM_VERSION);
Phase phase = new Phase(name, verboseLogging); Phase phase = new Phase(name, verboseLogging);
phase.withBinds(this.layersVolume, Folder.LAYERS); phase.withBinds(this.layersVolume, Folder.LAYERS);
phase.withBinds(this.applicationVolume, Folder.APPLICATION); phase.withBinds(this.applicationVolume, Folder.APPLICATION);

@ -26,6 +26,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
* Tests for {@link ApiVersion}. * Tests for {@link ApiVersion}.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Scott Frederick
*/ */
class ApiVersionTests { class ApiVersionTests {
@ -63,7 +64,7 @@ class ApiVersionTests {
void assertSupportsWhenDoesNotSupportThrowsException() { void assertSupportsWhenDoesNotSupportThrowsException() {
assertThatIllegalStateException() assertThatIllegalStateException()
.isThrownBy(() -> ApiVersion.parse("1.2").assertSupports(ApiVersion.parse("1.3"))) .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 @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}. * Tests for {@link Lifecycle}.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Scott Frederick
*/ */
class LifecycleTests { class LifecycleTests {
@ -67,43 +68,43 @@ class LifecycleTests {
private DockerApi docker; private DockerApi docker;
private Lifecycle lifecycle;
private Map<String, ContainerConfig> configs = new LinkedHashMap<>(); private Map<String, ContainerConfig> configs = new LinkedHashMap<>();
private Map<String, ContainerContent> content = new LinkedHashMap<>(); private Map<String, ContainerContent> content = new LinkedHashMap<>();
@BeforeEach @BeforeEach
void setup() throws Exception { void setup() {
this.out = new TestPrintStream(); this.out = new TestPrintStream();
this.docker = mockDockerApi(); this.docker = mockDockerApi();
BuildRequest request = getTestRequest();
this.lifecycle = createLifecycle(request);
} }
private DockerApi mockDockerApi() { @Test
DockerApi docker = mock(DockerApi.class); void executeExecutesPhasesWithCacherPhase() throws Exception {
ImageApi imageApi = mock(ImageApi.class); given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
ContainerApi containerApi = mock(ContainerApi.class); given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
VolumeApi volumeApi = mock(VolumeApi.class); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
given(docker.image()).willReturn(imageApi); createLifecycle("0.5").execute();
given(docker.container()).willReturn(containerApi); assertPhaseWasRun("detector", withExpectedConfig("lifecycle-detector.json"));
given(docker.volume()).willReturn(volumeApi); assertPhaseWasRun("restorer", withExpectedConfig("lifecycle-restorer-0.1.json"));
return docker; assertPhaseWasRun("analyzer", withExpectedConfig("lifecycle-analyzer-0.1.json"));
assertPhaseWasRun("builder", withExpectedConfig("lifecycle-builder.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 @Test
void executeExecutesPhases() throws Exception { void executeExecutesPhasesWithEmbeddedCaching() throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
this.lifecycle.execute(); createLifecycle("0.6").execute();
assertPhaseWasRun("detector", withExpectedConfig("lifecycle-detector.json")); assertPhaseWasRun("detector", withExpectedConfig("lifecycle-detector.json"));
assertPhaseWasRun("restorer", withExpectedConfig("lifecycle-restorer.json")); assertPhaseWasRun("analyzer", withExpectedConfig("lifecycle-analyzer-0.2.json"));
assertPhaseWasRun("analyzer", withExpectedConfig("lifecycle-analyzer.json")); assertPhaseWasRun("restorer", withExpectedConfig("lifecycle-restorer-0.2.json"));
assertPhaseWasRun("builder", withExpectedConfig("lifecycle-builder.json")); assertPhaseWasRun("builder", withExpectedConfig("lifecycle-builder.json"));
assertPhaseWasRun("exporter", withExpectedConfig("lifecycle-exporter.json")); assertPhaseWasRun("exporter", withExpectedConfig("lifecycle-exporter-0.2.json"));
assertPhaseWasRun("cacher", withExpectedConfig("lifecycle-cacher.json")); assertPhaseWasNotRun("cacher");
assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
} }
@ -112,7 +113,7 @@ class LifecycleTests {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
this.lifecycle.execute(); createLifecycle().execute();
assertThat(this.content).hasSize(1); 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())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
this.lifecycle.execute(); Lifecycle lifecycle = createLifecycle();
assertThatIllegalStateException().isThrownBy(this.lifecycle::execute) lifecycle.execute();
assertThatIllegalStateException().isThrownBy(lifecycle::execute)
.withMessage("Lifecycle has already been executed"); .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())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(9, null)); 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"); .withMessage("Builder lifecycle 'detector' failed with status code 9");
} }
@ -148,27 +150,50 @@ class LifecycleTests {
@Test @Test
void closeClearsVolumes() throws Exception { 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-layers-aaaaaaaaaa"), true);
verify(this.docker.volume()).delete(VolumeName.of("pack-app-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() { private BuildRequest getTestRequest() {
TarArchive content = mock(TarArchive.class); TarArchive content = mock(TarArchive.class);
ImageReference name = ImageReference.of("my-application"); ImageReference name = ImageReference.of("my-application");
BuildRequest request = BuildRequest.of(name, (owner) -> content); return BuildRequest.of(name, (owner) -> content);
return request; }
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 { 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"), return new TestLifecycle(BuildLog.to(this.out), this.docker, request, ImageReference.of("cloudfoundry/run"),
builder); builder);
} }
private EphemeralBuilder mockEphemeralBuilder() throws IOException { private EphemeralBuilder mockEphemeralBuilder(String version) throws IOException {
EphemeralBuilder builder = mock(EphemeralBuilder.class); 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)); BuilderMetadata metadata = BuilderMetadata.fromJson(new String(metadataContent, StandardCharsets.UTF_8));
given(builder.getName()).willReturn(ImageReference.of("pack.local/ephemeral-builder")); given(builder.getName()).willReturn(ImageReference.of("pack.local/ephemeral-builder"));
given(builder.getBuilderMetadata()).willReturn(metadata); given(builder.getBuilderMetadata()).willReturn(metadata);
@ -201,6 +226,11 @@ class LifecycleTests {
configConsumer.accept(this.configs.get(containerReference.toString())); 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) { private IOConsumer<ContainerConfig> withExpectedConfig(String name) {
return (config) -> { return (config) -> {
InputStream in = getClass().getResourceAsStream(name); 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" ]
}
}
Loading…
Cancel
Save