From f54f784f80202a9d9a030950ab29990c8749fda7 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Thu, 18 Feb 2021 17:28:25 -0600 Subject: [PATCH] Add buildpack option for image building This commit adds configuration to the Maven and Gradle plugins to allow a list of buildpacks to be provided to the image building goal and task. Fixes gh-21722 --- .../build.gradle | 1 + .../platform/build/BuildRequest.java | 60 ++++-- .../buildpack/platform/build/Builder.java | 160 ++++++++++------ .../platform/build/BuilderBuildpack.java | 117 ++++++++++++ .../platform/build/BuilderMetadata.java | 25 ++- .../buildpack/platform/build/Buildpack.java | 45 +++++ .../platform/build/BuildpackCoordinates.java | 140 ++++++++++++++ .../platform/build/BuildpackMetadata.java | 121 ++++++++++++ .../platform/build/BuildpackReference.java | 94 ++++++++++ .../platform/build/BuildpackResolver.java | 36 ++++ .../build/BuildpackResolverContext.java | 55 ++++++ .../platform/build/BuildpackResolvers.java | 80 ++++++++ .../buildpack/platform/build/Buildpacks.java | 88 +++++++++ .../platform/build/DirectoryBuildpack.java | 136 ++++++++++++++ .../platform/build/EphemeralBuilder.java | 8 +- .../platform/build/ImageBuildpack.java | 143 ++++++++++++++ .../buildpack/platform/build/ImageType.java | 9 +- .../platform/build/TarGzipBuildpack.java | 118 ++++++++++++ .../buildpack/platform/docker/DockerApi.java | 32 +++- .../boot/buildpack/platform/io/Content.java | 7 +- .../platform/io/FilePermissions.java | 68 +++++++ .../buildpack/platform/io/IOBiConsumer.java | 40 ++++ .../boot/buildpack/platform/io/Layout.java | 21 ++- .../platform/io/TarLayoutWriter.java | 11 +- .../platform/build/BuildRequestTests.java | 20 +- .../platform/build/BuilderBuildpackTests.java | 115 ++++++++++++ .../platform/build/BuilderMetadataTests.java | 20 +- .../platform/build/BuilderTests.java | 36 ++-- .../build/BuildpackCoordinatesTests.java | 172 +++++++++++++++++ .../build/BuildpackMetadataTests.java | 79 ++++++++ .../build/BuildpackReferenceTests.java | 96 ++++++++++ .../build/BuildpackResolversTests.java | 103 +++++++++++ .../platform/build/BuildpacksTests.java | 111 +++++++++++ .../build/DirectoryBuildpackTests.java | 174 ++++++++++++++++++ .../platform/build/EphemeralBuilderTests.java | 57 +++++- .../platform/build/ImageBuildpackTests.java | 138 ++++++++++++++ .../platform/build/TarGzipBuildpackTests.java | 94 ++++++++++ .../platform/build/TestBuildpack.java | 57 ++++++ .../buildpack/platform/build/TestTarGzip.java | 122 ++++++++++++ .../platform/docker/DockerApiTests.java | 46 ++++- .../docker/ProgressUpdateEventTests.java | 3 +- .../platform/io/FilePermissionsTests.java | 53 ++++++ .../platform/io/TarLayoutWriterTests.java | 7 +- .../platform/json/AbstractJsonTests.java | 12 +- .../platform/build/builder-metadata.json | 172 +++++++++++------ .../platform/build/buildpack-image.json | 78 ++++++++ .../platform/build/buildpack-metadata.json | 13 ++ .../buildpack/platform/build/buildpack.toml | 8 + .../boot/buildpack/platform/build/image.json | 2 +- .../boot/buildpack/platform/build/layer.tar | Bin 0 -> 3072 bytes .../platform/build/order-versions.toml | 15 ++ .../boot/buildpack/platform/build/order.toml | 15 ++ .../boot/buildpack/platform/docker/export.tar | Bin 0 -> 22016 bytes .../docs/asciidoc/packaging-oci-image.adoc | 66 +++++++ .../boot-build-image-buildpacks.gradle | 16 ++ .../boot-build-image-buildpacks.gradle.kts | 20 ++ .../gradle/tasks/bundling/BootBuildImage.java | 54 +++++- .../BootBuildImageIntegrationTests.java | 171 +++++++++++++++-- .../tasks/bundling/BootBuildImageTests.java | 31 +++- .../boot/gradle/testkit/GradleBuild.java | 5 +- ...buildsImageWithBuildpackFromBuilder.gradle | 11 ++ ...ildsImageWithBuildpackFromDirectory.gradle | 11 ++ ...s-buildsImageWithBuildpackFromImage.gradle | 11 ++ ...buildsImageWithBuildpackFromTarGzip.gradle | 11 ++ ...ests-failsWithBuildpackNotInBuilder.gradle | 11 ++ .../docs/asciidoc/packaging-oci-image.adoc | 70 +++++++ .../boot/maven/BuildImageTests.java | 28 ++- .../build-image-bad-buildpack/pom.xml | 36 ++++ .../main/java/org/test/SampleApplication.java | 28 +++ .../build-image-custom-buildpacks/pom.xml | 36 ++++ .../main/java/org/test/SampleApplication.java | 28 +++ .../boot/maven/BuildImageMojo.java | 4 +- .../org/springframework/boot/maven/Image.java | 11 +- .../boot/maven/ImageTests.java | 14 +- 74 files changed, 3895 insertions(+), 211 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuilderBuildpack.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Buildpack.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackCoordinates.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackMetadata.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackReference.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackResolver.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackResolverContext.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackResolvers.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Buildpacks.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/DirectoryBuildpack.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageBuildpack.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/TarGzipBuildpack.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/FilePermissions.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/IOBiConsumer.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderBuildpackTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackCoordinatesTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackMetadataTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackReferenceTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackResolversTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpacksTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/DirectoryBuildpackTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ImageBuildpackTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/TarGzipBuildpackTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/TestBuildpack.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/TestTarGzip.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/FilePermissionsTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/buildpack-image.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/buildpack-metadata.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/buildpack.toml create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/layer.tar create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/order-versions.toml create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/order.toml create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/export.tar create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-buildpacks.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-buildpacks.gradle.kts create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBuildpackFromBuilder.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBuildpackFromDirectory.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBuildpackFromImage.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBuildpackFromTarGzip.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWithBuildpackNotInBuilder.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bad-buildpack/pom.xml create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bad-buildpack/src/main/java/org/test/SampleApplication.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-custom-buildpacks/pom.xml create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-custom-buildpacks/src/main/java/org/test/SampleApplication.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/build.gradle index 56a9bc77170..492dec0c236 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/build.gradle @@ -13,6 +13,7 @@ dependencies { api("org.apache.commons:commons-compress:1.19") api("org.apache.httpcomponents:httpclient") api("org.springframework:spring-core") + api("org.tomlj:tomlj:1.0.0") testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) testImplementation("com.jayway.jsonpath:json-path") diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java index fc6b744e234..8ce587b3852 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -17,8 +17,10 @@ package org.springframework.boot.buildpack.platform.build; import java.io.File; +import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.function.Function; @@ -61,6 +63,8 @@ public class BuildRequest { private final boolean publish; + private final List buildpacks; + BuildRequest(ImageReference name, Function applicationContent) { Assert.notNull(name, "Name must not be null"); Assert.notNull(applicationContent, "ApplicationContent must not be null"); @@ -74,11 +78,12 @@ public class BuildRequest { this.pullPolicy = PullPolicy.ALWAYS; this.publish = false; this.creator = Creator.withVersion(""); + this.buildpacks = Collections.emptyList(); } BuildRequest(ImageReference name, Function applicationContent, ImageReference builder, ImageReference runImage, Creator creator, Map env, boolean cleanCache, - boolean verboseLogging, PullPolicy pullPolicy, boolean publish) { + boolean verboseLogging, PullPolicy pullPolicy, boolean publish, List buildpacks) { this.name = name; this.applicationContent = applicationContent; this.builder = builder; @@ -89,6 +94,7 @@ public class BuildRequest { this.verboseLogging = verboseLogging; this.pullPolicy = pullPolicy; this.publish = publish; + this.buildpacks = buildpacks; } /** @@ -99,7 +105,8 @@ public class BuildRequest { public BuildRequest withBuilder(ImageReference builder) { Assert.notNull(builder, "Builder must not be null"); return new BuildRequest(this.name, this.applicationContent, builder.inTaggedOrDigestForm(), this.runImage, - this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish); + this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, + this.buildpacks); } /** @@ -109,7 +116,8 @@ public class BuildRequest { */ public BuildRequest withRunImage(ImageReference runImageName) { return new BuildRequest(this.name, this.applicationContent, this.builder, runImageName.inTaggedOrDigestForm(), - this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish); + this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, + this.buildpacks); } /** @@ -120,7 +128,7 @@ public class BuildRequest { public BuildRequest withCreator(Creator creator) { Assert.notNull(creator, "Creator must not be null"); return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, creator, this.env, - this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish); + this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks); } /** @@ -135,7 +143,8 @@ public class BuildRequest { Map env = new LinkedHashMap<>(this.env); env.put(name, value); return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, - Collections.unmodifiableMap(env), this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish); + Collections.unmodifiableMap(env), this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, + this.buildpacks); } /** @@ -149,7 +158,7 @@ public class BuildRequest { updatedEnv.putAll(env); return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, Collections.unmodifiableMap(updatedEnv), this.cleanCache, this.verboseLogging, this.pullPolicy, - this.publish); + this.publish, this.buildpacks); } /** @@ -159,7 +168,7 @@ public class BuildRequest { */ public BuildRequest withCleanCache(boolean cleanCache) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, - cleanCache, this.verboseLogging, this.pullPolicy, this.publish); + cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks); } /** @@ -169,7 +178,7 @@ public class BuildRequest { */ public BuildRequest withVerboseLogging(boolean verboseLogging) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, - this.cleanCache, verboseLogging, this.pullPolicy, this.publish); + this.cleanCache, verboseLogging, this.pullPolicy, this.publish, this.buildpacks); } /** @@ -179,7 +188,7 @@ public class BuildRequest { */ public BuildRequest withPullPolicy(PullPolicy pullPolicy) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, - this.cleanCache, this.verboseLogging, pullPolicy, this.publish); + this.cleanCache, this.verboseLogging, pullPolicy, this.publish, this.buildpacks); } /** @@ -189,7 +198,28 @@ public class BuildRequest { */ public BuildRequest withPublish(boolean publish) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, - this.cleanCache, this.verboseLogging, this.pullPolicy, publish); + this.cleanCache, this.verboseLogging, this.pullPolicy, publish, this.buildpacks); + } + + /** + * Return a new {@link BuildRequest} with an updated buildpacks setting. + * @param buildpacks a collection of buildpacks to use when building the image + * @return an updated build request + */ + public BuildRequest withBuildpacks(BuildpackReference... buildpacks) { + Assert.notEmpty(buildpacks, "Buildpacks must not be empty"); + return withBuildpacks(Arrays.asList(buildpacks)); + } + + /** + * Return a new {@link BuildRequest} with an updated buildpacks setting. + * @param buildpacks a collection of buildpacks to use when building the image + * @return an updated build request + */ + public BuildRequest withBuildpacks(List buildpacks) { + Assert.notNull(buildpacks, "Buildpacks must not be null"); + return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, + this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, buildpacks); } /** @@ -275,6 +305,14 @@ public class BuildRequest { return this.pullPolicy; } + /** + * Return the collection of buildpacks to use when building the image, if provided. + * @return the collection of buildpacks + */ + public List getBuildpacks() { + return this.buildpacks; + } + /** * Factory method to create a new {@link BuildRequest} from a JAR file. * @param jarFile the source jar file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java index 7e8f2c72a14..e3c21e0f5ba 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java @@ -17,6 +17,7 @@ package org.springframework.boot.buildpack.platform.build; import java.io.IOException; +import java.util.List; import java.util.function.Consumer; import org.springframework.boot.buildpack.platform.build.BuilderMetadata.Stack; @@ -29,6 +30,8 @@ import org.springframework.boot.buildpack.platform.docker.configuration.DockerCo import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException; import org.springframework.boot.buildpack.platform.docker.type.Image; import org.springframework.boot.buildpack.platform.docker.type.ImageReference; +import org.springframework.boot.buildpack.platform.io.IOBiConsumer; +import org.springframework.boot.buildpack.platform.io.TarArchive; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -92,34 +95,35 @@ public class Builder { public void build(BuildRequest request) throws DockerEngineException, IOException { Assert.notNull(request, "Request must not be null"); this.log.start(request); - Image builderImage = getImage(request, ImageType.BUILDER); + String domain = request.getBuilder().getDomain(); + PullPolicy pullPolicy = request.getPullPolicy(); + ImageFetcher imageFetcher = new ImageFetcher(domain, getBuilderAuthHeader(), pullPolicy); + Image builderImage = imageFetcher.fetchImage(ImageType.BUILDER, request.getBuilder()); BuilderMetadata builderMetadata = BuilderMetadata.fromImage(builderImage); + request = withRunImageIfNeeded(request, builderMetadata.getStack()); + Image runImage = imageFetcher.fetchImage(ImageType.RUNNER, request.getRunImage()); + assertStackIdsMatch(runImage, builderImage); BuildOwner buildOwner = BuildOwner.fromEnv(builderImage.getConfig().getEnv()); - request = determineRunImage(request, builderImage, builderMetadata.getStack()); - EphemeralBuilder builder = new EphemeralBuilder(buildOwner, builderImage, builderMetadata, request.getCreator(), - request.getEnv()); - this.docker.image().load(builder.getArchive(), UpdateListener.none()); + Buildpacks buildpacks = getBuildpacks(request, imageFetcher, builderMetadata); + EphemeralBuilder ephemeralBuilder = new EphemeralBuilder(buildOwner, builderImage, builderMetadata, + request.getCreator(), request.getEnv(), buildpacks); + this.docker.image().load(ephemeralBuilder.getArchive(), UpdateListener.none()); try { - executeLifecycle(request, builder); + executeLifecycle(request, ephemeralBuilder); if (request.isPublish()) { pushImage(request.getName()); } } finally { - this.docker.image().remove(builder.getName(), true); + this.docker.image().remove(ephemeralBuilder.getName(), true); } } - private BuildRequest determineRunImage(BuildRequest request, Image builderImage, Stack builderStack) - throws IOException { - if (request.getRunImage() == null) { - ImageReference runImage = getRunImageReferenceForStack(builderStack); - request = request.withRunImage(runImage); + private BuildRequest withRunImageIfNeeded(BuildRequest request, Stack builderStack) { + if (request.getRunImage() != null) { + return request; } - assertImageRegistriesMatch(request); - Image runImage = getImage(request, ImageType.RUNNER); - assertStackIdsMatch(runImage, builderImage); - return request; + return request.withRunImage(getRunImageReferenceForStack(builderStack)); } private ImageReference getRunImageReferenceForStack(Stack stack) { @@ -128,32 +132,22 @@ public class Builder { return ImageReference.of(name).inTaggedOrDigestForm(); } - private Image getImage(BuildRequest request, ImageType imageType) throws IOException { - ImageReference imageReference = (imageType == ImageType.BUILDER) ? request.getBuilder() : request.getRunImage(); - - if (request.getPullPolicy() == PullPolicy.ALWAYS) { - return pullImage(imageReference, imageType); - } - - try { - return this.docker.image().inspect(imageReference); - } - catch (DockerEngineException exception) { - if (request.getPullPolicy() == PullPolicy.IF_NOT_PRESENT && exception.getStatusCode() == 404) { - return pullImage(imageReference, imageType); - } - else { - throw exception; - } - } + private void assertStackIdsMatch(Image runImage, Image builderImage) { + StackId runImageStackId = StackId.fromImage(runImage); + StackId builderImageStackId = StackId.fromImage(builderImage); + Assert.state(runImageStackId.equals(builderImageStackId), () -> "Run image stack '" + runImageStackId + + "' does not match builder stack '" + builderImageStackId + "'"); } - private Image pullImage(ImageReference reference, ImageType imageType) throws IOException { - Consumer progressConsumer = this.log.pullingImage(reference, imageType); - TotalProgressPullListener listener = new TotalProgressPullListener(progressConsumer); - Image image = this.docker.image().pull(reference, listener, getBuilderAuthHeader()); - this.log.pulledImage(image, imageType); - return image; + private Buildpacks getBuildpacks(BuildRequest request, ImageFetcher imageFetcher, BuilderMetadata builderMetadata) { + BuildpackResolverContext resolverContext = new BuilderResolverContext(imageFetcher, builderMetadata); + return BuildpackResolvers.resolveAll(resolverContext, request.getBuildpacks()); + } + + private void executeLifecycle(BuildRequest request, EphemeralBuilder builder) throws IOException { + try (Lifecycle lifecycle = new Lifecycle(this.log, this.docker, request, builder)) { + lifecycle.execute(); + } } private void pushImage(ImageReference reference) throws IOException { @@ -173,25 +167,83 @@ public class Builder { ? this.dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader() : null; } - private void assertImageRegistriesMatch(BuildRequest request) { - if (getBuilderAuthHeader() != null) { - Assert.state(request.getRunImage().getDomain().equals(request.getBuilder().getDomain()), - "Builder image '" + request.getBuilder() + "' and run image '" + request.getRunImage() - + "' must be pulled from the same authenticated registry"); + /** + * Internal utility class used to fetch images. + */ + private class ImageFetcher { + + private final String domain; + + private final String authHeader; + + private final PullPolicy pullPolicy; + + ImageFetcher(String domain, String authHeader, PullPolicy pullPolicy) { + this.domain = domain; + this.authHeader = authHeader; + this.pullPolicy = pullPolicy; } + + Image fetchImage(ImageType type, ImageReference reference) throws IOException { + Assert.notNull(type, "Type must not be null"); + Assert.notNull(reference, "Reference must not be null"); + Assert.state(this.authHeader == null || reference.getDomain().equals(this.domain), + () -> String.format("%s '%s' must be pulled from the '%s' authenticated registry", + StringUtils.capitalize(type.getDescription()), reference, this.domain)); + if (this.pullPolicy == PullPolicy.ALWAYS) { + return pullImage(reference, type); + } + try { + return Builder.this.docker.image().inspect(reference); + } + catch (DockerEngineException ex) { + if (this.pullPolicy == PullPolicy.IF_NOT_PRESENT && ex.getStatusCode() == 404) { + return pullImage(reference, type); + } + throw ex; + } + } + + private Image pullImage(ImageReference reference, ImageType imageType) throws IOException { + TotalProgressPullListener listener = new TotalProgressPullListener( + Builder.this.log.pullingImage(reference, imageType)); + Image image = Builder.this.docker.image().pull(reference, listener, this.authHeader); + Builder.this.log.pulledImage(image, imageType); + return image; + } + } - private void assertStackIdsMatch(Image runImage, Image builderImage) { - StackId runImageStackId = StackId.fromImage(runImage); - StackId builderImageStackId = StackId.fromImage(builderImage); - Assert.state(runImageStackId.equals(builderImageStackId), () -> "Run image stack '" + runImageStackId - + "' does not match builder stack '" + builderImageStackId + "'"); - } + /** + * {@link BuildpackResolverContext} implementation for the {@link Builder}. + */ + private class BuilderResolverContext implements BuildpackResolverContext { - private void executeLifecycle(BuildRequest request, EphemeralBuilder builder) throws IOException { - try (Lifecycle lifecycle = new Lifecycle(this.log, this.docker, request, builder)) { - lifecycle.execute(); + private final ImageFetcher imageFetcher; + + private final BuilderMetadata builderMetadata; + + BuilderResolverContext(ImageFetcher imageFetcher, BuilderMetadata builderMetadata) { + this.imageFetcher = imageFetcher; + this.builderMetadata = builderMetadata; } + + @Override + public List getBuildpackMetadata() { + return this.builderMetadata.getBuildpacks(); + } + + @Override + public Image fetchImage(ImageReference reference, ImageType imageType) throws IOException { + return this.imageFetcher.fetchImage(imageType, reference); + } + + @Override + public void exportImageLayers(ImageReference reference, IOBiConsumer exports) + throws IOException { + Builder.this.docker.image().exportLayers(reference, exports); + } + } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuilderBuildpack.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuilderBuildpack.java new file mode 100644 index 00000000000..d67b0000da9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuilderBuildpack.java @@ -0,0 +1,117 @@ +/* + * Copyright 2012-2021 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.io.IOException; + +import org.springframework.boot.buildpack.platform.docker.type.Layer; +import org.springframework.boot.buildpack.platform.io.IOConsumer; +import org.springframework.util.Assert; + +/** + * A {@link Buildpack} that references a buildpack contained in the builder. + * + * The buildpack reference must contain a buildpack ID (for example, + * {@code "example/buildpack"}) or a buildpack ID and version (for example, + * {@code "example/buildpack@1.0.0"}). The reference can optionally contain a prefix + * {@code urn:cnb:builder:} to unambiguously identify it as a builder buildpack reference. + * If a version is not provided, the reference will match any version of a buildpack with + * the same ID as the reference. + * + * @author Scott Frederick + */ +class BuilderBuildpack implements Buildpack { + + private static final String PREFIX = "urn:cnb:builder:"; + + private final BuildpackCoordinates coordinates; + + BuilderBuildpack(BuildpackMetadata buildpackMetadata) { + this.coordinates = BuildpackCoordinates.fromBuildpackMetadata(buildpackMetadata); + } + + @Override + public BuildpackCoordinates getCoordinates() { + return this.coordinates; + } + + @Override + public void apply(IOConsumer layers) throws IOException { + } + + /** + * A {@link BuildpackResolver} compatible method to resolve builder buildpacks. + * @param context the resolver context + * @param reference the buildpack reference + * @return the resolved {@link Buildpack} or {@code null} + */ + static Buildpack resolve(BuildpackResolverContext context, BuildpackReference reference) { + boolean unambiguous = reference.hasPrefix(PREFIX); + BuilderReference builderReference = BuilderReference + .of(unambiguous ? reference.getSubReference(PREFIX) : reference.toString()); + BuildpackMetadata buildpackMetadata = findBuildpackMetadata(context, builderReference); + if (unambiguous) { + Assert.isTrue(buildpackMetadata != null, () -> "Buildpack '" + reference + "' not found in builder"); + } + return (buildpackMetadata != null) ? new BuilderBuildpack(buildpackMetadata) : null; + } + + private static BuildpackMetadata findBuildpackMetadata(BuildpackResolverContext context, + BuilderReference builderReference) { + for (BuildpackMetadata candidate : context.getBuildpackMetadata()) { + if (builderReference.matches(candidate)) { + return candidate; + } + } + return null; + } + + /** + * A reference to a buildpack builder. + */ + static class BuilderReference { + + private final String id; + + private final String version; + + BuilderReference(String id, String version) { + this.id = id; + this.version = version; + } + + @Override + public String toString() { + return (this.version != null) ? this.id + "@" + this.version : this.id; + } + + boolean matches(BuildpackMetadata candidate) { + return this.id.equals(candidate.getId()) + && (this.version == null || this.version.equals(candidate.getVersion())); + } + + static BuilderReference of(String value) { + if (value.contains("@")) { + String[] parts = value.split("@"); + return new BuilderReference(parts[0], parts[1]); + } + return new BuilderReference(value, null); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuilderMetadata.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuilderMetadata.java index 25f18859169..d08c11bfa04 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuilderMetadata.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuilderMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -18,6 +18,9 @@ package org.springframework.boot.buildpack.platform.build; import java.io.IOException; import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.function.Consumer; import com.fasterxml.jackson.core.JsonProcessingException; @@ -50,11 +53,23 @@ class BuilderMetadata extends MappedObject { private final CreatedBy createdBy; + private final List buildpacks; + BuilderMetadata(JsonNode node) { super(node, MethodHandles.lookup()); this.stack = valueAt("/stack", Stack.class); this.lifecycle = valueAt("/lifecycle", Lifecycle.class); this.createdBy = valueAt("/createdBy", CreatedBy.class); + this.buildpacks = extractBuildpacks(getNode().at("/buildpacks")); + } + + private List extractBuildpacks(JsonNode node) { + if (node.isEmpty()) { + return Collections.emptyList(); + } + List entries = new ArrayList<>(); + node.forEach((child) -> entries.add(BuildpackMetadata.fromJson(child))); + return entries; } /** @@ -81,6 +96,14 @@ class BuilderMetadata extends MappedObject { return this.createdBy; } + /** + * Return the buildpacks that are bundled in the builder. + * @return the buildpacks + */ + List getBuildpacks() { + return this.buildpacks; + } + /** * Create an updated copy of this metadata. * @param update consumer to apply updates diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Buildpack.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Buildpack.java new file mode 100644 index 00000000000..0c4e86f484f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Buildpack.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2021 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.io.IOException; + +import org.springframework.boot.buildpack.platform.docker.type.Layer; +import org.springframework.boot.buildpack.platform.io.IOConsumer; + +/** + * A Buildpack that should be invoked by the builder during image building. + * + * @author Scott Frederick + * @see BuildpackResolver + */ +interface Buildpack { + + /** + * Return the coordinates of the builder. + * @return the builder coordinates + */ + BuildpackCoordinates getCoordinates(); + + /** + * Apply the necessary buildpack layers. + * @param layers a consumer that should accept the layers + * @throws IOException on IO error + */ + void apply(IOConsumer layers) throws IOException; + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackCoordinates.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackCoordinates.java new file mode 100644 index 00000000000..954ddd90258 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackCoordinates.java @@ -0,0 +1,140 @@ +/* + * Copyright 2012-2021 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.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; + +import org.tomlj.Toml; +import org.tomlj.TomlParseResult; + +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * A set of buildpack coordinates that uniquely identifies a buildpack. + * + * @author Scott Frederick + * @see Platform + * Interface Specification + */ +final class BuildpackCoordinates { + + private final String id; + + private final String version; + + private BuildpackCoordinates(String id, String version) { + Assert.hasText(id, "ID must not be empty"); + this.id = id; + this.version = version; + } + + String getId() { + return this.id; + } + + /** + * Return the buildpack ID with all "/" replaced by "_". + * @return the ID + */ + String getSanitizedId() { + return this.id.replace("/", "_"); + } + + String getVersion() { + return this.version; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + BuildpackCoordinates other = (BuildpackCoordinates) obj; + return this.id.equals(other.id) && ObjectUtils.nullSafeEquals(this.version, other.version); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + this.id.hashCode(); + result = prime * result + ObjectUtils.nullSafeHashCode(this.version); + return result; + } + + @Override + public String toString() { + return this.id + ((StringUtils.hasText(this.version)) ? "@" + this.version : ""); + } + + /** + * Create {@link BuildpackCoordinates} from a {@code buildpack.toml} + * file. + * @param inputStream an input stream containing {@code buildpack.toml} content + * @param path the path to the buildpack containing the {@code buildpack.toml} file + * @return a new {@link BuildpackCoordinates} instance + * @throws IOException on IO error + */ + static BuildpackCoordinates fromToml(InputStream inputStream, Path path) throws IOException { + return fromToml(Toml.parse(inputStream), path); + } + + private static BuildpackCoordinates fromToml(TomlParseResult toml, Path path) { + Assert.isTrue(!toml.isEmpty(), + () -> "Buildpack descriptor 'buildpack.toml' is required in buildpack '" + path + "'"); + Assert.hasText(toml.getString("buildpack.id"), + () -> "Buildpack descriptor must contain ID in buildpack '" + path + "'"); + Assert.hasText(toml.getString("buildpack.version"), + () -> "Buildpack descriptor must contain version in buildpack '" + path + "'"); + Assert.isTrue(toml.contains("stacks") || toml.contains("order"), + () -> "Buildpack descriptor must contain either 'stacks' or 'order' in buildpack '" + path + "'"); + Assert.isTrue(!(toml.contains("stacks") && toml.contains("order")), + () -> "Buildpack descriptor must not contain both 'stacks' and 'order' in buildpack '" + path + "'"); + return new BuildpackCoordinates(toml.getString("buildpack.id"), toml.getString("buildpack.version")); + } + + /** + * Create {@link BuildpackCoordinates} by extracting values from + * {@link BuildpackMetadata}. + * @param buildpackMetadata the buildpack metadata + * @return a new {@link BuildpackCoordinates} instance + */ + static BuildpackCoordinates fromBuildpackMetadata(BuildpackMetadata buildpackMetadata) { + Assert.notNull(buildpackMetadata, "BuildpackMetadata must not be null"); + return new BuildpackCoordinates(buildpackMetadata.getId(), buildpackMetadata.getVersion()); + } + + /** + * Create {@link BuildpackCoordinates} from an ID and version. + * @param id the buildpack ID + * @param version the buildpack version + * @return a new {@link BuildpackCoordinates} instance + */ + static BuildpackCoordinates of(String id, String version) { + return new BuildpackCoordinates(id, version); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackMetadata.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackMetadata.java new file mode 100644 index 00000000000..5b3d1e10986 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackMetadata.java @@ -0,0 +1,121 @@ +/* + * Copyright 2012-2021 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.io.IOException; +import java.lang.invoke.MethodHandles; + +import com.fasterxml.jackson.databind.JsonNode; + +import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.docker.type.ImageConfig; +import org.springframework.boot.buildpack.platform.json.MappedObject; +import org.springframework.boot.buildpack.platform.json.SharedObjectMapper; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Buildpack metadata information. + * + * @author Scott Frederick + */ +final class BuildpackMetadata extends MappedObject { + + private static final String LABEL_NAME = "io.buildpacks.buildpackage.metadata"; + + private final String id; + + private final String version; + + private final String homepage; + + private BuildpackMetadata(JsonNode node) { + super(node, MethodHandles.lookup()); + this.id = valueAt("/id", String.class); + this.version = valueAt("/version", String.class); + this.homepage = valueAt("/homepage", String.class); + } + + /** + * Return the buildpack ID. + * @return the ID + */ + String getId() { + return this.id; + } + + /** + * Return the buildpack version. + * @return the version + */ + String getVersion() { + return this.version; + } + + /** + * Return the buildpack homepage address. + * @return the homepage + */ + String getHomepage() { + return this.homepage; + } + + /** + * Factory method to extract {@link BuildpackMetadata} from an image. + * @param image the source image + * @return the builder metadata + * @throws IOException on IO error + */ + static BuildpackMetadata fromImage(Image image) throws IOException { + Assert.notNull(image, "Image must not be null"); + return fromImageConfig(image.getConfig()); + } + + /** + * Factory method to extract {@link BuildpackMetadata} from image config. + * @param imageConfig the source image config + * @return the builder metadata + * @throws IOException on IO error + */ + static BuildpackMetadata fromImageConfig(ImageConfig imageConfig) throws IOException { + Assert.notNull(imageConfig, "ImageConfig must not be null"); + String json = imageConfig.getLabels().get(LABEL_NAME); + Assert.notNull(json, () -> "No '" + LABEL_NAME + "' label found in image config labels '" + + StringUtils.collectionToCommaDelimitedString(imageConfig.getLabels().keySet()) + "'"); + return fromJson(json); + } + + /** + * Factory method create {@link BuildpackMetadata} from JSON. + * @param json the source JSON + * @return the builder metadata + * @throws IOException on IO error + */ + static BuildpackMetadata fromJson(String json) throws IOException { + return fromJson(SharedObjectMapper.get().readTree(json)); + } + + /** + * Factory method create {@link BuildpackMetadata} from JSON. + * @param node the source JSON + * @return the builder metadata + */ + static BuildpackMetadata fromJson(JsonNode node) { + return new BuildpackMetadata(node); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackReference.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackReference.java new file mode 100644 index 00000000000..86b163d20f8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackReference.java @@ -0,0 +1,94 @@ +/* + * Copyright 2012-2021 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.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.springframework.util.Assert; + +/** + * An opaque reference to a {@link Buildpack}. + * + * @author Phillip Webb + * @author Scott Frederick + * @since 2.5.0 + * @see BuildpackResolver + */ +public final class BuildpackReference { + + private final String value; + + private BuildpackReference(String value) { + this.value = value; + } + + boolean hasPrefix(String prefix) { + return this.value.startsWith(prefix); + } + + String getSubReference(String prefix) { + return this.value.startsWith(prefix) ? this.value.substring(prefix.length()) : null; + } + + Path asPath() { + try { + URL url = new URL(this.value); + if (url.getProtocol().equals("file")) { + return Paths.get(url.getPath()); + } + } + catch (MalformedURLException ex) { + // not a URL, fall through to attempting to find a plain file path + } + return Paths.get(this.value); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + return this.value.equals(((BuildpackReference) obj).value); + } + + @Override + public int hashCode() { + return this.value.hashCode(); + } + + @Override + public String toString() { + return this.value; + } + + /** + * Create a new {@link BuildpackReference} from the given value. + * @param value the value to use + * @return a new {@link BuildpackReference} + */ + public static BuildpackReference of(String value) { + Assert.hasText(value, "Value must not be empty"); + return new BuildpackReference(value); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackResolver.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackResolver.java new file mode 100644 index 00000000000..3711fdb5bee --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackResolver.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2021 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; + +/** + * Strategy inerface used to resolve a {@link BuildpackReference} to a {@link Buildpack}. + * + * @author Scott Frederick + * @author Phillip Webb + * @see BuildpackResolvers + */ +interface BuildpackResolver { + + /** + * Attempt to resolve the given {@link BuildpackReference}. + * @param context the resolver context + * @param reference the reference to resolve + * @return a resolved {@link Buildpack} instance or {@code null} + */ + Buildpack resolve(BuildpackResolverContext context, BuildpackReference reference); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackResolverContext.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackResolverContext.java new file mode 100644 index 00000000000..0dc76011571 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackResolverContext.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2021 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.io.IOException; +import java.util.List; + +import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.docker.type.ImageReference; +import org.springframework.boot.buildpack.platform.io.IOBiConsumer; +import org.springframework.boot.buildpack.platform.io.TarArchive; + +/** + * Context passed to a {@link BuildpackResolver}. + * + * @author Scott Frederick + * @author Phillip Webb + */ +interface BuildpackResolverContext { + + List getBuildpackMetadata(); + + /** + * Retrieve an image. + * @param reference the image reference + * @param type the type of image + * @return the retrieved image + * @throws IOException on IO error + */ + Image fetchImage(ImageReference reference, ImageType type) throws IOException; + + /** + * Export the layers of an image. + * @param reference the reference to export + * @param exports a consumer to receive the layers (contents can only be accessed + * during the callback) + * @throws IOException on IO error + */ + void exportImageLayers(ImageReference reference, IOBiConsumer exports) throws IOException; + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackResolvers.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackResolvers.java new file mode 100644 index 00000000000..1883df4264c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackResolvers.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012-2021 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.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * All {@link BuildpackResolver} instances that can be used to resolve + * {@link BuildpackReference BuildpackReferences}. + * + * @author Scott Frederick + * @author Phillip Webb + */ +final class BuildpackResolvers { + + private static final List resolvers = getResolvers(); + + private BuildpackResolvers() { + } + + private static List getResolvers() { + List resolvers = new ArrayList<>(); + resolvers.add(BuilderBuildpack::resolve); + resolvers.add(DirectoryBuildpack::resolve); + resolvers.add(TarGzipBuildpack::resolve); + resolvers.add(ImageBuildpack::resolve); + return Collections.unmodifiableList(resolvers); + } + + /** + * Resolve a collection of {@link BuildpackReference BuildpackReferences} to a + * {@link Buildpacks} instance. + * @param context the resolver context + * @param references the references to resolve + * @return a {@link Buildpacks} instance + */ + static Buildpacks resolveAll(BuildpackResolverContext context, Collection references) { + Assert.notNull(context, "Context must not be null"); + if (CollectionUtils.isEmpty(references)) { + return Buildpacks.EMPTY; + } + List buildpacks = new ArrayList<>(references.size()); + for (BuildpackReference reference : references) { + buildpacks.add(resolve(context, reference)); + } + return Buildpacks.of(buildpacks); + } + + private static Buildpack resolve(BuildpackResolverContext context, BuildpackReference reference) { + Assert.notNull(reference, "Reference must not be null"); + for (BuildpackResolver resolver : resolvers) { + Buildpack buildpack = resolver.resolve(context, reference); + if (buildpack != null) { + return buildpack; + } + } + throw new IllegalArgumentException("Invalid buildpack reference '" + reference + "'"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Buildpacks.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Buildpacks.java new file mode 100644 index 00000000000..bf04b623995 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Buildpacks.java @@ -0,0 +1,88 @@ +/* + * Copyright 2012-2021 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.io.IOException; +import java.util.Collections; +import java.util.List; + +import org.springframework.boot.buildpack.platform.docker.type.Layer; +import org.springframework.boot.buildpack.platform.io.Content; +import org.springframework.boot.buildpack.platform.io.IOConsumer; +import org.springframework.boot.buildpack.platform.io.Layout; +import org.springframework.boot.buildpack.platform.io.Owner; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +/** + * A collection of {@link Buildpack} instances that can be used to apply buildpack layers. + * + * @author Scott Frederick + * @author Phillip Webb + */ +final class Buildpacks { + + static final Buildpacks EMPTY = new Buildpacks(Collections.emptyList()); + + private final List buildpacks; + + private Buildpacks(List buildpacks) { + this.buildpacks = buildpacks; + } + + List getBuildpacks() { + return this.buildpacks; + } + + void apply(IOConsumer layers) throws IOException { + if (!this.buildpacks.isEmpty()) { + for (Buildpack buildpack : this.buildpacks) { + buildpack.apply(layers); + } + layers.accept(Layer.of(this::addOrderLayerContent)); + } + } + + void addOrderLayerContent(Layout layout) throws IOException { + layout.file("/cnb/order.toml", Owner.ROOT, Content.of(getOrderToml())); + } + + private String getOrderToml() { + StringBuilder builder = new StringBuilder(); + for (Buildpack buildpack : this.buildpacks) { + appendToOrderToml(builder, buildpack.getCoordinates()); + } + return builder.toString(); + } + + private void appendToOrderToml(StringBuilder builder, BuildpackCoordinates coordinates) { + builder.append("[[order]]\n"); + builder.append("group = [\n"); + builder.append(" { "); + builder.append("id = \"" + coordinates.getId() + "\""); + if (StringUtils.hasText(coordinates.getVersion())) { + builder.append(", version = \"" + coordinates.getVersion() + "\""); + } + builder.append(" }\n"); + builder.append("]\n\n"); + } + + static Buildpacks of(List buildpacks) { + return CollectionUtils.isEmpty(buildpacks) ? EMPTY : new Buildpacks(buildpacks); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/DirectoryBuildpack.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/DirectoryBuildpack.java new file mode 100644 index 00000000000..1ea832b2eda --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/DirectoryBuildpack.java @@ -0,0 +1,136 @@ +/* + * Copyright 2012-2021 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.io.IOException; +import java.io.InputStream; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.PosixFileAttributeView; + +import org.springframework.boot.buildpack.platform.docker.type.Layer; +import org.springframework.boot.buildpack.platform.io.Content; +import org.springframework.boot.buildpack.platform.io.FilePermissions; +import org.springframework.boot.buildpack.platform.io.IOConsumer; +import org.springframework.boot.buildpack.platform.io.Layout; +import org.springframework.boot.buildpack.platform.io.Owner; +import org.springframework.util.Assert; + +/** + * A {@link Buildpack} that references a buildpack in a directory on the local file + * system. + * + * The file system must contain a buildpack descriptor named {@code buildpack.toml} in the + * root of the directory. The contents of the directory tree will be provided as a single + * layer to be included in the builder image. + * + * @author Scott Frederick + */ +final class DirectoryBuildpack implements Buildpack { + + private final Path path; + + private final BuildpackCoordinates coordinates; + + private DirectoryBuildpack(Path path) { + this.path = path; + this.coordinates = findBuildpackCoordinates(path); + } + + private BuildpackCoordinates findBuildpackCoordinates(Path path) { + Path buildpackToml = path.resolve("buildpack.toml"); + Assert.isTrue(Files.exists(buildpackToml), + () -> "Buildpack descriptor 'buildpack.toml' is required in buildpack '" + path + "'"); + try { + try (InputStream inputStream = Files.newInputStream(buildpackToml)) { + return BuildpackCoordinates.fromToml(inputStream, path); + } + } + catch (IOException ex) { + throw new IllegalArgumentException("Error parsing descriptor for buildpack '" + path + "'", ex); + } + } + + @Override + public BuildpackCoordinates getCoordinates() { + return this.coordinates; + } + + @Override + public void apply(IOConsumer layers) throws IOException { + layers.accept(Layer.of(this::addLayerContent)); + } + + private void addLayerContent(Layout layout) throws IOException { + String id = this.coordinates.getSanitizedId(); + Path cnbPath = Paths.get("/cnb/buildpacks/", id, this.coordinates.getVersion()); + Files.walkFileTree(this.path, new LayoutFileVisitor(this.path, cnbPath, layout)); + } + + /** + * A {@link BuildpackResolver} compatible method to resolve directory buildpacks. + * @param context the resolver context + * @param reference the buildpack reference + * @return the resolved {@link Buildpack} or {@code null} + */ + static Buildpack resolve(BuildpackResolverContext context, BuildpackReference reference) { + Path path = reference.asPath(); + if (Files.exists(path) && Files.isDirectory(path)) { + return new DirectoryBuildpack(path); + } + return null; + } + + /** + * {@link SimpleFileVisitor} to used to create the {@link Layout}. + */ + private static class LayoutFileVisitor extends SimpleFileVisitor { + + private final Path basePath; + + private final Path layerPath; + + private final Layout layout; + + LayoutFileVisitor(Path basePath, Path layerPath, Layout layout) { + this.basePath = basePath; + this.layerPath = layerPath; + this.layout = layout; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + PosixFileAttributeView attributeView = Files.getFileAttributeView(file, PosixFileAttributeView.class); + Assert.state(attributeView != null, + "Buildpack content in a directory is not supported on this operating system"); + int mode = FilePermissions.posixPermissionsToUmask(attributeView.readAttributes().permissions()); + this.layout.file(relocate(file), Owner.ROOT, mode, Content.of(file.toFile())); + return FileVisitResult.CONTINUE; + } + + private String relocate(Path path) { + Path node = path.subpath(this.basePath.getNameCount(), path.getNameCount()); + return Paths.get(this.layerPath.toString(), node.toString()).toString(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilder.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilder.java index 67bbaf555ec..7d3abe5e87e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilder.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -49,10 +49,11 @@ class EphemeralBuilder { * @param builderMetadata the builder metadata * @param creator the builder creator * @param env the builder env + * @param buildpacks an optional set of buildpacks to apply * @throws IOException on IO error */ EphemeralBuilder(BuildOwner buildOwner, Image builderImage, BuilderMetadata builderMetadata, Creator creator, - Map env) throws IOException { + Map env, Buildpacks buildpacks) throws IOException { ImageReference name = ImageReference.random("pack.local/builder/").inTaggedForm(); this.buildOwner = buildOwner; this.creator = creator; @@ -63,6 +64,9 @@ class EphemeralBuilder { if (env != null && !env.isEmpty()) { update.withNewLayer(getEnvLayer(env)); } + if (buildpacks != null) { + buildpacks.apply(update::withNewLayer); + } }); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageBuildpack.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageBuildpack.java new file mode 100644 index 00000000000..45b03813e70 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageBuildpack.java @@ -0,0 +1,143 @@ +/* + * Copyright 2012-2021 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.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; + +import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException; +import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.docker.type.ImageReference; +import org.springframework.boot.buildpack.platform.docker.type.Layer; +import org.springframework.boot.buildpack.platform.io.IOConsumer; +import org.springframework.boot.buildpack.platform.io.TarArchive; +import org.springframework.util.StreamUtils; + +/** + * A {@link Buildpack} that references a buildpack contained in an OCI image. + * + * The reference must be an OCI image reference. The reference can optionally contain a + * prefix {@code docker://} to unambiguously identify it as an image buildpack reference. + * + * @author Scott Frederick + * @author Phillip Webb + */ +final class ImageBuildpack implements Buildpack { + + private static final String PREFIX = "docker://"; + + private final BuildpackCoordinates coordinates; + + private final ExportedLayers exportedLayers; + + private ImageBuildpack(BuildpackResolverContext context, ImageReference imageReference) { + try { + Image image = context.fetchImage(imageReference, ImageType.BUILDPACK); + BuildpackMetadata buildpackMetadata = BuildpackMetadata.fromImage(image); + this.coordinates = BuildpackCoordinates.fromBuildpackMetadata(buildpackMetadata); + this.exportedLayers = new ExportedLayers(context, imageReference); + } + catch (IOException | DockerEngineException ex) { + throw new IllegalArgumentException("Error pulling buildpack image '" + imageReference + "'", ex); + } + } + + @Override + public BuildpackCoordinates getCoordinates() { + return this.coordinates; + } + + @Override + public void apply(IOConsumer layers) throws IOException { + this.exportedLayers.apply(layers); + } + + /** + * A {@link BuildpackResolver} compatible method to resolve image buildpacks. + * @param context the resolver context + * @param reference the buildpack reference + * @return the resolved {@link Buildpack} or {@code null} + */ + static Buildpack resolve(BuildpackResolverContext context, BuildpackReference reference) { + boolean unambiguous = reference.hasPrefix(PREFIX); + try { + ImageReference imageReference = ImageReference + .of((unambiguous) ? reference.getSubReference(PREFIX) : reference.toString()); + return new ImageBuildpack(context, imageReference); + } + catch (IllegalArgumentException ex) { + if (unambiguous) { + throw ex; + } + return null; + } + } + + private static class ExportedLayers { + + private final List layerFiles; + + ExportedLayers(BuildpackResolverContext context, ImageReference imageReference) throws IOException { + List layerFiles = new ArrayList<>(); + context.exportImageLayers(imageReference, (name, archive) -> layerFiles.add(copyToTemp(name, archive))); + this.layerFiles = Collections.unmodifiableList(layerFiles); + } + + private Path copyToTemp(String name, TarArchive archive) throws IOException { + String[] parts = name.split("/"); + Path path = Files.createTempFile("create-builder-scratch-", parts[0]); + try (OutputStream out = Files.newOutputStream(path)) { + archive.writeTo(out); + } + return path; + } + + void apply(IOConsumer layers) throws IOException { + for (Path path : this.layerFiles) { + layers.accept(Layer.fromTarArchive((out) -> copyLayerTar(path, out))); + } + } + + private void copyLayerTar(Path path, OutputStream out) throws IOException { + try (TarArchiveInputStream tarIn = new TarArchiveInputStream(Files.newInputStream(path)); + TarArchiveOutputStream tarOut = new TarArchiveOutputStream(out)) { + TarArchiveEntry entry = tarIn.getNextTarEntry(); + while (entry != null) { + if (entry.isFile()) { + tarOut.putArchiveEntry(entry); + StreamUtils.copy(tarIn, tarOut); + tarOut.closeArchiveEntry(); + } + entry = tarIn.getNextTarEntry(); + } + tarOut.finish(); + } + Files.delete(path); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageType.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageType.java index 28bd79dc70b..6a8cd4a1ba4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageType.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageType.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -31,7 +31,12 @@ enum ImageType { /** * Run image. */ - RUNNER("run image"); + RUNNER("run image"), + + /** + * Buildpack image. + */ + BUILDPACK("buildpack image"); private final String description; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/TarGzipBuildpack.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/TarGzipBuildpack.java new file mode 100644 index 00000000000..23a0bf4f1b5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/TarGzipBuildpack.java @@ -0,0 +1,118 @@ +/* + * Copyright 2012-2021 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.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.apache.commons.compress.archivers.ArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; + +import org.springframework.boot.buildpack.platform.docker.type.Layer; +import org.springframework.boot.buildpack.platform.io.IOConsumer; +import org.springframework.util.StreamUtils; + +/** + * A {@link Buildpack} that references a buildpack contained in a local gzipped tar + * archive file. + * + * The archive must contain a buildpack descriptor named {@code buildpack.toml} at the + * root of the archive. The contents of the archive will be provided as a single layer to + * be included in the builder image. + * + * @author Scott Frederick + */ +final class TarGzipBuildpack implements Buildpack { + + private final Path path; + + private final BuildpackCoordinates coordinates; + + private TarGzipBuildpack(Path path) { + this.path = path; + this.coordinates = findBuildpackCoordinates(path); + } + + private BuildpackCoordinates findBuildpackCoordinates(Path path) { + try { + try (TarArchiveInputStream tar = new TarArchiveInputStream( + new GzipCompressorInputStream(Files.newInputStream(path)))) { + ArchiveEntry entry = tar.getNextEntry(); + while (entry != null) { + if ("buildpack.toml".equals(entry.getName())) { + return BuildpackCoordinates.fromToml(tar, path); + } + entry = tar.getNextEntry(); + } + throw new IllegalArgumentException( + "Buildpack descriptor 'buildpack.toml' is required in buildpack '" + path + "'"); + } + } + catch (IOException ex) { + throw new RuntimeException("Error parsing descriptor for buildpack '" + path + "'", ex); + } + } + + @Override + public BuildpackCoordinates getCoordinates() { + return this.coordinates; + } + + @Override + public void apply(IOConsumer layers) throws IOException { + layers.accept(Layer.fromTarArchive(this::copyAndRebaseEntries)); + } + + private void copyAndRebaseEntries(OutputStream outputStream) throws IOException { + String id = this.coordinates.getSanitizedId(); + Path basePath = Paths.get("/cnb/buildpacks/", id, this.coordinates.getVersion()); + try (TarArchiveInputStream tar = new TarArchiveInputStream( + new GzipCompressorInputStream(Files.newInputStream(this.path))); + TarArchiveOutputStream output = new TarArchiveOutputStream(outputStream)) { + TarArchiveEntry entry = tar.getNextTarEntry(); + while (entry != null) { + entry.setName(basePath + "/" + entry.getName()); + output.putArchiveEntry(entry); + StreamUtils.copy(tar, output); + output.closeArchiveEntry(); + entry = tar.getNextTarEntry(); + } + output.finish(); + } + } + + /** + * A {@link BuildpackResolver} compatible method to resolve tar-gzip buildpacks. + * @param context the resolver context + * @param reference the buildpack reference + * @return the resolved {@link Buildpack} or {@code null} + */ + static Buildpack resolve(BuildpackResolverContext context, BuildpackReference reference) { + Path path = reference.asPath(); + if (Files.exists(path) && Files.isRegularFile(path)) { + return new TarGzipBuildpack(path); + } + return null; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java index aaff3ccb20b..c07523d5440 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -24,6 +24,8 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.http.client.utils.URIBuilder; import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration; @@ -37,9 +39,12 @@ import org.springframework.boot.buildpack.platform.docker.type.Image; import org.springframework.boot.buildpack.platform.docker.type.ImageArchive; 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.IOBiConsumer; +import org.springframework.boot.buildpack.platform.io.TarArchive; import org.springframework.boot.buildpack.platform.json.JsonStream; import org.springframework.boot.buildpack.platform.json.SharedObjectMapper; import org.springframework.util.Assert; +import org.springframework.util.StreamUtils; import org.springframework.util.StringUtils; /** @@ -243,6 +248,31 @@ public class DockerApi { } } + /** + * Export the layers of an image. + * @param reference the reference to export + * @param exports a consumer to receive the layers (contents can only be accessed + * during the callback) + * @throws IOException on IO error + */ + public void exportLayers(ImageReference reference, IOBiConsumer exports) + throws IOException { + Assert.notNull(reference, "Reference must not be null"); + Assert.notNull(exports, "Exports must not be null"); + URI saveUri = buildUrl("/images/" + reference + "/get"); + Response response = http().get(saveUri); + try (TarArchiveInputStream tar = new TarArchiveInputStream(response.getContent())) { + TarArchiveEntry entry = tar.getNextTarEntry(); + while (entry != null) { + if (entry.getName().endsWith("/layer.tar")) { + TarArchive archive = (out) -> StreamUtils.copy(tar, out); + exports.accept(entry.getName(), archive); + } + entry = tar.getNextTarEntry(); + } + } + } + /** * Remove a specific image. * @param reference the reference the remove diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/Content.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/Content.java index 66b369b7e9a..911df2ad492 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/Content.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/Content.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -68,6 +68,11 @@ public interface Content { return of(bytes.length, () -> new ByteArrayInputStream(bytes)); } + /** + * Create a new {@link Content} from the given file. + * @param file the file to write + * @return a new {@link Content} instance + */ static Content of(File file) { Assert.notNull(file, "File must not be null"); return of((int) file.length(), () -> new FileInputStream(file)); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/FilePermissions.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/FilePermissions.java new file mode 100644 index 00000000000..4d345792035 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/FilePermissions.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012-2021 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.io; + +import java.nio.file.attribute.PosixFilePermission; +import java.util.Collection; + +import org.springframework.util.Assert; + +/** + * Utilities for dealing with file permissions and attributes. + * + * @author Scott Frederick + * @since 2.5.0 + */ +public final class FilePermissions { + + private FilePermissions() { + } + + /** + * Return the integer representation of a set of Posix file permissions, where the + * integer value conforms to the + * umask octal notation. + * @param permissions the set of {@code PosixFilePermission}s + * @return the integer representation + */ + public static int posixPermissionsToUmask(Collection permissions) { + Assert.notNull(permissions, "Permissions must not be null"); + int owner = permissionToUmask(permissions, PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.OWNER_WRITE, + PosixFilePermission.OWNER_READ); + int group = permissionToUmask(permissions, PosixFilePermission.GROUP_EXECUTE, PosixFilePermission.GROUP_WRITE, + PosixFilePermission.GROUP_READ); + int other = permissionToUmask(permissions, PosixFilePermission.OTHERS_EXECUTE, PosixFilePermission.OTHERS_WRITE, + PosixFilePermission.OTHERS_READ); + return Integer.parseInt("" + owner + group + other, 8); + } + + private static int permissionToUmask(Collection permissions, PosixFilePermission execute, + PosixFilePermission write, PosixFilePermission read) { + int value = 0; + if (permissions.contains(execute)) { + value += 1; + } + if (permissions.contains(write)) { + value += 2; + } + if (permissions.contains(read)) { + value += 4; + } + return value; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/IOBiConsumer.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/IOBiConsumer.java new file mode 100644 index 00000000000..1cda2c7d31d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/IOBiConsumer.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2021 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.io; + +import java.io.IOException; + +/** + * BiConsumer that can safely throw {@link IOException IO exceptions}. + * + * @param the first consumed type + * @param the second consumed type + * @author Phillip Webb + * @since 2.3.0 + */ +@FunctionalInterface +public interface IOBiConsumer { + + /** + * Performs this operation on the given argument. + * @param t the first instance to consume + * @param u the second instance to consumer + * @throws IOException on IO error + */ + void accept(T t, U u) throws IOException; + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/Layout.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/Layout.java index c0f7bed33f0..2e838d86ce7 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/Layout.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/Layout.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -22,13 +22,14 @@ import java.io.IOException; * Interface that can be used to write a file/directory layout. * * @author Phillip Webb + * @author Scott Frederick * @since 2.3.0 */ public interface Layout { /** * Add a directory to the content. - * @param name the full name of the directory to add. + * @param name the full name of the directory to add * @param owner the owner of the directory * @throws IOException on IO error */ @@ -36,11 +37,23 @@ public interface Layout { /** * Write a file to the content. - * @param name the full name of the file to add. + * @param name the full name of the file to add * @param owner the owner of the file * @param content the content to add * @throws IOException on IO error */ - void file(String name, Owner owner, Content content) throws IOException; + default void file(String name, Owner owner, Content content) throws IOException { + file(name, owner, 0644, content); + } + + /** + * Write a file to the content. + * @param name the full name of the file to add + * @param owner the owner of the file + * @param mode the permissions for the file + * @param content the content to add + * @throws IOException on IO error + */ + void file(String name, Owner owner, int mode, Content content) throws IOException; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/TarLayoutWriter.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/TarLayoutWriter.java index 9b074e4ea8a..da9140abdd1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/TarLayoutWriter.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/TarLayoutWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -30,6 +30,7 @@ import org.springframework.util.StreamUtils; * {@link Layout} for writing TAR archive content directly to an {@link OutputStream}. * * @author Phillip Webb + * @author Scott Frederick */ class TarLayoutWriter implements Layout, Closeable { @@ -49,8 +50,8 @@ class TarLayoutWriter implements Layout, Closeable { } @Override - public void file(String name, Owner owner, Content content) throws IOException { - this.outputStream.putArchiveEntry(createFileEntry(name, owner, content.size())); + public void file(String name, Owner owner, int mode, Content content) throws IOException { + this.outputStream.putArchiveEntry(createFileEntry(name, owner, mode, content.size())); content.writeTo(StreamUtils.nonClosing(this.outputStream)); this.outputStream.closeArchiveEntry(); } @@ -59,8 +60,8 @@ class TarLayoutWriter implements Layout, Closeable { return createEntry(name, owner, TarConstants.LF_DIR, 0755, 0); } - private TarArchiveEntry createFileEntry(String name, Owner owner, int size) { - return createEntry(name, owner, TarConstants.LF_NORMAL, 0644, size); + private TarArchiveEntry createFileEntry(String name, Owner owner, int mode, int size) { + return createEntry(name, owner, TarConstants.LF_NORMAL, mode, size); } private TarArchiveEntry createEntry(String name, Owner owner, byte linkFlag, int mode, int size) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java index e78ff3145e7..87c71af2184 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -22,6 +22,7 @@ import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; @@ -163,6 +164,23 @@ public class BuildRequestTests { .withMessage("Value must not be empty"); } + @Test + void withBuildpacksAddsBuildpacks() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); + BuildpackReference buildpackReference1 = BuildpackReference.of("example/buildpack1"); + BuildpackReference buildpackReference2 = BuildpackReference.of("example/buildpack2"); + BuildRequest withBuildpacks = request.withBuildpacks(buildpackReference1, buildpackReference2); + assertThat(request.getBuildpacks()).isEmpty(); + assertThat(withBuildpacks.getBuildpacks()).containsExactly(buildpackReference1, buildpackReference2); + } + + @Test + void withBuildpacksWhenBuildpacksIsNullThrowsException() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); + assertThatIllegalArgumentException().isThrownBy(() -> request.withBuildpacks((List) null)) + .withMessage("Buildpacks must not be null"); + } + private void hasExpectedJarContent(TarArchive archive) { try { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderBuildpackTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderBuildpackTests.java new file mode 100644 index 00000000000..0907c1a2c25 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderBuildpackTests.java @@ -0,0 +1,115 @@ +/* + * Copyright 2012-2021 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.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.docker.type.Layer; +import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link BuilderBuildpack}. + * + * @author Scott Frederick + */ +class BuilderBuildpackTests extends AbstractJsonTests { + + private BuildpackResolverContext resolverContext; + + @BeforeEach + void setUp() throws Exception { + BuilderMetadata metadata = BuilderMetadata.fromJson(getContentAsString("builder-metadata.json")); + this.resolverContext = mock(BuildpackResolverContext.class); + given(this.resolverContext.getBuildpackMetadata()).willReturn(metadata.getBuildpacks()); + } + + @Test + void resolveWhenFullyQualifiedBuildpackWithVersionResolves() throws Exception { + BuildpackReference reference = BuildpackReference.of("urn:cnb:builder:paketo-buildpacks/spring-boot@3.5.0"); + Buildpack buildpack = BuilderBuildpack.resolve(this.resolverContext, reference); + assertThat(buildpack.getCoordinates()) + .isEqualTo(BuildpackCoordinates.of("paketo-buildpacks/spring-boot", "3.5.0")); + assertThatNoLayersAreAdded(buildpack); + } + + @Test + void resolveWhenFullyQualifiedBuildpackWithoutVersionResolves() throws Exception { + BuildpackReference reference = BuildpackReference.of("urn:cnb:builder:paketo-buildpacks/spring-boot"); + Buildpack buildpack = BuilderBuildpack.resolve(this.resolverContext, reference); + assertThat(buildpack.getCoordinates()) + .isEqualTo(BuildpackCoordinates.of("paketo-buildpacks/spring-boot", "3.5.0")); + assertThatNoLayersAreAdded(buildpack); + } + + @Test + void resolveWhenUnqualifiedBuildpackWithVersionResolves() throws Exception { + BuildpackReference reference = BuildpackReference.of("paketo-buildpacks/spring-boot@3.5.0"); + Buildpack buildpack = BuilderBuildpack.resolve(this.resolverContext, reference); + assertThat(buildpack.getCoordinates()) + .isEqualTo(BuildpackCoordinates.of("paketo-buildpacks/spring-boot", "3.5.0")); + assertThatNoLayersAreAdded(buildpack); + } + + @Test + void resolveWhenUnqualifiedBuildpackWithoutVersionResolves() throws Exception { + BuildpackReference reference = BuildpackReference.of("paketo-buildpacks/spring-boot"); + Buildpack buildpack = BuilderBuildpack.resolve(this.resolverContext, reference); + assertThat(buildpack.getCoordinates()) + .isEqualTo(BuildpackCoordinates.of("paketo-buildpacks/spring-boot", "3.5.0")); + assertThatNoLayersAreAdded(buildpack); + } + + @Test + void resolveWhenFullyQualifiedBuildpackWithVersionNotInBuilderThrowsException() { + BuildpackReference reference = BuildpackReference.of("urn:cnb:builder:example/buildpack1@1.2.3"); + assertThatIllegalArgumentException().isThrownBy(() -> BuilderBuildpack.resolve(this.resolverContext, reference)) + .withMessageContaining("'urn:cnb:builder:example/buildpack1@1.2.3'") + .withMessageContaining("not found in builder"); + } + + @Test + void resolveWhenFullyQualifiedBuildpackWithoutVersionNotInBuilderThrowsException() { + BuildpackReference reference = BuildpackReference.of("urn:cnb:builder:example/buildpack1"); + assertThatIllegalArgumentException().isThrownBy(() -> BuilderBuildpack.resolve(this.resolverContext, reference)) + .withMessageContaining("'urn:cnb:builder:example/buildpack1'") + .withMessageContaining("not found in builder"); + } + + @Test + void resolveWhenUnqualifiedBuildpackNotInBuilderReturnsNull() { + BuildpackReference reference = BuildpackReference.of("example/buildpack1@1.2.3"); + Buildpack buildpack = BuilderBuildpack.resolve(this.resolverContext, reference); + assertThat(buildpack).isNull(); + } + + private void assertThatNoLayersAreAdded(Buildpack buildpack) throws IOException { + List layers = new ArrayList<>(); + buildpack.apply(layers::add); + assertThat(layers).isEmpty(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderMetadataTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderMetadataTests.java index 87385106f1b..ccb0ac23cd4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderMetadataTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderMetadataTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -16,12 +16,8 @@ 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; @@ -31,6 +27,7 @@ import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.tuple; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -55,6 +52,14 @@ class BuilderMetadataTests extends AbstractJsonTests { assertThat(metadata.getCreatedBy().getName()).isEqualTo("Pack CLI"); assertThat(metadata.getCreatedBy().getVersion()) .isEqualTo("v0.9.0 (git sha: d42c384a39f367588f2653f2a99702db910e5ad7)"); + assertThat(metadata.getBuildpacks()).extracting(BuildpackMetadata::getId, BuildpackMetadata::getVersion) + .contains(tuple("paketo-buildpacks/java", "4.10.0")) + .contains(tuple("paketo-buildpacks/spring-boot", "3.5.0")) + .contains(tuple("paketo-buildpacks/executable-jar", "3.1.3")) + .contains(tuple("paketo-buildpacks/graalvm", "4.1.0")) + .contains(tuple("paketo-buildpacks/java-native-image", "4.7.0")) + .contains(tuple("paketo-buildpacks/spring-boot-native-image", "2.0.1")) + .contains(tuple("paketo-buildpacks/bellsoft-liberica", "6.2.0")); } @Test @@ -124,9 +129,4 @@ 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")); - } - } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java index 0d5a5da8835..8e17071958c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java @@ -320,10 +320,8 @@ class BuilderTests { .willAnswer(withPulledImage(builderImage)); Builder builder = new Builder(BuildLog.to(out), docker, dockerConfiguration); BuildRequest request = getTestRequest(); - assertThatIllegalStateException().isThrownBy(() -> builder.build(request)) - .withMessageContaining(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME) - .withMessageContaining("example.com/custom/run:latest") - .withMessageContaining("must be pulled from the same authenticated registry"); + assertThatIllegalStateException().isThrownBy(() -> builder.build(request)).withMessage( + "Run image 'example.com/custom/run:latest' must be pulled from the 'docker.io' authenticated registry"); } @Test @@ -338,10 +336,26 @@ class BuilderTests { .willAnswer(withPulledImage(builderImage)); Builder builder = new Builder(BuildLog.to(out), docker, dockerConfiguration); BuildRequest request = getTestRequest().withRunImage(ImageReference.of("example.com/custom/run:latest")); - assertThatIllegalStateException().isThrownBy(() -> builder.build(request)) - .withMessageContaining(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME) - .withMessageContaining("example.com/custom/run:latest") - .withMessageContaining("must be pulled from the same authenticated registry"); + assertThatIllegalStateException().isThrownBy(() -> builder.build(request)).withMessage( + "Run image 'example.com/custom/run:latest' must be pulled from the 'docker.io' authenticated registry"); + } + + @Test + void buildWhenRequestedBuildpackNotInBuilderThrowsException() throws Exception { + TestPrintStream out = new TestPrintStream(); + DockerApi docker = mockDockerApiLifecycleError(); + Image builderImage = loadImage("image.json"); + Image runImage = loadImage("run-image.json"); + given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(), isNull())) + .willAnswer(withPulledImage(builderImage)); + given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull())) + .willAnswer(withPulledImage(runImage)); + Builder builder = new Builder(BuildLog.to(out), docker, null); + BuildpackReference reference = BuildpackReference.of("urn:cnb:builder:example/buildpack@1.2.3"); + BuildRequest request = getTestRequest().withBuildpacks(reference); + assertThatIllegalArgumentException().isThrownBy(() -> builder.build(request)) + .withMessageContaining("'urn:cnb:builder:example/buildpack@1.2.3'") + .withMessageContaining("not found in builder"); } private DockerApi mockDockerApi() throws IOException { @@ -349,15 +363,12 @@ class BuilderTests { ContainerReference reference = ContainerReference.of("container-ref"); given(containerApi.create(any(), any())).willReturn(reference); given(containerApi.wait(eq(reference))).willReturn(ContainerStatus.of(0, null)); - ImageApi imageApi = mock(ImageApi.class); VolumeApi volumeApi = mock(VolumeApi.class); - DockerApi docker = mock(DockerApi.class); given(docker.image()).willReturn(imageApi); given(docker.container()).willReturn(containerApi); given(docker.volume()).willReturn(volumeApi); - return docker; } @@ -366,15 +377,12 @@ class BuilderTests { ContainerReference reference = ContainerReference.of("container-ref"); given(containerApi.create(any(), any())).willReturn(reference); given(containerApi.wait(eq(reference))).willReturn(ContainerStatus.of(9, null)); - ImageApi imageApi = mock(ImageApi.class); VolumeApi volumeApi = mock(VolumeApi.class); - DockerApi docker = mock(DockerApi.class); given(docker.image()).willReturn(imageApi); given(docker.container()).willReturn(containerApi); given(docker.volume()).willReturn(volumeApi); - return docker; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackCoordinatesTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackCoordinatesTests.java new file mode 100644 index 00000000000..ef7711aef36 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackCoordinatesTests.java @@ -0,0 +1,172 @@ +/* + * Copyright 2012-2021 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.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link BuildpackCoordinates}. + * + * @author Scott Frederick + * @author Phillip Webb + */ +class BuildpackCoordinatesTests extends AbstractJsonTests { + + private final Path archive = Paths.get("/buildpack/path"); + + @Test + void fromToml() throws IOException { + BuildpackCoordinates coordinates = BuildpackCoordinates + .fromToml(createTomlStream("example/buildpack1", "0.0.1", true, false), this.archive); + assertThat(coordinates.getId()).isEqualTo("example/buildpack1"); + assertThat(coordinates.getVersion()).isEqualTo("0.0.1"); + } + + @Test + void fromTomlWhenMissingDescriptorThrowsException() throws Exception { + ByteArrayInputStream coordinates = new ByteArrayInputStream("".getBytes()); + assertThatIllegalArgumentException().isThrownBy(() -> BuildpackCoordinates.fromToml(coordinates, this.archive)) + .withMessageContaining("Buildpack descriptor 'buildpack.toml' is required") + .withMessageContaining(this.archive.toString()); + } + + @Test + void fromTomlWhenMissingIDThrowsException() throws Exception { + InputStream coordinates = createTomlStream(null, null, true, false); + assertThatIllegalArgumentException().isThrownBy(() -> BuildpackCoordinates.fromToml(coordinates, this.archive)) + .withMessageContaining("Buildpack descriptor must contain ID") + .withMessageContaining(this.archive.toString()); + } + + @Test + void fromTomlWhenMissingVersionThrowsException() throws Exception { + InputStream coordinates = createTomlStream("example/buildpack1", null, true, false); + assertThatIllegalArgumentException().isThrownBy(() -> BuildpackCoordinates.fromToml(coordinates, this.archive)) + .withMessageContaining("Buildpack descriptor must contain version") + .withMessageContaining(this.archive.toString()); + } + + @Test + void fromTomlWhenMissingStacksAndOrderThrowsException() throws Exception { + InputStream coordinates = createTomlStream("example/buildpack1", "0.0.1", false, false); + assertThatIllegalArgumentException().isThrownBy(() -> BuildpackCoordinates.fromToml(coordinates, this.archive)) + .withMessageContaining("Buildpack descriptor must contain either 'stacks' or 'order'") + .withMessageContaining(this.archive.toString()); + } + + @Test + void fromTomlWhenContainsBothStacksAndOrderThrowsException() throws Exception { + InputStream coordinates = createTomlStream("example/buildpack1", "0.0.1", true, true); + assertThatIllegalArgumentException().isThrownBy(() -> BuildpackCoordinates.fromToml(coordinates, this.archive)) + .withMessageContaining("Buildpack descriptor must not contain both 'stacks' and 'order'") + .withMessageContaining(this.archive.toString()); + } + + @Test + void fromBuildpackMetadataWhenMetadataIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> BuildpackCoordinates.fromBuildpackMetadata(null)) + .withMessage("BuildpackMetadata must not be null"); + } + + @Test + void fromBuildpackMetadataReturnsCoordinates() throws Exception { + BuildpackMetadata metadata = BuildpackMetadata.fromJson(getContentAsString("buildpack-metadata.json")); + BuildpackCoordinates coordinates = BuildpackCoordinates.fromBuildpackMetadata(metadata); + assertThat(coordinates.getId()).isEqualTo("example/hello-universe"); + assertThat(coordinates.getVersion()).isEqualTo("0.0.1"); + } + + @Test + void ofWhenIdIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> BuildpackCoordinates.of(null, null)) + .withMessage("ID must not be empty"); + } + + @Test + void ofReturnsCoordinates() { + BuildpackCoordinates coordinates = BuildpackCoordinates.of("id", "1"); + assertThat(coordinates).hasToString("id@1"); + } + + @Test + void getIdReturnsId() { + BuildpackCoordinates coordinates = BuildpackCoordinates.of("id", "1"); + assertThat(coordinates.getId()).isEqualTo("id"); + } + + @Test + void getVersionReturnsVersion() { + BuildpackCoordinates coordinates = BuildpackCoordinates.of("id", "1"); + assertThat(coordinates.getVersion()).isEqualTo("1"); + } + + @Test + void getVersionWhenVersionIsNullReturnsNull() { + BuildpackCoordinates coordinates = BuildpackCoordinates.of("id", null); + assertThat(coordinates.getVersion()).isNull(); + } + + @Test + void toStringReturnsNiceString() { + BuildpackCoordinates coordinates = BuildpackCoordinates.of("id", "1"); + assertThat(coordinates).hasToString("id@1"); + } + + @Test + void equalsAndHashCode() { + BuildpackCoordinates c1a = BuildpackCoordinates.of("id", "1"); + BuildpackCoordinates c1b = BuildpackCoordinates.of("id", "1"); + BuildpackCoordinates c2 = BuildpackCoordinates.of("id", "2"); + assertThat(c1a).isEqualTo(c1a).isEqualTo(c1b).isNotEqualTo(c2); + assertThat(c1a.hashCode()).isEqualTo(c1b.hashCode()); + } + + private InputStream createTomlStream(String id, String version, boolean includeStacks, boolean includeOrder) { + StringBuilder builder = new StringBuilder(); + builder.append("[buildpack]\n"); + if (id != null) { + builder.append("id = \"").append(id).append("\"\n"); + } + if (version != null) { + builder.append("version = \"").append(version).append("\"\n"); + } + builder.append("name = \"Example buildpack\"\n"); + builder.append("homepage = \"https://github.com/example/example-buildpack\"\n"); + if (includeStacks) { + builder.append("[[stacks]]\n"); + builder.append("id = \"io.buildpacks.stacks.bionic\"\n"); + } + if (includeOrder) { + builder.append("[[order]]\n"); + builder.append("group = [ { id = \"example/buildpack2\", version=\"0.0.2\" } ]\n"); + } + return new ByteArrayInputStream(builder.toString().getBytes(StandardCharsets.UTF_8)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackMetadataTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackMetadataTests.java new file mode 100644 index 00000000000..413fd2100e8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackMetadataTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-2021 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.io.IOException; +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.docker.type.ImageConfig; +import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link BuildpackMetadata}. + * + * @author Scott Frederick + */ +class BuildpackMetadataTests extends AbstractJsonTests { + + @Test + void fromImageLoadsMetadata() throws IOException { + Image image = Image.of(getContent("buildpack-image.json")); + BuildpackMetadata metadata = BuildpackMetadata.fromImage(image); + assertThat(metadata.getId()).isEqualTo("example/hello-universe"); + assertThat(metadata.getVersion()).isEqualTo("0.0.1"); + } + + @Test + void fromImageWhenImageIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> BuildpackMetadata.fromImage(null)) + .withMessage("Image must not be null"); + } + + @Test + void fromImageWhenImageConfigIsNullThrowsException() { + Image image = mock(Image.class); + assertThatIllegalArgumentException().isThrownBy(() -> BuildpackMetadata.fromImage(image)) + .withMessage("ImageConfig must not be null"); + } + + @Test + void fromImageConfigWhenLabelIsMissingThrowsException() { + Image image = mock(Image.class); + ImageConfig imageConfig = mock(ImageConfig.class); + given(image.getConfig()).willReturn(imageConfig); + given(imageConfig.getLabels()).willReturn(Collections.singletonMap("alpha", "a")); + assertThatIllegalArgumentException().isThrownBy(() -> BuildpackMetadata.fromImage(image)) + .withMessage("No 'io.buildpacks.buildpackage.metadata' label found in image config labels 'alpha'"); + } + + @Test + void fromJsonLoadsMetadata() throws IOException { + BuildpackMetadata metadata = BuildpackMetadata.fromJson(getContentAsString("buildpack-metadata.json")); + assertThat(metadata.getId()).isEqualTo("example/hello-universe"); + assertThat(metadata.getVersion()).isEqualTo("0.0.1"); + assertThat(metadata.getHomepage()).isEqualTo("https://github.com/example/tree/main/buildpacks/hello-universe"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackReferenceTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackReferenceTests.java new file mode 100644 index 00000000000..0b5a9ba932a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackReferenceTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012-2021 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.nio.file.Paths; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link BuildpackReference}. + * + * @author Phillip Webb + */ +class BuildpackReferenceTests { + + @Test + void ofWhenValueIsEmptyThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> BuildpackReference.of("")) + .withMessage("Value must not be empty"); + } + + @Test + void ofCreatesInstance() { + BuildpackReference reference = BuildpackReference.of("test"); + assertThat(reference).isNotNull(); + } + + @Test + void toStringReturnsValue() { + BuildpackReference reference = BuildpackReference.of("test"); + assertThat(reference).hasToString("test"); + } + + @Test + void equalsAndHashCode() { + BuildpackReference a = BuildpackReference.of("test1"); + BuildpackReference b = BuildpackReference.of("test1"); + BuildpackReference c = BuildpackReference.of("test2"); + assertThat(a).isEqualTo(a).isEqualTo(b).isNotEqualTo(c); + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + } + + @Test + void hasPrefixWhenPrefixMatchReturnsTrue() { + BuildpackReference reference = BuildpackReference.of("test"); + assertThat(reference.hasPrefix("te")).isTrue(); + } + + @Test + void hasPrefixWhenPrifixMismatchReturnsFalse() { + BuildpackReference reference = BuildpackReference.of("test"); + assertThat(reference.hasPrefix("st")).isFalse(); + } + + @Test + void getSubReferenceWhenPrefixMatchReturnsSubReference() { + BuildpackReference reference = BuildpackReference.of("test"); + assertThat(reference.getSubReference("te")).isEqualTo("st"); + } + + @Test + void getSubReferenceWhenPrefixMismatchReturnsNull() { + BuildpackReference reference = BuildpackReference.of("test"); + assertThat(reference.getSubReference("st")).isNull(); + } + + @Test + void asPathWhenFileUrlReturnsPath() { + BuildpackReference reference = BuildpackReference.of("file:///test.dat"); + assertThat(reference.asPath()).isEqualTo(Paths.get("/test.dat")); + } + + @Test + void asPathWhenPathReturnsPath() { + BuildpackReference reference = BuildpackReference.of("/test.dat"); + assertThat(reference.asPath()).isEqualTo(Paths.get("/test.dat")); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackResolversTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackResolversTests.java new file mode 100644 index 00000000000..760a99030e9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackResolversTests.java @@ -0,0 +1,103 @@ +/* + * Copyright 2012-2021 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.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link BuildpackResolvers}. + * + * @author Scott Frederick + */ +class BuildpackResolversTests extends AbstractJsonTests { + + private BuildpackResolverContext resolverContext; + + @BeforeEach + void setup() throws Exception { + BuilderMetadata metadata = BuilderMetadata.fromJson(getContentAsString("builder-metadata.json")); + this.resolverContext = mock(BuildpackResolverContext.class); + given(this.resolverContext.getBuildpackMetadata()).willReturn(metadata.getBuildpacks()); + } + + @Test + void resolveAllWithBuilderBuildpackReferenceReturnsExpectedBuildpack() throws IOException { + BuildpackReference reference = BuildpackReference.of("urn:cnb:builder:paketo-buildpacks/spring-boot@3.5.0"); + Buildpacks buildpacks = BuildpackResolvers.resolveAll(this.resolverContext, Collections.singleton(reference)); + assertThat(buildpacks.getBuildpacks()).hasSize(1); + assertThat(buildpacks.getBuildpacks().get(0)).isInstanceOf(BuilderBuildpack.class); + } + + @Test + void resolveAllWithDirectoryBuildpackReferenceReturnsExpectedBuildpack(@TempDir Path temp) throws IOException { + FileCopyUtils.copy(getClass().getResourceAsStream("buildpack.toml"), + Files.newOutputStream(temp.resolve("buildpack.toml"))); + BuildpackReference reference = BuildpackReference.of(temp.toAbsolutePath().toString()); + Buildpacks buildpacks = BuildpackResolvers.resolveAll(this.resolverContext, Collections.singleton(reference)); + assertThat(buildpacks.getBuildpacks()).hasSize(1); + assertThat(buildpacks.getBuildpacks().get(0)).isInstanceOf(DirectoryBuildpack.class); + } + + @Test + void resolveAllWithTarGzipBuildpackReferenceReturnsExpectedBuildpack(@TempDir File temp) throws Exception { + TestTarGzip testTarGzip = new TestTarGzip(temp); + Path archive = testTarGzip.createArchive(); + BuildpackReference reference = BuildpackReference.of(archive.toString()); + Buildpacks buildpacks = BuildpackResolvers.resolveAll(this.resolverContext, Collections.singleton(reference)); + assertThat(buildpacks.getBuildpacks()).hasSize(1); + assertThat(buildpacks.getBuildpacks().get(0)).isInstanceOf(TarGzipBuildpack.class); + } + + @Test + void resolveAllWithImageBuildpackReferenceReturnsExpectedBuildpack() throws IOException { + Image image = Image.of(getContent("buildpack-image.json")); + BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class); + given(resolverContext.fetchImage(any(), any())).willReturn(image); + BuildpackReference reference = BuildpackReference.of("docker://example/buildpack1:latest"); + Buildpacks buildpacks = BuildpackResolvers.resolveAll(resolverContext, Collections.singleton(reference)); + assertThat(buildpacks.getBuildpacks()).hasSize(1); + assertThat(buildpacks.getBuildpacks().get(0)).isInstanceOf(ImageBuildpack.class); + } + + @Test + void resolveAllWithInvalidLocatorThrowsException() throws IOException { + BuildpackReference reference = BuildpackReference.of("unknown-buildpack@0.0.1"); + assertThatIllegalArgumentException() + .isThrownBy(() -> BuildpackResolvers.resolveAll(this.resolverContext, Collections.singleton(reference))) + .withMessageContaining("Invalid buildpack reference") + .withMessageContaining("'unknown-buildpack@0.0.1'"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpacksTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpacksTests.java new file mode 100644 index 00000000000..a45b2a72472 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpacksTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2012-2021 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.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.docker.type.Layer; +import org.springframework.util.StreamUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Buildpacks}. + * + * @author Scott Frederick + * @author Phillip Webb + */ +class BuildpacksTests { + + @Test + void ofWhenBuildpacksIsNullReturnsEmpty() { + Buildpacks buildpacks = Buildpacks.of(null); + assertThat(buildpacks).isSameAs(Buildpacks.EMPTY); + assertThat(buildpacks.getBuildpacks()).isEmpty(); + } + + @Test + void ofReturnsBuildpacks() { + List buildpackList = new ArrayList<>(); + buildpackList.add(new TestBuildpack("example/buildpack1", "0.0.1")); + buildpackList.add(new TestBuildpack("example/buildpack2", "0.0.2")); + Buildpacks buildpacks = Buildpacks.of(buildpackList); + assertThat(buildpacks.getBuildpacks()).isEqualTo(buildpackList); + } + + @Test + void applyWritesLayersAndOrderLayer() throws Exception { + List buildpackList = new ArrayList<>(); + buildpackList.add(new TestBuildpack("example/buildpack1", "0.0.1")); + buildpackList.add(new TestBuildpack("example/buildpack2", "0.0.2")); + buildpackList.add(new TestBuildpack("example/buildpack3", null)); + Buildpacks buildpacks = Buildpacks.of(buildpackList); + List layers = new ArrayList<>(); + buildpacks.apply(layers::add); + assertThat(layers).hasSize(4); + assertThatLayerContentIsCorrect(layers.get(0), "example_buildpack1/0.0.1"); + assertThatLayerContentIsCorrect(layers.get(1), "example_buildpack2/0.0.2"); + assertThatLayerContentIsCorrect(layers.get(2), "example_buildpack3/null"); + assertThatOrderLayerContentIsCorrect(layers.get(3)); + } + + private void assertThatLayerContentIsCorrect(Layer layer, String path) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + layer.writeTo(out); + try (TarArchiveInputStream tar = new TarArchiveInputStream(new ByteArrayInputStream(out.toByteArray()))) { + assertThat(tar.getNextEntry().getName()).isEqualTo("/cnb/buildpacks/" + path + "/buildpack.toml"); + assertThat(tar.getNextEntry()).isNull(); + } + } + + private void assertThatOrderLayerContentIsCorrect(Layer layer) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + layer.writeTo(out); + try (TarArchiveInputStream tar = new TarArchiveInputStream(new ByteArrayInputStream(out.toByteArray()))) { + assertThat(tar.getNextEntry().getName()).isEqualTo("/cnb/order.toml"); + byte[] content = StreamUtils.copyToByteArray(tar); + String toml = new String(content, StandardCharsets.UTF_8); + assertThat(toml).isEqualTo(getExpectedToml()); + } + } + + private String getExpectedToml() { + StringBuilder toml = new StringBuilder(); + toml.append("[[order]]\n"); + toml.append("group = [\n"); + toml.append(" { id = \"example/buildpack1\", version = \"0.0.1\" }\n"); + toml.append("]\n\n"); + toml.append("[[order]]\n"); + toml.append("group = [\n"); + toml.append(" { id = \"example/buildpack2\", version = \"0.0.2\" }\n"); + toml.append("]\n\n"); + toml.append("[[order]]\n"); + toml.append("group = [\n"); + toml.append(" { id = \"example/buildpack3\" }\n"); + toml.append("]\n\n"); + return toml.toString(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/DirectoryBuildpackTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/DirectoryBuildpackTests.java new file mode 100644 index 00000000000..c88351cf8c5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/DirectoryBuildpackTests.java @@ -0,0 +1,174 @@ +/* + * Copyright 2012-2021 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.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link DirectoryBuildpack}. + * + * @author Scott Frederick + */ +@DisabledOnOs(OS.WINDOWS) +class DirectoryBuildpackTests { + + @TempDir + File temp; + + private File buildpackDir; + + private BuildpackResolverContext resolverContext; + + @BeforeEach + void setUp() { + this.buildpackDir = new File(this.temp, "buildpack"); + this.buildpackDir.mkdirs(); + this.resolverContext = mock(BuildpackResolverContext.class); + } + + @Test + void resolveWhenPath() throws Exception { + writeBuildpackDescriptor(); + writeScripts(); + BuildpackReference reference = BuildpackReference.of(this.buildpackDir.toString()); + Buildpack buildpack = DirectoryBuildpack.resolve(this.resolverContext, reference); + assertThat(buildpack).isNotNull(); + assertThat(buildpack.getCoordinates()).hasToString("example/buildpack1@0.0.1"); + assertHasExpectedLayers(buildpack); + } + + @Test + void resolveWhenFileUrl() throws Exception { + writeBuildpackDescriptor(); + writeScripts(); + BuildpackReference reference = BuildpackReference.of("file://" + this.buildpackDir.toString()); + Buildpack buildpack = DirectoryBuildpack.resolve(this.resolverContext, reference); + assertThat(buildpack).isNotNull(); + assertThat(buildpack.getCoordinates()).hasToString("example/buildpack1@0.0.1"); + assertHasExpectedLayers(buildpack); + } + + @Test + void resolveWhenDirectoryWithoutBuildpackTomlThrowsException() throws Exception { + Files.createDirectories(this.buildpackDir.toPath()); + BuildpackReference reference = BuildpackReference.of(this.buildpackDir.toString()); + assertThatIllegalArgumentException() + .isThrownBy(() -> DirectoryBuildpack.resolve(this.resolverContext, reference)) + .withMessageContaining("Buildpack descriptor 'buildpack.toml' is required") + .withMessageContaining(this.buildpackDir.getAbsolutePath()); + } + + @Test + void resolveWhenFileReturnsNull() throws Exception { + Path file = Files.createFile(Paths.get(this.buildpackDir.toString(), "test")); + BuildpackReference reference = BuildpackReference.of(file.toString()); + Buildpack buildpack = DirectoryBuildpack.resolve(this.resolverContext, reference); + assertThat(buildpack).isNull(); + } + + @Test + void resolveWhenDirectoryDoesNotExistReturnsNull() { + BuildpackReference reference = BuildpackReference.of("/test/a/missing/buildpack"); + Buildpack buildpack = DirectoryBuildpack.resolve(this.resolverContext, reference); + assertThat(buildpack).isNull(); + } + + @Test + void locateDirectoryAsUrlThatDoesNotExistThrowsException() { + BuildpackReference reference = BuildpackReference.of("file:///test/a/missing/buildpack"); + Buildpack buildpack = DirectoryBuildpack.resolve(this.resolverContext, reference); + assertThat(buildpack).isNull(); + } + + private void assertHasExpectedLayers(Buildpack buildpack) throws IOException { + List layers = new ArrayList<>(); + buildpack.apply((layer) -> { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + layer.writeTo(out); + layers.add(out); + }); + assertThat(layers).hasSize(1); + byte[] content = layers.get(0).toByteArray(); + try (TarArchiveInputStream tar = new TarArchiveInputStream(new ByteArrayInputStream(content))) { + List entries = new ArrayList<>(); + TarArchiveEntry entry = tar.getNextTarEntry(); + while (entry != null) { + entries.add(entry); + entry = tar.getNextTarEntry(); + } + assertThat(entries).extracting("name", "mode").containsExactlyInAnyOrder( + tuple("/cnb/buildpacks/example_buildpack1/0.0.1/buildpack.toml", 0644), + tuple("/cnb/buildpacks/example_buildpack1/0.0.1/bin/detect", 0744), + tuple("/cnb/buildpacks/example_buildpack1/0.0.1/bin/build", 0744)); + } + } + + private void writeBuildpackDescriptor() throws IOException { + File descriptor = new File(this.buildpackDir, "buildpack.toml"); + try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(descriptor.toPath()))) { + writer.println("[buildpack]"); + writer.println("id = \"example/buildpack1\""); + writer.println("version = \"0.0.1\""); + writer.println("name = \"Example buildpack\""); + writer.println("homepage = \"https://github.com/example/example-buildpack\""); + writer.println("[[stacks]]"); + writer.println("id = \"io.buildpacks.stacks.bionic\""); + } + } + + private void writeScripts() throws IOException { + File binDirectory = new File(this.buildpackDir, "bin"); + binDirectory.mkdirs(); + Path detect = Files.createFile(Paths.get(binDirectory.getAbsolutePath(), "detect"), + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwxr--r--"))); + try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(detect))) { + writer.println("#!/usr/bin/env bash"); + writer.println("echo \"---> detect\""); + } + Path build = Files.createFile(Paths.get(binDirectory.getAbsolutePath(), "build"), + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwxr--r--"))); + try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(build))) { + writer.println("#!/usr/bin/env bash"); + writer.println("echo \"---> build\""); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilderTests.java index 3f8f6411f18..2452003b4f8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilderTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -20,12 +20,17 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.time.OffsetDateTime; import java.time.ZoneId; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.apache.commons.compress.archivers.ArchiveEntry; @@ -40,6 +45,7 @@ import org.springframework.boot.buildpack.platform.docker.type.ImageArchive; import org.springframework.boot.buildpack.platform.docker.type.ImageConfig; import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; +import org.springframework.util.FileCopyUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -62,7 +68,9 @@ class EphemeralBuilderTests extends AbstractJsonTests { private Map env; - private Creator creator = Creator.withVersion("dev"); + private Buildpacks buildpacks; + + private final Creator creator = Creator.withVersion("dev"); @BeforeEach void setup() throws Exception { @@ -75,15 +83,18 @@ class EphemeralBuilderTests extends AbstractJsonTests { @Test void getNameHasRandomName() throws Exception { - EphemeralBuilder b1 = new EphemeralBuilder(this.owner, this.image, this.metadata, this.creator, this.env); - EphemeralBuilder b2 = new EphemeralBuilder(this.owner, this.image, this.metadata, this.creator, this.env); + EphemeralBuilder b1 = new EphemeralBuilder(this.owner, this.image, this.metadata, this.creator, this.env, + this.buildpacks); + EphemeralBuilder b2 = new EphemeralBuilder(this.owner, this.image, this.metadata, this.creator, this.env, + this.buildpacks); assertThat(b1.getName().toString()).startsWith("pack.local/builder/").endsWith(":latest"); assertThat(b1.getName().toString()).isNotEqualTo(b2.getName().toString()); } @Test void getArchiveHasCreatedByConfig() throws Exception { - EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.metadata, this.creator, this.env); + EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.metadata, this.creator, this.env, + this.buildpacks); ImageConfig config = builder.getArchive().getImageConfig(); BuilderMetadata ephemeralMetadata = BuilderMetadata.fromImageConfig(config); assertThat(ephemeralMetadata.getCreatedBy().getName()).isEqualTo("Spring Boot"); @@ -92,14 +103,16 @@ class EphemeralBuilderTests extends AbstractJsonTests { @Test void getArchiveHasTag() throws Exception { - EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.metadata, this.creator, this.env); + EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.metadata, this.creator, this.env, + this.buildpacks); ImageReference tag = builder.getArchive().getTag(); assertThat(tag.toString()).startsWith("pack.local/builder/").endsWith(":latest"); } @Test void getArchiveHasFixedCreateDate() throws Exception { - EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.metadata, this.creator, this.env); + EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.metadata, this.creator, this.env, + this.buildpacks); Instant createInstant = builder.getArchive().getCreateDate(); OffsetDateTime createDateTime = OffsetDateTime.ofInstant(createInstant, ZoneId.of("UTC")); assertThat(createDateTime.getYear()).isEqualTo(1980); @@ -112,12 +125,35 @@ class EphemeralBuilderTests extends AbstractJsonTests { @Test void getArchiveContainsEnvLayer() throws Exception { - EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.metadata, this.creator, this.env); + EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.metadata, this.creator, this.env, + this.buildpacks); File directory = unpack(getLayer(builder.getArchive(), 0), "env"); assertThat(new File(directory, "platform/env/spring")).usingCharset(StandardCharsets.UTF_8).hasContent("boot"); assertThat(new File(directory, "platform/env/empty")).usingCharset(StandardCharsets.UTF_8).hasContent(""); } + @Test + void getArchiveContainsBuildpackLayers() throws Exception { + List buildpackList = new ArrayList<>(); + buildpackList.add(new TestBuildpack("example/buildpack1", "0.0.1")); + buildpackList.add(new TestBuildpack("example/buildpack2", "0.0.2")); + buildpackList.add(new TestBuildpack("example/buildpack3", "0.0.3")); + this.buildpacks = Buildpacks.of(buildpackList); + EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.metadata, this.creator, null, + this.buildpacks); + assertBuildpackLayerContent(builder, 0, "/cnb/buildpacks/example_buildpack1/0.0.1/buildpack.toml"); + assertBuildpackLayerContent(builder, 1, "/cnb/buildpacks/example_buildpack2/0.0.2/buildpack.toml"); + assertBuildpackLayerContent(builder, 2, "/cnb/buildpacks/example_buildpack3/0.0.3/buildpack.toml"); + File orderDirectory = unpack(getLayer(builder.getArchive(), 3), "order"); + assertThat(new File(orderDirectory, "cnb/order.toml")).usingCharset(StandardCharsets.UTF_8) + .hasContent(content("order-versions.toml")); + } + + private void assertBuildpackLayerContent(EphemeralBuilder builder, int index, String s) throws Exception { + File buildpackDirectory = unpack(getLayer(builder.getArchive(), index), "buildpack"); + assertThat(new File(buildpackDirectory, s)).usingCharset(StandardCharsets.UTF_8).hasContent("[test]"); + } + private TarArchiveInputStream getLayer(ImageArchive archive, int index) throws Exception { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); archive.writeTo(outputStream); @@ -148,4 +184,9 @@ class EphemeralBuilderTests extends AbstractJsonTests { return directory; } + private String content(String fileName) throws IOException { + InputStream in = getClass().getResourceAsStream(fileName); + return FileCopyUtils.copyToString(new InputStreamReader(in, StandardCharsets.UTF_8)); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ImageBuildpackTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ImageBuildpackTests.java new file mode 100644 index 00000000000..ffb22304518 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ImageBuildpackTests.java @@ -0,0 +1,138 @@ +/* + * Copyright 2012-2021 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.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.junit.jupiter.api.Test; +import org.mockito.invocation.InvocationOnMock; + +import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.io.IOBiConsumer; +import org.springframework.boot.buildpack.platform.io.TarArchive; +import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ImageBuildpack}. + * + * @author Scott Frederick + * @author Phillip Webb + */ +class ImageBuildpackTests extends AbstractJsonTests { + + @Test + void resolveWhenFullyQualifiedReferenceReturnsBuilder() throws Exception { + Image image = Image.of(getContent("buildpack-image.json")); + BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class); + given(resolverContext.fetchImage(any(), any())).willReturn(image); + willAnswer(this::withMockLayers).given(resolverContext).exportImageLayers(any(), any()); + BuildpackReference reference = BuildpackReference.of("docker://example/buildpack1:latest"); + Buildpack buildpack = ImageBuildpack.resolve(resolverContext, reference); + assertThat(buildpack.getCoordinates()).hasToString("example/hello-universe@0.0.1"); + assertHasExpectedLayers(buildpack); + } + + @Test + void resolveWhenUnqualifiedReferenceReturnsBuilder() throws Exception { + Image image = Image.of(getContent("buildpack-image.json")); + BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class); + given(resolverContext.fetchImage(any(), any())).willReturn(image); + willAnswer(this::withMockLayers).given(resolverContext).exportImageLayers(any(), any()); + BuildpackReference reference = BuildpackReference.of("example/buildpack1:latest"); + Buildpack buildpack = ImageBuildpack.resolve(resolverContext, reference); + assertThat(buildpack.getCoordinates()).hasToString("example/hello-universe@0.0.1"); + assertHasExpectedLayers(buildpack); + } + + @Test + void resolveWhenWhenImageNotPulledThrowsException() throws Exception { + BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class); + given(resolverContext.fetchImage(any(), any())).willThrow(IOException.class); + BuildpackReference reference = BuildpackReference.of("docker://example/buildpack1:latest"); + assertThatIllegalArgumentException().isThrownBy(() -> ImageBuildpack.resolve(resolverContext, reference)) + .withMessageContaining("Error pulling buildpack image") + .withMessageContaining("example/buildpack1:latest"); + } + + @Test + void resolveWhenMissingMetadataLabelThrowsException() throws Exception { + Image image = Image.of(getContent("image.json")); + BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class); + given(resolverContext.fetchImage(any(), any())).willReturn(image); + BuildpackReference reference = BuildpackReference.of("docker://example/buildpack1:latest"); + assertThatIllegalArgumentException().isThrownBy(() -> ImageBuildpack.resolve(resolverContext, reference)) + .withMessageContaining("No 'io.buildpacks.buildpackage.metadata' label found"); + } + + @Test + void resolveWhenFullyQualifiedReferenceWithInvalidImageReferenceThrowsException() throws Exception { + BuildpackReference reference = BuildpackReference.of("docker://buildpack@0.0.1"); + BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class); + assertThatIllegalArgumentException().isThrownBy(() -> ImageBuildpack.resolve(resolverContext, reference)) + .withMessageContaining("Unable to parse image reference \"buildpack@0.0.1\""); + } + + @Test + void resolveWhenUnqualifiedReferenceWithInvalidImageReferenceReturnsNull() throws Exception { + BuildpackReference reference = BuildpackReference.of("buildpack@0.0.1"); + BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class); + Buildpack buildpack = ImageBuildpack.resolve(resolverContext, reference); + assertThat(buildpack).isNull(); + } + + private Object withMockLayers(InvocationOnMock invocation) throws Exception { + IOBiConsumer consumer = invocation.getArgument(1); + TarArchive archive = (out) -> FileCopyUtils.copy(getClass().getResourceAsStream("layer.tar"), out); + consumer.accept("test", archive); + return null; + } + + private void assertHasExpectedLayers(Buildpack buildpack) throws IOException { + List layers = new ArrayList<>(); + buildpack.apply((layer) -> { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + layer.writeTo(out); + layers.add(out); + }); + assertThat(layers).hasSize(1); + byte[] content = layers.get(0).toByteArray(); + List names = new ArrayList<>(); + try (TarArchiveInputStream tar = new TarArchiveInputStream(new ByteArrayInputStream(content))) { + TarArchiveEntry entry = tar.getNextTarEntry(); + while (entry != null) { + names.add(entry.getName()); + entry = tar.getNextTarEntry(); + } + } + assertThat(names).containsExactly("etc/apt/sources.list"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/TarGzipBuildpackTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/TarGzipBuildpackTests.java new file mode 100644 index 00000000000..9bc68cb8bbf --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/TarGzipBuildpackTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 2012-2021 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.io.File; +import java.nio.file.Path; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link TarGzipBuildpack}. + * + * @author Scott Frederick + */ +class TarGzipBuildpackTests { + + private File buildpackDir; + + private TestTarGzip testTarGzip; + + private BuildpackResolverContext resolverContext; + + @BeforeEach + void setUp(@TempDir File temp) { + this.buildpackDir = new File(temp, "buildpack"); + this.buildpackDir.mkdirs(); + this.testTarGzip = new TestTarGzip(this.buildpackDir); + this.resolverContext = mock(BuildpackResolverContext.class); + } + + @Test + void resolveWhenFilePathReturnsBuildpack() throws Exception { + Path compressedArchive = this.testTarGzip.createArchive(); + BuildpackReference reference = BuildpackReference.of(compressedArchive.toString()); + Buildpack buildpack = TarGzipBuildpack.resolve(this.resolverContext, reference); + assertThat(buildpack).isNotNull(); + assertThat(buildpack.getCoordinates()).hasToString("example/buildpack1@0.0.1"); + this.testTarGzip.assertHasExpectedLayers(buildpack); + } + + @Test + void resolveWhenFileUrlReturnsBuildpack() throws Exception { + Path compressedArchive = this.testTarGzip.createArchive(); + BuildpackReference reference = BuildpackReference.of("file://" + compressedArchive.toString()); + Buildpack buildpack = TarGzipBuildpack.resolve(this.resolverContext, reference); + assertThat(buildpack).isNotNull(); + assertThat(buildpack.getCoordinates()).hasToString("example/buildpack1@0.0.1"); + this.testTarGzip.assertHasExpectedLayers(buildpack); + } + + @Test + void resolveWhenArchiveWithoutDescriptorThrowsException() throws Exception { + Path compressedArchive = this.testTarGzip.createEmptyArchive(); + BuildpackReference reference = BuildpackReference.of(compressedArchive.toString()); + assertThatIllegalArgumentException().isThrownBy(() -> TarGzipBuildpack.resolve(this.resolverContext, reference)) + .withMessageContaining("Buildpack descriptor 'buildpack.toml' is required") + .withMessageContaining(compressedArchive.toString()); + } + + @Test + void resolveWhenArchiveWithDirectoryReturnsNull() { + BuildpackReference reference = BuildpackReference.of(this.buildpackDir.getAbsolutePath()); + Buildpack buildpack = TarGzipBuildpack.resolve(this.resolverContext, reference); + assertThat(buildpack).isNull(); + } + + @Test + void resolveWhenArchiveThatDoesNotExistReturnsNull() { + BuildpackReference reference = BuildpackReference.of("/test/i/am/missing/buildpack.tar"); + Buildpack buildpack = TarGzipBuildpack.resolve(this.resolverContext, reference); + assertThat(buildpack).isNull(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/TestBuildpack.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/TestBuildpack.java new file mode 100644 index 00000000000..8775f4c0365 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/TestBuildpack.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2021 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.io.IOException; + +import org.springframework.boot.buildpack.platform.docker.type.Layer; +import org.springframework.boot.buildpack.platform.io.Content; +import org.springframework.boot.buildpack.platform.io.IOConsumer; +import org.springframework.boot.buildpack.platform.io.Layout; +import org.springframework.boot.buildpack.platform.io.Owner; + +/** + * A test {@link Buildpack}. + * + * @author Scott Frederick + * @author Phillip Webb + */ +class TestBuildpack implements Buildpack { + + private final BuildpackCoordinates coordinates; + + TestBuildpack(String id, String version) { + this.coordinates = BuildpackCoordinates.of(id, version); + } + + @Override + public BuildpackCoordinates getCoordinates() { + return this.coordinates; + } + + @Override + public void apply(IOConsumer layers) throws IOException { + layers.accept(Layer.of(this::getContent)); + } + + private void getContent(Layout layout) throws IOException { + String id = this.coordinates.getSanitizedId(); + String dir = "/cnb/buildpacks/" + id + "/" + this.coordinates.getVersion(); + layout.file(dir + "/buildpack.toml", Owner.ROOT, Content.of("[test]")); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/TestTarGzip.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/TestTarGzip.java new file mode 100644 index 00000000000..94bae1cd5be --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/TestTarGzip.java @@ -0,0 +1,122 @@ +/* + * Copyright 2012-2021 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.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream; +import org.apache.commons.compress.utils.IOUtils; + +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Utility to create test tgz files. + * + * @author Scott Frederick + */ +class TestTarGzip { + + private final File buildpackDir; + + TestTarGzip(File buildpackDir) { + this.buildpackDir = buildpackDir; + } + + Path createArchive() throws Exception { + return createArchive(true); + } + + Path createEmptyArchive() throws Exception { + return createArchive(false); + } + + private Path createArchive(boolean addContent) throws Exception { + Path path = Paths.get(this.buildpackDir.getAbsolutePath(), "buildpack.tar"); + Path archive = Files.createFile(path); + if (addContent) { + writeBuildpackContentToArchive(archive); + } + return compressBuildpackArchive(archive); + } + + private Path compressBuildpackArchive(Path archive) throws Exception { + Path tgzPath = Paths.get(this.buildpackDir.getAbsolutePath(), "buildpack.tgz"); + FileCopyUtils.copy(Files.newInputStream(archive), + new GzipCompressorOutputStream(Files.newOutputStream(tgzPath))); + return tgzPath; + } + + private void writeBuildpackContentToArchive(Path archive) throws Exception { + StringBuilder buildpackToml = new StringBuilder(); + buildpackToml.append("[buildpack]\n"); + buildpackToml.append("id = \"example/buildpack1\"\n"); + buildpackToml.append("version = \"0.0.1\"\n"); + buildpackToml.append("name = \"Example buildpack\"\n"); + buildpackToml.append("homepage = \"https://github.com/example/example-buildpack\"\n"); + buildpackToml.append("[[stacks]]\n"); + buildpackToml.append("id = \"io.buildpacks.stacks.bionic\"\n"); + String detectScript = "#!/usr/bin/env bash\n" + "echo \"---> detect\"\n"; + String buildScript = "#!/usr/bin/env bash\n" + "echo \"---> build\"\n"; + try (TarArchiveOutputStream tar = new TarArchiveOutputStream(Files.newOutputStream(archive))) { + writeEntry(tar, "buildpack.toml", buildpackToml.toString()); + writeEntry(tar, "bin/detect", detectScript); + writeEntry(tar, "bin/build", buildScript); + tar.finish(); + } + } + + private void writeEntry(TarArchiveOutputStream tar, String entryName, String content) throws IOException { + TarArchiveEntry entry = new TarArchiveEntry(entryName); + entry.setSize(content.length()); + tar.putArchiveEntry(entry); + IOUtils.copy(new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)), tar); + tar.closeArchiveEntry(); + } + + void assertHasExpectedLayers(Buildpack buildpack) throws IOException { + List layers = new ArrayList<>(); + buildpack.apply((layer) -> { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + layer.writeTo(out); + layers.add(out); + }); + assertThat(layers).hasSize(1); + byte[] content = layers.get(0).toByteArray(); + try (TarArchiveInputStream tar = new TarArchiveInputStream(new ByteArrayInputStream(content))) { + assertThat(tar.getNextEntry().getName()) + .isEqualTo("cnb/buildpacks/example_buildpack1/0.0.1/buildpack.toml"); + assertThat(tar.getNextEntry().getName()).isEqualTo("cnb/buildpacks/example_buildpack1/0.0.1/bin/detect"); + assertThat(tar.getNextEntry().getName()).isEqualTo("cnb/buildpacks/example_buildpack1/0.0.1/bin/build"); + assertThat(tar.getNextEntry()).isNull(); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java index fac9554e169..912d5af9755 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -16,11 +16,14 @@ package org.springframework.boot.buildpack.platform.docker; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -48,6 +51,8 @@ import org.springframework.boot.buildpack.platform.io.Content; import org.springframework.boot.buildpack.platform.io.IOConsumer; import org.springframework.boot.buildpack.platform.io.Owner; import org.springframework.boot.buildpack.platform.io.TarArchive; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -304,6 +309,45 @@ class DockerApiTests { assertThat(image.getLayers()).hasSize(46); } + @Test + void exportLayersWhenReferenceIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.api.exportLayers(null, (name, archive) -> { + })).withMessage("Reference must not be null"); + } + + @Test + void exportLayersWhenExportsIsNullThrowsException() { + ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base"); + assertThatIllegalArgumentException().isThrownBy(() -> this.api.exportLayers(reference, null)) + .withMessage("Exports must not be null"); + } + + @Test + void exportLayersExportsLayerTars() throws Exception { + ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base"); + URI exportUri = new URI(IMAGES_URL + "/gcr.io/paketo-buildpacks/builder:base/get"); + given(DockerApiTests.this.http.get(exportUri)).willReturn(responseOf("export.tar")); + MultiValueMap contents = new LinkedMultiValueMap<>(); + this.api.exportLayers(reference, (name, archive) -> { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + archive.writeTo(out); + try (TarArchiveInputStream in = new TarArchiveInputStream( + new ByteArrayInputStream(out.toByteArray()))) { + TarArchiveEntry entry = in.getNextTarEntry(); + while (entry != null) { + contents.add(name, entry.getName()); + entry = in.getNextTarEntry(); + } + } + }); + assertThat(contents).hasSize(3).containsKeys( + "1bf6c63a1e9ed1dd7cb961273bf60b8e0f440361faf273baf866f408e4910601/layer.tar", + "8fdfb915302159a842cbfae6faec5311b00c071ebf14e12da7116ae7532e9319/layer.tar", + "93cd584bb189bfca4f51744bd19d836fd36da70710395af5a1523ee88f208c6a/layer.tar"); + assertThat(contents.get("1bf6c63a1e9ed1dd7cb961273bf60b8e0f440361faf273baf866f408e4910601/layer.tar")) + .containsExactly("etc/", "etc/apt/", "etc/apt/sources.list"); + } + } @Nested diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ProgressUpdateEventTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ProgressUpdateEventTests.java index 480d0eeb85d..421f9e94039 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ProgressUpdateEventTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ProgressUpdateEventTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -25,6 +25,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** * Tests for {@link ProgressUpdateEvent}. * + * @param The event type * @author Phillip Webb * @author Scott Frederick */ diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/FilePermissionsTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/FilePermissionsTests.java new file mode 100644 index 00000000000..0f05c177dfa --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/FilePermissionsTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2021 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.io; + +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.Collections; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link FilePermissions}. + * + * @author Scott Frederick + */ +class FilePermissionsTests { + + @Test + void posixPermissionsToUmask() { + Set permissions = PosixFilePermissions.fromString("rwxrw-r--"); + assertThat(FilePermissions.posixPermissionsToUmask(permissions)).isEqualTo(0764); + } + + @Test + void posixPermissionsToUmaskWithEmptyPermissions() { + Set permissions = Collections.emptySet(); + assertThat(FilePermissions.posixPermissionsToUmask(permissions)).isEqualTo(0); + } + + @Test + void posixPermissionsToUmaskWithNullPermissions() { + assertThatIllegalArgumentException().isThrownBy(() -> FilePermissions.posixPermissionsToUmask(null)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/TarLayoutWriterTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/TarLayoutWriterTests.java index ad1d8145785..05f5fae8ed5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/TarLayoutWriterTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/TarLayoutWriterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -31,6 +31,7 @@ import static org.assertj.core.api.Assertions.assertThat; * Tests for {@link TarLayoutWriter}. * * @author Phillip Webb + * @author Scott Frederick */ class TarLayoutWriterTests { @@ -39,7 +40,7 @@ class TarLayoutWriterTests { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); try (TarLayoutWriter writer = new TarLayoutWriter(outputStream)) { writer.directory("/foo", Owner.ROOT); - writer.file("/foo/bar.txt", Owner.of(1, 1), Content.of("test")); + writer.file("/foo/bar.txt", Owner.of(1, 1), 0777, Content.of("test")); } try (TarArchiveInputStream tarInputStream = new TarArchiveInputStream( new ByteArrayInputStream(outputStream.toByteArray()))) { @@ -54,7 +55,7 @@ class TarLayoutWriterTests { assertThat(directoryEntry.getLongGroupId()).isEqualTo(0); assertThat(directoryEntry.getModTime()).isEqualTo(new Date(TarLayoutWriter.NORMALIZED_MOD_TIME)); assertThat(fileEntry.getName()).isEqualTo("/foo/bar.txt"); - assertThat(fileEntry.getMode()).isEqualTo(0644); + assertThat(fileEntry.getMode()).isEqualTo(0777); assertThat(fileEntry.getLongUserId()).isEqualTo(1); assertThat(fileEntry.getLongGroupId()).isEqualTo(1); assertThat(fileEntry.getModTime()).isEqualTo(new Date(TarLayoutWriter.NORMALIZED_MOD_TIME)); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/json/AbstractJsonTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/json/AbstractJsonTests.java index 84ad2dcacb3..7065adeba8b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/json/AbstractJsonTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/json/AbstractJsonTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -16,7 +16,11 @@ package org.springframework.boot.buildpack.platform.json; +import java.io.BufferedReader; import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.stream.Collectors; import com.fasterxml.jackson.databind.ObjectMapper; @@ -26,6 +30,7 @@ import static org.assertj.core.api.Assertions.assertThat; * Abstract base class for JSON based tests. * * @author Phillip Webb + * @author Scott Frederick */ public abstract class AbstractJsonTests { @@ -39,4 +44,9 @@ public abstract class AbstractJsonTests { return result; } + protected final String getContentAsString(String name) { + return new BufferedReader(new InputStreamReader(getContent(name), StandardCharsets.UTF_8)).lines() + .collect(Collectors.joining("\n")); + } + } 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.json index 1343dc29425..b2470b87a31 100644 --- 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.json @@ -2,124 +2,174 @@ "description": "Ubuntu bionic base image with buildpacks for Java, NodeJS and Golang", "buildpacks": [ { - "id": "org.cloudfoundry.googlestackdriver", - "version": "v1.1.11" + "id": "paketo-buildpacks/dotnet-core", + "version": "0.0.9", + "homepage": "https://github.com/paketo-buildpacks/dotnet-core" }, { - "id": "org.cloudfoundry.springboot", - "version": "v1.2.13" + "id": "paketo-buildpacks/dotnet-core-runtime", + "version": "0.0.201", + "homepage": "https://github.com/paketo-buildpacks/dotnet-core-runtime" }, { - "id": "org.cloudfoundry.debug", - "version": "v1.2.11" + "id": "paketo-buildpacks/dotnet-core-sdk", + "version": "0.0.196", + "homepage": "https://github.com/paketo-buildpacks/dotnet-core-sdk" }, { - "id": "org.cloudfoundry.tomcat", - "version": "v1.3.18" + "id": "paketo-buildpacks/dotnet-execute", + "version": "0.0.180", + "homepage": "https://github.com/paketo-buildpacks/dotnet-execute" }, { - "id": "org.cloudfoundry.go", - "version": "v0.0.4" + "id": "paketo-buildpacks/dotnet-publish", + "version": "0.0.121", + "homepage": "https://github.com/paketo-buildpacks/dotnet-publish" }, { - "id": "org.cloudfoundry.openjdk", - "version": "v1.2.14" + "id": "paketo-buildpacks/dotnet-core-aspnet", + "version": "0.0.196", + "homepage": "https://github.com/paketo-buildpacks/dotnet-core-aspnet" }, { - "id": "org.cloudfoundry.buildsystem", - "version": "v1.2.15" + "id": "paketo-buildpacks/java-native-image", + "version": "4.7.0", + "homepage": "https://github.com/paketo-buildpacks/java-native-image" }, { - "id": "org.cloudfoundry.jvmapplication", - "version": "v1.1.12" + "id": "paketo-buildpacks/spring-boot", + "version": "3.5.0", + "homepage": "https://github.com/paketo-buildpacks/spring-boot" }, { - "id": "org.cloudfoundry.springautoreconfiguration", - "version": "v1.1.11" + "id": "paketo-buildpacks/executable-jar", + "version": "3.1.3", + "homepage": "https://github.com/paketo-buildpacks/executable-jar" }, { - "id": "org.cloudfoundry.archiveexpanding", - "version": "v1.0.102" + "id": "paketo-buildpacks/graalvm", + "version": "4.1.0", + "homepage": "https://github.com/paketo-buildpacks/graalvm" }, { - "id": "org.cloudfoundry.jmx", - "version": "v1.1.12" + "id": "paketo-buildpacks/gradle", + "version": "3.5.0", + "homepage": "https://github.com/paketo-buildpacks/gradle" }, { - "id": "org.cloudfoundry.nodejs", - "version": "v2.0.8" + "id": "paketo-buildpacks/leiningen", + "version": "1.2.1", + "homepage": "https://github.com/paketo-buildpacks/leiningen" }, { - "id": "org.cloudfoundry.jdbc", - "version": "v1.1.14" + "id": "paketo-buildpacks/procfile", + "version": "3.0.0", + "homepage": "https://github.com/paketo-buildpacks/procfile" }, { - "id": "org.cloudfoundry.procfile", - "version": "v1.1.12" + "id": "paketo-buildpacks/sbt", + "version": "3.6.0", + "homepage": "https://github.com/paketo-buildpacks/sbt" }, { - "id": "org.cloudfoundry.dotnet-core", - "version": "v0.0.6" + "id": "paketo-buildpacks/spring-boot-native-image", + "version": "2.0.1", + "homepage": "https://github.com/paketo-buildpacks/spring-boot-native-image" }, { - "id": "org.cloudfoundry.azureapplicationinsights", - "version": "v1.1.12" + "id": "paketo-buildpacks/environment-variables", + "version": "2.1.2", + "homepage": "https://github.com/paketo-buildpacks/environment-variables" }, { - "id": "org.cloudfoundry.distzip", - "version": "v1.1.12" + "id": "paketo-buildpacks/image-labels", + "version": "2.0.7", + "homepage": "https://github.com/paketo-buildpacks/image-labels" }, { - "id": "org.cloudfoundry.dep", - "version": "0.0.101" + "id": "paketo-buildpacks/maven", + "version": "3.2.1", + "homepage": "https://github.com/paketo-buildpacks/maven" }, { - "id": "org.cloudfoundry.go-compiler", - "version": "0.0.105" + "id": "paketo-buildpacks/java", + "version": "4.10.0", + "homepage": "https://github.com/paketo-buildpacks/java" }, { - "id": "org.cloudfoundry.go-mod", - "version": "0.0.89" + "id": "paketo-buildpacks/ca-certificates", + "version": "1.0.1", + "homepage": "https://github.com/paketo-buildpacks/ca-certificates" }, { - "id": "org.cloudfoundry.node-engine", - "version": "0.0.163" + "id": "paketo-buildpacks/environment-variables", + "version": "2.1.2", + "homepage": "https://github.com/paketo-buildpacks/environment-variables" }, { - "id": "org.cloudfoundry.npm", - "version": "0.1.3" + "id": "paketo-buildpacks/executable-jar", + "version": "3.1.3", + "homepage": "https://github.com/paketo-buildpacks/executable-jar" }, { - "id": "org.cloudfoundry.yarn-install", - "version": "0.1.10" + "id": "paketo-buildpacks/procfile", + "version": "3.0.0", + "homepage": "https://github.com/paketo-buildpacks/procfile" }, { - "id": "org.cloudfoundry.dotnet-core-aspnet", - "version": "0.0.118" + "id": "paketo-buildpacks/apache-tomcat", + "version": "3.2.0", + "homepage": "https://github.com/paketo-buildpacks/apache-tomcat" }, { - "id": "org.cloudfoundry.dotnet-core-build", - "version": "0.0.68" + "id": "paketo-buildpacks/gradle", + "version": "3.5.0", + "homepage": "https://github.com/paketo-buildpacks/gradle" }, { - "id": "org.cloudfoundry.dotnet-core-conf", - "version": "0.0.115" + "id": "paketo-buildpacks/maven", + "version": "3.2.1", + "homepage": "https://github.com/paketo-buildpacks/maven" }, { - "id": "org.cloudfoundry.dotnet-core-runtime", - "version": "0.0.127" + "id": "paketo-buildpacks/sbt", + "version": "3.6.0", + "homepage": "https://github.com/paketo-buildpacks/sbt" }, { - "id": "org.cloudfoundry.dotnet-core-sdk", - "version": "0.0.122" + "id": "paketo-buildpacks/bellsoft-liberica", + "version": "6.2.0", + "homepage": "https://github.com/paketo-buildpacks/bellsoft-liberica" }, { - "id": "org.cloudfoundry.icu", - "version": "0.0.43" + "id": "paketo-buildpacks/image-labels", + "version": "2.0.7", + "homepage": "https://github.com/paketo-buildpacks/image-labels" }, { - "id": "org.cloudfoundry.node-engine", - "version": "0.0.158" + "id": "paketo-buildpacks/debug", + "version": "2.1.4", + "homepage": "https://github.com/paketo-buildpacks/debug" + }, + { + "id": "paketo-buildpacks/dist-zip", + "version": "2.2.2", + "homepage": "https://github.com/paketo-buildpacks/dist-zip" + }, + { + "id": "paketo-buildpacks/spring-boot", + "version": "3.5.0", + "homepage": "https://github.com/paketo-buildpacks/spring-boot" + }, + { + "id": "paketo-buildpacks/jmx", + "version": "2.1.4", + "homepage": "https://github.com/paketo-buildpacks/jmx" + }, + { + "id": "paketo-buildpacks/leiningen", + "version": "1.2.1", + "homepage": "https://github.com/paketo-buildpacks/leiningen" } ], "stack": { @@ -139,4 +189,4 @@ "name": "Pack CLI", "version": "v0.9.0 (git sha: d42c384a39f367588f2653f2a99702db910e5ad7)" } -} \ 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/buildpack-image.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/buildpack-image.json new file mode 100644 index 00000000000..41a3777526d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/buildpack-image.json @@ -0,0 +1,78 @@ +{ + "Id": "sha256:a266647e285b52403b556adc963f1809556aa999f2f694e8dc54098c570ee55a", + "RepoTags": [ + "example/hello-universe:latest" + ], + "RepoDigests": [], + "Parent": "", + "Comment": "", + "Created": "1980-01-01T00:00:01Z", + "Container": "", + "ContainerConfig": { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": null, + "Image": "", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": null + }, + "DockerVersion": "", + "Author": "", + "Config": { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": null, + "Image": "", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": { + "io.buildpacks.buildpackage.metadata": "{\"id\":\"example/hello-universe\",\"version\":\"0.0.1\",\"homepage\":\"https://github.com/buildpacks/example/tree/main/buildpacks/hello-universe\",\"stacks\":[{\"id\":\"io.buildpacks.example.stacks.alpine\"},{\"id\":\"io.buildpacks.stacks.bionic\"}]}", + "io.buildpacks.buildpack.layers": "{\"example/hello-moon\":{\"0.0.3\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.alpine\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:4bfdc8714aee68da6662c43bc28d3b41202c88e915641c356523dabe729814c2\",\"homepage\":\"https://github.com/example/tree/main/buildpacks/hello-moon\"}},\"example/hello-universe\":{\"0.0.1\":{\"api\":\"0.2\",\"order\":[{\"group\":[{\"id\":\"example/hello-world\",\"version\":\"0.0.2\"},{\"id\":\"example/hello-moon\",\"version\":\"0.0.2\"}]}],\"layerDiffID\":\"sha256:739b4e8f3caae7237584a1bfe029ebdb05403752b1a60a4f9be991b1d51dbb69\",\"homepage\":\"https://github.com/example/tree/main/buildpacks/hello-universe\"}},\"example/hello-world\":{\"0.0.2\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.alpine\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:f752fe099c846e501bdc991d1a22f98c055ddc62f01cfc0495fff2c69f8eb940\",\"homepage\":\"https://github.com/example/tree/main/buildpacks/hello-world\"}}}" + } + }, + "Architecture": "amd64", + "Os": "linux", + "Size": 4654, + "VirtualSize": 4654, + "GraphDriver": { + "Data": { + "LowerDir": "/var/lib/docker/overlay2/cbf39b4508463beeb1d0a553c3e2baa84b8cd8dbc95681aaecc243e3ca77bcf4/diff:/var/lib/docker/overlay2/15e3d01b65c962b50a3da1b6663b8196284fb3c7e7f8497f2c1a0a736d0ec237/diff", + "MergedDir": "/var/lib/docker/overlay2/1425ea68b0daff01bcc32e55e09eeeada2318d7dd1dc4e184711359da8425bb7/merged", + "UpperDir": "/var/lib/docker/overlay2/1425ea68b0daff01bcc32e55e09eeeada2318d7dd1dc4e184711359da8425bb7/diff", + "WorkDir": "/var/lib/docker/overlay2/1425ea68b0daff01bcc32e55e09eeeada2318d7dd1dc4e184711359da8425bb7/work" + }, + "Name": "overlay2" + }, + "RootFS": { + "Type": "layers", + "Layers": [ + "sha256:4bfdc8714aee68da6662c43bc28d3b41202c88e915641c356523dabe729814c2", + "sha256:f752fe099c846e501bdc991d1a22f98c055ddc62f01cfc0495fff2c69f8eb940", + "sha256:739b4e8f3caae7237584a1bfe029ebdb05403752b1a60a4f9be991b1d51dbb69" + ] + }, + "Metadata": { + "LastTagTime": "2021-01-27T22:56:06.4599859Z" + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/buildpack-metadata.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/buildpack-metadata.json new file mode 100644 index 00000000000..bdb2b126584 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/buildpack-metadata.json @@ -0,0 +1,13 @@ +{ + "id": "example/hello-universe", + "version": "0.0.1", + "homepage": "https://github.com/example/tree/main/buildpacks/hello-universe", + "stacks": [ + { + "id": "io.buildpacks.stacks.alpine" + }, + { + "id": "io.buildpacks.stacks.bionic" + } + ] +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/buildpack.toml b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/buildpack.toml new file mode 100644 index 00000000000..2a15b01943c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/buildpack.toml @@ -0,0 +1,8 @@ +[buildpack] +id = "test"; +version = "1.0.0" +name = "Example buildpack" +homepage = "https://github.com/example/example-buildpack" + +[[stacks]] +id = "io.buildpacks.stacks.bionic" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/image.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/image.json index 7794b34cec7..ade232f0a48 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/image.json +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/image.json @@ -58,7 +58,7 @@ "Entrypoint": null, "OnBuild": null, "Labels": { - "io.buildpacks.builder.metadata": "{\"description\":\"Ubuntu bionic base image with buildpacks for Java, NodeJS and Golang\",\"buildpacks\":[{\"id\":\"org.cloudfoundry.googlestackdriver\",\"version\":\"v1.1.11\"},{\"id\":\"org.cloudfoundry.springboot\",\"version\":\"v1.2.13\"},{\"id\":\"org.cloudfoundry.debug\",\"version\":\"v1.2.11\"},{\"id\":\"org.cloudfoundry.tomcat\",\"version\":\"v1.3.18\"},{\"id\":\"org.cloudfoundry.go\",\"version\":\"v0.0.4\"},{\"id\":\"org.cloudfoundry.openjdk\",\"version\":\"v1.2.14\"},{\"id\":\"org.cloudfoundry.buildsystem\",\"version\":\"v1.2.15\"},{\"id\":\"org.cloudfoundry.jvmapplication\",\"version\":\"v1.1.12\"},{\"id\":\"org.cloudfoundry.springautoreconfiguration\",\"version\":\"v1.1.11\"},{\"id\":\"org.cloudfoundry.archiveexpanding\",\"version\":\"v1.0.102\"},{\"id\":\"org.cloudfoundry.jmx\",\"version\":\"v1.1.12\"},{\"id\":\"org.cloudfoundry.nodejs\",\"version\":\"v2.0.8\"},{\"id\":\"org.cloudfoundry.jdbc\",\"version\":\"v1.1.14\"},{\"id\":\"org.cloudfoundry.procfile\",\"version\":\"v1.1.12\"},{\"id\":\"org.cloudfoundry.dotnet-core\",\"version\":\"v0.0.6\"},{\"id\":\"org.cloudfoundry.azureapplicationinsights\",\"version\":\"v1.1.12\"},{\"id\":\"org.cloudfoundry.distzip\",\"version\":\"v1.1.12\"},{\"id\":\"org.cloudfoundry.dep\",\"version\":\"0.0.101\"},{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.105\"},{\"id\":\"org.cloudfoundry.go-mod\",\"version\":\"0.0.89\"},{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.163\"},{\"id\":\"org.cloudfoundry.npm\",\"version\":\"0.1.3\"},{\"id\":\"org.cloudfoundry.yarn-install\",\"version\":\"0.1.10\"},{\"id\":\"org.cloudfoundry.dotnet-core-aspnet\",\"version\":\"0.0.118\"},{\"id\":\"org.cloudfoundry.dotnet-core-build\",\"version\":\"0.0.68\"},{\"id\":\"org.cloudfoundry.dotnet-core-conf\",\"version\":\"0.0.115\"},{\"id\":\"org.cloudfoundry.dotnet-core-runtime\",\"version\":\"0.0.127\"},{\"id\":\"org.cloudfoundry.dotnet-core-sdk\",\"version\":\"0.0.122\"},{\"id\":\"org.cloudfoundry.icu\",\"version\":\"0.0.43\"},{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.158\"}],\"stack\":{\"runImage\":{\"image\":\"cloudfoundry/run:base-cnb\",\"mirrors\":null}},\"lifecycle\":{\"version\":\"0.7.2\",\"api\":{\"buildpack\":\"0.2\",\"platform\":\"0.3\"}},\"createdBy\":{\"name\":\"Pack CLI\",\"version\":\"v0.9.0 (git sha: d42c384a39f367588f2653f2a99702db910e5ad7)\"}}", + "io.buildpacks.builder.metadata": "{\"description\":\"Ubuntu bionic base image with buildpacks for Java, NodeJS and Golang\",\"buildpacks\":[{\"id\":\"paketo-buildpacks/dotnet-core\",\"version\":\"0.0.9\",\"homepage\":\"https://github.com/paketo-buildpacks/dotnet-core\"},{\"id\":\"paketo-buildpacks/dotnet-core-runtime\",\"version\":\"0.0.201\",\"homepage\":\"https://github.com/paketo-buildpacks/dotnet-core-runtime\"},{\"id\":\"paketo-buildpacks/dotnet-core-sdk\",\"version\":\"0.0.196\",\"homepage\":\"https://github.com/paketo-buildpacks/dotnet-core-sdk\"},{\"id\":\"paketo-buildpacks/dotnet-execute\",\"version\":\"0.0.180\",\"homepage\":\"https://github.com/paketo-buildpacks/dotnet-execute\"},{\"id\":\"paketo-buildpacks/dotnet-publish\",\"version\":\"0.0.121\",\"homepage\":\"https://github.com/paketo-buildpacks/dotnet-publish\"},{\"id\":\"paketo-buildpacks/dotnet-core-aspnet\",\"version\":\"0.0.196\",\"homepage\":\"https://github.com/paketo-buildpacks/dotnet-core-aspnet\"},{\"id\":\"paketo-buildpacks/java-native-image\",\"version\":\"4.7.0\",\"homepage\":\"https://github.com/paketo-buildpacks/java-native-image\"},{\"id\":\"paketo-buildpacks/spring-boot\",\"version\":\"3.5.0\",\"homepage\":\"https://github.com/paketo-buildpacks/spring-boot\"},{\"id\":\"paketo-buildpacks/executable-jar\",\"version\":\"3.1.3\",\"homepage\":\"https://github.com/paketo-buildpacks/executable-jar\"},{\"id\":\"paketo-buildpacks/graalvm\",\"version\":\"4.1.0\",\"homepage\":\"https://github.com/paketo-buildpacks/graalvm\"},{\"id\":\"paketo-buildpacks/gradle\",\"version\":\"3.5.0\",\"homepage\":\"https://github.com/paketo-buildpacks/gradle\"},{\"id\":\"paketo-buildpacks/leiningen\",\"version\":\"1.2.1\",\"homepage\":\"https://github.com/paketo-buildpacks/leiningen\"},{\"id\":\"paketo-buildpacks/sbt\",\"version\":\"3.6.0\",\"homepage\":\"https://github.com/paketo-buildpacks/sbt\"},{\"id\":\"paketo-buildpacks/spring-boot-native-image\",\"version\":\"2.0.1\",\"homepage\":\"https://github.com/paketo-buildpacks/spring-boot-native-image\"},{\"id\":\"paketo-buildpacks/environment-variables\",\"version\":\"2.1.2\",\"homepage\":\"https://github.com/paketo-buildpacks/environment-variables\"},{\"id\":\"paketo-buildpacks/image-labels\",\"version\":\"2.0.7\",\"homepage\":\"https://github.com/paketo-buildpacks/image-labels\"},{\"id\":\"paketo-buildpacks/maven\",\"version\":\"3.2.1\",\"homepage\":\"https://github.com/paketo-buildpacks/maven\"},{\"id\":\"paketo-buildpacks/java\",\"version\":\"4.10.0\",\"homepage\":\"https://github.com/paketo-buildpacks/java\"},{\"id\":\"paketo-buildpacks/ca-certificates\",\"version\":\"1.0.1\",\"homepage\":\"https://github.com/paketo-buildpacks/ca-certificates\"},{\"id\":\"paketo-buildpacks/environment-variables\",\"version\":\"2.1.2\",\"homepage\":\"https://github.com/paketo-buildpacks/environment-variables\"},{\"id\":\"paketo-buildpacks/executable-jar\",\"version\":\"3.1.3\",\"homepage\":\"https://github.com/paketo-buildpacks/executable-jar\"},{\"id\":\"paketo-buildpacks/procfile\",\"version\":\"3.0.0\",\"homepage\":\"https://github.com/paketo-buildpacks/procfile\"},{\"id\":\"paketo-buildpacks/apache-tomcat\",\"version\":\"3.2.0\",\"homepage\":\"https://github.com/paketo-buildpacks/apache-tomcat\"},{\"id\":\"paketo-buildpacks/gradle\",\"version\":\"3.5.0\",\"homepage\":\"https://github.com/paketo-buildpacks/gradle\"},{\"id\":\"paketo-buildpacks/maven\",\"version\":\"3.2.1\",\"homepage\":\"https://github.com/paketo-buildpacks/maven\"},{\"id\":\"paketo-buildpacks/sbt\",\"version\":\"3.6.0\",\"homepage\":\"https://github.com/paketo-buildpacks/sbt\"},{\"id\":\"paketo-buildpacks/bellsoft-liberica\",\"version\":\"6.2.0\",\"homepage\":\"https://github.com/paketo-buildpacks/bellsoft-liberica\"},{\"id\":\"paketo-buildpacks/google-stackdriver\",\"version\":\"2.16.0\",\"homepage\":\"https://github.com/paketo-buildpacks/google-stackdriver\"},{\"id\":\"paketo-buildpacks/image-labels\",\"version\":\"2.0.7\",\"homepage\":\"https://github.com/paketo-buildpacks/image-labels\"},{\"id\":\"paketo-buildpacks/dist-zip\",\"version\":\"2.2.2\",\"homepage\":\"https://github.com/paketo-buildpacks/dist-zip\"},{\"id\":\"paketo-buildpacks/spring-boot\",\"version\":\"3.5.0\",\"homepage\":\"https://github.com/paketo-buildpacks/spring-boot\"},{\"id\":\"paketo-buildpacks/jmx\",\"version\":\"2.1.4\",\"homepage\":\"https://github.com/paketo-buildpacks/jmx\"},{\"id\":\"paketo-buildpacks/leiningen\",\"version\":\"1.2.1\",\"homepage\":\"https://github.com/paketo-buildpacks/leiningen\"}],\"stack\":{\"runImage\":{\"image\":\"cloudfoundry/run:base-cnb\",\"mirrors\":null}},\"lifecycle\":{\"version\":\"0.7.2\",\"api\":{\"buildpack\":\"0.2\",\"platform\":\"0.3\"}},\"createdBy\":{\"name\":\"Pack CLI\",\"version\":\"v0.9.0 (git sha: d42c384a39f367588f2653f2a99702db910e5ad7)\"}}", "io.buildpacks.buildpack.layers": "{\"org.cloudfoundry.archiveexpanding\":{\"v1.0.102\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:73b1a8ac1f7fca3d545766ce7fd3c56b40a63724ab78e464d71a29da0c6ac31c\"}},\"org.cloudfoundry.azureapplicationinsights\":{\"v1.1.12\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:a0a2f7c467efbb8b1ac222f09013b88b68f3c117ec6b6e9dc95564be50f271ab\"}},\"org.cloudfoundry.buildsystem\":{\"v1.2.15\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:3f50d3a0e1a969a9606b59e5295842d731e425108cb349ce6c69a5b30ea1bab9\"}},\"org.cloudfoundry.debug\":{\"v1.2.11\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:04559213a01cfac69a8d6a6facb58b8681666525c74f605207c40a61a0f4c9b7\"}},\"org.cloudfoundry.dep\":{\"0.0.101\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.tiny\"}],\"layerDiffID\":\"sha256:6aae3a2d671d369eec34dc9146ef267d06c87461f271fbfbe9136775ecf5dfb8\"}},\"org.cloudfoundry.distzip\":{\"v1.1.12\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:a0715e661e13d7d3ded5bdc068edd01e5b3aa0e2805152f4c8a1428b4e0673df\"}},\"org.cloudfoundry.dotnet-core\":{\"v0.0.6\":{\"api\":\"0.2\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.158\",\"optional\":true},{\"id\":\"org.cloudfoundry.icu\",\"version\":\"0.0.43\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-runtime\",\"version\":\"0.0.127\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-aspnet\",\"version\":\"0.0.118\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-sdk\",\"version\":\"0.0.122\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-build\",\"version\":\"0.0.68\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-conf\",\"version\":\"0.0.115\"}]}],\"layerDiffID\":\"sha256:aa0effdf787ecfe74d60d6771006717fd1a9ce1ce0a8161624baa61b68120357\"}},\"org.cloudfoundry.dotnet-core-aspnet\":{\"0.0.118\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:a06615b5adc1a3afb7abd524e82f6900a28910927fcf0d4e9b85fd1fcbeb53ad\"}},\"org.cloudfoundry.dotnet-core-build\":{\"0.0.68\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:26d6f1e76275d17860005f7ab9b74fdd2283fcf84e0446bd88d49a6b4e9609f9\"}},\"org.cloudfoundry.dotnet-core-conf\":{\"0.0.115\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:55f7c052cf70c8ca01b8e241c0c5c8a9675599d4904c69bfb961a472e246238d\"}},\"org.cloudfoundry.dotnet-core-runtime\":{\"0.0.127\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:d9958b816a9ad179fca8c18d17c07e9814b152d461c685e1443bec6f990ab990\"}},\"org.cloudfoundry.dotnet-core-sdk\":{\"0.0.122\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:52142799a4b687fe6e5cf397c41064499ea6cc554b94904d46c1acade998e11f\"}},\"org.cloudfoundry.go\":{\"v0.0.4\":{\"api\":\"0.2\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.105\"},{\"id\":\"org.cloudfoundry.go-mod\",\"version\":\"0.0.89\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.105\"},{\"id\":\"org.cloudfoundry.dep\",\"version\":\"0.0.101\"}]}],\"layerDiffID\":\"sha256:352a299d6af4773322ed3643d8f98b01aad6f15d838d1852e52a0a3ca56c6efb\"}},\"org.cloudfoundry.go-compiler\":{\"0.0.105\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.tiny\"}],\"layerDiffID\":\"sha256:cb21f14e306d94e437c5418d275bcc6efcea6bc9b3d26a400bdf54fa62242c24\"}},\"org.cloudfoundry.go-mod\":{\"0.0.89\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.tiny\"}],\"layerDiffID\":\"sha256:c9da8171f5ca048109ffba5e940e3a7d2db567eda281f92b0eb483173df06add\"}},\"org.cloudfoundry.googlestackdriver\":{\"v1.1.11\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:ff29efc56c31eeccc79a33c6e4abd7b1ab3547d95e1cf83974af65a493576c41\"}},\"org.cloudfoundry.icu\":{\"0.0.43\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:48063dcdd043f9c88604d10fe9542569be8f8111d46806c96b08d77763ffa347\"}},\"org.cloudfoundry.jdbc\":{\"v1.1.14\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:a9c9bbbd69c212b7ab3c1a7f03011ccc4d99a6fce1bf1c785325c7bcad789e62\"}},\"org.cloudfoundry.jmx\":{\"v1.1.12\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:da62dec6eb4ed884952a1b867fd89e3bfe3c510e5c849cc0ac7050ff867a2469\"}},\"org.cloudfoundry.jvmapplication\":{\"v1.1.12\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:c10732392b97c121a78a5f20201c2a5e834a2b8677196cdd49260a489a54fd22\"}},\"org.cloudfoundry.node-engine\":{\"0.0.158\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:70cf83155575fdb607f23ace41e31b1d5cb1c24dbbbf56f71c383b583724d339\"},\"0.0.163\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:11486cb955594f9d43909b60f94209bb6854f502a5a093207b657afbaa38a777\"}},\"org.cloudfoundry.nodejs\":{\"v2.0.8\":{\"api\":\"0.2\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.163\"},{\"id\":\"org.cloudfoundry.yarn-install\",\"version\":\"0.1.10\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.163\"},{\"id\":\"org.cloudfoundry.npm\",\"version\":\"0.1.3\"}]}],\"layerDiffID\":\"sha256:76fe727e4aafc7f56f01282296ab736521c38b9d19c1ae5ebb193f9cd55fa109\"}},\"org.cloudfoundry.npm\":{\"0.1.3\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:243bbd007cb0ee99b704bfe0cf62e1301baa4095ab4c39b01293787a0e4234f1\"}},\"org.cloudfoundry.openjdk\":{\"v1.2.14\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:486b2abf434bb90cf04bab74f2f8bd2eb488ff90632b56eac4bddcbbf02e8151\"}},\"org.cloudfoundry.procfile\":{\"v1.1.12\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:b7b78159dfdaa0dd484c58652e02fa6b755abfd0adb88f106d16178144e46f33\"}},\"org.cloudfoundry.springautoreconfiguration\":{\"v1.1.11\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:c185540c10fea822c6db1b987fcfe22b55a4662648124b98475db4c9dcddb2ab\"}},\"org.cloudfoundry.springboot\":{\"v1.2.13\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:b87e68574cc7dccbe974fa760702ef650711036bf144fd9da1f3a2d8f6ac335f\"}},\"org.cloudfoundry.tomcat\":{\"v1.3.18\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:467c0082c57b80b48487a9b8429887c0744ddc5b066b3f7678866bde89b78ab2\"}},\"org.cloudfoundry.yarn-install\":{\"0.1.10\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:6aefa0ba7ce01584b4a531b18e36470298cee3b30ecae0e0c64b532a5cebd6e7\"}}}", "io.buildpacks.buildpack.order": "[{\"group\":[{\"id\":\"org.cloudfoundry.openjdk\"},{\"id\":\"org.cloudfoundry.buildsystem\",\"optional\":true},{\"id\":\"org.cloudfoundry.jvmapplication\"},{\"id\":\"org.cloudfoundry.tomcat\",\"optional\":true},{\"id\":\"org.cloudfoundry.springboot\",\"optional\":true},{\"id\":\"org.cloudfoundry.distzip\",\"optional\":true},{\"id\":\"org.cloudfoundry.procfile\",\"optional\":true},{\"id\":\"org.cloudfoundry.azureapplicationinsights\",\"optional\":true},{\"id\":\"org.cloudfoundry.debug\",\"optional\":true},{\"id\":\"org.cloudfoundry.googlestackdriver\",\"optional\":true},{\"id\":\"org.cloudfoundry.jdbc\",\"optional\":true},{\"id\":\"org.cloudfoundry.jmx\",\"optional\":true},{\"id\":\"org.cloudfoundry.springautoreconfiguration\",\"optional\":true}]},{\"group\":[{\"id\":\"org.cloudfoundry.nodejs\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.go\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.dotnet-core\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.procfile\"}]}]", "io.buildpacks.stack.id": "io.buildpacks.stacks.bionic", diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/layer.tar b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/layer.tar new file mode 100644 index 0000000000000000000000000000000000000000..e6b99b0181429b797a42582daa48f3253ce4c5ad GIT binary patch literal 3072 zcmeHGOAf*y5M}KtxPa}@LP@+0MG{HuCop5;?WHzunwZqox{=v1km1ee&2wl=wzC09 z&`L3wZFx?wOh^z=sS31a$%9HTu6C~L2~QDCNF{B}wQO!H-jDvf#J?GF*Bw~NKZsh_ zO#>7E4d~4OFIM2ie;g=S7mKTp`0c!to*OBbx_{lFmP`HDLM8q;YjRB^f3E+tYxxCn zJe8#h_Tq1@pq6^1!VXY4;CPc?Oy5j5}J^(q=csA(bcfg{v6g=MF Tqpewi9Fr%ICy*!bj}v$RnnR7A literal 0 HcmV?d00001 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/order-versions.toml b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/order-versions.toml new file mode 100644 index 00000000000..365b2c49527 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/order-versions.toml @@ -0,0 +1,15 @@ +[[order]] +group = [ + { id = "example/buildpack1", version = "0.0.1" } +] + +[[order]] +group = [ + { id = "example/buildpack2", version = "0.0.2" } +] + +[[order]] +group = [ + { id = "example/buildpack3", version = "0.0.3" } +] + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/order.toml b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/order.toml new file mode 100644 index 00000000000..90415774d6b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/order.toml @@ -0,0 +1,15 @@ +[[order]] +group = [ + { id = "example/buildpack1" } +] + +[[order]] +group = [ + { id = "example/buildpack2" } +] + +[[order]] +group = [ + { id = "example/buildpack3" } +] + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/export.tar b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/export.tar new file mode 100644 index 0000000000000000000000000000000000000000..9f2ed1e92261467c54676c371bc84d5a44e83d9c GIT binary patch literal 22016 zcmeHPS#R7n5cacwg`j8eE)R*44CJA00yIDo7fqT14T6BDtwbvcI?@_J{(FbgdTnpK zm*o}gn#3A9NRBx3aY%7K%2*gC8Rb|fT4ALkDH4WBMBzRXu|_5c5M|hKV{dX3GiCx5 z>mb31Av`Ux@`b@R~Awf+nfz|u8(Pg%MOqQR>F zQyc>RqmKVWm0nS5sJOA}D=oZ>rYTQnGhWGy^sAn>qRpFjDvSB7KlMafIy?L88{ps-@LMCmgc%$ow5msy2R%vs&UfNi8bkA`Xa29YDcxo0Hy#D)K7x@;Y zVQSp8{zDNBSJr1PyM0Yz(4UzUg^BCvM|L_9EXHZqbQOnP*@VI z2o^M?nkE#-gxiQyWFqJqRB4464opB)A|wG>veNL9&^0WH&;()ly+i1<`~7VT6nO)0 zBf9zoXC^)OPrv!U7Il;JxrQb{iI;_aUAeygv({Cg^HbAsdGWSUY3`pIp4IxqU*0QP zP+|3^uAu7T=DX%{d45*v{I;C#>MWPgh(-BV`B(omZ?o*g|78vu^{21r{Cv^S_eIvu zb$yldS5bXQ^YfSKqP4H`rn)SP)S9-hdzSy)rrA=|Z(QhXQT_XgU%;uKrFr|W|9y*J z+U%jg&)&cKekjLSWOu$m{z@8csPLhv zl&(I)spl;&rsq}BmT=31w5h2Rou5OH!M5dx*2K`7{Ul!UD!)^dHU0jgCTR|PQnKt0 zkz`8mC(`bPY3>k+ror9<%c_tTnOMnpQo{F^ST~sbFFFnk z(Hn5ac6i>;eccG*`HK-^?Q4b0a29YDa27a}1qR3e4#WRqLPlQyJ=6H_g8v=b4Q}$k zTp1}%pf!-XimE6im?FUv5*yBGl1R#h6b#cughq-$a;;T@BO-uSzydq|cSQboDDa2k zf0^_DLk;=2im+ zyFT0qW&vLCe>e&|isb5OvuwJ}|6ok){?Adr{~W4*ug8<)e`cNk4x;zBTF#ngKm1Y1 z#vcE-YO$rjQ^gD(_flfBfMC;Ia3A?x@S!Vpd5=%fW4J!N--c#D4~4 z`hl8e?XsW$V-`C64}^vj>nz|b;4IKvU~v5BK>VNm{~s>?bNF7m;QxkpgPZ(c6b6(; zDvAh>;0sW!u{2Vt*f1r8;F=2fr(#CNQb_n1lmt8yjA#;(fF=@0iR1r9f2hvYMfdBvi literal 0 HcmV?d00001 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc index b3d55479d37..0d89b4af080 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc @@ -130,6 +130,18 @@ Acceptable values are `ALWAYS`, `NEVER`, and `IF_NOT_PRESENT`. | Environment variables that should be passed to the builder. | +| `buildpacks` +| +a|Buildpacks that the builder should use when building the image. +Only the specified buildpacks will be used, overriding the default buildpacks included in the builder. +Buildpack references must be in one of the following forms: + +* Buildpack in the builder - [urn:cnb:builder:][@] +* Buildpack in a directory on the file system - [file://] +* Buildpack in a gzipped tar (.tgz) file on the file system - [file://]/ +* Buildpack in an OCI image - [docker://]/[:][@] +| None, indicating the builder should use the buildpacks included in it. + | `cleanCache` | `--cleanCache` | Whether to clean the cache before building. @@ -249,6 +261,58 @@ The image name can be specified on the command line as well, as shown in this ex $ gradle bootBuildImage --imageName=example.com/library/my-app:v1 ---- + + +[[build-image-example-buildpacks]] +==== Buildpacks +By default, the builder will use buildpacks included in the builder image and apply them in a pre-defined order. +An alternative set of buildpacks can be provided to apply buildpacks that are not included in the builder, or to change the order of included buildpacks. +When one or more buildpacks are provided, only the specified buildpacks will be applied. + +The following example instructs the builder to use a custom buildpack packaged in a `.tgz` file, followed by a buildpack included in the builder. + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/packaging/boot-build-image-buildpacks.gradle[tags=buildpacks] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/packaging/boot-build-image-buildpacks.gradle.kts[tags=buildpacks] +---- + +Buildpacks can be specified in any of the forms shown below. + +A buildpack located in a CNB Builder (version may be omitted if there is only one buildpack in the builder matching the `buildpack-id`): + +* `urn:cnb:builder:buildpack-id` +* `urn:cnb:builder:buildpack-id@0.0.1` +* `buildpack-id` +* `buildpack-id@0.0.1` + +A path to a directory containing buildpack content (not supported on Windows): + +* `\file:///path/to/buildpack/` +* `/path/to/buildpack/`BootBuildImageIntegrationTests + +A path to a gzipped tar file containing buildpack content: + +* `\file:///path/to/buildpack.tgz` +* `/path/to/buildpack.tgz` + +An OCI image containing a https://buildpacks.io/docs/buildpack-author-guide/package-a-buildpack/[packaged buildpack]: + +* `docker://example/buildpack` +* `docker:///example/buildpack:latest` +* `docker:///example/buildpack@sha256:45b23dee08...` +* `example/buildpack` +* `example/buildpack:latest` +* `example/buildpack@sha256:45b23dee08...` + + + [[build-image-example-publish]] ==== Image Publishing The generated image can be published to a Docker registry by enabling a `publish` option and configuring authentication for the registry using `docker.publishRegistry` properties. @@ -272,6 +336,8 @@ The publish option can be specified on the command line as well, as shown in thi $ gradle bootBuildImage --imageName=docker.example.com/library/my-app:v1 --publishImage ---- + + [[build-image-example-docker]] ==== Docker Configuration If you need the plugin to communicate with the Docker daemon using a remote connection instead of the default local connection, the connection details can be provided using `docker` properties as shown in the following example: diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-buildpacks.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-buildpacks.gradle new file mode 100644 index 00000000000..c1f5c6745e4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-buildpacks.gradle @@ -0,0 +1,16 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +// tag::buildpacks[] +bootBuildImage { + buildpacks = ["file:///path/to/example-buildpack.tgz", "urn:cnb:builder:paketo-buildpacks/java"] +} +// end::buildpacks[] + +task bootBuildImageBuildpacks { + doFirst { + bootBuildImage.buildpacks.each { reference -> println "$reference" } + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-buildpacks.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-buildpacks.gradle.kts new file mode 100644 index 00000000000..a6757e3539a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-buildpacks.gradle.kts @@ -0,0 +1,20 @@ +import org.springframework.boot.gradle.tasks.bundling.BootBuildImage + +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +// tag::buildpacks[] +tasks.getByName("bootBuildImage") { + buildpacks = listOf("file:///path/to/example-buildpack.tgz", "urn:cnb:builder:paketo-buildpacks/java") +} +// end::buildpacks[] + +tasks.register("bootBuildImageBuildpacks") { + doFirst { + for((reference) in tasks.getByName("bootBuildImage").buildpacks) { + print(reference) + } + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java index 802351f9ce8..15b1caff068 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -18,7 +18,9 @@ package org.springframework.boot.gradle.tasks.bundling; import java.io.IOException; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import groovy.lang.Closure; import org.gradle.api.Action; @@ -28,6 +30,7 @@ import org.gradle.api.JavaVersion; import org.gradle.api.Project; import org.gradle.api.Task; import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.Property; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Nested; @@ -38,6 +41,7 @@ import org.gradle.util.ConfigureUtil; import org.springframework.boot.buildpack.platform.build.BuildRequest; import org.springframework.boot.buildpack.platform.build.Builder; +import org.springframework.boot.buildpack.platform.build.BuildpackReference; import org.springframework.boot.buildpack.platform.build.Creator; import org.springframework.boot.buildpack.platform.build.PullPolicy; import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException; @@ -83,6 +87,8 @@ public class BootBuildImage extends DefaultTask { private boolean publish; + private ListProperty buildpacks; + private DockerSpec docker = new DockerSpec(); public BootBuildImage() { @@ -92,6 +98,7 @@ public class BootBuildImage extends DefaultTask { this.projectVersion = getProject().getObjects().property(String.class); Project project = getProject(); this.projectVersion.set(getProject().provider(() -> project.getVersion().toString())); + this.buildpacks = getProject().getObjects().listProperty(String.class); } /** @@ -283,6 +290,40 @@ public class BootBuildImage extends DefaultTask { this.publish = publish; } + /** + * Returns the buildpacks that will be used when building the image. + * @return the buildpacks + */ + @Input + @Optional + public List getBuildpacks() { + return this.buildpacks.getOrNull(); + } + + /** + * Sets the buildpacks that will be used when building the image. + * @param buildpacks the buildpacks + */ + public void setBuildpacks(List buildpacks) { + this.buildpacks.set(buildpacks); + } + + /** + * Add an entry to the buildpacks that will be used when building the image. + * @param buildpack the buildpack reference + */ + public void buildpack(String buildpack) { + this.buildpacks.add(buildpack); + } + + /** + * Adds entries to the environment that will be used when building the image. + * @param buildpacks the buildpack references + */ + public void buildpacks(List buildpacks) { + this.buildpacks.addAll(buildpacks); + } + /** * Returns the Docker configuration the builder will use. * @return docker configuration. @@ -316,8 +357,8 @@ public class BootBuildImage extends DefaultTask { if (!this.jar.isPresent()) { throw new GradleException("Executable jar file required for building image"); } - Builder builder = new Builder(this.docker.asDockerConfiguration()); BuildRequest request = createRequest(); + Builder builder = new Builder(this.docker.asDockerConfiguration()); builder.build(request); } @@ -346,6 +387,7 @@ public class BootBuildImage extends DefaultTask { request = request.withVerboseLogging(this.verboseLogging); request = customizePullPolicy(request); request = customizePublish(request); + request = customizeBuildpacks(request); return request; } @@ -398,6 +440,14 @@ public class BootBuildImage extends DefaultTask { return request; } + private BuildRequest customizeBuildpacks(BuildRequest request) { + List buildpacks = this.buildpacks.getOrNull(); + if (buildpacks != null && !buildpacks.isEmpty()) { + return request.withBuildpacks(buildpacks.stream().map(BuildpackReference::of).collect(Collectors.toList())); + } + return request; + } + private String translateTargetJavaVersion() { return this.targetJavaVersion.get().getMajorVersion() + ".*"; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java index 3dced2f6ce3..cfe5c2501d1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -23,11 +23,21 @@ import java.io.PrintWriter; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; import java.util.Random; +import java.util.Set; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream; +import org.apache.commons.compress.utils.IOUtils; import org.gradle.testkit.runner.BuildResult; import org.gradle.testkit.runner.TaskOutcome; import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; @@ -145,7 +155,72 @@ class BootBuildImageIntegrationTests { } @TestTemplate - void failsWithLaunchScript() { + void buildsImageWithBuildpackFromBuilder() throws IOException { + writeMainClass(); + writeLongNameResource(); + BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT"); + String projectName = this.gradleBuild.getProjectDir().getName(); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("docker.io/library/" + projectName); + ImageReference imageReference = ImageReference.of(ImageName.of(projectName)); + try (GenericContainer container = new GenericContainer<>(imageReference.toString())) { + container.waitingFor(Wait.forLogMessage("Launched\\n", 1)).start(); + } + finally { + new DockerApi().image().remove(imageReference, false); + } + } + + @TestTemplate + @DisabledOnOs(OS.WINDOWS) + void buildsImageWithBuildpackFromDirectory() throws IOException { + writeMainClass(); + writeLongNameResource(); + writeBuildpackContent(); + BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT"); + String projectName = this.gradleBuild.getProjectDir().getName(); + ImageReference imageReference = ImageReference.of(ImageName.of(projectName)); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("docker.io/library/" + projectName); + assertThat(result.getOutput()).contains("---> Hello World buildpack"); + new DockerApi().image().remove(imageReference, false); + } + + @TestTemplate + @DisabledOnOs(OS.WINDOWS) + void buildsImageWithBuildpackFromTarGzip() throws IOException { + writeMainClass(); + writeLongNameResource(); + writeBuildpackContent(); + tarGzipBuildpackContent(); + BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT"); + String projectName = this.gradleBuild.getProjectDir().getName(); + ImageReference imageReference = ImageReference.of(ImageName.of(projectName)); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("docker.io/library/" + projectName); + assertThat(result.getOutput()).contains("---> Hello World buildpack"); + new DockerApi().image().remove(imageReference, false); + } + + @TestTemplate + void buildsImageWithBuildpackFromImage() throws IOException { + writeMainClass(); + writeLongNameResource(); + BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT"); + String projectName = this.gradleBuild.getProjectDir().getName(); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("docker.io/library/" + projectName); + ImageReference imageReference = ImageReference.of(ImageName.of(projectName)); + try (GenericContainer container = new GenericContainer<>(imageReference.toString())) { + container.waitingFor(Wait.forLogMessage("Launched\\n", 1)).start(); + } + finally { + new DockerApi().image().remove(imageReference, false); + } + } + + @TestTemplate + void failsWithLaunchScript() throws IOException { writeMainClass(); writeLongNameResource(); BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage"); @@ -154,7 +229,7 @@ class BootBuildImageIntegrationTests { } @TestTemplate - void failsWithBuilderError() { + void failsWithBuilderError() throws IOException { writeMainClass(); writeLongNameResource(); BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT"); @@ -163,7 +238,7 @@ class BootBuildImageIntegrationTests { } @TestTemplate - void failsWithInvalidImageName() { + void failsWithInvalidImageName() throws IOException { writeMainClass(); writeLongNameResource(); BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage", "--imageName=example/Invalid-Image-Name"); @@ -173,7 +248,7 @@ class BootBuildImageIntegrationTests { } @TestTemplate - void failsWithPublishMissingPublishRegistry() { + void failsWithPublishMissingPublishRegistry() throws IOException { writeMainClass(); writeLongNameResource(); BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage", "--publishImage"); @@ -182,7 +257,7 @@ class BootBuildImageIntegrationTests { } @TestTemplate - void failsWithWarPackaging() { + void failsWithWarPackaging() throws IOException { writeMainClass(); writeLongNameResource(); BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage", "-PapplyWarPlugin"); @@ -190,6 +265,15 @@ class BootBuildImageIntegrationTests { assertThat(result.getOutput()).contains("Executable jar file required for building image"); } + @TestTemplate + void failsWithBuildpackNotInBuilder() throws IOException { + writeMainClass(); + writeLongNameResource(); + BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT"); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.FAILED); + assertThat(result.getOutput()).contains("'urn:cnb:builder:example/does-not-exist:0.0.1' not found in builder"); + } + @TestTemplate void buildsImageWithWarPackagingAndJarConfiguration() throws IOException { writeMainClass(); @@ -210,7 +294,7 @@ class BootBuildImageIntegrationTests { } } - private void writeMainClass() { + private void writeMainClass() throws IOException { File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/example"); examplePackage.mkdirs(); File main = new File(examplePackage, "Main.java"); @@ -230,23 +314,70 @@ class BootBuildImageIntegrationTests { writer.println(); writer.println("}"); } - catch (IOException ex) { - throw new RuntimeException(ex); + } + + private void writeLongNameResource() throws IOException { + StringBuilder name = new StringBuilder(); + new Random().ints('a', 'z' + 1).limit(128).forEach((i) -> name.append((char) i)); + Path path = this.gradleBuild.getProjectDir().toPath() + .resolve(Paths.get("src", "main", "resources", name.toString())); + Files.createDirectories(path.getParent()); + Files.createFile(path); + } + + private void writeBuildpackContent() throws IOException { + File buildpackDir = new File(this.gradleBuild.getProjectDir(), "buildpack/hello-world"); + buildpackDir.mkdirs(); + File descriptor = new File(buildpackDir, "buildpack.toml"); + try (PrintWriter writer = new PrintWriter(new FileWriter(descriptor))) { + writer.println("api = \"0.2\""); + writer.println("[buildpack]"); + writer.println("id = \"example/hello-world\""); + writer.println("version = \"0.0.1\""); + writer.println("name = \"Hello World Buildpack\""); + writer.println("homepage = \"https://github.com/buildpacks/samples/tree/main/buildpacks/hello-world\""); + writer.println("[[stacks]]\n"); + writer.println("id = \"io.buildpacks.stacks.bionic\""); + } + File binDir = new File(buildpackDir, "bin"); + binDir.mkdirs(); + FileAttribute> attribute = PosixFilePermissions + .asFileAttribute(PosixFilePermissions.fromString("rwxrwxrwx")); + File detect = Files.createFile(Paths.get(binDir.getAbsolutePath(), "detect"), attribute).toFile(); + try (PrintWriter writer = new PrintWriter(new FileWriter(detect))) { + writer.println("#!/usr/bin/env bash"); + writer.println("set -eo pipefail"); + writer.println("exit 0"); + } + File build = Files.createFile(Paths.get(binDir.getAbsolutePath(), "build"), attribute).toFile(); + try (PrintWriter writer = new PrintWriter(new FileWriter(build))) { + writer.println("#!/usr/bin/env bash"); + writer.println("set -eo pipefail"); + writer.println("echo \"---> Hello World buildpack\""); + writer.println("echo \"---> done\""); + writer.println("exit 0"); } } - private void writeLongNameResource() { - StringBuilder name = new StringBuilder(); - new Random().ints('a', 'z' + 1).limit(128).forEach((i) -> name.append((char) i)); - try { - Path path = this.gradleBuild.getProjectDir().toPath() - .resolve(Paths.get("src", "main", "resources", name.toString())); - Files.createDirectories(path.getParent()); - Files.createFile(path); - } - catch (IOException ex) { - throw new RuntimeException(ex); + private void tarGzipBuildpackContent() throws IOException { + Path tarGzipPath = Paths.get(this.gradleBuild.getProjectDir().getAbsolutePath(), "hello-world.tgz"); + try (TarArchiveOutputStream tar = new TarArchiveOutputStream( + new GzipCompressorOutputStream(Files.newOutputStream(Files.createFile(tarGzipPath))))) { + writeFileToTar(tar, new File(this.gradleBuild.getProjectDir(), "buildpack/hello-world/buildpack.toml"), + "buildpack.toml", 0644); + writeFileToTar(tar, new File(this.gradleBuild.getProjectDir(), "buildpack/hello-world/bin/detect"), + "bin/detect", 0777); + writeFileToTar(tar, new File(this.gradleBuild.getProjectDir(), "buildpack/hello-world/bin/build"), + "bin/build", 0777); } } + private void writeFileToTar(TarArchiveOutputStream tar, File file, String name, int mode) throws IOException { + TarArchiveEntry entry = new TarArchiveEntry(file, name); + entry.setMode(mode); + tar.putArchiveEntry(entry); + IOUtils.copy(Files.newInputStream(file.toPath()), tar); + tar.closeArchiveEntry(); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java index 293cd66a14e..1fcd6310340 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -17,6 +17,7 @@ package org.springframework.boot.gradle.tasks.bundling; import java.io.File; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -28,6 +29,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.springframework.boot.buildpack.platform.build.BuildRequest; +import org.springframework.boot.buildpack.platform.build.BuildpackReference; import org.springframework.boot.buildpack.platform.build.PullPolicy; import static org.assertj.core.api.Assertions.assertThat; @@ -221,4 +223,31 @@ class BootBuildImageTests { assertThat(this.buildImage.createRequest().getPullPolicy()).isEqualTo(PullPolicy.NEVER); } + @Test + void whenNoBuildpacksAreConfiguredThenRequestUsesDefaultBuildpacks() { + assertThat(this.buildImage.createRequest().getBuildpacks()).isEmpty(); + } + + @Test + void whenBuildpacksAreConfiguredThenRequestHasBuildpacks() { + this.buildImage.setBuildpacks(Arrays.asList("example/buildpack1", "example/buildpack2")); + assertThat(this.buildImage.createRequest().getBuildpacks()).containsExactly( + BuildpackReference.of("example/buildpack1"), BuildpackReference.of("example/buildpack2")); + } + + @Test + void whenEntriesAreAddedToBuildpacksThenRequestHasBuildpacks() { + this.buildImage.buildpacks(Arrays.asList("example/buildpack1", "example/buildpack2")); + assertThat(this.buildImage.createRequest().getBuildpacks()).containsExactly( + BuildpackReference.of("example/buildpack1"), BuildpackReference.of("example/buildpack2")); + } + + @Test + void whenIndividualEntriesAreAddedToBuildpacksThenRequestHasBuildpacks() { + this.buildImage.buildpack("example/buildpack1"); + this.buildImage.buildpack("example/buildpack2"); + assertThat(this.buildImage.createRequest().getBuildpacks()).containsExactly( + BuildpackReference.of("example/buildpack1"), BuildpackReference.of("example/buildpack2")); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/GradleBuild.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/GradleBuild.java index bd711f9f180..fc379a98e57 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/GradleBuild.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/GradleBuild.java @@ -37,6 +37,7 @@ import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; import com.sun.jna.Platform; import io.spring.gradle.dependencymanagement.DependencyManagementPlugin; import io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension; +import org.antlr.v4.runtime.Lexer; import org.apache.commons.compress.archivers.ArchiveEntry; import org.apache.http.HttpRequest; import org.apache.http.conn.HttpClientConnectionManager; @@ -49,6 +50,7 @@ import org.jetbrains.kotlin.daemon.client.KotlinCompilerClient; import org.jetbrains.kotlin.gradle.model.KotlinProject; import org.jetbrains.kotlin.gradle.plugin.KotlinGradleSubplugin; import org.jetbrains.kotlin.gradle.plugin.KotlinPlugin; +import org.tomlj.Toml; import org.springframework.asm.ClassVisitor; import org.springframework.boot.buildpack.platform.build.BuildRequest; @@ -116,7 +118,8 @@ public class GradleBuild { new File(pathOfJarContaining(HttpRequest.class)), new File(pathOfJarContaining(Module.class)), new File(pathOfJarContaining(Versioned.class)), new File(pathOfJarContaining(ParameterNamesModule.class)), - new File(pathOfJarContaining(JsonView.class)), new File(pathOfJarContaining(Platform.class))); + new File(pathOfJarContaining(JsonView.class)), new File(pathOfJarContaining(Platform.class)), + new File(pathOfJarContaining(Toml.class)), new File(pathOfJarContaining(Lexer.class))); } private String pathOfJarContaining(Class type) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBuildpackFromBuilder.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBuildpackFromBuilder.gradle new file mode 100644 index 00000000000..7f725438958 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBuildpackFromBuilder.gradle @@ -0,0 +1,11 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +sourceCompatibility = '1.8' +targetCompatibility = '1.8' + +bootBuildImage { + buildpacks = [ "paketo-buildpacks/java" ] +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBuildpackFromDirectory.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBuildpackFromDirectory.gradle new file mode 100644 index 00000000000..dcdc9052e5c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBuildpackFromDirectory.gradle @@ -0,0 +1,11 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +sourceCompatibility = '1.8' +targetCompatibility = '1.8' + +bootBuildImage { + buildpacks = [ "file://${projectDir}/buildpack/hello-world" ] +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBuildpackFromImage.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBuildpackFromImage.gradle new file mode 100644 index 00000000000..5890a4fc655 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBuildpackFromImage.gradle @@ -0,0 +1,11 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +sourceCompatibility = '1.8' +targetCompatibility = '1.8' + +bootBuildImage { + buildpacks = [ "gcr.io/paketo-buildpacks/java:latest" ] +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBuildpackFromTarGzip.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBuildpackFromTarGzip.gradle new file mode 100644 index 00000000000..77266efffec --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBuildpackFromTarGzip.gradle @@ -0,0 +1,11 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +sourceCompatibility = '1.8' +targetCompatibility = '1.8' + +bootBuildImage { + buildpacks = [ "file://${projectDir}/hello-world.tgz" ] +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWithBuildpackNotInBuilder.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWithBuildpackNotInBuilder.gradle new file mode 100644 index 00000000000..bcd932c7707 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWithBuildpackNotInBuilder.gradle @@ -0,0 +1,11 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +sourceCompatibility = '1.8' +targetCompatibility = '1.8' + +bootBuildImage { + buildpacks = [ "urn:cnb:builder:example/does-not-exist:0.0.1" ] +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc index c06699fb9aa..79cb4a3b2f3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc @@ -152,6 +152,18 @@ Acceptable values are `ALWAYS`, `NEVER`, and `IF_NOT_PRESENT`. | | +| `buildpacks` +a|Buildpacks that the builder should use when building the image. +Only the specified buildpacks will be used, overriding the default buildpacks included in the builder. +Buildpack references must be in one of the following forms: + +* Buildpack in the builder - [urn:cnb:builder:][@] +* Buildpack in a directory on the file system - [file://] +* Buildpack in a gzipped tar (.tgz) file on the file system - [file://]/ +* Buildpack in an OCI image - [docker://]/[:][@] +| +| None, indicating the builder should use the buildpacks included in it. + | `cleanCache` | Whether to clean the cache before building. | `spring-boot.build-image.cleanCache` @@ -311,6 +323,64 @@ The image name can be specified on the command line as well, as shown in this ex +[[build-image-example-buildpacks]] +==== Buildpacks +By default, the builder will use buildpacks included in the builder image and apply them in a pre-defined order. +An alternative set of buildpacks can be provided to apply buildpacks that are not included in the builder, or to change the order of included buildpacks. +When one or more buildpacks are provided, only the specified buildpacks will be applied. + +The following example instructs the builder to use a custom buildpack packaged in a `.tgz` file, followed by a buildpack included in the builder. + +[source,xml,indent=0,subs="verbatim,attributes"] +---- + + + + + org.springframework.boot + spring-boot-maven-plugin + + + file:///path/to/example-buildpack.tgz + urn:cnb:builder:paketo-buildpacks/java + + + + + + +---- + +Buildpacks can be specified in any of the forms shown below. + +A buildpack located in a CNB Builder (version may be omitted if there is only one buildpack in the builder matching the `buildpack-id`): + +* `urn:cnb:builder:buildpack-id` +* `urn:cnb:builder:buildpack-id@0.0.1` +* `buildpack-id` +* `buildpack-id@0.0.1` + +A path to a directory containing buildpack content (not supported on Windows): + +* `\file:///path/to/buildpack/` +* `/path/to/buildpack/` + +A path to a gzipped tar file containing buildpack content: + +* `\file:///path/to/buildpack.tgz` +* `/path/to/buildpack.tgz` + +An OCI image: + +* `docker://example/buildpack` +* `docker:///example/buildpack:latest` +* `docker:///example/buildpack@sha256:45b23dee08...` +* `example/buildpack` +* `example/buildpack:latest` +* `example/buildpack@sha256:45b23dee08...` + + + [[build-image-example-publish]] ==== Image Publishing The generated image can be published to a Docker registry by enabling a `publish` option and configuring authentication for the registry using `docker.publishRegistry` parameters. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java index 61a537ed04e..c194ecafac4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -150,6 +150,24 @@ public class BuildImageTests extends AbstractArchiveIntegrationTests { }); } + @TestTemplate + void whenBuildImageIsInvokedWithBuildpacks(MavenBuild mavenBuild) { + mavenBuild.project("build-image-custom-buildpacks").goals("package") + .systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT").execute((project) -> { + assertThat(buildLog(project)).contains("Building image") + .contains("docker.io/library/build-image-custom-buildpacks:0.0.1.BUILD-SNAPSHOT") + .contains("Successfully built image"); + ImageReference imageReference = ImageReference + .of("docker.io/library/build-image-custom-buildpacks:0.0.1.BUILD-SNAPSHOT"); + try (GenericContainer container = new GenericContainer<>(imageReference.toString())) { + container.waitingFor(Wait.forLogMessage("Launched\\n", 1)).start(); + } + finally { + removeImage(imageReference); + } + }); + } + @TestTemplate void failsWhenPublishWithoutPublishRegistryConfigured(MavenBuild mavenBuild) { mavenBuild.project("build-image").goals("package").systemProperty("spring-boot.build-image.publish", "true") @@ -170,6 +188,14 @@ public class BuildImageTests extends AbstractArchiveIntegrationTests { (project) -> assertThat(buildLog(project)).contains("Executable jar file required for building image")); } + @TestTemplate + void failsWithBuildpackNotInBuilder(MavenBuild mavenBuild) { + mavenBuild.project("build-image-bad-buildpack").goals("package") + .systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT") + .executeAndFail((project) -> assertThat(buildLog(project)) + .contains("'urn:cnb:builder:example/does-not-exist:0.0.1' not found in builder")); + } + private void writeLongNameResource(File project) { StringBuilder name = new StringBuilder(); new Random().ints('a', 'z' + 1).limit(128).forEach((i) -> name.append((char) i)); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bad-buildpack/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bad-buildpack/pom.xml new file mode 100644 index 00000000000..209d6d6e3a0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bad-buildpack/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + org.springframework.boot.maven.it + build-image-custom-buildpacks + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + build-image + + + + + urn:cnb:builder:example/does-not-exist:0.0.1 + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bad-buildpack/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bad-buildpack/src/main/java/org/test/SampleApplication.java new file mode 100644 index 00000000000..e964724deac --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bad-buildpack/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2021 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.test; + +public class SampleApplication { + + public static void main(String[] args) throws Exception { + System.out.println("Launched"); + synchronized(args) { + args.wait(); // Prevent exit + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-custom-buildpacks/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-custom-buildpacks/pom.xml new file mode 100644 index 00000000000..360d426dd6b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-custom-buildpacks/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + org.springframework.boot.maven.it + build-image-custom-buildpacks + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + build-image + + + + + paketo-buildpacks/java + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-custom-buildpacks/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-custom-buildpacks/src/main/java/org/test/SampleApplication.java new file mode 100644 index 00000000000..e964724deac --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-custom-buildpacks/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2021 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.test; + +public class SampleApplication { + + public static void main(String[] args) throws Exception { + System.out.println("Launched"); + synchronized(args) { + args.wait(); // Prevent exit + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java index ddd5ce1b7a7..72d6980deb9 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -176,8 +176,8 @@ public class BuildImageMojo extends AbstractPackagerMojo { try { DockerConfiguration dockerConfiguration = (this.docker != null) ? this.docker.asDockerConfiguration() : null; - Builder builder = new Builder(new MojoBuildLog(this::getLog), dockerConfiguration); BuildRequest request = getBuildRequest(libraries); + Builder builder = new Builder(new MojoBuildLog(this::getLog), dockerConfiguration); builder.build(request); } catch (IOException ex) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java index bdec55f3e10..5b52ac926b2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -16,12 +16,15 @@ package org.springframework.boot.maven; +import java.util.List; import java.util.Map; import java.util.function.Function; +import java.util.stream.Collectors; import org.apache.maven.artifact.Artifact; import org.springframework.boot.buildpack.platform.build.BuildRequest; +import org.springframework.boot.buildpack.platform.build.BuildpackReference; import org.springframework.boot.buildpack.platform.build.PullPolicy; import org.springframework.boot.buildpack.platform.docker.type.ImageName; import org.springframework.boot.buildpack.platform.docker.type.ImageReference; @@ -54,6 +57,8 @@ public class Image { Boolean publish; + List buildpacks; + /** * The name of the created image. * @return the image name @@ -174,6 +179,10 @@ public class Image { if (this.publish != null) { request = request.withPublish(this.publish); } + if (this.buildpacks != null && !this.buildpacks.isEmpty()) { + request = request + .withBuildpacks(this.buildpacks.stream().map(BuildpackReference::of).collect(Collectors.toList())); + } return request; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java index 64c901fafee..78278e35d38 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -16,6 +16,7 @@ package org.springframework.boot.maven; +import java.util.Arrays; import java.util.Collections; import java.util.function.Function; @@ -26,6 +27,7 @@ import org.apache.maven.artifact.versioning.VersionRange; import org.junit.jupiter.api.Test; import org.springframework.boot.buildpack.platform.build.BuildRequest; +import org.springframework.boot.buildpack.platform.build.BuildpackReference; import org.springframework.boot.buildpack.platform.build.PullPolicy; import org.springframework.boot.buildpack.platform.io.Owner; import org.springframework.boot.buildpack.platform.io.TarArchive; @@ -65,6 +67,7 @@ class ImageTests { assertThat(request.isCleanCache()).isFalse(); assertThat(request.isVerboseLogging()).isFalse(); assertThat(request.getPullPolicy()).isEqualTo(PullPolicy.ALWAYS); + assertThat(request.getBuildpacks()).isEmpty(); } @Test @@ -123,6 +126,15 @@ class ImageTests { assertThat(request.isPublish()).isTrue(); } + @Test + void getBuildRequestWhenHasBuildpacksUsesBuildpacks() { + Image image = new Image(); + image.buildpacks = Arrays.asList("example/buildpack1@0.0.1", "example/buildpack2@0.0.2"); + BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); + assertThat(request.getBuildpacks()).containsExactly(BuildpackReference.of("example/buildpack1@0.0.1"), + BuildpackReference.of("example/buildpack2@0.0.2")); + } + private Artifact createArtifact() { return new DefaultArtifact("com.example", "my-app", VersionRange.createFromVersion("0.0.1-SNAPSHOT"), "compile", "jar", null, new DefaultArtifactHandler());