From d07062652e9b539e76c4ea43baf81496dc34ab8b Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Wed, 5 Feb 2020 17:10:26 -0600 Subject: [PATCH] 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 --- .../buildpack/platform/build/ApiVersion.java | 35 ++- .../buildpack/platform/build/ApiVersions.java | 91 +++++++ .../buildpack/platform/build/Lifecycle.java | 52 ++-- .../platform/build/ApiVersionTests.java | 3 +- .../platform/build/ApiVersionsTests.java | 57 +++++ .../platform/build/LifecycleTests.java | 90 ++++--- ...json => builder-metadata-version-0.5.json} | 0 .../build/builder-metadata-version-0.6.json | 222 ++++++++++++++++++ ...lyzer.json => lifecycle-analyzer-0.1.json} | 0 .../build/lifecycle-analyzer-0.2.json | 11 + ...orter.json => lifecycle-exporter-0.1.json} | 0 .../build/lifecycle-exporter-0.2.json | 11 + ...torer.json => lifecycle-restorer-0.1.json} | 0 .../build/lifecycle-restorer-0.2.json | 11 + 14 files changed, 529 insertions(+), 54 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ApiVersions.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ApiVersionsTests.java rename spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/{builder-metadata.json => builder-metadata-version-0.5.json} (100%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/builder-metadata-version-0.6.json rename spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/{lifecycle-analyzer.json => lifecycle-analyzer-0.1.json} (100%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-analyzer-0.2.json rename spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/{lifecycle-exporter.json => lifecycle-exporter-0.1.json} (100%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-exporter-0.2.json rename spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/{lifecycle-restorer.json => lifecycle-restorer-0.1.json} (100%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-restorer-0.2.json diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ApiVersion.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ApiVersion.java index fc03474503..fdac62b48a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ApiVersion.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ApiVersion.java @@ -22,16 +22,14 @@ import java.util.regex.Pattern; 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 Scott Frederick */ final class ApiVersion { - /** - * The platform API version supported by this release. - */ - static final ApiVersion PLATFORM = new ApiVersion(0, 1); + private static final ApiVersion PLATFORM_0_1 = new ApiVersion(0, 1); private static final Pattern PATTERN = Pattern.compile("^v?(\\d+)\\.(\\d*)$"); @@ -68,7 +66,7 @@ final class ApiVersion { void assertSupports(ApiVersion other) { if (!supports(other)) { 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 * same major version and a minor that is equal or greater. * @param other the version to check against - * @return of the specified API is supported + * @return if the specified API is supported * @see #assertSupports(ApiVersion) */ boolean supports(ApiVersion other) { @@ -90,6 +88,25 @@ final class ApiVersion { 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 public boolean equals(Object obj) { if (this == obj) { @@ -132,4 +149,8 @@ final class ApiVersion { } } + static ApiVersion of(int major, int minor) { + return new ApiVersion(major, minor); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ApiVersions.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ApiVersions.java new file mode 100644 index 0000000000..0749e4552a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ApiVersions.java @@ -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 versions = Arrays.stream(values).map(ApiVersion::parse).collect(Collectors.toList()); + return new ApiVersions(versions.toArray(new ApiVersion[] {})); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java index 6db3d556b0..e6343f2b30 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java @@ -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.io.TarArchive; 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 @@ -40,7 +39,7 @@ import org.springframework.util.StringUtils; */ 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; @@ -52,7 +51,9 @@ class Lifecycle implements Closeable { private final EphemeralBuilder builder; - private final LifecycleVersion version; + private final LifecycleVersion lifecycleVersion; + + private final ApiVersion platformVersion; private final VolumeName layersVolume; @@ -76,17 +77,18 @@ class Lifecycle implements Closeable { */ Lifecycle(BuildLog log, DockerApi docker, BuildRequest request, ImageReference runImageReference, EphemeralBuilder builder) { - checkPlatformVersion(builder); this.log = log; this.docker = docker; this.request = request; this.runImageReference = runImageReference; 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.applicationVolume = createRandomVolumeName("pack-app-"); this.buildCacheVolume = createCacheVolumeName(request, ".build"); this.launchCacheVolume = createCacheVolumeName(request, ".launch"); + checkPlatformVersion(this.platformVersion); } protected VolumeName createRandomVolumeName(String prefix) { @@ -97,11 +99,8 @@ class Lifecycle implements Closeable { return VolumeName.basedOn(request.getName(), ImageReference::toLegacyString, "pack-cache-", suffix, 6); } - private void checkPlatformVersion(EphemeralBuilder ephemeralBuilder) { - String platformVersion = ephemeralBuilder.getBuilderMetadata().getLifecycle().getApi().getPlatform(); - if (StringUtils.hasText(platformVersion)) { - ApiVersion.PLATFORM.assertSupports(ApiVersion.parse(platformVersion)); - } + private void checkPlatformVersion(ApiVersion platformVersion) { + ApiVersions.SUPPORTED_PLATFORMS.assertSupports(platformVersion); } /** @@ -111,16 +110,24 @@ class Lifecycle implements Closeable { void execute() throws IOException { Assert.state(!this.executed, "Lifecycle has already been executed"); 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()) { deleteVolume(this.buildCacheVolume); } run(detectPhase()); - run(restorePhase()); - run(analyzePhase()); + if (this.platformVersion.analyzeFollowsRestore()) { + run(restorePhase()); + run(analyzePhase()); + } + else { + run(analyzePhase()); + run(restorePhase()); + } run(buildPhase()); run(exportPhase()); - run(cachePhase()); + if (this.platformVersion.hasCachePhase()) { + run(cachePhase()); + } this.log.executedLifecycle(this.request); } @@ -133,9 +140,10 @@ class Lifecycle implements Closeable { } private Phase restorePhase() { + String cacheDirArg = this.platformVersion.hasCachePhase() ? "-path" : "-cache-dir"; Phase phase = createPhase("restorer"); phase.withDaemonAccess(); - phase.withArgs("-path", Folder.CACHE); + phase.withArgs(cacheDirArg, Folder.CACHE); phase.withArgs("-layers", Folder.LAYERS); phase.withLogLevelArg(); phase.withBinds(this.buildCacheVolume, Folder.CACHE); @@ -151,7 +159,13 @@ class Lifecycle implements Closeable { } phase.withArgs("-daemon"); phase.withArgs("-layers", Folder.LAYERS); + if (!this.platformVersion.hasCachePhase()) { + phase.withArgs("-cache-dir", Folder.CACHE); + } phase.withArgs(this.request.getName()); + if (!this.platformVersion.hasCachePhase()) { + phase.withBinds(this.buildCacheVolume, Folder.CACHE); + } return phase; } @@ -172,8 +186,14 @@ class Lifecycle implements Closeable { phase.withArgs("-app", Folder.APPLICATION); phase.withArgs("-daemon"); phase.withArgs("-launch-cache", Folder.LAUNCH_CACHE); + if (!this.platformVersion.hasCachePhase()) { + phase.withArgs("-cache-dir", Folder.CACHE); + } phase.withArgs(this.request.getName()); phase.withBinds(this.launchCacheVolume, Folder.LAUNCH_CACHE); + if (!this.platformVersion.hasCachePhase()) { + phase.withBinds(this.buildCacheVolume, Folder.CACHE); + } return phase; } @@ -189,7 +209,7 @@ class Lifecycle implements Closeable { private Phase createPhase(String name) { boolean verboseLogging = this.request.isVerboseLogging() - && this.version.isEqualOrGreaterThan(LOGGING_SUPPORTED_VERSION); + && this.lifecycleVersion.isEqualOrGreaterThan(LOGGING_MINIMUM_VERSION); Phase phase = new Phase(name, verboseLogging); phase.withBinds(this.layersVolume, Folder.LAYERS); phase.withBinds(this.applicationVolume, Folder.APPLICATION); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ApiVersionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ApiVersionTests.java index 47af377570..e2fd3b106f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ApiVersionTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ApiVersionTests.java @@ -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 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ApiVersionsTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ApiVersionsTests.java new file mode 100644 index 0000000000..d516d25f44 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ApiVersionsTests.java @@ -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); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java index 0bc249defd..971de6d738 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java @@ -60,6 +60,7 @@ import static org.mockito.Mockito.verify; * Tests for {@link Lifecycle}. * * @author Phillip Webb + * @author Scott Frederick */ class LifecycleTests { @@ -67,43 +68,43 @@ class LifecycleTests { private DockerApi docker; - private Lifecycle lifecycle; - private Map configs = new LinkedHashMap<>(); private Map 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 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)); + createLifecycle("0.5").execute(); + assertPhaseWasRun("detector", withExpectedConfig("lifecycle-detector.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-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 executeExecutesPhases() throws Exception { + 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)); - this.lifecycle.execute(); + createLifecycle("0.6").execute(); assertPhaseWasRun("detector", withExpectedConfig("lifecycle-detector.json")); - assertPhaseWasRun("restorer", withExpectedConfig("lifecycle-restorer.json")); - assertPhaseWasRun("analyzer", withExpectedConfig("lifecycle-analyzer.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.json")); - assertPhaseWasRun("cacher", withExpectedConfig("lifecycle-cacher.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'"); } @@ -112,7 +113,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(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 withExpectedConfig(String name) { return (config) -> { InputStream in = getClass().getResourceAsStream(name); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/builder-metadata.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/builder-metadata-version-0.5.json similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/builder-metadata.json rename to spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/builder-metadata-version-0.5.json diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/builder-metadata-version-0.6.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/builder-metadata-version-0.6.json new file mode 100644 index 0000000000..1844135420 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/builder-metadata-version-0.6.json @@ -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" + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-analyzer.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-analyzer-0.1.json similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-analyzer.json rename to spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-analyzer-0.1.json diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-analyzer-0.2.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-analyzer-0.2.json new file mode 100644 index 0000000000..5b86308adf --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-analyzer-0.2.json @@ -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" ] + } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-exporter.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-exporter-0.1.json similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-exporter.json rename to spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-exporter-0.1.json diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-exporter-0.2.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-exporter-0.2.json new file mode 100644 index 0000000000..840eea6c66 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-exporter-0.2.json @@ -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" ] + } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-restorer.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-restorer-0.1.json similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-restorer.json rename to spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-restorer-0.1.json diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-restorer-0.2.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-restorer-0.2.json new file mode 100644 index 0000000000..6aab9fb629 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-restorer-0.2.json @@ -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" ] + } +} \ No newline at end of file