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:
parent
d8fe9de682
commit
f54f784f80
|
@ -13,6 +13,7 @@ dependencies {
|
|||
api("org.apache.commons:commons-compress:1.19")
|
||||
api("org.apache.httpcomponents:httpclient")
|
||||
api("org.springframework:spring-core")
|
||||
api("org.tomlj:tomlj:1.0.0")
|
||||
|
||||
testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support"))
|
||||
testImplementation("com.jayway.jsonpath:json-path")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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 + "'");
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
|
@ -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'");
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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\"");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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]"));
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
File diff suppressed because one or more lines are too long
Binary file not shown.
|
@ -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" }
|
||||
]
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
[[order]]
|
||||
group = [
|
||||
{ id = "example/buildpack1" }
|
||||
]
|
||||
|
||||
[[order]]
|
||||
group = [
|
||||
{ id = "example/buildpack2" }
|
||||
]
|
||||
|
||||
[[order]]
|
||||
group = [
|
||||
{ id = "example/buildpack3" }
|
||||
]
|
||||
|
Binary file not shown.
|
@ -130,6 +130,18 @@ Acceptable values are `ALWAYS`, `NEVER`, and `IF_NOT_PRESENT`.
|
|||
| Environment variables that should be passed to the builder.
|
||||
|
|
||||
|
||||
| `buildpacks`
|
||||
|
|
||||
a|Buildpacks that the builder should use when building the image.
|
||||
Only the specified buildpacks will be used, overriding the default buildpacks included in the builder.
|
||||
Buildpack references must be in one of the following forms:
|
||||
|
||||
* Buildpack in the builder - [urn:cnb:builder:]<buildpack id>[@<version>]
|
||||
* Buildpack in a directory on the file system - [file://]<path>
|
||||
* Buildpack in a gzipped tar (.tgz) file on the file system - [file://]<path>/<file name>
|
||||
* Buildpack in an OCI image - [docker://]<host>/<repo>[:<tag>][@<digest>]
|
||||
| None, indicating the builder should use the buildpacks included in it.
|
||||
|
||||
| `cleanCache`
|
||||
| `--cleanCache`
|
||||
| Whether to clean the cache before building.
|
||||
|
@ -249,6 +261,58 @@ The image name can be specified on the command line as well, as shown in this ex
|
|||
$ gradle bootBuildImage --imageName=example.com/library/my-app:v1
|
||||
----
|
||||
|
||||
|
||||
|
||||
[[build-image-example-buildpacks]]
|
||||
==== Buildpacks
|
||||
By default, the builder will use buildpacks included in the builder image and apply them in a pre-defined order.
|
||||
An alternative set of buildpacks can be provided to apply buildpacks that are not included in the builder, or to change the order of included buildpacks.
|
||||
When one or more buildpacks are provided, only the specified buildpacks will be applied.
|
||||
|
||||
The following example instructs the builder to use a custom buildpack packaged in a `.tgz` file, followed by a buildpack included in the builder.
|
||||
|
||||
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
|
||||
.Groovy
|
||||
----
|
||||
include::../gradle/packaging/boot-build-image-buildpacks.gradle[tags=buildpacks]
|
||||
----
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
include::../gradle/packaging/boot-build-image-buildpacks.gradle.kts[tags=buildpacks]
|
||||
----
|
||||
|
||||
Buildpacks can be specified in any of the forms shown below.
|
||||
|
||||
A buildpack located in a CNB Builder (version may be omitted if there is only one buildpack in the builder matching the `buildpack-id`):
|
||||
|
||||
* `urn:cnb:builder:buildpack-id`
|
||||
* `urn:cnb:builder:buildpack-id@0.0.1`
|
||||
* `buildpack-id`
|
||||
* `buildpack-id@0.0.1`
|
||||
|
||||
A path to a directory containing buildpack content (not supported on Windows):
|
||||
|
||||
* `\file:///path/to/buildpack/`
|
||||
* `/path/to/buildpack/`BootBuildImageIntegrationTests
|
||||
|
||||
A path to a gzipped tar file containing buildpack content:
|
||||
|
||||
* `\file:///path/to/buildpack.tgz`
|
||||
* `/path/to/buildpack.tgz`
|
||||
|
||||
An OCI image containing a https://buildpacks.io/docs/buildpack-author-guide/package-a-buildpack/[packaged buildpack]:
|
||||
|
||||
* `docker://example/buildpack`
|
||||
* `docker:///example/buildpack:latest`
|
||||
* `docker:///example/buildpack@sha256:45b23dee08...`
|
||||
* `example/buildpack`
|
||||
* `example/buildpack:latest`
|
||||
* `example/buildpack@sha256:45b23dee08...`
|
||||
|
||||
|
||||
|
||||
[[build-image-example-publish]]
|
||||
==== Image Publishing
|
||||
The generated image can be published to a Docker registry by enabling a `publish` option and configuring authentication for the registry using `docker.publishRegistry` properties.
|
||||
|
@ -272,6 +336,8 @@ The publish option can be specified on the command line as well, as shown in thi
|
|||
$ gradle bootBuildImage --imageName=docker.example.com/library/my-app:v1 --publishImage
|
||||
----
|
||||
|
||||
|
||||
|
||||
[[build-image-example-docker]]
|
||||
==== Docker Configuration
|
||||
If you need the plugin to communicate with the Docker daemon using a remote connection instead of the default local connection, the connection details can be provided using `docker` properties as shown in the following example:
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '{gradle-project-version}'
|
||||
}
|
||||
|
||||
// tag::buildpacks[]
|
||||
bootBuildImage {
|
||||
buildpacks = ["file:///path/to/example-buildpack.tgz", "urn:cnb:builder:paketo-buildpacks/java"]
|
||||
}
|
||||
// end::buildpacks[]
|
||||
|
||||
task bootBuildImageBuildpacks {
|
||||
doFirst {
|
||||
bootBuildImage.buildpacks.each { reference -> println "$reference" }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import org.springframework.boot.gradle.tasks.bundling.BootBuildImage
|
||||
|
||||
plugins {
|
||||
java
|
||||
id("org.springframework.boot") version "{gradle-project-version}"
|
||||
}
|
||||
|
||||
// tag::buildpacks[]
|
||||
tasks.getByName<BootBuildImage>("bootBuildImage") {
|
||||
buildpacks = listOf("file:///path/to/example-buildpack.tgz", "urn:cnb:builder:paketo-buildpacks/java")
|
||||
}
|
||||
// end::buildpacks[]
|
||||
|
||||
tasks.register("bootBuildImageBuildpacks") {
|
||||
doFirst {
|
||||
for((reference) in tasks.getByName<BootBuildImage>("bootBuildImage").buildpacks) {
|
||||
print(reference)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -18,7 +18,9 @@ package org.springframework.boot.gradle.tasks.bundling;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import groovy.lang.Closure;
|
||||
import org.gradle.api.Action;
|
||||
|
@ -28,6 +30,7 @@ import org.gradle.api.JavaVersion;
|
|||
import org.gradle.api.Project;
|
||||
import org.gradle.api.Task;
|
||||
import org.gradle.api.file.RegularFileProperty;
|
||||
import org.gradle.api.provider.ListProperty;
|
||||
import org.gradle.api.provider.Property;
|
||||
import org.gradle.api.tasks.Input;
|
||||
import org.gradle.api.tasks.Nested;
|
||||
|
@ -38,6 +41,7 @@ import org.gradle.util.ConfigureUtil;
|
|||
|
||||
import org.springframework.boot.buildpack.platform.build.BuildRequest;
|
||||
import org.springframework.boot.buildpack.platform.build.Builder;
|
||||
import org.springframework.boot.buildpack.platform.build.BuildpackReference;
|
||||
import org.springframework.boot.buildpack.platform.build.Creator;
|
||||
import org.springframework.boot.buildpack.platform.build.PullPolicy;
|
||||
import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException;
|
||||
|
@ -83,6 +87,8 @@ public class BootBuildImage extends DefaultTask {
|
|||
|
||||
private boolean publish;
|
||||
|
||||
private ListProperty<String> buildpacks;
|
||||
|
||||
private DockerSpec docker = new DockerSpec();
|
||||
|
||||
public BootBuildImage() {
|
||||
|
@ -92,6 +98,7 @@ public class BootBuildImage extends DefaultTask {
|
|||
this.projectVersion = getProject().getObjects().property(String.class);
|
||||
Project project = getProject();
|
||||
this.projectVersion.set(getProject().provider(() -> project.getVersion().toString()));
|
||||
this.buildpacks = getProject().getObjects().listProperty(String.class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -283,6 +290,40 @@ public class BootBuildImage extends DefaultTask {
|
|||
this.publish = publish;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the buildpacks that will be used when building the image.
|
||||
* @return the buildpacks
|
||||
*/
|
||||
@Input
|
||||
@Optional
|
||||
public List<String> getBuildpacks() {
|
||||
return this.buildpacks.getOrNull();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the buildpacks that will be used when building the image.
|
||||
* @param buildpacks the buildpacks
|
||||
*/
|
||||
public void setBuildpacks(List<String> buildpacks) {
|
||||
this.buildpacks.set(buildpacks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an entry to the buildpacks that will be used when building the image.
|
||||
* @param buildpack the buildpack reference
|
||||
*/
|
||||
public void buildpack(String buildpack) {
|
||||
this.buildpacks.add(buildpack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds entries to the environment that will be used when building the image.
|
||||
* @param buildpacks the buildpack references
|
||||
*/
|
||||
public void buildpacks(List<String> buildpacks) {
|
||||
this.buildpacks.addAll(buildpacks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Docker configuration the builder will use.
|
||||
* @return docker configuration.
|
||||
|
@ -316,8 +357,8 @@ public class BootBuildImage extends DefaultTask {
|
|||
if (!this.jar.isPresent()) {
|
||||
throw new GradleException("Executable jar file required for building image");
|
||||
}
|
||||
Builder builder = new Builder(this.docker.asDockerConfiguration());
|
||||
BuildRequest request = createRequest();
|
||||
Builder builder = new Builder(this.docker.asDockerConfiguration());
|
||||
builder.build(request);
|
||||
}
|
||||
|
||||
|
@ -346,6 +387,7 @@ public class BootBuildImage extends DefaultTask {
|
|||
request = request.withVerboseLogging(this.verboseLogging);
|
||||
request = customizePullPolicy(request);
|
||||
request = customizePublish(request);
|
||||
request = customizeBuildpacks(request);
|
||||
return request;
|
||||
}
|
||||
|
||||
|
@ -398,6 +440,14 @@ public class BootBuildImage extends DefaultTask {
|
|||
return request;
|
||||
}
|
||||
|
||||
private BuildRequest customizeBuildpacks(BuildRequest request) {
|
||||
List<String> buildpacks = this.buildpacks.getOrNull();
|
||||
if (buildpacks != null && !buildpacks.isEmpty()) {
|
||||
return request.withBuildpacks(buildpacks.stream().map(BuildpackReference::of).collect(Collectors.toList()));
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
private String translateTargetJavaVersion() {
|
||||
return this.targetJavaVersion.get().getMajorVersion() + ".*";
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -23,11 +23,21 @@ import java.io.PrintWriter;
|
|||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.attribute.FileAttribute;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.nio.file.attribute.PosixFilePermissions;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
|
||||
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
|
||||
import org.apache.commons.compress.utils.IOUtils;
|
||||
import org.gradle.testkit.runner.BuildResult;
|
||||
import org.gradle.testkit.runner.TaskOutcome;
|
||||
import org.junit.jupiter.api.TestTemplate;
|
||||
import org.junit.jupiter.api.condition.DisabledOnOs;
|
||||
import org.junit.jupiter.api.condition.OS;
|
||||
import org.testcontainers.containers.GenericContainer;
|
||||
import org.testcontainers.containers.wait.strategy.Wait;
|
||||
|
||||
|
@ -145,7 +155,72 @@ class BootBuildImageIntegrationTests {
|
|||
}
|
||||
|
||||
@TestTemplate
|
||||
void failsWithLaunchScript() {
|
||||
void buildsImageWithBuildpackFromBuilder() throws IOException {
|
||||
writeMainClass();
|
||||
writeLongNameResource();
|
||||
BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT");
|
||||
String projectName = this.gradleBuild.getProjectDir().getName();
|
||||
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
|
||||
assertThat(result.getOutput()).contains("docker.io/library/" + projectName);
|
||||
ImageReference imageReference = ImageReference.of(ImageName.of(projectName));
|
||||
try (GenericContainer<?> container = new GenericContainer<>(imageReference.toString())) {
|
||||
container.waitingFor(Wait.forLogMessage("Launched\\n", 1)).start();
|
||||
}
|
||||
finally {
|
||||
new DockerApi().image().remove(imageReference, false);
|
||||
}
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
@DisabledOnOs(OS.WINDOWS)
|
||||
void buildsImageWithBuildpackFromDirectory() throws IOException {
|
||||
writeMainClass();
|
||||
writeLongNameResource();
|
||||
writeBuildpackContent();
|
||||
BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT");
|
||||
String projectName = this.gradleBuild.getProjectDir().getName();
|
||||
ImageReference imageReference = ImageReference.of(ImageName.of(projectName));
|
||||
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
|
||||
assertThat(result.getOutput()).contains("docker.io/library/" + projectName);
|
||||
assertThat(result.getOutput()).contains("---> Hello World buildpack");
|
||||
new DockerApi().image().remove(imageReference, false);
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
@DisabledOnOs(OS.WINDOWS)
|
||||
void buildsImageWithBuildpackFromTarGzip() throws IOException {
|
||||
writeMainClass();
|
||||
writeLongNameResource();
|
||||
writeBuildpackContent();
|
||||
tarGzipBuildpackContent();
|
||||
BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT");
|
||||
String projectName = this.gradleBuild.getProjectDir().getName();
|
||||
ImageReference imageReference = ImageReference.of(ImageName.of(projectName));
|
||||
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
|
||||
assertThat(result.getOutput()).contains("docker.io/library/" + projectName);
|
||||
assertThat(result.getOutput()).contains("---> Hello World buildpack");
|
||||
new DockerApi().image().remove(imageReference, false);
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void buildsImageWithBuildpackFromImage() throws IOException {
|
||||
writeMainClass();
|
||||
writeLongNameResource();
|
||||
BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT");
|
||||
String projectName = this.gradleBuild.getProjectDir().getName();
|
||||
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
|
||||
assertThat(result.getOutput()).contains("docker.io/library/" + projectName);
|
||||
ImageReference imageReference = ImageReference.of(ImageName.of(projectName));
|
||||
try (GenericContainer<?> container = new GenericContainer<>(imageReference.toString())) {
|
||||
container.waitingFor(Wait.forLogMessage("Launched\\n", 1)).start();
|
||||
}
|
||||
finally {
|
||||
new DockerApi().image().remove(imageReference, false);
|
||||
}
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void failsWithLaunchScript() throws IOException {
|
||||
writeMainClass();
|
||||
writeLongNameResource();
|
||||
BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage");
|
||||
|
@ -154,7 +229,7 @@ class BootBuildImageIntegrationTests {
|
|||
}
|
||||
|
||||
@TestTemplate
|
||||
void failsWithBuilderError() {
|
||||
void failsWithBuilderError() throws IOException {
|
||||
writeMainClass();
|
||||
writeLongNameResource();
|
||||
BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT");
|
||||
|
@ -163,7 +238,7 @@ class BootBuildImageIntegrationTests {
|
|||
}
|
||||
|
||||
@TestTemplate
|
||||
void failsWithInvalidImageName() {
|
||||
void failsWithInvalidImageName() throws IOException {
|
||||
writeMainClass();
|
||||
writeLongNameResource();
|
||||
BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage", "--imageName=example/Invalid-Image-Name");
|
||||
|
@ -173,7 +248,7 @@ class BootBuildImageIntegrationTests {
|
|||
}
|
||||
|
||||
@TestTemplate
|
||||
void failsWithPublishMissingPublishRegistry() {
|
||||
void failsWithPublishMissingPublishRegistry() throws IOException {
|
||||
writeMainClass();
|
||||
writeLongNameResource();
|
||||
BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage", "--publishImage");
|
||||
|
@ -182,7 +257,7 @@ class BootBuildImageIntegrationTests {
|
|||
}
|
||||
|
||||
@TestTemplate
|
||||
void failsWithWarPackaging() {
|
||||
void failsWithWarPackaging() throws IOException {
|
||||
writeMainClass();
|
||||
writeLongNameResource();
|
||||
BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage", "-PapplyWarPlugin");
|
||||
|
@ -190,6 +265,15 @@ class BootBuildImageIntegrationTests {
|
|||
assertThat(result.getOutput()).contains("Executable jar file required for building image");
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void failsWithBuildpackNotInBuilder() throws IOException {
|
||||
writeMainClass();
|
||||
writeLongNameResource();
|
||||
BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT");
|
||||
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.FAILED);
|
||||
assertThat(result.getOutput()).contains("'urn:cnb:builder:example/does-not-exist:0.0.1' not found in builder");
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void buildsImageWithWarPackagingAndJarConfiguration() throws IOException {
|
||||
writeMainClass();
|
||||
|
@ -210,7 +294,7 @@ class BootBuildImageIntegrationTests {
|
|||
}
|
||||
}
|
||||
|
||||
private void writeMainClass() {
|
||||
private void writeMainClass() throws IOException {
|
||||
File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/example");
|
||||
examplePackage.mkdirs();
|
||||
File main = new File(examplePackage, "Main.java");
|
||||
|
@ -230,23 +314,70 @@ class BootBuildImageIntegrationTests {
|
|||
writer.println();
|
||||
writer.println("}");
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
|
||||
private void writeLongNameResource() throws IOException {
|
||||
StringBuilder name = new StringBuilder();
|
||||
new Random().ints('a', 'z' + 1).limit(128).forEach((i) -> name.append((char) i));
|
||||
Path path = this.gradleBuild.getProjectDir().toPath()
|
||||
.resolve(Paths.get("src", "main", "resources", name.toString()));
|
||||
Files.createDirectories(path.getParent());
|
||||
Files.createFile(path);
|
||||
}
|
||||
|
||||
private void writeBuildpackContent() throws IOException {
|
||||
File buildpackDir = new File(this.gradleBuild.getProjectDir(), "buildpack/hello-world");
|
||||
buildpackDir.mkdirs();
|
||||
File descriptor = new File(buildpackDir, "buildpack.toml");
|
||||
try (PrintWriter writer = new PrintWriter(new FileWriter(descriptor))) {
|
||||
writer.println("api = \"0.2\"");
|
||||
writer.println("[buildpack]");
|
||||
writer.println("id = \"example/hello-world\"");
|
||||
writer.println("version = \"0.0.1\"");
|
||||
writer.println("name = \"Hello World Buildpack\"");
|
||||
writer.println("homepage = \"https://github.com/buildpacks/samples/tree/main/buildpacks/hello-world\"");
|
||||
writer.println("[[stacks]]\n");
|
||||
writer.println("id = \"io.buildpacks.stacks.bionic\"");
|
||||
}
|
||||
File binDir = new File(buildpackDir, "bin");
|
||||
binDir.mkdirs();
|
||||
FileAttribute<Set<PosixFilePermission>> attribute = PosixFilePermissions
|
||||
.asFileAttribute(PosixFilePermissions.fromString("rwxrwxrwx"));
|
||||
File detect = Files.createFile(Paths.get(binDir.getAbsolutePath(), "detect"), attribute).toFile();
|
||||
try (PrintWriter writer = new PrintWriter(new FileWriter(detect))) {
|
||||
writer.println("#!/usr/bin/env bash");
|
||||
writer.println("set -eo pipefail");
|
||||
writer.println("exit 0");
|
||||
}
|
||||
File build = Files.createFile(Paths.get(binDir.getAbsolutePath(), "build"), attribute).toFile();
|
||||
try (PrintWriter writer = new PrintWriter(new FileWriter(build))) {
|
||||
writer.println("#!/usr/bin/env bash");
|
||||
writer.println("set -eo pipefail");
|
||||
writer.println("echo \"---> Hello World buildpack\"");
|
||||
writer.println("echo \"---> done\"");
|
||||
writer.println("exit 0");
|
||||
}
|
||||
}
|
||||
|
||||
private void writeLongNameResource() {
|
||||
StringBuilder name = new StringBuilder();
|
||||
new Random().ints('a', 'z' + 1).limit(128).forEach((i) -> name.append((char) i));
|
||||
try {
|
||||
Path path = this.gradleBuild.getProjectDir().toPath()
|
||||
.resolve(Paths.get("src", "main", "resources", name.toString()));
|
||||
Files.createDirectories(path.getParent());
|
||||
Files.createFile(path);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
private void tarGzipBuildpackContent() throws IOException {
|
||||
Path tarGzipPath = Paths.get(this.gradleBuild.getProjectDir().getAbsolutePath(), "hello-world.tgz");
|
||||
try (TarArchiveOutputStream tar = new TarArchiveOutputStream(
|
||||
new GzipCompressorOutputStream(Files.newOutputStream(Files.createFile(tarGzipPath))))) {
|
||||
writeFileToTar(tar, new File(this.gradleBuild.getProjectDir(), "buildpack/hello-world/buildpack.toml"),
|
||||
"buildpack.toml", 0644);
|
||||
writeFileToTar(tar, new File(this.gradleBuild.getProjectDir(), "buildpack/hello-world/bin/detect"),
|
||||
"bin/detect", 0777);
|
||||
writeFileToTar(tar, new File(this.gradleBuild.getProjectDir(), "buildpack/hello-world/bin/build"),
|
||||
"bin/build", 0777);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeFileToTar(TarArchiveOutputStream tar, File file, String name, int mode) throws IOException {
|
||||
TarArchiveEntry entry = new TarArchiveEntry(file, name);
|
||||
entry.setMode(mode);
|
||||
tar.putArchiveEntry(entry);
|
||||
IOUtils.copy(Files.newInputStream(file.toPath()), tar);
|
||||
tar.closeArchiveEntry();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -17,6 +17,7 @@
|
|||
package org.springframework.boot.gradle.tasks.bundling;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
|
@ -28,6 +29,7 @@ import org.junit.jupiter.api.Test;
|
|||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import org.springframework.boot.buildpack.platform.build.BuildRequest;
|
||||
import org.springframework.boot.buildpack.platform.build.BuildpackReference;
|
||||
import org.springframework.boot.buildpack.platform.build.PullPolicy;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
@ -221,4 +223,31 @@ class BootBuildImageTests {
|
|||
assertThat(this.buildImage.createRequest().getPullPolicy()).isEqualTo(PullPolicy.NEVER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenNoBuildpacksAreConfiguredThenRequestUsesDefaultBuildpacks() {
|
||||
assertThat(this.buildImage.createRequest().getBuildpacks()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenBuildpacksAreConfiguredThenRequestHasBuildpacks() {
|
||||
this.buildImage.setBuildpacks(Arrays.asList("example/buildpack1", "example/buildpack2"));
|
||||
assertThat(this.buildImage.createRequest().getBuildpacks()).containsExactly(
|
||||
BuildpackReference.of("example/buildpack1"), BuildpackReference.of("example/buildpack2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenEntriesAreAddedToBuildpacksThenRequestHasBuildpacks() {
|
||||
this.buildImage.buildpacks(Arrays.asList("example/buildpack1", "example/buildpack2"));
|
||||
assertThat(this.buildImage.createRequest().getBuildpacks()).containsExactly(
|
||||
BuildpackReference.of("example/buildpack1"), BuildpackReference.of("example/buildpack2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenIndividualEntriesAreAddedToBuildpacksThenRequestHasBuildpacks() {
|
||||
this.buildImage.buildpack("example/buildpack1");
|
||||
this.buildImage.buildpack("example/buildpack2");
|
||||
assertThat(this.buildImage.createRequest().getBuildpacks()).containsExactly(
|
||||
BuildpackReference.of("example/buildpack1"), BuildpackReference.of("example/buildpack2"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
|
|||
import com.sun.jna.Platform;
|
||||
import io.spring.gradle.dependencymanagement.DependencyManagementPlugin;
|
||||
import io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension;
|
||||
import org.antlr.v4.runtime.Lexer;
|
||||
import org.apache.commons.compress.archivers.ArchiveEntry;
|
||||
import org.apache.http.HttpRequest;
|
||||
import org.apache.http.conn.HttpClientConnectionManager;
|
||||
|
@ -49,6 +50,7 @@ import org.jetbrains.kotlin.daemon.client.KotlinCompilerClient;
|
|||
import org.jetbrains.kotlin.gradle.model.KotlinProject;
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinGradleSubplugin;
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinPlugin;
|
||||
import org.tomlj.Toml;
|
||||
|
||||
import org.springframework.asm.ClassVisitor;
|
||||
import org.springframework.boot.buildpack.platform.build.BuildRequest;
|
||||
|
@ -116,7 +118,8 @@ public class GradleBuild {
|
|||
new File(pathOfJarContaining(HttpRequest.class)), new File(pathOfJarContaining(Module.class)),
|
||||
new File(pathOfJarContaining(Versioned.class)),
|
||||
new File(pathOfJarContaining(ParameterNamesModule.class)),
|
||||
new File(pathOfJarContaining(JsonView.class)), new File(pathOfJarContaining(Platform.class)));
|
||||
new File(pathOfJarContaining(JsonView.class)), new File(pathOfJarContaining(Platform.class)),
|
||||
new File(pathOfJarContaining(Toml.class)), new File(pathOfJarContaining(Lexer.class)));
|
||||
}
|
||||
|
||||
private String pathOfJarContaining(Class<?> type) {
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '{version}'
|
||||
}
|
||||
|
||||
sourceCompatibility = '1.8'
|
||||
targetCompatibility = '1.8'
|
||||
|
||||
bootBuildImage {
|
||||
buildpacks = [ "paketo-buildpacks/java" ]
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '{version}'
|
||||
}
|
||||
|
||||
sourceCompatibility = '1.8'
|
||||
targetCompatibility = '1.8'
|
||||
|
||||
bootBuildImage {
|
||||
buildpacks = [ "file://${projectDir}/buildpack/hello-world" ]
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '{version}'
|
||||
}
|
||||
|
||||
sourceCompatibility = '1.8'
|
||||
targetCompatibility = '1.8'
|
||||
|
||||
bootBuildImage {
|
||||
buildpacks = [ "gcr.io/paketo-buildpacks/java:latest" ]
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '{version}'
|
||||
}
|
||||
|
||||
sourceCompatibility = '1.8'
|
||||
targetCompatibility = '1.8'
|
||||
|
||||
bootBuildImage {
|
||||
buildpacks = [ "file://${projectDir}/hello-world.tgz" ]
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '{version}'
|
||||
}
|
||||
|
||||
sourceCompatibility = '1.8'
|
||||
targetCompatibility = '1.8'
|
||||
|
||||
bootBuildImage {
|
||||
buildpacks = [ "urn:cnb:builder:example/does-not-exist:0.0.1" ]
|
||||
}
|
|
@ -152,6 +152,18 @@ Acceptable values are `ALWAYS`, `NEVER`, and `IF_NOT_PRESENT`.
|
|||
|
|
||||
|
|
||||
|
||||
| `buildpacks`
|
||||
a|Buildpacks that the builder should use when building the image.
|
||||
Only the specified buildpacks will be used, overriding the default buildpacks included in the builder.
|
||||
Buildpack references must be in one of the following forms:
|
||||
|
||||
* Buildpack in the builder - [urn:cnb:builder:]<buildpack id>[@<version>]
|
||||
* Buildpack in a directory on the file system - [file://]<path>
|
||||
* Buildpack in a gzipped tar (.tgz) file on the file system - [file://]<path>/<file name>
|
||||
* Buildpack in an OCI image - [docker://]<host>/<repo>[:<tag>][@<digest>]
|
||||
|
|
||||
| None, indicating the builder should use the buildpacks included in it.
|
||||
|
||||
| `cleanCache`
|
||||
| Whether to clean the cache before building.
|
||||
| `spring-boot.build-image.cleanCache`
|
||||
|
@ -311,6 +323,64 @@ The image name can be specified on the command line as well, as shown in this ex
|
|||
|
||||
|
||||
|
||||
[[build-image-example-buildpacks]]
|
||||
==== Buildpacks
|
||||
By default, the builder will use buildpacks included in the builder image and apply them in a pre-defined order.
|
||||
An alternative set of buildpacks can be provided to apply buildpacks that are not included in the builder, or to change the order of included buildpacks.
|
||||
When one or more buildpacks are provided, only the specified buildpacks will be applied.
|
||||
|
||||
The following example instructs the builder to use a custom buildpack packaged in a `.tgz` file, followed by a buildpack included in the builder.
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,attributes"]
|
||||
----
|
||||
<project>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<buildpacks>
|
||||
<buildpack>file:///path/to/example-buildpack.tgz</buildpack>
|
||||
<buildpack>urn:cnb:builder:paketo-buildpacks/java</buildpack>
|
||||
</image>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
----
|
||||
|
||||
Buildpacks can be specified in any of the forms shown below.
|
||||
|
||||
A buildpack located in a CNB Builder (version may be omitted if there is only one buildpack in the builder matching the `buildpack-id`):
|
||||
|
||||
* `urn:cnb:builder:buildpack-id`
|
||||
* `urn:cnb:builder:buildpack-id@0.0.1`
|
||||
* `buildpack-id`
|
||||
* `buildpack-id@0.0.1`
|
||||
|
||||
A path to a directory containing buildpack content (not supported on Windows):
|
||||
|
||||
* `\file:///path/to/buildpack/`
|
||||
* `/path/to/buildpack/`
|
||||
|
||||
A path to a gzipped tar file containing buildpack content:
|
||||
|
||||
* `\file:///path/to/buildpack.tgz`
|
||||
* `/path/to/buildpack.tgz`
|
||||
|
||||
An OCI image:
|
||||
|
||||
* `docker://example/buildpack`
|
||||
* `docker:///example/buildpack:latest`
|
||||
* `docker:///example/buildpack@sha256:45b23dee08...`
|
||||
* `example/buildpack`
|
||||
* `example/buildpack:latest`
|
||||
* `example/buildpack@sha256:45b23dee08...`
|
||||
|
||||
|
||||
|
||||
[[build-image-example-publish]]
|
||||
==== Image Publishing
|
||||
The generated image can be published to a Docker registry by enabling a `publish` option and configuring authentication for the registry using `docker.publishRegistry` parameters.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -150,6 +150,24 @@ public class BuildImageTests extends AbstractArchiveIntegrationTests {
|
|||
});
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void whenBuildImageIsInvokedWithBuildpacks(MavenBuild mavenBuild) {
|
||||
mavenBuild.project("build-image-custom-buildpacks").goals("package")
|
||||
.systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT").execute((project) -> {
|
||||
assertThat(buildLog(project)).contains("Building image")
|
||||
.contains("docker.io/library/build-image-custom-buildpacks:0.0.1.BUILD-SNAPSHOT")
|
||||
.contains("Successfully built image");
|
||||
ImageReference imageReference = ImageReference
|
||||
.of("docker.io/library/build-image-custom-buildpacks:0.0.1.BUILD-SNAPSHOT");
|
||||
try (GenericContainer<?> container = new GenericContainer<>(imageReference.toString())) {
|
||||
container.waitingFor(Wait.forLogMessage("Launched\\n", 1)).start();
|
||||
}
|
||||
finally {
|
||||
removeImage(imageReference);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void failsWhenPublishWithoutPublishRegistryConfigured(MavenBuild mavenBuild) {
|
||||
mavenBuild.project("build-image").goals("package").systemProperty("spring-boot.build-image.publish", "true")
|
||||
|
@ -170,6 +188,14 @@ public class BuildImageTests extends AbstractArchiveIntegrationTests {
|
|||
(project) -> assertThat(buildLog(project)).contains("Executable jar file required for building image"));
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void failsWithBuildpackNotInBuilder(MavenBuild mavenBuild) {
|
||||
mavenBuild.project("build-image-bad-buildpack").goals("package")
|
||||
.systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT")
|
||||
.executeAndFail((project) -> assertThat(buildLog(project))
|
||||
.contains("'urn:cnb:builder:example/does-not-exist:0.0.1' not found in builder"));
|
||||
}
|
||||
|
||||
private void writeLongNameResource(File project) {
|
||||
StringBuilder name = new StringBuilder();
|
||||
new Random().ints('a', 'z' + 1).limit(128).forEach((i) -> name.append((char) i));
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.springframework.boot.maven.it</groupId>
|
||||
<artifactId>build-image-custom-buildpacks</artifactId>
|
||||
<version>0.0.1.BUILD-SNAPSHOT</version>
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>@java.version@</maven.compiler.source>
|
||||
<maven.compiler.target>@java.version@</maven.compiler.target>
|
||||
</properties>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>@project.groupId@</groupId>
|
||||
<artifactId>@project.artifactId@</artifactId>
|
||||
<version>@project.version@</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>build-image</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<image>
|
||||
<buildpacks>
|
||||
<buildpack>urn:cnb:builder:example/does-not-exist:0.0.1</buildpack>
|
||||
</buildpacks>
|
||||
</image>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.test;
|
||||
|
||||
public class SampleApplication {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
System.out.println("Launched");
|
||||
synchronized(args) {
|
||||
args.wait(); // Prevent exit
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.springframework.boot.maven.it</groupId>
|
||||
<artifactId>build-image-custom-buildpacks</artifactId>
|
||||
<version>0.0.1.BUILD-SNAPSHOT</version>
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>@java.version@</maven.compiler.source>
|
||||
<maven.compiler.target>@java.version@</maven.compiler.target>
|
||||
</properties>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>@project.groupId@</groupId>
|
||||
<artifactId>@project.artifactId@</artifactId>
|
||||
<version>@project.version@</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>build-image</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<image>
|
||||
<buildpacks>
|
||||
<buildpack>paketo-buildpacks/java</buildpack>
|
||||
</buildpacks>
|
||||
</image>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.test;
|
||||
|
||||
public class SampleApplication {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
System.out.println("Launched");
|
||||
synchronized(args) {
|
||||
args.wait(); // Prevent exit
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -176,8 +176,8 @@ public class BuildImageMojo extends AbstractPackagerMojo {
|
|||
try {
|
||||
DockerConfiguration dockerConfiguration = (this.docker != null) ? this.docker.asDockerConfiguration()
|
||||
: null;
|
||||
Builder builder = new Builder(new MojoBuildLog(this::getLog), dockerConfiguration);
|
||||
BuildRequest request = getBuildRequest(libraries);
|
||||
Builder builder = new Builder(new MojoBuildLog(this::getLog), dockerConfiguration);
|
||||
builder.build(request);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -16,12 +16,15 @@
|
|||
|
||||
package org.springframework.boot.maven;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.maven.artifact.Artifact;
|
||||
|
||||
import org.springframework.boot.buildpack.platform.build.BuildRequest;
|
||||
import org.springframework.boot.buildpack.platform.build.BuildpackReference;
|
||||
import org.springframework.boot.buildpack.platform.build.PullPolicy;
|
||||
import org.springframework.boot.buildpack.platform.docker.type.ImageName;
|
||||
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
|
||||
|
@ -54,6 +57,8 @@ public class Image {
|
|||
|
||||
Boolean publish;
|
||||
|
||||
List<String> buildpacks;
|
||||
|
||||
/**
|
||||
* The name of the created image.
|
||||
* @return the image name
|
||||
|
@ -174,6 +179,10 @@ public class Image {
|
|||
if (this.publish != null) {
|
||||
request = request.withPublish(this.publish);
|
||||
}
|
||||
if (this.buildpacks != null && !this.buildpacks.isEmpty()) {
|
||||
request = request
|
||||
.withBuildpacks(this.buildpacks.stream().map(BuildpackReference::of).collect(Collectors.toList()));
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -16,6 +16,7 @@
|
|||
|
||||
package org.springframework.boot.maven;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.function.Function;
|
||||
|
||||
|
@ -26,6 +27,7 @@ import org.apache.maven.artifact.versioning.VersionRange;
|
|||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.buildpack.platform.build.BuildRequest;
|
||||
import org.springframework.boot.buildpack.platform.build.BuildpackReference;
|
||||
import org.springframework.boot.buildpack.platform.build.PullPolicy;
|
||||
import org.springframework.boot.buildpack.platform.io.Owner;
|
||||
import org.springframework.boot.buildpack.platform.io.TarArchive;
|
||||
|
@ -65,6 +67,7 @@ class ImageTests {
|
|||
assertThat(request.isCleanCache()).isFalse();
|
||||
assertThat(request.isVerboseLogging()).isFalse();
|
||||
assertThat(request.getPullPolicy()).isEqualTo(PullPolicy.ALWAYS);
|
||||
assertThat(request.getBuildpacks()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -123,6 +126,15 @@ class ImageTests {
|
|||
assertThat(request.isPublish()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBuildRequestWhenHasBuildpacksUsesBuildpacks() {
|
||||
Image image = new Image();
|
||||
image.buildpacks = Arrays.asList("example/buildpack1@0.0.1", "example/buildpack2@0.0.2");
|
||||
BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent());
|
||||
assertThat(request.getBuildpacks()).containsExactly(BuildpackReference.of("example/buildpack1@0.0.1"),
|
||||
BuildpackReference.of("example/buildpack2@0.0.2"));
|
||||
}
|
||||
|
||||
private Artifact createArtifact() {
|
||||
return new DefaultArtifact("com.example", "my-app", VersionRange.createFromVersion("0.0.1-SNAPSHOT"), "compile",
|
||||
"jar", null, new DefaultArtifactHandler());
|
||||
|
|
Loading…
Reference in New Issue