Set platform API version when invoking image builder
The CNB specifications allow builders to support multiple platform API versions. The supported versions are published in the builder image metadata as an array of version numbers, while a single supported version number was published in earlier builder metadata. These changes read the supported versions from the builder metadata and fall back to the single version if the array is not present. A CNB_PLATFORM_API environment variable is set on each lifecycle phase invocation to request a specific version as recommended in the CNB platform spec. Fixes gh-23682
This commit is contained in:
@@ -64,7 +64,7 @@ class ApiVersionTests {
|
||||
void assertSupportsWhenDoesNotSupportThrowsException() {
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> ApiVersion.parse("1.2").assertSupports(ApiVersion.parse("1.3")))
|
||||
.withMessage("Detected platform API version 'v1.3' does not match supported version 'v1.2'");
|
||||
.withMessage("Detected platform API version '1.3' does not match supported version '1.2'");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -99,7 +99,7 @@ class ApiVersionTests {
|
||||
|
||||
@Test
|
||||
void toStringReturnsString() {
|
||||
assertThat(ApiVersion.parse("1.2").toString()).isEqualTo("v1.2");
|
||||
assertThat(ApiVersion.parse("1.2").toString()).isEqualTo("1.2");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -29,20 +29,45 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
class ApiVersionsTests {
|
||||
|
||||
@Test
|
||||
void assertSupportsWhenAllSupports() {
|
||||
ApiVersions.parse("1.1", "2.2").assertSupports(ApiVersion.parse("1.0"));
|
||||
void findsLatestWhenOneMatchesMajor() {
|
||||
ApiVersion version = ApiVersions.parse("1.1", "2.2").findLatestSupported("1.0");
|
||||
assertThat(version).isEqualTo(ApiVersion.parse("1.1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void assertSupportsWhenDoesNoneSupportedThrowsException() {
|
||||
void findsLatestWhenOneMatchesWithReleaseVersions() {
|
||||
ApiVersion version = ApiVersions.parse("1.1", "1.2").findLatestSupported("1.1");
|
||||
assertThat(version).isEqualTo(ApiVersion.parse("1.2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void findsLatestWhenOneMatchesWithPreReleaseVersions() {
|
||||
ApiVersion version = ApiVersions.parse("0.2", "0.3").findLatestSupported("0.2");
|
||||
assertThat(version).isEqualTo(ApiVersion.parse("0.2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void findsLatestWhenMultipleMatchesWithReleaseVersions() {
|
||||
ApiVersion version = ApiVersions.parse("1.1", "1.2").findLatestSupported("1.1", "1.2");
|
||||
assertThat(version).isEqualTo(ApiVersion.parse("1.2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void findsLatestWhenMultipleMatchesWithPreReleaseVersions() {
|
||||
ApiVersion version = ApiVersions.parse("0.2", "0.3").findLatestSupported("0.2", "0.3");
|
||||
assertThat(version).isEqualTo(ApiVersion.parse("0.3"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void findLatestWhenNoneSupportedThrowsException() {
|
||||
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'");
|
||||
.isThrownBy(() -> ApiVersions.parse("1.1", "1.2").findLatestSupported("1.3", "1.4")).withMessage(
|
||||
"Detected platform API versions '1.3,1.4' are not included in supported versions '1.1,1.2'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toStringReturnsString() {
|
||||
assertThat(ApiVersions.parse("1.1", "2.2", "3.3").toString()).isEqualTo("v1.1,v2.2,v3.3");
|
||||
assertThat(ApiVersions.parse("1.1", "2.2", "3.3").toString()).isEqualTo("1.1,2.2,3.3");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -16,8 +16,12 @@
|
||||
|
||||
package org.springframework.boot.buildpack.platform.build;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -76,6 +80,28 @@ class BuilderMetadataTests extends AbstractJsonTests {
|
||||
.withMessage("No 'io.buildpacks.builder.metadata' label found in image config labels 'alpha'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromJsonLoadsMetadataWithoutSupportedApis() throws IOException {
|
||||
BuilderMetadata metadata = BuilderMetadata.fromJson(getContentAsString("builder-metadata.json"));
|
||||
assertThat(metadata.getStack().getRunImage().getImage()).isEqualTo("cloudfoundry/run:base-cnb");
|
||||
assertThat(metadata.getStack().getRunImage().getMirrors()).isEmpty();
|
||||
assertThat(metadata.getLifecycle().getVersion()).isEqualTo("0.7.2");
|
||||
assertThat(metadata.getLifecycle().getApi().getBuildpack()).isEqualTo("0.2");
|
||||
assertThat(metadata.getLifecycle().getApi().getPlatform()).isEqualTo("0.3");
|
||||
assertThat(metadata.getLifecycle().getApis().getBuildpack()).isNull();
|
||||
assertThat(metadata.getLifecycle().getApis().getPlatform()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromJsonLoadsMetadataWithSupportedApis() throws IOException {
|
||||
BuilderMetadata metadata = BuilderMetadata.fromJson(getContentAsString("builder-metadata-supported-apis.json"));
|
||||
assertThat(metadata.getLifecycle().getVersion()).isEqualTo("0.7.2");
|
||||
assertThat(metadata.getLifecycle().getApi().getBuildpack()).isEqualTo("0.2");
|
||||
assertThat(metadata.getLifecycle().getApi().getPlatform()).isEqualTo("0.4");
|
||||
assertThat(metadata.getLifecycle().getApis().getBuildpack()).containsExactly("0.1", "0.2", "0.3");
|
||||
assertThat(metadata.getLifecycle().getApis().getPlatform()).containsExactly("0.3", "0.4");
|
||||
}
|
||||
|
||||
@Test
|
||||
void copyWithUpdatedCreatedByReturnsNewMetadata() throws IOException {
|
||||
Image image = Image.of(getContent("image.json"));
|
||||
@@ -98,4 +124,9 @@ class BuilderMetadataTests extends AbstractJsonTests {
|
||||
.isEqualTo(metadata.getStack().getRunImage().getImage());
|
||||
}
|
||||
|
||||
private String getContentAsString(String name) {
|
||||
return new BufferedReader(new InputStreamReader(getContent(name), StandardCharsets.UTF_8)).lines()
|
||||
.collect(Collectors.joining("\n"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -129,6 +129,35 @@ class LifecycleTests {
|
||||
verify(this.docker.volume()).delete(name, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeWhenPlatformApiNotSupportedThrowsException() throws Exception {
|
||||
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
|
||||
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
|
||||
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> createLifecycle("builder-metadata-unsupported-api.json").execute())
|
||||
.withMessage("Detected platform API versions '0.2' are not included in supported versions '0.3'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeWhenMultiplePlatformApisNotSupportedThrowsException() throws Exception {
|
||||
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
|
||||
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
|
||||
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> createLifecycle("builder-metadata-unsupported-apis.json").execute())
|
||||
.withMessage("Detected platform API versions '0.4,0.5' are not included in supported versions '0.3'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeWhenMultiplePlatformApisSupportedExecutesPhase() 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("builder-metadata-supported-apis.json").execute();
|
||||
assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator.json"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void closeClearsVolumes() throws Exception {
|
||||
createLifecycle().close();
|
||||
@@ -159,12 +188,25 @@ class LifecycleTests {
|
||||
|
||||
private Lifecycle createLifecycle(BuildRequest request) throws IOException {
|
||||
EphemeralBuilder builder = mockEphemeralBuilder();
|
||||
return new TestLifecycle(BuildLog.to(this.out), this.docker, request, builder);
|
||||
return createLifecycle(request, builder);
|
||||
}
|
||||
|
||||
private Lifecycle createLifecycle(String builderMetadata) throws IOException {
|
||||
EphemeralBuilder builder = mockEphemeralBuilder(builderMetadata);
|
||||
return createLifecycle(getTestRequest(), builder);
|
||||
}
|
||||
|
||||
private Lifecycle createLifecycle(BuildRequest request, EphemeralBuilder ephemeralBuilder) {
|
||||
return new TestLifecycle(BuildLog.to(this.out), this.docker, request, ephemeralBuilder);
|
||||
}
|
||||
|
||||
private EphemeralBuilder mockEphemeralBuilder() throws IOException {
|
||||
return mockEphemeralBuilder("builder-metadata.json");
|
||||
}
|
||||
|
||||
private EphemeralBuilder mockEphemeralBuilder(String builderMetadata) throws IOException {
|
||||
EphemeralBuilder builder = mock(EphemeralBuilder.class);
|
||||
byte[] metadataContent = FileCopyUtils.copyToByteArray(getClass().getResourceAsStream("builder-metadata.json"));
|
||||
byte[] metadataContent = FileCopyUtils.copyToByteArray(getClass().getResourceAsStream(builderMetadata));
|
||||
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);
|
||||
|
||||
@@ -117,4 +117,18 @@ class PhaseTests {
|
||||
verifyNoMoreInteractions(update);
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyWhenWithEnvUpdatesConfigurationWithEnv() {
|
||||
Phase phase = new Phase("test", false);
|
||||
phase.withEnv("name1", "value1");
|
||||
phase.withEnv("name2", "value2");
|
||||
Update update = mock(Update.class);
|
||||
phase.apply(update);
|
||||
verify(update).withCommand("/cnb/lifecycle/test");
|
||||
verify(update).withLabel("author", "spring-boot");
|
||||
verify(update).withEnv("name1", "value1");
|
||||
verify(update).withEnv("name2", "value2");
|
||||
verifyNoMoreInteractions(update);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
||||
* Tests for {@link ContainerConfig}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class ContainerConfigTests extends AbstractJsonTests {
|
||||
|
||||
@@ -56,6 +57,8 @@ class ContainerConfigTests extends AbstractJsonTests {
|
||||
update.withArgs("-h");
|
||||
update.withLabel("spring", "boot");
|
||||
update.withBind("bind-source", "bind-dest");
|
||||
update.withEnv("name1", "value1");
|
||||
update.withEnv("name2", "value2");
|
||||
});
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
containerConfig.writeTo(outputStream);
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"description": "Ubuntu bionic base image with buildpacks for Java, NodeJS and Golang",
|
||||
"buildpacks": [
|
||||
{
|
||||
"id": "org.cloudfoundry.springboot",
|
||||
"version": "v1.2.13"
|
||||
}
|
||||
],
|
||||
"stack": {
|
||||
"runImage": {
|
||||
"image": "cloudfoundry/run:base-cnb",
|
||||
"mirrors": null
|
||||
}
|
||||
},
|
||||
"lifecycle": {
|
||||
"version": "0.7.2",
|
||||
"api": {
|
||||
"buildpack": "0.2",
|
||||
"platform": "0.4"
|
||||
},
|
||||
"apis": {
|
||||
"buildpack": {
|
||||
"deprecated": [],
|
||||
"supported": [
|
||||
"0.1",
|
||||
"0.2",
|
||||
"0.3"
|
||||
]
|
||||
},
|
||||
"platform": {
|
||||
"deprecated": [],
|
||||
"supported": [
|
||||
"0.3",
|
||||
"0.4"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"createdBy": {
|
||||
"name": "Pack CLI",
|
||||
"version": "v0.9.0 (git sha: d42c384a39f367588f2653f2a99702db910e5ad7)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"description": "Ubuntu bionic base image with buildpacks for Java, NodeJS and Golang",
|
||||
"buildpacks": [
|
||||
{
|
||||
"id": "org.cloudfoundry.springboot",
|
||||
"version": "v1.2.13"
|
||||
}
|
||||
],
|
||||
"stack": {
|
||||
"runImage": {
|
||||
"image": "cloudfoundry/run:base-cnb",
|
||||
"mirrors": null
|
||||
}
|
||||
},
|
||||
"lifecycle": {
|
||||
"version": "0.7.2",
|
||||
"api": {
|
||||
"buildpack": "0.2",
|
||||
"platform": "0.2"
|
||||
}
|
||||
},
|
||||
"createdBy": {
|
||||
"name": "Pack CLI",
|
||||
"version": "v0.9.0 (git sha: d42c384a39f367588f2653f2a99702db910e5ad7)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"description": "Ubuntu bionic base image with buildpacks for Java, NodeJS and Golang",
|
||||
"buildpacks": [
|
||||
{
|
||||
"id": "org.cloudfoundry.springboot",
|
||||
"version": "v1.2.13"
|
||||
}
|
||||
],
|
||||
"stack": {
|
||||
"runImage": {
|
||||
"image": "cloudfoundry/run:base-cnb",
|
||||
"mirrors": null
|
||||
}
|
||||
},
|
||||
"lifecycle": {
|
||||
"version": "0.7.2",
|
||||
"api": {
|
||||
"buildpack": "0.2",
|
||||
"platform": "0.3"
|
||||
},
|
||||
"apis": {
|
||||
"buildpack": {
|
||||
"deprecated": [],
|
||||
"supported": [
|
||||
"0.1",
|
||||
"0.2",
|
||||
"0.3"
|
||||
]
|
||||
},
|
||||
"platform": {
|
||||
"deprecated": [],
|
||||
"supported": [
|
||||
"0.4",
|
||||
"0.5"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"createdBy": {
|
||||
"name": "Pack CLI",
|
||||
"version": "v0.9.0 (git sha: d42c384a39f367588f2653f2a99702db910e5ad7)"
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
"User" : "root",
|
||||
"Image" : "pack.local/ephemeral-builder",
|
||||
"Cmd" : [ "/cnb/lifecycle/creator", "-app", "/workspace", "-platform", "/platform", "-run-image", "docker.io/cloudfoundry/run:latest", "-layers", "/layers", "-cache-dir", "/cache", "-launch-cache", "/launch-cache", "-daemon", "-skip-restore", "docker.io/library/my-application:latest" ],
|
||||
"Env" : [ "CNB_PLATFORM_API=0.3" ],
|
||||
"Labels" : {
|
||||
"author" : "spring-boot"
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"User" : "root",
|
||||
"Image" : "pack.local/ephemeral-builder",
|
||||
"Cmd" : [ "/cnb/lifecycle/creator", "-app", "/workspace", "-platform", "/platform", "-run-image", "docker.io/cloudfoundry/run:latest", "-layers", "/layers", "-cache-dir", "/cache", "-launch-cache", "/launch-cache", "-daemon", "docker.io/library/my-application:latest" ],
|
||||
"Env" : [ "CNB_PLATFORM_API=0.3" ],
|
||||
"Labels" : {
|
||||
"author" : "spring-boot"
|
||||
},
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
"-l",
|
||||
"-h"
|
||||
],
|
||||
"Env": [
|
||||
"name1=value1",
|
||||
"name2=value2"
|
||||
],
|
||||
"Labels": {
|
||||
"spring": "boot"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user