Commit f54f784f authored by Scott Frederick's avatar Scott Frederick

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
parent d8fe9de6
......@@ -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")
......
/*
* 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<BuildpackReference> buildpacks;
BuildRequest(ImageReference name, Function<Owner, TarArchive> 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<Owner, TarArchive> applicationContent, ImageReference builder,
ImageReference runImage, Creator creator, Map<String, String> env, boolean cleanCache,
boolean verboseLogging, PullPolicy pullPolicy, boolean publish) {
boolean verboseLogging, PullPolicy pullPolicy, boolean publish, List<BuildpackReference> 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<String, String> 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<BuildpackReference> 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<BuildpackReference> getBuildpacks() {
return this.buildpacks;
}
/**
* Factory method to create a new {@link BuildRequest} from a JAR file.
* @param jarFile the source jar file
......
......@@ -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,35 +95,36 @@ 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);
}
assertImageRegistriesMatch(request);
Image runImage = getImage(request, ImageType.RUNNER);
assertStackIdsMatch(runImage, builderImage);
private BuildRequest withRunImageIfNeeded(BuildRequest request, Stack builderStack) {
if (request.getRunImage() != null) {
return request;
}
return request.withRunImage(getRunImageReferenceForStack(builderStack));
}
private ImageReference getRunImageReferenceForStack(Stack stack) {
String name = stack.getRunImage().getImage();
......@@ -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);
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 + "'");
}
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 Buildpacks getBuildpacks(BuildRequest request, ImageFetcher imageFetcher, BuilderMetadata builderMetadata) {
BuildpackResolverContext resolverContext = new BuilderResolverContext(imageFetcher, builderMetadata);
return BuildpackResolvers.resolveAll(resolverContext, request.getBuildpacks());
}
private Image pullImage(ImageReference reference, ImageType imageType) throws IOException {
Consumer<TotalProgressEvent> 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 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 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 {
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 executeLifecycle(BuildRequest request, EphemeralBuilder builder) throws IOException {
try (Lifecycle lifecycle = new Lifecycle(this.log, this.docker, request, builder)) {
lifecycle.execute();
}
/**
* {@link BuildpackResolverContext} implementation for the {@link Builder}.
*/
private class BuilderResolverContext implements BuildpackResolverContext {
private final ImageFetcher imageFetcher;
private final BuilderMetadata builderMetadata;
BuilderResolverContext(ImageFetcher imageFetcher, BuilderMetadata builderMetadata) {
this.imageFetcher = imageFetcher;
this.builderMetadata = builderMetadata;
}
@Override
public List<BuildpackMetadata> 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<String, TarArchive> exports)
throws IOException {
Builder.this.docker.image().exportLayers(reference, exports);
}
}
}
/*
* 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<Layer> 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);
}
}
}
/*
* 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<BuildpackMetadata> 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<BuildpackMetadata> extractBuildpacks(JsonNode node) {
if (node.isEmpty()) {
return Collections.emptyList();
}
List<BuildpackMetadata> 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<BuildpackMetadata> getBuildpacks() {
return this.buildpacks;
}
/**
* Create an updated copy of this metadata.
* @param update consumer to apply updates
......
/*
* 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<Layer> layers) throws IOException;
}
/*
* 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 <a href=
* "https://github.com/buildpacks/spec/blob/main/platform.md#ordertoml-toml">Platform
* Interface Specification</a>
*/
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 <a href=
* "https://github.com/buildpacks/spec/blob/main/buildpack.md#buildpacktoml-toml">{@code buildpack.toml}</a>
* 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);
}
}
/*
* 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);
}
}
/*
* 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);
}
}
/*
* 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);
}
/*
* 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<BuildpackMetadata> 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<String, TarArchive> exports) throws IOException;
}
/*
* 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<BuildpackResolver> resolvers = getResolvers();
private BuildpackResolvers() {
}
private static List<BuildpackResolver> getResolvers() {
List<BuildpackResolver> 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<BuildpackReference> references) {
Assert.notNull(context, "Context must not be null");
if (CollectionUtils.isEmpty(references)) {
return Buildpacks.EMPTY;
}
List<Buildpack> 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 + "'");
}
}
/*
* 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<Buildpack> buildpacks;
private Buildpacks(List<Buildpack> buildpacks) {
this.buildpacks = buildpacks;
}
List<Buildpack> getBuildpacks() {
return this.buildpacks;
}
void apply(IOConsumer<Layer> 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<Buildpack> buildpacks) {
return CollectionUtils.isEmpty(buildpacks) ? EMPTY : new Buildpacks(buildpacks);
}
}
/*
* 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<Layer> 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<Path> {
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();
}
}
}
/*
* 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<String, String> env) throws IOException {
Map<String, String> 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);
}
});
}
......
/*
* 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<Layer> 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<Path> layerFiles;
ExportedLayers(BuildpackResolverContext context, ImageReference imageReference) throws IOException {
List<Path> 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<Layer> 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);
}
}
}
/*
* 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;
......
/*
* 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<Layer> 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;
}
}
/*
* 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<String, TarArchive> 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
......
/*
* 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));
......
/*
* 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
* <a href="https://en.wikipedia.org/wiki/Umask">umask</a> octal notation.
* @param permissions the set of {@code PosixFilePermission}s
* @return the integer representation
*/
public static int posixPermissionsToUmask(Collection<PosixFilePermission> 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<PosixFilePermission> 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;
}
}
/*
* 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 <T> the first consumed type
* @param <U> the second consumed type
* @author Phillip Webb
* @since 2.3.0
*/
@FunctionalInterface
public interface IOBiConsumer<T, U> {
/**
* 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;
}
/*
* 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;
}
/*
* 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) {
......
/*
* 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<BuildpackReference>) null))
.withMessage("Buildpacks must not be null");
}
private void hasExpectedJarContent(TarArchive archive) {
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
......
/*
* 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<Layer> layers = new ArrayList<>();
buildpack.apply(layers::add);
assertThat(layers).isEmpty();
}
}
/*
* 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"));
}
}
......@@ -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;
}
......
/*
* 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));
}
}
/*
* 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");
}
}
/*
* 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"));
}
}
/*
* 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'");
}
}
/*
* 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<Buildpack> 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<Buildpack> 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<Layer> 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();
}
}
/*
* 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<ByteArrayOutputStream> 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<TarArchiveEntry> 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\"");
}
}
}
/*
* 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<String, String> 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<Buildpack> 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));
}
}
/*
* 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<String, TarArchive> 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<ByteArrayOutputStream> 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<String> 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");
}
}
/*
* 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();
}
}
/*
* 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<Layer> 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]"));
}
}
/*
* 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<ByteArrayOutputStream> 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();
}
}
}
/*
* 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<String, String> 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
......
/*
* 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 <E> The event type
* @author Phillip Webb
* @author Scott Frederick
*/
......
/*
* 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<PosixFilePermission> permissions = PosixFilePermissions.fromString("rwxrw-r--");
assertThat(FilePermissions.posixPermissionsToUmask(permissions)).isEqualTo(0764);
}
@Test
void posixPermissionsToUmaskWithEmptyPermissions() {
Set<PosixFilePermission> permissions = Collections.emptySet();
assertThat(FilePermissions.posixPermissionsToUmask(permissions)).isEqualTo(0);
}
@Test
void posixPermissionsToUmaskWithNullPermissions() {
assertThatIllegalArgumentException().isThrownBy(() -> FilePermissions.posixPermissionsToUmask(null));
}
}
/*
* 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));
......
/*
* 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"));
}
}
......@@ -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": {
......
{
"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"
}
}
{
"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"
}
]
}
[buildpack]
id = "test";
version = "1.0.0"
name = "Example buildpack"
homepage = "https://github.com/example/example-buildpack"
[[stacks]]
id = "io.buildpacks.stacks.bionic"
[[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" }
]
[[order]]
group = [
{ id = "example/buildpack1" }
]
[[order]]
group = [
{ id = "example/buildpack2" }
]
[[order]]
group = [
{ id = "example/buildpack3" }
]
......@@ -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 id>[@<version>]
* Buildpack in a directory on the file system - [file://]<path>
* Buildpack in a gzipped tar (.tgz) file on the file system - [file://]<path>/<file name>
* Buildpack in an OCI image - [docker://]<host>/<repo>[:<tag>][@<digest>]
| 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:
......
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" }
}
}
import org.springframework.boot.gradle.tasks.bundling.BootBuildImage
plugins {
java
id("org.springframework.boot") version "{gradle-project-version}"
}
// tag::buildpacks[]
tasks.getByName<BootBuildImage>("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>("bootBuildImage").buildpacks) {
print(reference)
}
}
}
/*
* 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<String> 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<String> getBuildpacks() {
return this.buildpacks.getOrNull();
}
/**
* Sets the buildpacks that will be used when building the image.
* @param buildpacks the buildpacks
*/
public void setBuildpacks(List<String> 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<String> 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<String> 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() + ".*";
}
......
/*
* 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"));
}
}
......@@ -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) {
......
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
}
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
bootBuildImage {
buildpacks = [ "paketo-buildpacks/java" ]
}
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
}
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
bootBuildImage {
buildpacks = [ "file://${projectDir}/buildpack/hello-world" ]
}
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
}
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
bootBuildImage {
buildpacks = [ "gcr.io/paketo-buildpacks/java:latest" ]
}
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
}
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
bootBuildImage {
buildpacks = [ "file://${projectDir}/hello-world.tgz" ]
}
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" ]
}
......@@ -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 id>[@<version>]
* Buildpack in a directory on the file system - [file://]<path>
* Buildpack in a gzipped tar (.tgz) file on the file system - [file://]<path>/<file name>
* Buildpack in an OCI image - [docker://]<host>/<repo>[:<tag>][@<digest>]
|
| 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"]
----
<project>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<buildpacks>
<buildpack>file:///path/to/example-buildpack.tgz</buildpack>
<buildpack>urn:cnb:builder:paketo-buildpacks/java</buildpack>
</image>
</configuration>
</plugin>
</plugins>
</build>
</project>
----
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.
......
/*
* 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));
......
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>build-image-custom-buildpacks</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>@java.version@</maven.compiler.source>
<maven.compiler.target>@java.version@</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>@project.groupId@</groupId>
<artifactId>@project.artifactId@</artifactId>
<version>@project.version@</version>
<executions>
<execution>
<goals>
<goal>build-image</goal>
</goals>
<configuration>
<image>
<buildpacks>
<buildpack>urn:cnb:builder:example/does-not-exist:0.0.1</buildpack>
</buildpacks>
</image>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
/*
* 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
}
}
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>build-image-custom-buildpacks</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>@java.version@</maven.compiler.source>
<maven.compiler.target>@java.version@</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>@project.groupId@</groupId>
<artifactId>@project.artifactId@</artifactId>
<version>@project.version@</version>
<executions>
<execution>
<goals>
<goal>build-image</goal>
</goals>
<configuration>
<image>
<buildpacks>
<buildpack>paketo-buildpacks/java</buildpack>
</buildpacks>
</image>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
/*
* 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
}
}
}
/*
* 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) {
......
/*
* 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<String> 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;
}
......
/*
* 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());
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment