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
This commit is contained in:
Scott Frederick
2021-02-18 17:28:25 -06:00
parent d8fe9de682
commit f54f784f80
74 changed files with 3895 additions and 211 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,8 +17,10 @@
package org.springframework.boot.buildpack.platform.build;
import java.io.File;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
@@ -61,6 +63,8 @@ public class BuildRequest {
private final boolean publish;
private final List<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

View 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,34 +95,35 @@ public class Builder {
public void build(BuildRequest request) throws DockerEngineException, IOException {
Assert.notNull(request, "Request must not be null");
this.log.start(request);
Image builderImage = getImage(request, ImageType.BUILDER);
String domain = request.getBuilder().getDomain();
PullPolicy pullPolicy = request.getPullPolicy();
ImageFetcher imageFetcher = new ImageFetcher(domain, getBuilderAuthHeader(), pullPolicy);
Image builderImage = imageFetcher.fetchImage(ImageType.BUILDER, request.getBuilder());
BuilderMetadata builderMetadata = BuilderMetadata.fromImage(builderImage);
request = withRunImageIfNeeded(request, builderMetadata.getStack());
Image runImage = imageFetcher.fetchImage(ImageType.RUNNER, request.getRunImage());
assertStackIdsMatch(runImage, builderImage);
BuildOwner buildOwner = BuildOwner.fromEnv(builderImage.getConfig().getEnv());
request = determineRunImage(request, builderImage, builderMetadata.getStack());
EphemeralBuilder builder = new EphemeralBuilder(buildOwner, builderImage, builderMetadata, request.getCreator(),
request.getEnv());
this.docker.image().load(builder.getArchive(), UpdateListener.none());
Buildpacks buildpacks = getBuildpacks(request, imageFetcher, builderMetadata);
EphemeralBuilder ephemeralBuilder = new EphemeralBuilder(buildOwner, builderImage, builderMetadata,
request.getCreator(), request.getEnv(), buildpacks);
this.docker.image().load(ephemeralBuilder.getArchive(), UpdateListener.none());
try {
executeLifecycle(request, builder);
executeLifecycle(request, ephemeralBuilder);
if (request.isPublish()) {
pushImage(request.getName());
}
}
finally {
this.docker.image().remove(builder.getName(), true);
this.docker.image().remove(ephemeralBuilder.getName(), true);
}
}
private BuildRequest determineRunImage(BuildRequest request, Image builderImage, Stack builderStack)
throws IOException {
if (request.getRunImage() == null) {
ImageReference runImage = getRunImageReferenceForStack(builderStack);
request = request.withRunImage(runImage);
private BuildRequest withRunImageIfNeeded(BuildRequest request, Stack builderStack) {
if (request.getRunImage() != null) {
return request;
}
assertImageRegistriesMatch(request);
Image runImage = getImage(request, ImageType.RUNNER);
assertStackIdsMatch(runImage, builderImage);
return request;
return request.withRunImage(getRunImageReferenceForStack(builderStack));
}
private ImageReference getRunImageReferenceForStack(Stack stack) {
@@ -128,32 +132,22 @@ public class Builder {
return ImageReference.of(name).inTaggedOrDigestForm();
}
private Image getImage(BuildRequest request, ImageType imageType) throws IOException {
ImageReference imageReference = (imageType == ImageType.BUILDER) ? request.getBuilder() : request.getRunImage();
if (request.getPullPolicy() == PullPolicy.ALWAYS) {
return pullImage(imageReference, imageType);
}
try {
return this.docker.image().inspect(imageReference);
}
catch (DockerEngineException exception) {
if (request.getPullPolicy() == PullPolicy.IF_NOT_PRESENT && exception.getStatusCode() == 404) {
return pullImage(imageReference, imageType);
}
else {
throw exception;
}
}
private void assertStackIdsMatch(Image runImage, Image builderImage) {
StackId runImageStackId = StackId.fromImage(runImage);
StackId builderImageStackId = StackId.fromImage(builderImage);
Assert.state(runImageStackId.equals(builderImageStackId), () -> "Run image stack '" + runImageStackId
+ "' does not match builder stack '" + builderImageStackId + "'");
}
private Image pullImage(ImageReference reference, ImageType imageType) throws IOException {
Consumer<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 Buildpacks getBuildpacks(BuildRequest request, ImageFetcher imageFetcher, BuilderMetadata builderMetadata) {
BuildpackResolverContext resolverContext = new BuilderResolverContext(imageFetcher, builderMetadata);
return BuildpackResolvers.resolveAll(resolverContext, request.getBuildpacks());
}
private void executeLifecycle(BuildRequest request, EphemeralBuilder builder) throws IOException {
try (Lifecycle lifecycle = new Lifecycle(this.log, this.docker, request, builder)) {
lifecycle.execute();
}
}
private void pushImage(ImageReference reference) throws IOException {
@@ -173,25 +167,83 @@ public class Builder {
? this.dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader() : null;
}
private void assertImageRegistriesMatch(BuildRequest request) {
if (getBuilderAuthHeader() != null) {
Assert.state(request.getRunImage().getDomain().equals(request.getBuilder().getDomain()),
"Builder image '" + request.getBuilder() + "' and run image '" + request.getRunImage()
+ "' must be pulled from the same authenticated registry");
/**
* Internal utility class used to fetch images.
*/
private class ImageFetcher {
private final String domain;
private final String authHeader;
private final PullPolicy pullPolicy;
ImageFetcher(String domain, String authHeader, PullPolicy pullPolicy) {
this.domain = domain;
this.authHeader = authHeader;
this.pullPolicy = pullPolicy;
}
Image fetchImage(ImageType type, ImageReference reference) throws IOException {
Assert.notNull(type, "Type must not be null");
Assert.notNull(reference, "Reference must not be null");
Assert.state(this.authHeader == null || reference.getDomain().equals(this.domain),
() -> String.format("%s '%s' must be pulled from the '%s' authenticated registry",
StringUtils.capitalize(type.getDescription()), reference, this.domain));
if (this.pullPolicy == PullPolicy.ALWAYS) {
return pullImage(reference, type);
}
try {
return Builder.this.docker.image().inspect(reference);
}
catch (DockerEngineException ex) {
if (this.pullPolicy == PullPolicy.IF_NOT_PRESENT && ex.getStatusCode() == 404) {
return pullImage(reference, type);
}
throw ex;
}
}
private Image pullImage(ImageReference reference, ImageType imageType) throws IOException {
TotalProgressPullListener listener = new TotalProgressPullListener(
Builder.this.log.pullingImage(reference, imageType));
Image image = Builder.this.docker.image().pull(reference, listener, this.authHeader);
Builder.this.log.pulledImage(image, imageType);
return image;
}
}
private void assertStackIdsMatch(Image runImage, Image builderImage) {
StackId runImageStackId = StackId.fromImage(runImage);
StackId builderImageStackId = StackId.fromImage(builderImage);
Assert.state(runImageStackId.equals(builderImageStackId), () -> "Run image stack '" + runImageStackId
+ "' does not match builder stack '" + builderImageStackId + "'");
}
/**
* {@link BuildpackResolverContext} implementation for the {@link Builder}.
*/
private class BuilderResolverContext implements BuildpackResolverContext {
private void executeLifecycle(BuildRequest request, EphemeralBuilder builder) throws IOException {
try (Lifecycle lifecycle = new Lifecycle(this.log, this.docker, request, builder)) {
lifecycle.execute();
private final ImageFetcher imageFetcher;
private final BuilderMetadata builderMetadata;
BuilderResolverContext(ImageFetcher imageFetcher, BuilderMetadata builderMetadata) {
this.imageFetcher = imageFetcher;
this.builderMetadata = builderMetadata;
}
@Override
public List<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);
}
}
}

View File

@@ -0,0 +1,117 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.build;
import java.io.IOException;
import org.springframework.boot.buildpack.platform.docker.type.Layer;
import org.springframework.boot.buildpack.platform.io.IOConsumer;
import org.springframework.util.Assert;
/**
* A {@link Buildpack} that references a buildpack contained in the builder.
*
* The buildpack reference must contain a buildpack ID (for example,
* {@code "example/buildpack"}) or a buildpack ID and version (for example,
* {@code "example/buildpack@1.0.0"}). The reference can optionally contain a prefix
* {@code urn:cnb:builder:} to unambiguously identify it as a builder buildpack reference.
* If a version is not provided, the reference will match any version of a buildpack with
* the same ID as the reference.
*
* @author Scott Frederick
*/
class BuilderBuildpack implements Buildpack {
private static final String PREFIX = "urn:cnb:builder:";
private final BuildpackCoordinates coordinates;
BuilderBuildpack(BuildpackMetadata buildpackMetadata) {
this.coordinates = BuildpackCoordinates.fromBuildpackMetadata(buildpackMetadata);
}
@Override
public BuildpackCoordinates getCoordinates() {
return this.coordinates;
}
@Override
public void apply(IOConsumer<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);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,6 +18,9 @@ package org.springframework.boot.buildpack.platform.build;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import com.fasterxml.jackson.core.JsonProcessingException;
@@ -50,11 +53,23 @@ class BuilderMetadata extends MappedObject {
private final CreatedBy createdBy;
private final List<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

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.build;
import java.io.IOException;
import org.springframework.boot.buildpack.platform.docker.type.Layer;
import org.springframework.boot.buildpack.platform.io.IOConsumer;
/**
* A Buildpack that should be invoked by the builder during image building.
*
* @author Scott Frederick
* @see BuildpackResolver
*/
interface Buildpack {
/**
* Return the coordinates of the builder.
* @return the builder coordinates
*/
BuildpackCoordinates getCoordinates();
/**
* Apply the necessary buildpack layers.
* @param layers a consumer that should accept the layers
* @throws IOException on IO error
*/
void apply(IOConsumer<Layer> layers) throws IOException;
}

View File

@@ -0,0 +1,140 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.build;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import org.tomlj.Toml;
import org.tomlj.TomlParseResult;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* A set of buildpack coordinates that uniquely identifies a buildpack.
*
* @author Scott Frederick
* @see <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);
}
}

View File

@@ -0,0 +1,121 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.build;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.boot.buildpack.platform.docker.type.Image;
import org.springframework.boot.buildpack.platform.docker.type.ImageConfig;
import org.springframework.boot.buildpack.platform.json.MappedObject;
import org.springframework.boot.buildpack.platform.json.SharedObjectMapper;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Buildpack metadata information.
*
* @author Scott Frederick
*/
final class BuildpackMetadata extends MappedObject {
private static final String LABEL_NAME = "io.buildpacks.buildpackage.metadata";
private final String id;
private final String version;
private final String homepage;
private BuildpackMetadata(JsonNode node) {
super(node, MethodHandles.lookup());
this.id = valueAt("/id", String.class);
this.version = valueAt("/version", String.class);
this.homepage = valueAt("/homepage", String.class);
}
/**
* Return the buildpack ID.
* @return the ID
*/
String getId() {
return this.id;
}
/**
* Return the buildpack version.
* @return the version
*/
String getVersion() {
return this.version;
}
/**
* Return the buildpack homepage address.
* @return the homepage
*/
String getHomepage() {
return this.homepage;
}
/**
* Factory method to extract {@link BuildpackMetadata} from an image.
* @param image the source image
* @return the builder metadata
* @throws IOException on IO error
*/
static BuildpackMetadata fromImage(Image image) throws IOException {
Assert.notNull(image, "Image must not be null");
return fromImageConfig(image.getConfig());
}
/**
* Factory method to extract {@link BuildpackMetadata} from image config.
* @param imageConfig the source image config
* @return the builder metadata
* @throws IOException on IO error
*/
static BuildpackMetadata fromImageConfig(ImageConfig imageConfig) throws IOException {
Assert.notNull(imageConfig, "ImageConfig must not be null");
String json = imageConfig.getLabels().get(LABEL_NAME);
Assert.notNull(json, () -> "No '" + LABEL_NAME + "' label found in image config labels '"
+ StringUtils.collectionToCommaDelimitedString(imageConfig.getLabels().keySet()) + "'");
return fromJson(json);
}
/**
* Factory method create {@link BuildpackMetadata} from JSON.
* @param json the source JSON
* @return the builder metadata
* @throws IOException on IO error
*/
static BuildpackMetadata fromJson(String json) throws IOException {
return fromJson(SharedObjectMapper.get().readTree(json));
}
/**
* Factory method create {@link BuildpackMetadata} from JSON.
* @param node the source JSON
* @return the builder metadata
*/
static BuildpackMetadata fromJson(JsonNode node) {
return new BuildpackMetadata(node);
}
}

View File

@@ -0,0 +1,94 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.build;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.springframework.util.Assert;
/**
* An opaque reference to a {@link Buildpack}.
*
* @author Phillip Webb
* @author Scott Frederick
* @since 2.5.0
* @see BuildpackResolver
*/
public final class BuildpackReference {
private final String value;
private BuildpackReference(String value) {
this.value = value;
}
boolean hasPrefix(String prefix) {
return this.value.startsWith(prefix);
}
String getSubReference(String prefix) {
return this.value.startsWith(prefix) ? this.value.substring(prefix.length()) : null;
}
Path asPath() {
try {
URL url = new URL(this.value);
if (url.getProtocol().equals("file")) {
return Paths.get(url.getPath());
}
}
catch (MalformedURLException ex) {
// not a URL, fall through to attempting to find a plain file path
}
return Paths.get(this.value);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
return this.value.equals(((BuildpackReference) obj).value);
}
@Override
public int hashCode() {
return this.value.hashCode();
}
@Override
public String toString() {
return this.value;
}
/**
* Create a new {@link BuildpackReference} from the given value.
* @param value the value to use
* @return a new {@link BuildpackReference}
*/
public static BuildpackReference of(String value) {
Assert.hasText(value, "Value must not be empty");
return new BuildpackReference(value);
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.build;
/**
* Strategy inerface used to resolve a {@link BuildpackReference} to a {@link Buildpack}.
*
* @author Scott Frederick
* @author Phillip Webb
* @see BuildpackResolvers
*/
interface BuildpackResolver {
/**
* Attempt to resolve the given {@link BuildpackReference}.
* @param context the resolver context
* @param reference the reference to resolve
* @return a resolved {@link Buildpack} instance or {@code null}
*/
Buildpack resolve(BuildpackResolverContext context, BuildpackReference reference);
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.build;
import java.io.IOException;
import java.util.List;
import org.springframework.boot.buildpack.platform.docker.type.Image;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.io.IOBiConsumer;
import org.springframework.boot.buildpack.platform.io.TarArchive;
/**
* Context passed to a {@link BuildpackResolver}.
*
* @author Scott Frederick
* @author Phillip Webb
*/
interface BuildpackResolverContext {
List<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;
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.build;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* All {@link BuildpackResolver} instances that can be used to resolve
* {@link BuildpackReference BuildpackReferences}.
*
* @author Scott Frederick
* @author Phillip Webb
*/
final class BuildpackResolvers {
private static final List<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 + "'");
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.build;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import org.springframework.boot.buildpack.platform.docker.type.Layer;
import org.springframework.boot.buildpack.platform.io.Content;
import org.springframework.boot.buildpack.platform.io.IOConsumer;
import org.springframework.boot.buildpack.platform.io.Layout;
import org.springframework.boot.buildpack.platform.io.Owner;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
* A collection of {@link Buildpack} instances that can be used to apply buildpack layers.
*
* @author Scott Frederick
* @author Phillip Webb
*/
final class Buildpacks {
static final Buildpacks EMPTY = new Buildpacks(Collections.emptyList());
private final List<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);
}
}

View File

@@ -0,0 +1,136 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.build;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.PosixFileAttributeView;
import org.springframework.boot.buildpack.platform.docker.type.Layer;
import org.springframework.boot.buildpack.platform.io.Content;
import org.springframework.boot.buildpack.platform.io.FilePermissions;
import org.springframework.boot.buildpack.platform.io.IOConsumer;
import org.springframework.boot.buildpack.platform.io.Layout;
import org.springframework.boot.buildpack.platform.io.Owner;
import org.springframework.util.Assert;
/**
* A {@link Buildpack} that references a buildpack in a directory on the local file
* system.
*
* The file system must contain a buildpack descriptor named {@code buildpack.toml} in the
* root of the directory. The contents of the directory tree will be provided as a single
* layer to be included in the builder image.
*
* @author Scott Frederick
*/
final class DirectoryBuildpack implements Buildpack {
private final Path path;
private final BuildpackCoordinates coordinates;
private DirectoryBuildpack(Path path) {
this.path = path;
this.coordinates = findBuildpackCoordinates(path);
}
private BuildpackCoordinates findBuildpackCoordinates(Path path) {
Path buildpackToml = path.resolve("buildpack.toml");
Assert.isTrue(Files.exists(buildpackToml),
() -> "Buildpack descriptor 'buildpack.toml' is required in buildpack '" + path + "'");
try {
try (InputStream inputStream = Files.newInputStream(buildpackToml)) {
return BuildpackCoordinates.fromToml(inputStream, path);
}
}
catch (IOException ex) {
throw new IllegalArgumentException("Error parsing descriptor for buildpack '" + path + "'", ex);
}
}
@Override
public BuildpackCoordinates getCoordinates() {
return this.coordinates;
}
@Override
public void apply(IOConsumer<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();
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -49,10 +49,11 @@ class EphemeralBuilder {
* @param builderMetadata the builder metadata
* @param creator the builder creator
* @param env the builder env
* @param buildpacks an optional set of buildpacks to apply
* @throws IOException on IO error
*/
EphemeralBuilder(BuildOwner buildOwner, Image builderImage, BuilderMetadata builderMetadata, Creator creator,
Map<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);
}
});
}

View File

@@ -0,0 +1,143 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.build;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException;
import org.springframework.boot.buildpack.platform.docker.type.Image;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.docker.type.Layer;
import org.springframework.boot.buildpack.platform.io.IOConsumer;
import org.springframework.boot.buildpack.platform.io.TarArchive;
import org.springframework.util.StreamUtils;
/**
* A {@link Buildpack} that references a buildpack contained in an OCI image.
*
* The reference must be an OCI image reference. The reference can optionally contain a
* prefix {@code docker://} to unambiguously identify it as an image buildpack reference.
*
* @author Scott Frederick
* @author Phillip Webb
*/
final class ImageBuildpack implements Buildpack {
private static final String PREFIX = "docker://";
private final BuildpackCoordinates coordinates;
private final ExportedLayers exportedLayers;
private ImageBuildpack(BuildpackResolverContext context, ImageReference imageReference) {
try {
Image image = context.fetchImage(imageReference, ImageType.BUILDPACK);
BuildpackMetadata buildpackMetadata = BuildpackMetadata.fromImage(image);
this.coordinates = BuildpackCoordinates.fromBuildpackMetadata(buildpackMetadata);
this.exportedLayers = new ExportedLayers(context, imageReference);
}
catch (IOException | DockerEngineException ex) {
throw new IllegalArgumentException("Error pulling buildpack image '" + imageReference + "'", ex);
}
}
@Override
public BuildpackCoordinates getCoordinates() {
return this.coordinates;
}
@Override
public void apply(IOConsumer<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);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -31,7 +31,12 @@ enum ImageType {
/**
* Run image.
*/
RUNNER("run image");
RUNNER("run image"),
/**
* Buildpack image.
*/
BUILDPACK("buildpack image");
private final String description;

View File

@@ -0,0 +1,118 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.build;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
import org.springframework.boot.buildpack.platform.docker.type.Layer;
import org.springframework.boot.buildpack.platform.io.IOConsumer;
import org.springframework.util.StreamUtils;
/**
* A {@link Buildpack} that references a buildpack contained in a local gzipped tar
* archive file.
*
* The archive must contain a buildpack descriptor named {@code buildpack.toml} at the
* root of the archive. The contents of the archive will be provided as a single layer to
* be included in the builder image.
*
* @author Scott Frederick
*/
final class TarGzipBuildpack implements Buildpack {
private final Path path;
private final BuildpackCoordinates coordinates;
private TarGzipBuildpack(Path path) {
this.path = path;
this.coordinates = findBuildpackCoordinates(path);
}
private BuildpackCoordinates findBuildpackCoordinates(Path path) {
try {
try (TarArchiveInputStream tar = new TarArchiveInputStream(
new GzipCompressorInputStream(Files.newInputStream(path)))) {
ArchiveEntry entry = tar.getNextEntry();
while (entry != null) {
if ("buildpack.toml".equals(entry.getName())) {
return BuildpackCoordinates.fromToml(tar, path);
}
entry = tar.getNextEntry();
}
throw new IllegalArgumentException(
"Buildpack descriptor 'buildpack.toml' is required in buildpack '" + path + "'");
}
}
catch (IOException ex) {
throw new RuntimeException("Error parsing descriptor for buildpack '" + path + "'", ex);
}
}
@Override
public BuildpackCoordinates getCoordinates() {
return this.coordinates;
}
@Override
public void apply(IOConsumer<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;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -24,6 +24,8 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.http.client.utils.URIBuilder;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
@@ -37,9 +39,12 @@ import org.springframework.boot.buildpack.platform.docker.type.Image;
import org.springframework.boot.buildpack.platform.docker.type.ImageArchive;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.docker.type.VolumeName;
import org.springframework.boot.buildpack.platform.io.IOBiConsumer;
import org.springframework.boot.buildpack.platform.io.TarArchive;
import org.springframework.boot.buildpack.platform.json.JsonStream;
import org.springframework.boot.buildpack.platform.json.SharedObjectMapper;
import org.springframework.util.Assert;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
/**
@@ -243,6 +248,31 @@ public class DockerApi {
}
}
/**
* Export the layers of an image.
* @param reference the reference to export
* @param exports a consumer to receive the layers (contents can only be accessed
* during the callback)
* @throws IOException on IO error
*/
public void exportLayers(ImageReference reference, IOBiConsumer<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

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -68,6 +68,11 @@ public interface Content {
return of(bytes.length, () -> new ByteArrayInputStream(bytes));
}
/**
* Create a new {@link Content} from the given file.
* @param file the file to write
* @return a new {@link Content} instance
*/
static Content of(File file) {
Assert.notNull(file, "File must not be null");
return of((int) file.length(), () -> new FileInputStream(file));

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.io;
import java.nio.file.attribute.PosixFilePermission;
import java.util.Collection;
import org.springframework.util.Assert;
/**
* Utilities for dealing with file permissions and attributes.
*
* @author Scott Frederick
* @since 2.5.0
*/
public final class FilePermissions {
private FilePermissions() {
}
/**
* Return the integer representation of a set of Posix file permissions, where the
* integer value conforms to the
* <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;
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.io;
import java.io.IOException;
/**
* BiConsumer that can safely throw {@link IOException IO exceptions}.
*
* @param <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;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,13 +22,14 @@ import java.io.IOException;
* Interface that can be used to write a file/directory layout.
*
* @author Phillip Webb
* @author Scott Frederick
* @since 2.3.0
*/
public interface Layout {
/**
* Add a directory to the content.
* @param name the full name of the directory to add.
* @param name the full name of the directory to add
* @param owner the owner of the directory
* @throws IOException on IO error
*/
@@ -36,11 +37,23 @@ public interface Layout {
/**
* Write a file to the content.
* @param name the full name of the file to add.
* @param name the full name of the file to add
* @param owner the owner of the file
* @param content the content to add
* @throws IOException on IO error
*/
void file(String name, Owner owner, Content content) throws IOException;
default void file(String name, Owner owner, Content content) throws IOException {
file(name, owner, 0644, content);
}
/**
* Write a file to the content.
* @param name the full name of the file to add
* @param owner the owner of the file
* @param mode the permissions for the file
* @param content the content to add
* @throws IOException on IO error
*/
void file(String name, Owner owner, int mode, Content content) throws IOException;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@ import org.springframework.util.StreamUtils;
* {@link Layout} for writing TAR archive content directly to an {@link OutputStream}.
*
* @author Phillip Webb
* @author Scott Frederick
*/
class TarLayoutWriter implements Layout, Closeable {
@@ -49,8 +50,8 @@ class TarLayoutWriter implements Layout, Closeable {
}
@Override
public void file(String name, Owner owner, Content content) throws IOException {
this.outputStream.putArchiveEntry(createFileEntry(name, owner, content.size()));
public void file(String name, Owner owner, int mode, Content content) throws IOException {
this.outputStream.putArchiveEntry(createFileEntry(name, owner, mode, content.size()));
content.writeTo(StreamUtils.nonClosing(this.outputStream));
this.outputStream.closeArchiveEntry();
}
@@ -59,8 +60,8 @@ class TarLayoutWriter implements Layout, Closeable {
return createEntry(name, owner, TarConstants.LF_DIR, 0755, 0);
}
private TarArchiveEntry createFileEntry(String name, Owner owner, int size) {
return createEntry(name, owner, TarConstants.LF_NORMAL, 0644, size);
private TarArchiveEntry createFileEntry(String name, Owner owner, int mode, int size) {
return createEntry(name, owner, TarConstants.LF_NORMAL, mode, size);
}
private TarArchiveEntry createEntry(String name, Owner owner, byte linkFlag, int mode, int size) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,6 +22,7 @@ import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
@@ -163,6 +164,23 @@ public class BuildRequestTests {
.withMessage("Value must not be empty");
}
@Test
void withBuildpacksAddsBuildpacks() throws IOException {
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));
BuildpackReference buildpackReference1 = BuildpackReference.of("example/buildpack1");
BuildpackReference buildpackReference2 = BuildpackReference.of("example/buildpack2");
BuildRequest withBuildpacks = request.withBuildpacks(buildpackReference1, buildpackReference2);
assertThat(request.getBuildpacks()).isEmpty();
assertThat(withBuildpacks.getBuildpacks()).containsExactly(buildpackReference1, buildpackReference2);
}
@Test
void withBuildpacksWhenBuildpacksIsNullThrowsException() throws IOException {
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));
assertThatIllegalArgumentException().isThrownBy(() -> request.withBuildpacks((List<BuildpackReference>) null))
.withMessage("Buildpacks must not be null");
}
private void hasExpectedJarContent(TarArchive archive) {
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

View File

@@ -0,0 +1,115 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.build;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.buildpack.platform.docker.type.Layer;
import org.springframework.boot.buildpack.platform.json.AbstractJsonTests;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link BuilderBuildpack}.
*
* @author Scott Frederick
*/
class BuilderBuildpackTests extends AbstractJsonTests {
private BuildpackResolverContext resolverContext;
@BeforeEach
void setUp() throws Exception {
BuilderMetadata metadata = BuilderMetadata.fromJson(getContentAsString("builder-metadata.json"));
this.resolverContext = mock(BuildpackResolverContext.class);
given(this.resolverContext.getBuildpackMetadata()).willReturn(metadata.getBuildpacks());
}
@Test
void resolveWhenFullyQualifiedBuildpackWithVersionResolves() throws Exception {
BuildpackReference reference = BuildpackReference.of("urn:cnb:builder:paketo-buildpacks/spring-boot@3.5.0");
Buildpack buildpack = BuilderBuildpack.resolve(this.resolverContext, reference);
assertThat(buildpack.getCoordinates())
.isEqualTo(BuildpackCoordinates.of("paketo-buildpacks/spring-boot", "3.5.0"));
assertThatNoLayersAreAdded(buildpack);
}
@Test
void resolveWhenFullyQualifiedBuildpackWithoutVersionResolves() throws Exception {
BuildpackReference reference = BuildpackReference.of("urn:cnb:builder:paketo-buildpacks/spring-boot");
Buildpack buildpack = BuilderBuildpack.resolve(this.resolverContext, reference);
assertThat(buildpack.getCoordinates())
.isEqualTo(BuildpackCoordinates.of("paketo-buildpacks/spring-boot", "3.5.0"));
assertThatNoLayersAreAdded(buildpack);
}
@Test
void resolveWhenUnqualifiedBuildpackWithVersionResolves() throws Exception {
BuildpackReference reference = BuildpackReference.of("paketo-buildpacks/spring-boot@3.5.0");
Buildpack buildpack = BuilderBuildpack.resolve(this.resolverContext, reference);
assertThat(buildpack.getCoordinates())
.isEqualTo(BuildpackCoordinates.of("paketo-buildpacks/spring-boot", "3.5.0"));
assertThatNoLayersAreAdded(buildpack);
}
@Test
void resolveWhenUnqualifiedBuildpackWithoutVersionResolves() throws Exception {
BuildpackReference reference = BuildpackReference.of("paketo-buildpacks/spring-boot");
Buildpack buildpack = BuilderBuildpack.resolve(this.resolverContext, reference);
assertThat(buildpack.getCoordinates())
.isEqualTo(BuildpackCoordinates.of("paketo-buildpacks/spring-boot", "3.5.0"));
assertThatNoLayersAreAdded(buildpack);
}
@Test
void resolveWhenFullyQualifiedBuildpackWithVersionNotInBuilderThrowsException() {
BuildpackReference reference = BuildpackReference.of("urn:cnb:builder:example/buildpack1@1.2.3");
assertThatIllegalArgumentException().isThrownBy(() -> BuilderBuildpack.resolve(this.resolverContext, reference))
.withMessageContaining("'urn:cnb:builder:example/buildpack1@1.2.3'")
.withMessageContaining("not found in builder");
}
@Test
void resolveWhenFullyQualifiedBuildpackWithoutVersionNotInBuilderThrowsException() {
BuildpackReference reference = BuildpackReference.of("urn:cnb:builder:example/buildpack1");
assertThatIllegalArgumentException().isThrownBy(() -> BuilderBuildpack.resolve(this.resolverContext, reference))
.withMessageContaining("'urn:cnb:builder:example/buildpack1'")
.withMessageContaining("not found in builder");
}
@Test
void resolveWhenUnqualifiedBuildpackNotInBuilderReturnsNull() {
BuildpackReference reference = BuildpackReference.of("example/buildpack1@1.2.3");
Buildpack buildpack = BuilderBuildpack.resolve(this.resolverContext, reference);
assertThat(buildpack).isNull();
}
private void assertThatNoLayersAreAdded(Buildpack buildpack) throws IOException {
List<Layer> layers = new ArrayList<>();
buildpack.apply(layers::add);
assertThat(layers).isEmpty();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,12 +16,8 @@
package org.springframework.boot.buildpack.platform.build;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
@@ -31,6 +27,7 @@ import org.springframework.boot.buildpack.platform.json.AbstractJsonTests;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.tuple;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
@@ -55,6 +52,14 @@ class BuilderMetadataTests extends AbstractJsonTests {
assertThat(metadata.getCreatedBy().getName()).isEqualTo("Pack CLI");
assertThat(metadata.getCreatedBy().getVersion())
.isEqualTo("v0.9.0 (git sha: d42c384a39f367588f2653f2a99702db910e5ad7)");
assertThat(metadata.getBuildpacks()).extracting(BuildpackMetadata::getId, BuildpackMetadata::getVersion)
.contains(tuple("paketo-buildpacks/java", "4.10.0"))
.contains(tuple("paketo-buildpacks/spring-boot", "3.5.0"))
.contains(tuple("paketo-buildpacks/executable-jar", "3.1.3"))
.contains(tuple("paketo-buildpacks/graalvm", "4.1.0"))
.contains(tuple("paketo-buildpacks/java-native-image", "4.7.0"))
.contains(tuple("paketo-buildpacks/spring-boot-native-image", "2.0.1"))
.contains(tuple("paketo-buildpacks/bellsoft-liberica", "6.2.0"));
}
@Test
@@ -124,9 +129,4 @@ class BuilderMetadataTests extends AbstractJsonTests {
.isEqualTo(metadata.getStack().getRunImage().getImage());
}
private String getContentAsString(String name) {
return new BufferedReader(new InputStreamReader(getContent(name), StandardCharsets.UTF_8)).lines()
.collect(Collectors.joining("\n"));
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,172 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.build;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.junit.jupiter.api.Test;
import org.springframework.boot.buildpack.platform.json.AbstractJsonTests;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link BuildpackCoordinates}.
*
* @author Scott Frederick
* @author Phillip Webb
*/
class BuildpackCoordinatesTests extends AbstractJsonTests {
private final Path archive = Paths.get("/buildpack/path");
@Test
void fromToml() throws IOException {
BuildpackCoordinates coordinates = BuildpackCoordinates
.fromToml(createTomlStream("example/buildpack1", "0.0.1", true, false), this.archive);
assertThat(coordinates.getId()).isEqualTo("example/buildpack1");
assertThat(coordinates.getVersion()).isEqualTo("0.0.1");
}
@Test
void fromTomlWhenMissingDescriptorThrowsException() throws Exception {
ByteArrayInputStream coordinates = new ByteArrayInputStream("".getBytes());
assertThatIllegalArgumentException().isThrownBy(() -> BuildpackCoordinates.fromToml(coordinates, this.archive))
.withMessageContaining("Buildpack descriptor 'buildpack.toml' is required")
.withMessageContaining(this.archive.toString());
}
@Test
void fromTomlWhenMissingIDThrowsException() throws Exception {
InputStream coordinates = createTomlStream(null, null, true, false);
assertThatIllegalArgumentException().isThrownBy(() -> BuildpackCoordinates.fromToml(coordinates, this.archive))
.withMessageContaining("Buildpack descriptor must contain ID")
.withMessageContaining(this.archive.toString());
}
@Test
void fromTomlWhenMissingVersionThrowsException() throws Exception {
InputStream coordinates = createTomlStream("example/buildpack1", null, true, false);
assertThatIllegalArgumentException().isThrownBy(() -> BuildpackCoordinates.fromToml(coordinates, this.archive))
.withMessageContaining("Buildpack descriptor must contain version")
.withMessageContaining(this.archive.toString());
}
@Test
void fromTomlWhenMissingStacksAndOrderThrowsException() throws Exception {
InputStream coordinates = createTomlStream("example/buildpack1", "0.0.1", false, false);
assertThatIllegalArgumentException().isThrownBy(() -> BuildpackCoordinates.fromToml(coordinates, this.archive))
.withMessageContaining("Buildpack descriptor must contain either 'stacks' or 'order'")
.withMessageContaining(this.archive.toString());
}
@Test
void fromTomlWhenContainsBothStacksAndOrderThrowsException() throws Exception {
InputStream coordinates = createTomlStream("example/buildpack1", "0.0.1", true, true);
assertThatIllegalArgumentException().isThrownBy(() -> BuildpackCoordinates.fromToml(coordinates, this.archive))
.withMessageContaining("Buildpack descriptor must not contain both 'stacks' and 'order'")
.withMessageContaining(this.archive.toString());
}
@Test
void fromBuildpackMetadataWhenMetadataIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> BuildpackCoordinates.fromBuildpackMetadata(null))
.withMessage("BuildpackMetadata must not be null");
}
@Test
void fromBuildpackMetadataReturnsCoordinates() throws Exception {
BuildpackMetadata metadata = BuildpackMetadata.fromJson(getContentAsString("buildpack-metadata.json"));
BuildpackCoordinates coordinates = BuildpackCoordinates.fromBuildpackMetadata(metadata);
assertThat(coordinates.getId()).isEqualTo("example/hello-universe");
assertThat(coordinates.getVersion()).isEqualTo("0.0.1");
}
@Test
void ofWhenIdIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> BuildpackCoordinates.of(null, null))
.withMessage("ID must not be empty");
}
@Test
void ofReturnsCoordinates() {
BuildpackCoordinates coordinates = BuildpackCoordinates.of("id", "1");
assertThat(coordinates).hasToString("id@1");
}
@Test
void getIdReturnsId() {
BuildpackCoordinates coordinates = BuildpackCoordinates.of("id", "1");
assertThat(coordinates.getId()).isEqualTo("id");
}
@Test
void getVersionReturnsVersion() {
BuildpackCoordinates coordinates = BuildpackCoordinates.of("id", "1");
assertThat(coordinates.getVersion()).isEqualTo("1");
}
@Test
void getVersionWhenVersionIsNullReturnsNull() {
BuildpackCoordinates coordinates = BuildpackCoordinates.of("id", null);
assertThat(coordinates.getVersion()).isNull();
}
@Test
void toStringReturnsNiceString() {
BuildpackCoordinates coordinates = BuildpackCoordinates.of("id", "1");
assertThat(coordinates).hasToString("id@1");
}
@Test
void equalsAndHashCode() {
BuildpackCoordinates c1a = BuildpackCoordinates.of("id", "1");
BuildpackCoordinates c1b = BuildpackCoordinates.of("id", "1");
BuildpackCoordinates c2 = BuildpackCoordinates.of("id", "2");
assertThat(c1a).isEqualTo(c1a).isEqualTo(c1b).isNotEqualTo(c2);
assertThat(c1a.hashCode()).isEqualTo(c1b.hashCode());
}
private InputStream createTomlStream(String id, String version, boolean includeStacks, boolean includeOrder) {
StringBuilder builder = new StringBuilder();
builder.append("[buildpack]\n");
if (id != null) {
builder.append("id = \"").append(id).append("\"\n");
}
if (version != null) {
builder.append("version = \"").append(version).append("\"\n");
}
builder.append("name = \"Example buildpack\"\n");
builder.append("homepage = \"https://github.com/example/example-buildpack\"\n");
if (includeStacks) {
builder.append("[[stacks]]\n");
builder.append("id = \"io.buildpacks.stacks.bionic\"\n");
}
if (includeOrder) {
builder.append("[[order]]\n");
builder.append("group = [ { id = \"example/buildpack2\", version=\"0.0.2\" } ]\n");
}
return new ByteArrayInputStream(builder.toString().getBytes(StandardCharsets.UTF_8));
}
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.build;
import java.io.IOException;
import java.util.Collections;
import org.junit.jupiter.api.Test;
import org.springframework.boot.buildpack.platform.docker.type.Image;
import org.springframework.boot.buildpack.platform.docker.type.ImageConfig;
import org.springframework.boot.buildpack.platform.json.AbstractJsonTests;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link BuildpackMetadata}.
*
* @author Scott Frederick
*/
class BuildpackMetadataTests extends AbstractJsonTests {
@Test
void fromImageLoadsMetadata() throws IOException {
Image image = Image.of(getContent("buildpack-image.json"));
BuildpackMetadata metadata = BuildpackMetadata.fromImage(image);
assertThat(metadata.getId()).isEqualTo("example/hello-universe");
assertThat(metadata.getVersion()).isEqualTo("0.0.1");
}
@Test
void fromImageWhenImageIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> BuildpackMetadata.fromImage(null))
.withMessage("Image must not be null");
}
@Test
void fromImageWhenImageConfigIsNullThrowsException() {
Image image = mock(Image.class);
assertThatIllegalArgumentException().isThrownBy(() -> BuildpackMetadata.fromImage(image))
.withMessage("ImageConfig must not be null");
}
@Test
void fromImageConfigWhenLabelIsMissingThrowsException() {
Image image = mock(Image.class);
ImageConfig imageConfig = mock(ImageConfig.class);
given(image.getConfig()).willReturn(imageConfig);
given(imageConfig.getLabels()).willReturn(Collections.singletonMap("alpha", "a"));
assertThatIllegalArgumentException().isThrownBy(() -> BuildpackMetadata.fromImage(image))
.withMessage("No 'io.buildpacks.buildpackage.metadata' label found in image config labels 'alpha'");
}
@Test
void fromJsonLoadsMetadata() throws IOException {
BuildpackMetadata metadata = BuildpackMetadata.fromJson(getContentAsString("buildpack-metadata.json"));
assertThat(metadata.getId()).isEqualTo("example/hello-universe");
assertThat(metadata.getVersion()).isEqualTo("0.0.1");
assertThat(metadata.getHomepage()).isEqualTo("https://github.com/example/tree/main/buildpacks/hello-universe");
}
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.build;
import java.nio.file.Paths;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link BuildpackReference}.
*
* @author Phillip Webb
*/
class BuildpackReferenceTests {
@Test
void ofWhenValueIsEmptyThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> BuildpackReference.of(""))
.withMessage("Value must not be empty");
}
@Test
void ofCreatesInstance() {
BuildpackReference reference = BuildpackReference.of("test");
assertThat(reference).isNotNull();
}
@Test
void toStringReturnsValue() {
BuildpackReference reference = BuildpackReference.of("test");
assertThat(reference).hasToString("test");
}
@Test
void equalsAndHashCode() {
BuildpackReference a = BuildpackReference.of("test1");
BuildpackReference b = BuildpackReference.of("test1");
BuildpackReference c = BuildpackReference.of("test2");
assertThat(a).isEqualTo(a).isEqualTo(b).isNotEqualTo(c);
assertThat(a.hashCode()).isEqualTo(b.hashCode());
}
@Test
void hasPrefixWhenPrefixMatchReturnsTrue() {
BuildpackReference reference = BuildpackReference.of("test");
assertThat(reference.hasPrefix("te")).isTrue();
}
@Test
void hasPrefixWhenPrifixMismatchReturnsFalse() {
BuildpackReference reference = BuildpackReference.of("test");
assertThat(reference.hasPrefix("st")).isFalse();
}
@Test
void getSubReferenceWhenPrefixMatchReturnsSubReference() {
BuildpackReference reference = BuildpackReference.of("test");
assertThat(reference.getSubReference("te")).isEqualTo("st");
}
@Test
void getSubReferenceWhenPrefixMismatchReturnsNull() {
BuildpackReference reference = BuildpackReference.of("test");
assertThat(reference.getSubReference("st")).isNull();
}
@Test
void asPathWhenFileUrlReturnsPath() {
BuildpackReference reference = BuildpackReference.of("file:///test.dat");
assertThat(reference.asPath()).isEqualTo(Paths.get("/test.dat"));
}
@Test
void asPathWhenPathReturnsPath() {
BuildpackReference reference = BuildpackReference.of("/test.dat");
assertThat(reference.asPath()).isEqualTo(Paths.get("/test.dat"));
}
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.build;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.buildpack.platform.docker.type.Image;
import org.springframework.boot.buildpack.platform.json.AbstractJsonTests;
import org.springframework.util.FileCopyUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link BuildpackResolvers}.
*
* @author Scott Frederick
*/
class BuildpackResolversTests extends AbstractJsonTests {
private BuildpackResolverContext resolverContext;
@BeforeEach
void setup() throws Exception {
BuilderMetadata metadata = BuilderMetadata.fromJson(getContentAsString("builder-metadata.json"));
this.resolverContext = mock(BuildpackResolverContext.class);
given(this.resolverContext.getBuildpackMetadata()).willReturn(metadata.getBuildpacks());
}
@Test
void resolveAllWithBuilderBuildpackReferenceReturnsExpectedBuildpack() throws IOException {
BuildpackReference reference = BuildpackReference.of("urn:cnb:builder:paketo-buildpacks/spring-boot@3.5.0");
Buildpacks buildpacks = BuildpackResolvers.resolveAll(this.resolverContext, Collections.singleton(reference));
assertThat(buildpacks.getBuildpacks()).hasSize(1);
assertThat(buildpacks.getBuildpacks().get(0)).isInstanceOf(BuilderBuildpack.class);
}
@Test
void resolveAllWithDirectoryBuildpackReferenceReturnsExpectedBuildpack(@TempDir Path temp) throws IOException {
FileCopyUtils.copy(getClass().getResourceAsStream("buildpack.toml"),
Files.newOutputStream(temp.resolve("buildpack.toml")));
BuildpackReference reference = BuildpackReference.of(temp.toAbsolutePath().toString());
Buildpacks buildpacks = BuildpackResolvers.resolveAll(this.resolverContext, Collections.singleton(reference));
assertThat(buildpacks.getBuildpacks()).hasSize(1);
assertThat(buildpacks.getBuildpacks().get(0)).isInstanceOf(DirectoryBuildpack.class);
}
@Test
void resolveAllWithTarGzipBuildpackReferenceReturnsExpectedBuildpack(@TempDir File temp) throws Exception {
TestTarGzip testTarGzip = new TestTarGzip(temp);
Path archive = testTarGzip.createArchive();
BuildpackReference reference = BuildpackReference.of(archive.toString());
Buildpacks buildpacks = BuildpackResolvers.resolveAll(this.resolverContext, Collections.singleton(reference));
assertThat(buildpacks.getBuildpacks()).hasSize(1);
assertThat(buildpacks.getBuildpacks().get(0)).isInstanceOf(TarGzipBuildpack.class);
}
@Test
void resolveAllWithImageBuildpackReferenceReturnsExpectedBuildpack() throws IOException {
Image image = Image.of(getContent("buildpack-image.json"));
BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class);
given(resolverContext.fetchImage(any(), any())).willReturn(image);
BuildpackReference reference = BuildpackReference.of("docker://example/buildpack1:latest");
Buildpacks buildpacks = BuildpackResolvers.resolveAll(resolverContext, Collections.singleton(reference));
assertThat(buildpacks.getBuildpacks()).hasSize(1);
assertThat(buildpacks.getBuildpacks().get(0)).isInstanceOf(ImageBuildpack.class);
}
@Test
void resolveAllWithInvalidLocatorThrowsException() throws IOException {
BuildpackReference reference = BuildpackReference.of("unknown-buildpack@0.0.1");
assertThatIllegalArgumentException()
.isThrownBy(() -> BuildpackResolvers.resolveAll(this.resolverContext, Collections.singleton(reference)))
.withMessageContaining("Invalid buildpack reference")
.withMessageContaining("'unknown-buildpack@0.0.1'");
}
}

View File

@@ -0,0 +1,111 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.build;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.junit.jupiter.api.Test;
import org.springframework.boot.buildpack.platform.docker.type.Layer;
import org.springframework.util.StreamUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link Buildpacks}.
*
* @author Scott Frederick
* @author Phillip Webb
*/
class BuildpacksTests {
@Test
void ofWhenBuildpacksIsNullReturnsEmpty() {
Buildpacks buildpacks = Buildpacks.of(null);
assertThat(buildpacks).isSameAs(Buildpacks.EMPTY);
assertThat(buildpacks.getBuildpacks()).isEmpty();
}
@Test
void ofReturnsBuildpacks() {
List<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();
}
}

View File

@@ -0,0 +1,174 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.build;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.api.io.TempDir;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.tuple;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link DirectoryBuildpack}.
*
* @author Scott Frederick
*/
@DisabledOnOs(OS.WINDOWS)
class DirectoryBuildpackTests {
@TempDir
File temp;
private File buildpackDir;
private BuildpackResolverContext resolverContext;
@BeforeEach
void setUp() {
this.buildpackDir = new File(this.temp, "buildpack");
this.buildpackDir.mkdirs();
this.resolverContext = mock(BuildpackResolverContext.class);
}
@Test
void resolveWhenPath() throws Exception {
writeBuildpackDescriptor();
writeScripts();
BuildpackReference reference = BuildpackReference.of(this.buildpackDir.toString());
Buildpack buildpack = DirectoryBuildpack.resolve(this.resolverContext, reference);
assertThat(buildpack).isNotNull();
assertThat(buildpack.getCoordinates()).hasToString("example/buildpack1@0.0.1");
assertHasExpectedLayers(buildpack);
}
@Test
void resolveWhenFileUrl() throws Exception {
writeBuildpackDescriptor();
writeScripts();
BuildpackReference reference = BuildpackReference.of("file://" + this.buildpackDir.toString());
Buildpack buildpack = DirectoryBuildpack.resolve(this.resolverContext, reference);
assertThat(buildpack).isNotNull();
assertThat(buildpack.getCoordinates()).hasToString("example/buildpack1@0.0.1");
assertHasExpectedLayers(buildpack);
}
@Test
void resolveWhenDirectoryWithoutBuildpackTomlThrowsException() throws Exception {
Files.createDirectories(this.buildpackDir.toPath());
BuildpackReference reference = BuildpackReference.of(this.buildpackDir.toString());
assertThatIllegalArgumentException()
.isThrownBy(() -> DirectoryBuildpack.resolve(this.resolverContext, reference))
.withMessageContaining("Buildpack descriptor 'buildpack.toml' is required")
.withMessageContaining(this.buildpackDir.getAbsolutePath());
}
@Test
void resolveWhenFileReturnsNull() throws Exception {
Path file = Files.createFile(Paths.get(this.buildpackDir.toString(), "test"));
BuildpackReference reference = BuildpackReference.of(file.toString());
Buildpack buildpack = DirectoryBuildpack.resolve(this.resolverContext, reference);
assertThat(buildpack).isNull();
}
@Test
void resolveWhenDirectoryDoesNotExistReturnsNull() {
BuildpackReference reference = BuildpackReference.of("/test/a/missing/buildpack");
Buildpack buildpack = DirectoryBuildpack.resolve(this.resolverContext, reference);
assertThat(buildpack).isNull();
}
@Test
void locateDirectoryAsUrlThatDoesNotExistThrowsException() {
BuildpackReference reference = BuildpackReference.of("file:///test/a/missing/buildpack");
Buildpack buildpack = DirectoryBuildpack.resolve(this.resolverContext, reference);
assertThat(buildpack).isNull();
}
private void assertHasExpectedLayers(Buildpack buildpack) throws IOException {
List<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\"");
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,12 +20,17 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.compress.archivers.ArchiveEntry;
@@ -40,6 +45,7 @@ import org.springframework.boot.buildpack.platform.docker.type.ImageArchive;
import org.springframework.boot.buildpack.platform.docker.type.ImageConfig;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.json.AbstractJsonTests;
import org.springframework.util.FileCopyUtils;
import static org.assertj.core.api.Assertions.assertThat;
@@ -62,7 +68,9 @@ class EphemeralBuilderTests extends AbstractJsonTests {
private Map<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));
}
}

View File

@@ -0,0 +1,138 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.build;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.junit.jupiter.api.Test;
import org.mockito.invocation.InvocationOnMock;
import org.springframework.boot.buildpack.platform.docker.type.Image;
import org.springframework.boot.buildpack.platform.io.IOBiConsumer;
import org.springframework.boot.buildpack.platform.io.TarArchive;
import org.springframework.boot.buildpack.platform.json.AbstractJsonTests;
import org.springframework.util.FileCopyUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link ImageBuildpack}.
*
* @author Scott Frederick
* @author Phillip Webb
*/
class ImageBuildpackTests extends AbstractJsonTests {
@Test
void resolveWhenFullyQualifiedReferenceReturnsBuilder() throws Exception {
Image image = Image.of(getContent("buildpack-image.json"));
BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class);
given(resolverContext.fetchImage(any(), any())).willReturn(image);
willAnswer(this::withMockLayers).given(resolverContext).exportImageLayers(any(), any());
BuildpackReference reference = BuildpackReference.of("docker://example/buildpack1:latest");
Buildpack buildpack = ImageBuildpack.resolve(resolverContext, reference);
assertThat(buildpack.getCoordinates()).hasToString("example/hello-universe@0.0.1");
assertHasExpectedLayers(buildpack);
}
@Test
void resolveWhenUnqualifiedReferenceReturnsBuilder() throws Exception {
Image image = Image.of(getContent("buildpack-image.json"));
BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class);
given(resolverContext.fetchImage(any(), any())).willReturn(image);
willAnswer(this::withMockLayers).given(resolverContext).exportImageLayers(any(), any());
BuildpackReference reference = BuildpackReference.of("example/buildpack1:latest");
Buildpack buildpack = ImageBuildpack.resolve(resolverContext, reference);
assertThat(buildpack.getCoordinates()).hasToString("example/hello-universe@0.0.1");
assertHasExpectedLayers(buildpack);
}
@Test
void resolveWhenWhenImageNotPulledThrowsException() throws Exception {
BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class);
given(resolverContext.fetchImage(any(), any())).willThrow(IOException.class);
BuildpackReference reference = BuildpackReference.of("docker://example/buildpack1:latest");
assertThatIllegalArgumentException().isThrownBy(() -> ImageBuildpack.resolve(resolverContext, reference))
.withMessageContaining("Error pulling buildpack image")
.withMessageContaining("example/buildpack1:latest");
}
@Test
void resolveWhenMissingMetadataLabelThrowsException() throws Exception {
Image image = Image.of(getContent("image.json"));
BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class);
given(resolverContext.fetchImage(any(), any())).willReturn(image);
BuildpackReference reference = BuildpackReference.of("docker://example/buildpack1:latest");
assertThatIllegalArgumentException().isThrownBy(() -> ImageBuildpack.resolve(resolverContext, reference))
.withMessageContaining("No 'io.buildpacks.buildpackage.metadata' label found");
}
@Test
void resolveWhenFullyQualifiedReferenceWithInvalidImageReferenceThrowsException() throws Exception {
BuildpackReference reference = BuildpackReference.of("docker://buildpack@0.0.1");
BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class);
assertThatIllegalArgumentException().isThrownBy(() -> ImageBuildpack.resolve(resolverContext, reference))
.withMessageContaining("Unable to parse image reference \"buildpack@0.0.1\"");
}
@Test
void resolveWhenUnqualifiedReferenceWithInvalidImageReferenceReturnsNull() throws Exception {
BuildpackReference reference = BuildpackReference.of("buildpack@0.0.1");
BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class);
Buildpack buildpack = ImageBuildpack.resolve(resolverContext, reference);
assertThat(buildpack).isNull();
}
private Object withMockLayers(InvocationOnMock invocation) throws Exception {
IOBiConsumer<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");
}
}

View File

@@ -0,0 +1,94 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.build;
import java.io.File;
import java.nio.file.Path;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link TarGzipBuildpack}.
*
* @author Scott Frederick
*/
class TarGzipBuildpackTests {
private File buildpackDir;
private TestTarGzip testTarGzip;
private BuildpackResolverContext resolverContext;
@BeforeEach
void setUp(@TempDir File temp) {
this.buildpackDir = new File(temp, "buildpack");
this.buildpackDir.mkdirs();
this.testTarGzip = new TestTarGzip(this.buildpackDir);
this.resolverContext = mock(BuildpackResolverContext.class);
}
@Test
void resolveWhenFilePathReturnsBuildpack() throws Exception {
Path compressedArchive = this.testTarGzip.createArchive();
BuildpackReference reference = BuildpackReference.of(compressedArchive.toString());
Buildpack buildpack = TarGzipBuildpack.resolve(this.resolverContext, reference);
assertThat(buildpack).isNotNull();
assertThat(buildpack.getCoordinates()).hasToString("example/buildpack1@0.0.1");
this.testTarGzip.assertHasExpectedLayers(buildpack);
}
@Test
void resolveWhenFileUrlReturnsBuildpack() throws Exception {
Path compressedArchive = this.testTarGzip.createArchive();
BuildpackReference reference = BuildpackReference.of("file://" + compressedArchive.toString());
Buildpack buildpack = TarGzipBuildpack.resolve(this.resolverContext, reference);
assertThat(buildpack).isNotNull();
assertThat(buildpack.getCoordinates()).hasToString("example/buildpack1@0.0.1");
this.testTarGzip.assertHasExpectedLayers(buildpack);
}
@Test
void resolveWhenArchiveWithoutDescriptorThrowsException() throws Exception {
Path compressedArchive = this.testTarGzip.createEmptyArchive();
BuildpackReference reference = BuildpackReference.of(compressedArchive.toString());
assertThatIllegalArgumentException().isThrownBy(() -> TarGzipBuildpack.resolve(this.resolverContext, reference))
.withMessageContaining("Buildpack descriptor 'buildpack.toml' is required")
.withMessageContaining(compressedArchive.toString());
}
@Test
void resolveWhenArchiveWithDirectoryReturnsNull() {
BuildpackReference reference = BuildpackReference.of(this.buildpackDir.getAbsolutePath());
Buildpack buildpack = TarGzipBuildpack.resolve(this.resolverContext, reference);
assertThat(buildpack).isNull();
}
@Test
void resolveWhenArchiveThatDoesNotExistReturnsNull() {
BuildpackReference reference = BuildpackReference.of("/test/i/am/missing/buildpack.tar");
Buildpack buildpack = TarGzipBuildpack.resolve(this.resolverContext, reference);
assertThat(buildpack).isNull();
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.build;
import java.io.IOException;
import org.springframework.boot.buildpack.platform.docker.type.Layer;
import org.springframework.boot.buildpack.platform.io.Content;
import org.springframework.boot.buildpack.platform.io.IOConsumer;
import org.springframework.boot.buildpack.platform.io.Layout;
import org.springframework.boot.buildpack.platform.io.Owner;
/**
* A test {@link Buildpack}.
*
* @author Scott Frederick
* @author Phillip Webb
*/
class TestBuildpack implements Buildpack {
private final BuildpackCoordinates coordinates;
TestBuildpack(String id, String version) {
this.coordinates = BuildpackCoordinates.of(id, version);
}
@Override
public BuildpackCoordinates getCoordinates() {
return this.coordinates;
}
@Override
public void apply(IOConsumer<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]"));
}
}

View File

@@ -0,0 +1,122 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.build;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
import org.apache.commons.compress.utils.IOUtils;
import org.springframework.util.FileCopyUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Utility to create test tgz files.
*
* @author Scott Frederick
*/
class TestTarGzip {
private final File buildpackDir;
TestTarGzip(File buildpackDir) {
this.buildpackDir = buildpackDir;
}
Path createArchive() throws Exception {
return createArchive(true);
}
Path createEmptyArchive() throws Exception {
return createArchive(false);
}
private Path createArchive(boolean addContent) throws Exception {
Path path = Paths.get(this.buildpackDir.getAbsolutePath(), "buildpack.tar");
Path archive = Files.createFile(path);
if (addContent) {
writeBuildpackContentToArchive(archive);
}
return compressBuildpackArchive(archive);
}
private Path compressBuildpackArchive(Path archive) throws Exception {
Path tgzPath = Paths.get(this.buildpackDir.getAbsolutePath(), "buildpack.tgz");
FileCopyUtils.copy(Files.newInputStream(archive),
new GzipCompressorOutputStream(Files.newOutputStream(tgzPath)));
return tgzPath;
}
private void writeBuildpackContentToArchive(Path archive) throws Exception {
StringBuilder buildpackToml = new StringBuilder();
buildpackToml.append("[buildpack]\n");
buildpackToml.append("id = \"example/buildpack1\"\n");
buildpackToml.append("version = \"0.0.1\"\n");
buildpackToml.append("name = \"Example buildpack\"\n");
buildpackToml.append("homepage = \"https://github.com/example/example-buildpack\"\n");
buildpackToml.append("[[stacks]]\n");
buildpackToml.append("id = \"io.buildpacks.stacks.bionic\"\n");
String detectScript = "#!/usr/bin/env bash\n" + "echo \"---> detect\"\n";
String buildScript = "#!/usr/bin/env bash\n" + "echo \"---> build\"\n";
try (TarArchiveOutputStream tar = new TarArchiveOutputStream(Files.newOutputStream(archive))) {
writeEntry(tar, "buildpack.toml", buildpackToml.toString());
writeEntry(tar, "bin/detect", detectScript);
writeEntry(tar, "bin/build", buildScript);
tar.finish();
}
}
private void writeEntry(TarArchiveOutputStream tar, String entryName, String content) throws IOException {
TarArchiveEntry entry = new TarArchiveEntry(entryName);
entry.setSize(content.length());
tar.putArchiveEntry(entry);
IOUtils.copy(new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)), tar);
tar.closeArchiveEntry();
}
void assertHasExpectedLayers(Buildpack buildpack) throws IOException {
List<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();
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,11 +16,14 @@
package org.springframework.boot.buildpack.platform.docker;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
@@ -48,6 +51,8 @@ import org.springframework.boot.buildpack.platform.io.Content;
import org.springframework.boot.buildpack.platform.io.IOConsumer;
import org.springframework.boot.buildpack.platform.io.Owner;
import org.springframework.boot.buildpack.platform.io.TarArchive;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -304,6 +309,45 @@ class DockerApiTests {
assertThat(image.getLayers()).hasSize(46);
}
@Test
void exportLayersWhenReferenceIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.api.exportLayers(null, (name, archive) -> {
})).withMessage("Reference must not be null");
}
@Test
void exportLayersWhenExportsIsNullThrowsException() {
ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base");
assertThatIllegalArgumentException().isThrownBy(() -> this.api.exportLayers(reference, null))
.withMessage("Exports must not be null");
}
@Test
void exportLayersExportsLayerTars() throws Exception {
ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base");
URI exportUri = new URI(IMAGES_URL + "/gcr.io/paketo-buildpacks/builder:base/get");
given(DockerApiTests.this.http.get(exportUri)).willReturn(responseOf("export.tar"));
MultiValueMap<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

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,6 +25,7 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ProgressUpdateEvent}.
*
* @param <E> The event type
* @author Phillip Webb
* @author Scott Frederick
*/

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.io;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.Collections;
import java.util.Set;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link FilePermissions}.
*
* @author Scott Frederick
*/
class FilePermissionsTests {
@Test
void posixPermissionsToUmask() {
Set<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));
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -31,6 +31,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* Tests for {@link TarLayoutWriter}.
*
* @author Phillip Webb
* @author Scott Frederick
*/
class TarLayoutWriterTests {
@@ -39,7 +40,7 @@ class TarLayoutWriterTests {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try (TarLayoutWriter writer = new TarLayoutWriter(outputStream)) {
writer.directory("/foo", Owner.ROOT);
writer.file("/foo/bar.txt", Owner.of(1, 1), Content.of("test"));
writer.file("/foo/bar.txt", Owner.of(1, 1), 0777, Content.of("test"));
}
try (TarArchiveInputStream tarInputStream = new TarArchiveInputStream(
new ByteArrayInputStream(outputStream.toByteArray()))) {
@@ -54,7 +55,7 @@ class TarLayoutWriterTests {
assertThat(directoryEntry.getLongGroupId()).isEqualTo(0);
assertThat(directoryEntry.getModTime()).isEqualTo(new Date(TarLayoutWriter.NORMALIZED_MOD_TIME));
assertThat(fileEntry.getName()).isEqualTo("/foo/bar.txt");
assertThat(fileEntry.getMode()).isEqualTo(0644);
assertThat(fileEntry.getMode()).isEqualTo(0777);
assertThat(fileEntry.getLongUserId()).isEqualTo(1);
assertThat(fileEntry.getLongGroupId()).isEqualTo(1);
assertThat(fileEntry.getModTime()).isEqualTo(new Date(TarLayoutWriter.NORMALIZED_MOD_TIME));

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,7 +16,11 @@
package org.springframework.boot.buildpack.platform.json;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -26,6 +30,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* Abstract base class for JSON based tests.
*
* @author Phillip Webb
* @author Scott Frederick
*/
public abstract class AbstractJsonTests {
@@ -39,4 +44,9 @@ public abstract class AbstractJsonTests {
return result;
}
protected final String getContentAsString(String name) {
return new BufferedReader(new InputStreamReader(getContent(name), StandardCharsets.UTF_8)).lines()
.collect(Collectors.joining("\n"));
}
}

View File

@@ -2,124 +2,174 @@
"description": "Ubuntu bionic base image with buildpacks for Java, NodeJS and Golang",
"buildpacks": [
{
"id": "org.cloudfoundry.googlestackdriver",
"version": "v1.1.11"
"id": "paketo-buildpacks/dotnet-core",
"version": "0.0.9",
"homepage": "https://github.com/paketo-buildpacks/dotnet-core"
},
{
"id": "org.cloudfoundry.springboot",
"version": "v1.2.13"
"id": "paketo-buildpacks/dotnet-core-runtime",
"version": "0.0.201",
"homepage": "https://github.com/paketo-buildpacks/dotnet-core-runtime"
},
{
"id": "org.cloudfoundry.debug",
"version": "v1.2.11"
"id": "paketo-buildpacks/dotnet-core-sdk",
"version": "0.0.196",
"homepage": "https://github.com/paketo-buildpacks/dotnet-core-sdk"
},
{
"id": "org.cloudfoundry.tomcat",
"version": "v1.3.18"
"id": "paketo-buildpacks/dotnet-execute",
"version": "0.0.180",
"homepage": "https://github.com/paketo-buildpacks/dotnet-execute"
},
{
"id": "org.cloudfoundry.go",
"version": "v0.0.4"
"id": "paketo-buildpacks/dotnet-publish",
"version": "0.0.121",
"homepage": "https://github.com/paketo-buildpacks/dotnet-publish"
},
{
"id": "org.cloudfoundry.openjdk",
"version": "v1.2.14"
"id": "paketo-buildpacks/dotnet-core-aspnet",
"version": "0.0.196",
"homepage": "https://github.com/paketo-buildpacks/dotnet-core-aspnet"
},
{
"id": "org.cloudfoundry.buildsystem",
"version": "v1.2.15"
"id": "paketo-buildpacks/java-native-image",
"version": "4.7.0",
"homepage": "https://github.com/paketo-buildpacks/java-native-image"
},
{
"id": "org.cloudfoundry.jvmapplication",
"version": "v1.1.12"
"id": "paketo-buildpacks/spring-boot",
"version": "3.5.0",
"homepage": "https://github.com/paketo-buildpacks/spring-boot"
},
{
"id": "org.cloudfoundry.springautoreconfiguration",
"version": "v1.1.11"
"id": "paketo-buildpacks/executable-jar",
"version": "3.1.3",
"homepage": "https://github.com/paketo-buildpacks/executable-jar"
},
{
"id": "org.cloudfoundry.archiveexpanding",
"version": "v1.0.102"
"id": "paketo-buildpacks/graalvm",
"version": "4.1.0",
"homepage": "https://github.com/paketo-buildpacks/graalvm"
},
{
"id": "org.cloudfoundry.jmx",
"version": "v1.1.12"
"id": "paketo-buildpacks/gradle",
"version": "3.5.0",
"homepage": "https://github.com/paketo-buildpacks/gradle"
},
{
"id": "org.cloudfoundry.nodejs",
"version": "v2.0.8"
"id": "paketo-buildpacks/leiningen",
"version": "1.2.1",
"homepage": "https://github.com/paketo-buildpacks/leiningen"
},
{
"id": "org.cloudfoundry.jdbc",
"version": "v1.1.14"
"id": "paketo-buildpacks/procfile",
"version": "3.0.0",
"homepage": "https://github.com/paketo-buildpacks/procfile"
},
{
"id": "org.cloudfoundry.procfile",
"version": "v1.1.12"
"id": "paketo-buildpacks/sbt",
"version": "3.6.0",
"homepage": "https://github.com/paketo-buildpacks/sbt"
},
{
"id": "org.cloudfoundry.dotnet-core",
"version": "v0.0.6"
"id": "paketo-buildpacks/spring-boot-native-image",
"version": "2.0.1",
"homepage": "https://github.com/paketo-buildpacks/spring-boot-native-image"
},
{
"id": "org.cloudfoundry.azureapplicationinsights",
"version": "v1.1.12"
"id": "paketo-buildpacks/environment-variables",
"version": "2.1.2",
"homepage": "https://github.com/paketo-buildpacks/environment-variables"
},
{
"id": "org.cloudfoundry.distzip",
"version": "v1.1.12"
"id": "paketo-buildpacks/image-labels",
"version": "2.0.7",
"homepage": "https://github.com/paketo-buildpacks/image-labels"
},
{
"id": "org.cloudfoundry.dep",
"version": "0.0.101"
"id": "paketo-buildpacks/maven",
"version": "3.2.1",
"homepage": "https://github.com/paketo-buildpacks/maven"
},
{
"id": "org.cloudfoundry.go-compiler",
"version": "0.0.105"
"id": "paketo-buildpacks/java",
"version": "4.10.0",
"homepage": "https://github.com/paketo-buildpacks/java"
},
{
"id": "org.cloudfoundry.go-mod",
"version": "0.0.89"
"id": "paketo-buildpacks/ca-certificates",
"version": "1.0.1",
"homepage": "https://github.com/paketo-buildpacks/ca-certificates"
},
{
"id": "org.cloudfoundry.node-engine",
"version": "0.0.163"
"id": "paketo-buildpacks/environment-variables",
"version": "2.1.2",
"homepage": "https://github.com/paketo-buildpacks/environment-variables"
},
{
"id": "org.cloudfoundry.npm",
"version": "0.1.3"
"id": "paketo-buildpacks/executable-jar",
"version": "3.1.3",
"homepage": "https://github.com/paketo-buildpacks/executable-jar"
},
{
"id": "org.cloudfoundry.yarn-install",
"version": "0.1.10"
"id": "paketo-buildpacks/procfile",
"version": "3.0.0",
"homepage": "https://github.com/paketo-buildpacks/procfile"
},
{
"id": "org.cloudfoundry.dotnet-core-aspnet",
"version": "0.0.118"
"id": "paketo-buildpacks/apache-tomcat",
"version": "3.2.0",
"homepage": "https://github.com/paketo-buildpacks/apache-tomcat"
},
{
"id": "org.cloudfoundry.dotnet-core-build",
"version": "0.0.68"
"id": "paketo-buildpacks/gradle",
"version": "3.5.0",
"homepage": "https://github.com/paketo-buildpacks/gradle"
},
{
"id": "org.cloudfoundry.dotnet-core-conf",
"version": "0.0.115"
"id": "paketo-buildpacks/maven",
"version": "3.2.1",
"homepage": "https://github.com/paketo-buildpacks/maven"
},
{
"id": "org.cloudfoundry.dotnet-core-runtime",
"version": "0.0.127"
"id": "paketo-buildpacks/sbt",
"version": "3.6.0",
"homepage": "https://github.com/paketo-buildpacks/sbt"
},
{
"id": "org.cloudfoundry.dotnet-core-sdk",
"version": "0.0.122"
"id": "paketo-buildpacks/bellsoft-liberica",
"version": "6.2.0",
"homepage": "https://github.com/paketo-buildpacks/bellsoft-liberica"
},
{
"id": "org.cloudfoundry.icu",
"version": "0.0.43"
"id": "paketo-buildpacks/image-labels",
"version": "2.0.7",
"homepage": "https://github.com/paketo-buildpacks/image-labels"
},
{
"id": "org.cloudfoundry.node-engine",
"version": "0.0.158"
"id": "paketo-buildpacks/debug",
"version": "2.1.4",
"homepage": "https://github.com/paketo-buildpacks/debug"
},
{
"id": "paketo-buildpacks/dist-zip",
"version": "2.2.2",
"homepage": "https://github.com/paketo-buildpacks/dist-zip"
},
{
"id": "paketo-buildpacks/spring-boot",
"version": "3.5.0",
"homepage": "https://github.com/paketo-buildpacks/spring-boot"
},
{
"id": "paketo-buildpacks/jmx",
"version": "2.1.4",
"homepage": "https://github.com/paketo-buildpacks/jmx"
},
{
"id": "paketo-buildpacks/leiningen",
"version": "1.2.1",
"homepage": "https://github.com/paketo-buildpacks/leiningen"
}
],
"stack": {
@@ -139,4 +189,4 @@
"name": "Pack CLI",
"version": "v0.9.0 (git sha: d42c384a39f367588f2653f2a99702db910e5ad7)"
}
}
}

View File

@@ -0,0 +1,78 @@
{
"Id": "sha256:a266647e285b52403b556adc963f1809556aa999f2f694e8dc54098c570ee55a",
"RepoTags": [
"example/hello-universe:latest"
],
"RepoDigests": [],
"Parent": "",
"Comment": "",
"Created": "1980-01-01T00:00:01Z",
"Container": "",
"ContainerConfig": {
"Hostname": "",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": null,
"Cmd": null,
"Image": "",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": null
},
"DockerVersion": "",
"Author": "",
"Config": {
"Hostname": "",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": null,
"Cmd": null,
"Image": "",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": {
"io.buildpacks.buildpackage.metadata": "{\"id\":\"example/hello-universe\",\"version\":\"0.0.1\",\"homepage\":\"https://github.com/buildpacks/example/tree/main/buildpacks/hello-universe\",\"stacks\":[{\"id\":\"io.buildpacks.example.stacks.alpine\"},{\"id\":\"io.buildpacks.stacks.bionic\"}]}",
"io.buildpacks.buildpack.layers": "{\"example/hello-moon\":{\"0.0.3\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.alpine\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:4bfdc8714aee68da6662c43bc28d3b41202c88e915641c356523dabe729814c2\",\"homepage\":\"https://github.com/example/tree/main/buildpacks/hello-moon\"}},\"example/hello-universe\":{\"0.0.1\":{\"api\":\"0.2\",\"order\":[{\"group\":[{\"id\":\"example/hello-world\",\"version\":\"0.0.2\"},{\"id\":\"example/hello-moon\",\"version\":\"0.0.2\"}]}],\"layerDiffID\":\"sha256:739b4e8f3caae7237584a1bfe029ebdb05403752b1a60a4f9be991b1d51dbb69\",\"homepage\":\"https://github.com/example/tree/main/buildpacks/hello-universe\"}},\"example/hello-world\":{\"0.0.2\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.alpine\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:f752fe099c846e501bdc991d1a22f98c055ddc62f01cfc0495fff2c69f8eb940\",\"homepage\":\"https://github.com/example/tree/main/buildpacks/hello-world\"}}}"
}
},
"Architecture": "amd64",
"Os": "linux",
"Size": 4654,
"VirtualSize": 4654,
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/cbf39b4508463beeb1d0a553c3e2baa84b8cd8dbc95681aaecc243e3ca77bcf4/diff:/var/lib/docker/overlay2/15e3d01b65c962b50a3da1b6663b8196284fb3c7e7f8497f2c1a0a736d0ec237/diff",
"MergedDir": "/var/lib/docker/overlay2/1425ea68b0daff01bcc32e55e09eeeada2318d7dd1dc4e184711359da8425bb7/merged",
"UpperDir": "/var/lib/docker/overlay2/1425ea68b0daff01bcc32e55e09eeeada2318d7dd1dc4e184711359da8425bb7/diff",
"WorkDir": "/var/lib/docker/overlay2/1425ea68b0daff01bcc32e55e09eeeada2318d7dd1dc4e184711359da8425bb7/work"
},
"Name": "overlay2"
},
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:4bfdc8714aee68da6662c43bc28d3b41202c88e915641c356523dabe729814c2",
"sha256:f752fe099c846e501bdc991d1a22f98c055ddc62f01cfc0495fff2c69f8eb940",
"sha256:739b4e8f3caae7237584a1bfe029ebdb05403752b1a60a4f9be991b1d51dbb69"
]
},
"Metadata": {
"LastTagTime": "2021-01-27T22:56:06.4599859Z"
}
}

View File

@@ -0,0 +1,13 @@
{
"id": "example/hello-universe",
"version": "0.0.1",
"homepage": "https://github.com/example/tree/main/buildpacks/hello-universe",
"stacks": [
{
"id": "io.buildpacks.stacks.alpine"
},
{
"id": "io.buildpacks.stacks.bionic"
}
]
}

View File

@@ -0,0 +1,8 @@
[buildpack]
id = "test";
version = "1.0.0"
name = "Example buildpack"
homepage = "https://github.com/example/example-buildpack"
[[stacks]]
id = "io.buildpacks.stacks.bionic"

View File

@@ -0,0 +1,15 @@
[[order]]
group = [
{ id = "example/buildpack1", version = "0.0.1" }
]
[[order]]
group = [
{ id = "example/buildpack2", version = "0.0.2" }
]
[[order]]
group = [
{ id = "example/buildpack3", version = "0.0.3" }
]

View File

@@ -0,0 +1,15 @@
[[order]]
group = [
{ id = "example/buildpack1" }
]
[[order]]
group = [
{ id = "example/buildpack2" }
]
[[order]]
group = [
{ id = "example/buildpack3" }
]