Add imagePlatform option for image building

An `imagePlatform` option for the Maven and Gradle image-building
goal/task can be used to specify the os/architecture of any
builder, run, and buildpack images that are pulled during image
building.

Closes gh-40944
This commit is contained in:
Scott Frederick
2024-07-30 12:54:14 -05:00
parent 67bae524b2
commit dfab18c965
45 changed files with 1373 additions and 275 deletions

View File

@@ -36,7 +36,7 @@ class DockerApiIntegrationTests {
@Test
void pullImage() throws IOException {
this.docker.image()
.pull(ImageReference.of("gcr.io/paketo-buildpacks/builder:base"),
.pull(ImageReference.of("gcr.io/paketo-buildpacks/builder:base"), null,
new TotalProgressPullListener(new TotalProgressBar("Pulling: ")));
}

View File

@@ -22,6 +22,7 @@ import java.util.function.Consumer;
import org.springframework.boot.buildpack.platform.docker.LogUpdateEvent;
import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent;
import org.springframework.boot.buildpack.platform.docker.type.Image;
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.docker.type.VolumeName;
@@ -43,8 +44,17 @@ public abstract class AbstractBuildLog implements BuildLog {
}
@Override
public Consumer<TotalProgressEvent> pullingImage(ImageReference imageReference, ImageType imageType) {
return getProgressConsumer(String.format(" > Pulling %s '%s'", imageType.getDescription(), imageReference));
public Consumer<TotalProgressEvent> pullingImage(ImageReference imageReference, ImagePlatform platform,
ImageType imageType) {
String message;
if (platform != null) {
message = String.format(" > Pulling %s '%s' for platform '%s'", imageType.getDescription(), imageReference,
platform);
}
else {
message = String.format(" > Pulling %s '%s'", imageType.getDescription(), imageReference);
}
return getProgressConsumer(message);
}
@Override

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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.
@@ -19,6 +19,7 @@ package org.springframework.boot.buildpack.platform.build;
import java.util.Arrays;
import java.util.stream.IntStream;
import org.springframework.boot.buildpack.platform.docker.type.ApiVersion;
import org.springframework.util.StringUtils;
/**

View File

@@ -22,6 +22,7 @@ import java.util.function.Consumer;
import org.springframework.boot.buildpack.platform.docker.LogUpdateEvent;
import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent;
import org.springframework.boot.buildpack.platform.docker.type.Image;
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.docker.type.VolumeName;
@@ -46,10 +47,12 @@ public interface BuildLog {
/**
* Log that an image is being pulled.
* @param imageReference the image reference
* @param platform the platform of the image
* @param imageType the image type
* @return a consumer for progress update events
*/
Consumer<TotalProgressEvent> pullingImage(ImageReference imageReference, ImageType imageType);
Consumer<TotalProgressEvent> pullingImage(ImageReference imageReference, ImagePlatform platform,
ImageType imageType);
/**
* Log that an image has been pulled.

View File

@@ -27,6 +27,7 @@ import java.util.Map;
import java.util.function.Function;
import org.springframework.boot.buildpack.platform.docker.type.Binding;
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.io.Owner;
import org.springframework.boot.buildpack.platform.io.TarArchive;
@@ -102,6 +103,8 @@ public class BuildRequest {
private final List<String> securityOptions;
private final ImagePlatform platform;
BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent) {
Assert.notNull(name, "Name must not be null");
Assert.notNull(applicationContent, "ApplicationContent must not be null");
@@ -126,17 +129,19 @@ public class BuildRequest {
this.createdDate = null;
this.applicationDirectory = null;
this.securityOptions = null;
this.platform = null;
}
BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent, ImageReference builder,
ImageReference runImage, Creator creator, Map<String, String> env, boolean cleanCache,
Boolean trustBuilder, ImageReference runImage, Creator creator, Map<String, String> env, boolean cleanCache,
boolean verboseLogging, PullPolicy pullPolicy, boolean publish, List<BuildpackReference> buildpacks,
List<Binding> bindings, String network, List<ImageReference> tags, Cache buildWorkspace, Cache buildCache,
Cache launchCache, Instant createdDate, String applicationDirectory, List<String> securityOptions,
Boolean trustBuilder) {
ImagePlatform platform) {
this.name = name;
this.applicationContent = applicationContent;
this.builder = builder;
this.trustBuilder = trustBuilder;
this.runImage = runImage;
this.creator = creator;
this.env = env;
@@ -154,7 +159,7 @@ public class BuildRequest {
this.createdDate = createdDate;
this.applicationDirectory = applicationDirectory;
this.securityOptions = securityOptions;
this.trustBuilder = trustBuilder;
this.platform = platform;
}
/**
@@ -164,10 +169,11 @@ 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.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache,
this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions, this.trustBuilder);
return new BuildRequest(this.name, this.applicationContent, builder.inTaggedOrDigestForm(), this.trustBuilder,
this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy,
this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace,
this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions,
this.platform);
}
/**
@@ -178,10 +184,10 @@ public class BuildRequest {
* @since 3.4.0
*/
public BuildRequest withTrustBuilder(boolean trustBuilder) {
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate,
this.applicationDirectory, this.securityOptions, trustBuilder);
return new BuildRequest(this.name, this.applicationContent, this.builder, trustBuilder, this.runImage,
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache,
this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions, this.platform);
}
/**
@@ -190,10 +196,11 @@ public class BuildRequest {
* @return an updated build request
*/
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.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache,
this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions, this.trustBuilder);
return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder,
runImageName.inTaggedOrDigestForm(), this.creator, this.env, this.cleanCache, this.verboseLogging,
this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, this.tags,
this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory,
this.securityOptions, this.platform);
}
/**
@@ -203,10 +210,10 @@ 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.buildpacks, this.bindings,
this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate,
this.applicationDirectory, this.securityOptions, this.trustBuilder);
return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage,
creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks,
this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache,
this.createdDate, this.applicationDirectory, this.securityOptions, this.platform);
}
/**
@@ -220,10 +227,11 @@ public class BuildRequest {
Assert.hasText(value, "Value must not be empty");
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,
this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache,
this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions, this.trustBuilder);
return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage,
this.creator, Collections.unmodifiableMap(env), this.cleanCache, this.verboseLogging, this.pullPolicy,
this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace,
this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions,
this.platform);
}
/**
@@ -235,11 +243,11 @@ public class BuildRequest {
Assert.notNull(env, "Env must not be null");
Map<String, String> updatedEnv = new LinkedHashMap<>(this.env);
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.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace,
this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions,
this.trustBuilder);
return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage,
this.creator, Collections.unmodifiableMap(updatedEnv), this.cleanCache, this.verboseLogging,
this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, this.tags,
this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory,
this.securityOptions, this.platform);
}
/**
@@ -248,10 +256,10 @@ public class BuildRequest {
* @return an updated build request
*/
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, this.buildpacks, this.bindings,
this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate,
this.applicationDirectory, this.securityOptions, this.trustBuilder);
return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage,
this.creator, this.env, cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks,
this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache,
this.createdDate, this.applicationDirectory, this.securityOptions, this.platform);
}
/**
@@ -260,10 +268,10 @@ public class BuildRequest {
* @return an updated build request
*/
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.buildpacks, this.bindings,
this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate,
this.applicationDirectory, this.securityOptions, this.trustBuilder);
return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage,
this.creator, this.env, this.cleanCache, verboseLogging, this.pullPolicy, this.publish, this.buildpacks,
this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache,
this.createdDate, this.applicationDirectory, this.securityOptions, this.platform);
}
/**
@@ -272,10 +280,10 @@ public class BuildRequest {
* @return an updated build request
*/
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.buildpacks, this.bindings,
this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate,
this.applicationDirectory, this.securityOptions, this.trustBuilder);
return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage,
this.creator, this.env, this.cleanCache, this.verboseLogging, pullPolicy, this.publish, this.buildpacks,
this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache,
this.createdDate, this.applicationDirectory, this.securityOptions, this.platform);
}
/**
@@ -284,10 +292,10 @@ public class BuildRequest {
* @return an updated build request
*/
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.buildpacks, this.bindings,
this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate,
this.applicationDirectory, this.securityOptions, this.trustBuilder);
return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage,
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, publish, this.buildpacks,
this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache,
this.createdDate, this.applicationDirectory, this.securityOptions, this.platform);
}
/**
@@ -309,10 +317,10 @@ public class BuildRequest {
*/
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, this.bindings,
this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate,
this.applicationDirectory, this.securityOptions, this.trustBuilder);
return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage,
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, buildpacks,
this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache,
this.createdDate, this.applicationDirectory, this.securityOptions, this.platform);
}
/**
@@ -334,10 +342,10 @@ public class BuildRequest {
*/
public BuildRequest withBindings(List<Binding> bindings) {
Assert.notNull(bindings, "Bindings 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, this.buildpacks, bindings,
this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate,
this.applicationDirectory, this.securityOptions, this.trustBuilder);
return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage,
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
this.buildpacks, bindings, this.network, this.tags, this.buildWorkspace, this.buildCache,
this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions, this.platform);
}
/**
@@ -347,10 +355,10 @@ public class BuildRequest {
* @since 2.6.0
*/
public BuildRequest withNetwork(String network) {
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate,
this.applicationDirectory, this.securityOptions, this.trustBuilder);
return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage,
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
this.buildpacks, this.bindings, network, this.tags, this.buildWorkspace, this.buildCache,
this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions, this.platform);
}
/**
@@ -370,10 +378,10 @@ public class BuildRequest {
*/
public BuildRequest withTags(List<ImageReference> tags) {
Assert.notNull(tags, "Tags 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, this.buildpacks, this.bindings,
this.network, tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate,
this.applicationDirectory, this.securityOptions, this.trustBuilder);
return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage,
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
this.buildpacks, this.bindings, this.network, tags, this.buildWorkspace, this.buildCache,
this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions, this.platform);
}
/**
@@ -384,10 +392,10 @@ public class BuildRequest {
*/
public BuildRequest withBuildWorkspace(Cache buildWorkspace) {
Assert.notNull(buildWorkspace, "BuildWorkspace 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, this.buildpacks, this.bindings,
this.network, this.tags, buildWorkspace, this.buildCache, this.launchCache, this.createdDate,
this.applicationDirectory, this.securityOptions, this.trustBuilder);
return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage,
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
this.buildpacks, this.bindings, this.network, this.tags, buildWorkspace, this.buildCache,
this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions, this.platform);
}
/**
@@ -397,10 +405,10 @@ public class BuildRequest {
*/
public BuildRequest withBuildCache(Cache buildCache) {
Assert.notNull(buildCache, "BuildCache 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, this.buildpacks, this.bindings,
this.network, this.tags, this.buildWorkspace, buildCache, this.launchCache, this.createdDate,
this.applicationDirectory, this.securityOptions, this.trustBuilder);
return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage,
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, buildCache,
this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions, this.platform);
}
/**
@@ -410,10 +418,10 @@ public class BuildRequest {
*/
public BuildRequest withLaunchCache(Cache launchCache) {
Assert.notNull(launchCache, "LaunchCache 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, this.buildpacks, this.bindings,
this.network, this.tags, this.buildWorkspace, this.buildCache, launchCache, this.createdDate,
this.applicationDirectory, this.securityOptions, this.trustBuilder);
return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage,
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache,
launchCache, this.createdDate, this.applicationDirectory, this.securityOptions, this.platform);
}
/**
@@ -423,10 +431,11 @@ public class BuildRequest {
*/
public BuildRequest withCreatedDate(String createdDate) {
Assert.notNull(createdDate, "CreatedDate 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, this.buildpacks, this.bindings,
this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache,
parseCreatedDate(createdDate), this.applicationDirectory, this.securityOptions, this.trustBuilder);
return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage,
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache,
this.launchCache, parseCreatedDate(createdDate), this.applicationDirectory, this.securityOptions,
this.platform);
}
private Instant parseCreatedDate(String createdDate) {
@@ -448,10 +457,10 @@ public class BuildRequest {
*/
public BuildRequest withApplicationDirectory(String applicationDirectory) {
Assert.notNull(applicationDirectory, "ApplicationDirectory 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, this.buildpacks, this.bindings,
this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate,
applicationDirectory, this.securityOptions, this.trustBuilder);
return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage,
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache,
this.launchCache, this.createdDate, applicationDirectory, this.securityOptions, this.platform);
}
/**
@@ -462,10 +471,25 @@ public class BuildRequest {
*/
public BuildRequest withSecurityOptions(List<String> securityOptions) {
Assert.notNull(securityOptions, "SecurityOption 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, this.buildpacks, this.bindings,
this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate,
this.applicationDirectory, securityOptions, this.trustBuilder);
return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage,
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache,
this.launchCache, this.createdDate, this.applicationDirectory, securityOptions, this.platform);
}
/**
* Return a new {@link BuildRequest} with an updated image platform.
* @param platform the image platform
* @return an updated build request
* @since 3.4.0
*/
public BuildRequest withImagePlatform(String platform) {
Assert.notNull(platform, "Platform must not be null");
return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage,
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache,
this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions,
ImagePlatform.of(platform));
}
/**
@@ -648,6 +672,15 @@ public class BuildRequest {
return this.securityOptions;
}
/**
* Return the platform that should be used when pulling images.
* @return the platform or {@code null}
* @since 3.4.0
*/
public ImagePlatform getImagePlatform() {
return this.platform;
}
/**
* Factory method to create a new {@link BuildRequest} from a JAR file.
* @param jarFile the source jar file

View File

@@ -29,6 +29,7 @@ import org.springframework.boot.buildpack.platform.docker.configuration.DockerCo
import org.springframework.boot.buildpack.platform.docker.configuration.ResolvedDockerHost;
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.ImagePlatform;
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;
@@ -99,7 +100,8 @@ public class Builder {
this.log.start(request);
String domain = request.getBuilder().getDomain();
PullPolicy pullPolicy = request.getPullPolicy();
ImageFetcher imageFetcher = new ImageFetcher(domain, getBuilderAuthHeader(), pullPolicy);
ImageFetcher imageFetcher = new ImageFetcher(domain, getBuilderAuthHeader(), pullPolicy,
request.getImagePlatform());
Image builderImage = imageFetcher.fetchImage(ImageType.BUILDER, request.getBuilder());
BuilderMetadata builderMetadata = BuilderMetadata.fromImage(builderImage);
request = withRunImageIfNeeded(request, builderMetadata);
@@ -208,10 +210,13 @@ public class Builder {
private final PullPolicy pullPolicy;
ImageFetcher(String domain, String authHeader, PullPolicy pullPolicy) {
private ImagePlatform defaultPlatform;
ImageFetcher(String domain, String authHeader, PullPolicy pullPolicy, ImagePlatform platform) {
this.domain = domain;
this.authHeader = authHeader;
this.pullPolicy = pullPolicy;
this.defaultPlatform = platform;
}
Image fetchImage(ImageType type, ImageReference reference) throws IOException {
@@ -236,9 +241,12 @@ public class Builder {
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.pullingImage(reference, this.defaultPlatform, imageType));
Image image = Builder.this.docker.image().pull(reference, this.defaultPlatform, listener, this.authHeader);
Builder.this.log.pulledImage(image, imageType);
if (this.defaultPlatform == null) {
this.defaultPlatform = ImagePlatform.from(image);
}
return image;
}

View File

@@ -29,6 +29,7 @@ import com.sun.jna.Platform;
import org.springframework.boot.buildpack.platform.docker.DockerApi;
import org.springframework.boot.buildpack.platform.docker.LogUpdateEvent;
import org.springframework.boot.buildpack.platform.docker.configuration.ResolvedDockerHost;
import org.springframework.boot.buildpack.platform.docker.type.ApiVersion;
import org.springframework.boot.buildpack.platform.docker.type.Binding;
import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig;
import org.springframework.boot.buildpack.platform.docker.type.ContainerContent;
@@ -363,7 +364,7 @@ class Lifecycle implements Closeable {
private ContainerReference createContainer(ContainerConfig config, boolean requiresAppUpload) throws IOException {
if (!requiresAppUpload || this.applicationVolumePopulated) {
return this.docker.container().create(config);
return this.docker.container().create(config, this.request.getImagePlatform());
}
try {
if (this.application.getBind() != null) {
@@ -371,7 +372,8 @@ class Lifecycle implements Closeable {
}
TarArchive applicationContent = this.request.getApplicationContent(this.builder.getBuildOwner());
return this.docker.container()
.create(config, ContainerContent.of(applicationContent, this.applicationDirectory));
.create(config, this.request.getImagePlatform(),
ContainerContent.of(applicationContent, this.applicationDirectory));
}
finally {
this.applicationVolumePopulated = true;

View File

@@ -28,17 +28,20 @@ import java.util.Collections;
import java.util.List;
import java.util.Objects;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.net.URIBuilder;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration;
import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport;
import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport.Response;
import org.springframework.boot.buildpack.platform.docker.type.ApiVersion;
import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig;
import org.springframework.boot.buildpack.platform.docker.type.ContainerContent;
import org.springframework.boot.buildpack.platform.docker.type.ContainerReference;
import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus;
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.ImagePlatform;
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;
@@ -61,7 +64,9 @@ public class DockerApi {
private static final List<String> FORCE_PARAMS = Collections.unmodifiableList(Arrays.asList("force", "1"));
static final String API_VERSION = "v1.24";
static final ApiVersion MINIMUM_API_VERSION = ApiVersion.parse("1.24");
static final String API_VERSION_HEADER_NAME = "API-Version";
private final HttpTransport http;
@@ -73,6 +78,10 @@ public class DockerApi {
private final VolumeApi volume;
private final SystemApi system;
private ApiVersion apiVersion = null;
/**
* Create a new {@link DockerApi} instance.
*/
@@ -100,6 +109,7 @@ public class DockerApi {
this.image = new ImageApi();
this.container = new ContainerApi();
this.volume = new VolumeApi();
this.system = new SystemApi();
}
private HttpTransport http() {
@@ -116,7 +126,10 @@ public class DockerApi {
private URI buildUrl(String path, Object... params) {
try {
URIBuilder builder = new URIBuilder("/" + API_VERSION + path);
if (this.apiVersion == null) {
this.apiVersion = this.system.getApiVersion();
}
URIBuilder builder = new URIBuilder("/v" + this.apiVersion + path);
int param = 0;
while (param < params.length) {
builder.addParameter(Objects.toString(params[param++]), Objects.toString(params[param++]));
@@ -128,6 +141,13 @@ public class DockerApi {
}
}
private void verifyApiVersionForPlatform() {
ApiVersion minimumPlatformApiVersion = ApiVersion.of(1, 41);
Assert.isTrue(this.apiVersion.supports(minimumPlatformApiVersion),
"Docker API version must be at least " + minimumPlatformApiVersion
+ " to support the 'imagePlatform' option, but current API version is " + this.apiVersion);
}
/**
* Return the Docker API for image operations.
* @return the image API
@@ -148,6 +168,10 @@ public class DockerApi {
return this.volume;
}
SystemApi system() {
return this.system;
}
/**
* Docker API for image operations.
*/
@@ -159,27 +183,37 @@ public class DockerApi {
/**
* Pull an image from a registry.
* @param reference the image reference to pull
* @param platform the platform (os/architecture/variant) of the image to pull
* @param listener a pull listener to receive update events
* @return the {@link ImageApi pulled image} instance
* @throws IOException on IO error
*/
public Image pull(ImageReference reference, UpdateListener<PullImageUpdateEvent> listener) throws IOException {
return pull(reference, listener, null);
public Image pull(ImageReference reference, ImagePlatform platform,
UpdateListener<PullImageUpdateEvent> listener) throws IOException {
return pull(reference, platform, listener, null);
}
/**
* Pull an image from a registry.
* @param reference the image reference to pull
* @param platform the platform (os/architecture/variant) of the image to pull
* @param listener a pull listener to receive update events
* @param registryAuth registry authentication credentials
* @return the {@link ImageApi pulled image} instance
* @throws IOException on IO error
*/
public Image pull(ImageReference reference, UpdateListener<PullImageUpdateEvent> listener, String registryAuth)
throws IOException {
public Image pull(ImageReference reference, ImagePlatform platform,
UpdateListener<PullImageUpdateEvent> listener, String registryAuth) throws IOException {
Assert.notNull(reference, "Reference must not be null");
Assert.notNull(listener, "Listener must not be null");
URI createUri = buildUrl("/images/create", "fromImage", reference);
URI createUri;
if (platform != null) {
createUri = buildUrl("/images/create", "fromImage", reference, "platform", platform);
verifyApiVersionForPlatform();
}
else {
createUri = buildUrl("/images/create", "fromImage", reference);
}
DigestCaptureUpdateListener digestCapture = new DigestCaptureUpdateListener();
listener.onStart();
try {
@@ -348,22 +382,32 @@ public class DockerApi {
/**
* Create a new container a {@link ContainerConfig}.
* @param config the container config
* @param platform the platform (os/architecture/variant) of the image the
* container should be created from
* @param contents additional contents to include
* @return a {@link ContainerReference} for the newly created container
* @throws IOException on IO error
*/
public ContainerReference create(ContainerConfig config, ContainerContent... contents) throws IOException {
public ContainerReference create(ContainerConfig config, ImagePlatform platform, ContainerContent... contents)
throws IOException {
Assert.notNull(config, "Config must not be null");
Assert.noNullElements(contents, "Contents must not contain null elements");
ContainerReference containerReference = createContainer(config);
ContainerReference containerReference = createContainer(config, platform);
for (ContainerContent content : contents) {
uploadContainerContent(containerReference, content);
}
return containerReference;
}
private ContainerReference createContainer(ContainerConfig config) throws IOException {
URI createUri = buildUrl("/containers/create");
private ContainerReference createContainer(ContainerConfig config, ImagePlatform platform) throws IOException {
URI createUri;
if (platform != null) {
createUri = buildUrl("/containers/create", "platform", platform);
verifyApiVersionForPlatform();
}
else {
createUri = buildUrl("/containers/create");
}
try (Response response = http().post(createUri, "application/json", config::writeTo)) {
return ContainerReference
.of(SharedObjectMapper.get().readTree(response.getContent()).at("/Id").asText());
@@ -460,6 +504,39 @@ public class DockerApi {
}
/**
* Docker API for system operations.
*/
class SystemApi {
SystemApi() {
}
/**
* Get the API version supported by the Docker daemon.
* @return the Docker daemon API version
*/
ApiVersion getApiVersion() {
try {
URI uri = new URIBuilder("/_ping").build();
try (Response response = http().head(uri)) {
Header apiVersionHeader = response.getHeader(API_VERSION_HEADER_NAME);
if (apiVersionHeader != null) {
return ApiVersion.parse(apiVersionHeader.getValue());
}
}
catch (Exception ex) {
// fall through to return default value
}
return MINIMUM_API_VERSION;
}
catch (URISyntaxException ex) {
throw new IllegalStateException(ex);
}
}
}
/**
* {@link UpdateListener} used to capture the image digest.
*/

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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.
@@ -26,11 +26,13 @@ import java.net.URISyntaxException;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.classic.methods.HttpDelete;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpHead;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.classic.methods.HttpPut;
import org.apache.hc.client5.http.classic.methods.HttpUriRequest;
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.io.entity.AbstractHttpEntity;
@@ -130,6 +132,16 @@ abstract class HttpClientTransport implements HttpTransport {
return execute(new HttpDelete(uri));
}
/**
* Perform an HTTP HEAD operation.
* @param uri the destination URI
* @return the operation response
*/
@Override
public Response head(URI uri) {
return execute(new HttpHead(uri));
}
private Response execute(HttpUriRequestBase request, String contentType, IOConsumer<OutputStream> writer) {
request.setEntity(new WritableHttpEntity(contentType, writer));
return execute(request);
@@ -257,6 +269,11 @@ abstract class HttpClientTransport implements HttpTransport {
return this.response.getEntity().getContent();
}
@Override
public Header getHeader(String name) {
return this.response.getFirstHeader(name);
}
@Override
public void close() throws IOException {
this.response.close();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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,8 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import org.apache.hc.core5.http.Header;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerHost;
import org.springframework.boot.buildpack.platform.docker.configuration.ResolvedDockerHost;
@@ -89,6 +91,14 @@ public interface HttpTransport {
*/
Response delete(URI uri) throws IOException;
/**
* Perform an HTTP HEAD operation.
* @param uri the destination URI (excluding any host/port)
* @return the operation response
* @throws IOException on IO error
*/
Response head(URI uri) throws IOException;
/**
* Create the most suitable {@link HttpTransport} based on the {@link DockerHost}.
* @param dockerHost the Docker host information
@@ -112,6 +122,10 @@ public interface HttpTransport {
*/
InputStream getContent() throws IOException;
default Header getHeader(String name) {
throw new UnsupportedOperationException();
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2024 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.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.build;
package org.springframework.boot.buildpack.platform.docker.type;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -26,8 +26,9 @@ import org.springframework.util.Assert;
*
* @author Phillip Webb
* @author Scott Frederick
* @since 3.4.0
*/
final class ApiVersion {
public final class ApiVersion {
private static final Pattern PATTERN = Pattern.compile("^v?(\\d+)\\.(\\d*)$");
@@ -56,27 +57,14 @@ final class ApiVersion {
return this.minor;
}
/**
* Assert that this API version supports the specified version.
* @param other the version to check against
* @see #supports(ApiVersion)
*/
void assertSupports(ApiVersion other) {
if (!supports(other)) {
throw new IllegalStateException(
"Detected platform API version '" + other + "' does not match supported version '" + this + "'");
}
}
/**
* Returns if this API version supports the given version. A {@code 0.x} matches only
* the same version number. A 1.x or higher release matches when the versions have the
* same major version and a minor that is equal or greater.
* @param other the version to check against
* @return if the specified API version is supported
* @see #assertSupports(ApiVersion)
*/
boolean supports(ApiVersion other) {
public boolean supports(ApiVersion other) {
if (equals(other)) {
return true;
}
@@ -92,7 +80,7 @@ final class ApiVersion {
* @return if any of the specified API versions are supported
* @see #supports(ApiVersion)
*/
boolean supportsAny(ApiVersion... others) {
public boolean supportsAny(ApiVersion... others) {
for (ApiVersion other : others) {
if (supports(other)) {
return true;
@@ -129,7 +117,7 @@ final class ApiVersion {
* @return the corresponding {@link ApiVersion}
* @throws IllegalArgumentException if the value could not be parsed
*/
static ApiVersion parse(String value) {
public static ApiVersion parse(String value) {
Assert.hasText(value, "Value must not be empty");
Matcher matcher = PATTERN.matcher(value);
Assert.isTrue(matcher.matches(), () -> "Malformed version number '" + value + "'");
@@ -143,7 +131,7 @@ final class ApiVersion {
}
}
static ApiVersion of(int major, int minor) {
public static ApiVersion of(int major, int minor) {
return new ApiVersion(major, minor);
}

View File

@@ -31,6 +31,7 @@ import org.springframework.boot.buildpack.platform.json.MappedObject;
* Image details as returned from {@code Docker inspect}.
*
* @author Phillip Webb
* @author Scott Frederick
* @since 2.3.0
*/
public class Image extends MappedObject {
@@ -43,6 +44,10 @@ public class Image extends MappedObject {
private final String os;
private final String architecture;
private final String variant;
private final String created;
Image(JsonNode node) {
@@ -51,6 +56,8 @@ public class Image extends MappedObject {
this.config = new ImageConfig(getNode().at("/Config"));
this.layers = extractLayers(valueAt("/RootFS/Layers", String[].class));
this.os = valueAt("/Os", String.class);
this.architecture = valueAt("/Architecture", String.class);
this.variant = valueAt("/Variant", String.class);
this.created = valueAt("/Created", String.class);
}
@@ -93,6 +100,22 @@ public class Image extends MappedObject {
return (this.os != null) ? this.os : "linux";
}
/**
* Return the architecture of the image.
* @return the image architecture
*/
public String getArchitecture() {
return this.architecture;
}
/**
* Return the variant of the image.
* @return the image variant
*/
public String getVariant() {
return this.variant;
}
/**
* Return the created date of the image.
* @return the image created date

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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.
@@ -76,17 +76,23 @@ public class ImageArchive implements TarArchive {
private final String os;
private final String architecture;
private final String variant;
private final List<LayerId> existingLayers;
private final List<Layer> newLayers;
ImageArchive(ObjectMapper objectMapper, ImageConfig imageConfig, Instant createDate, ImageReference tag, String os,
List<LayerId> existingLayers, List<Layer> newLayers) {
String architecture, String variant, List<LayerId> existingLayers, List<Layer> newLayers) {
this.objectMapper = objectMapper;
this.imageConfig = imageConfig;
this.createDate = createDate;
this.tag = tag;
this.os = os;
this.architecture = architecture;
this.variant = variant;
this.existingLayers = existingLayers;
this.newLayers = newLayers;
}
@@ -164,11 +170,13 @@ public class ImageArchive implements TarArchive {
private ObjectNode createConfig(List<LayerId> writtenLayers) {
ObjectNode config = this.objectMapper.createObjectNode();
config.set("config", this.imageConfig.getNodeCopy());
config.set("created", config.textNode(getCreatedDate()));
config.set("history", createHistory(writtenLayers));
config.set("os", config.textNode(this.os));
config.set("rootfs", createRootFs(writtenLayers));
config.set("Config", this.imageConfig.getNodeCopy());
config.set("Created", config.textNode(getCreatedDate()));
config.set("History", createHistory(writtenLayers));
config.set("Os", config.textNode(this.os));
config.set("Architecture", config.textNode(this.architecture));
config.set("Variant", config.textNode(this.variant));
config.set("RootFS", createRootFs(writtenLayers));
return config;
}
@@ -264,7 +272,8 @@ public class ImageArchive implements TarArchive {
update.accept(this);
Instant createDate = (this.createDate != null) ? this.createDate : WINDOWS_EPOCH_PLUS_SECOND;
return new ImageArchive(SharedObjectMapper.get(), this.config, createDate, this.tag, this.image.getOs(),
this.image.getLayers(), Collections.unmodifiableList(this.newLayers));
this.image.getArchitecture(), this.image.getVariant(), this.image.getLayers(),
Collections.unmodifiableList(this.newLayers));
}
/**

View File

@@ -0,0 +1,101 @@
/*
* Copyright 2012-2024 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.docker.type;
import java.util.Objects;
import org.springframework.util.Assert;
/**
* A platform specification for a Docker image.
*
* @author Scott Frederick
* @since 3.4.0
*/
public class ImagePlatform {
private final String os;
private final String architecture;
private final String variant;
ImagePlatform(String os, String architecture, String variant) {
Assert.hasText(os, "OS must not be empty");
this.os = os;
this.architecture = architecture;
this.variant = variant;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof ImagePlatform that)) {
return false;
}
return Objects.equals(this.os, that.os) && Objects.equals(this.architecture, that.architecture)
&& Objects.equals(this.variant, that.variant);
}
@Override
public int hashCode() {
return Objects.hash(this.os, this.architecture, this.variant);
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder(this.os);
if (this.architecture != null) {
builder.append("/").append(this.architecture);
}
if (this.variant != null) {
builder.append("/").append(this.variant);
}
return builder.toString();
}
/**
* Create a new {@link ImagePlatform} from the given value in the form
* {@code os[/architecture[/variant]]}.
* @param value the value to parse
* @return an {@link ImagePlatform} instance
*/
public static ImagePlatform of(String value) {
Assert.hasText(value, "Value must not be empty");
String[] split = value.split("/+");
return switch (split.length) {
case 1 -> new ImagePlatform(split[0], null, null);
case 2 -> new ImagePlatform(split[0], split[1], null);
case 3 -> new ImagePlatform(split[0], split[1], split[2]);
default -> throw new IllegalArgumentException(
"ImagePlatform value '" + value + "' must be in the form of os[/architecture[/variant]]");
};
}
/**
* Create a new {@link ImagePlatform} matching the platform information from the
* provided {@link Image}.
* @param image the image to get platform information from
* @return an {@link ImagePlatform} instance
*/
public static ImagePlatform from(Image image) {
return new ImagePlatform(image.getOs(), image.getArchitecture(), image.getVariant());
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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,6 +20,8 @@ import java.util.stream.IntStream;
import org.junit.jupiter.api.Test;
import org.springframework.boot.buildpack.platform.docker.type.ApiVersion;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;

View File

@@ -39,6 +39,7 @@ import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.boot.buildpack.platform.docker.type.Binding;
import org.springframework.boot.buildpack.platform.docker.type.ImageName;
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.io.Owner;
import org.springframework.boot.buildpack.platform.io.TarArchive;
@@ -392,6 +393,13 @@ class BuildRequestTests {
assertThat(withAppDir.getSecurityOptions()).containsExactly("label=user:USER", "label=role:ROLE");
}
@Test
void withPlatformSetsPlatform() throws Exception {
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));
BuildRequest withAppDir = request.withImagePlatform("linux/arm64");
assertThat(withAppDir.getImagePlatform()).isEqualTo(ImagePlatform.of("linux/arm64"));
}
private void hasExpectedJarContent(TarArchive archive) {
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

View File

@@ -36,6 +36,7 @@ import org.springframework.boot.buildpack.platform.docker.type.ContainerReferenc
import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus;
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.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.io.TarArchive;
@@ -86,9 +87,12 @@ class BuilderTests {
DockerApi docker = mockDockerApi();
Image builderImage = loadImage("image.json");
Image runImage = loadImage("run-image.json");
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), isNull()))
.willAnswer(withPulledImage(builderImage));
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)),
any(), isNull()))
.willAnswer(withPulledImage(runImage));
Builder builder = new Builder(BuildLog.to(out), docker, null);
BuildRequest request = getTestRequest();
@@ -97,9 +101,10 @@ class BuilderTests {
assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
ArgumentCaptor<ImageArchive> archive = ArgumentCaptor.forClass(ImageArchive.class);
then(docker.image()).should()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), isNull());
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), isNull());
then(docker.image()).should()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull());
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)),
any(), isNull());
then(docker.image()).should().load(archive.capture(), any());
then(docker.image()).should().remove(archive.getValue().getTag(), true);
then(docker.image()).shouldHaveNoMoreInteractions();
@@ -115,12 +120,12 @@ class BuilderTests {
.withBuilderRegistryTokenAuthentication("builder token")
.withPublishRegistryTokenAuthentication("publish token");
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(),
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(),
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())))
.willAnswer(withPulledImage(builderImage));
given(docker.image()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(),
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())))
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)),
any(), eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())))
.willAnswer(withPulledImage(runImage));
Builder builder = new Builder(BuildLog.to(out), docker, dockerConfiguration);
BuildRequest request = getTestRequest().withPublish(true);
@@ -129,11 +134,11 @@ class BuilderTests {
assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
ArgumentCaptor<ImageArchive> archive = ArgumentCaptor.forClass(ImageArchive.class);
then(docker.image()).should()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(),
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(),
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()));
then(docker.image()).should()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(),
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()));
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)),
any(), eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()));
then(docker.image()).should()
.push(eq(request.getName()), any(),
eq(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()));
@@ -148,9 +153,12 @@ class BuilderTests {
DockerApi docker = mockDockerApi();
Image builderImage = loadImage("image-with-no-run-image-tag.json");
Image runImage = loadImage("run-image.json");
given(docker.image().pull(eq(ImageReference.of("gcr.io/paketo-buildpacks/builder:latest")), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of("gcr.io/paketo-buildpacks/builder:latest")), isNull(), any(), isNull()))
.willAnswer(withPulledImage(builderImage));
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:latest")), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:latest")), eq(ImagePlatform.from(builderImage)),
any(), isNull()))
.willAnswer(withPulledImage(runImage));
Builder builder = new Builder(BuildLog.to(out), docker, null);
BuildRequest request = getTestRequest().withBuilder(ImageReference.of("gcr.io/paketo-buildpacks/builder"));
@@ -168,12 +176,13 @@ class BuilderTests {
DockerApi docker = mockDockerApi();
Image builderImage = loadImage("image-with-run-image-digest.json");
Image runImage = loadImage("run-image.json");
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), isNull()))
.willAnswer(withPulledImage(builderImage));
given(docker.image()
.pull(eq(ImageReference
.of("docker.io/cloudfoundry/run@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d")),
any(), isNull()))
eq(ImagePlatform.from(builderImage)), any(), isNull()))
.willAnswer(withPulledImage(runImage));
Builder builder = new Builder(BuildLog.to(out), docker, null);
BuildRequest request = getTestRequest();
@@ -191,9 +200,12 @@ class BuilderTests {
DockerApi docker = mockDockerApi();
Image builderImage = loadImage("image-with-empty-stack.json");
Image runImage = loadImage("run-image.json");
given(docker.image().pull(eq(ImageReference.of("gcr.io/paketo-buildpacks/builder:latest")), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of("gcr.io/paketo-buildpacks/builder:latest")), isNull(), any(), isNull()))
.willAnswer(withPulledImage(builderImage));
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)),
any(), isNull()))
.willAnswer(withPulledImage(runImage));
Builder builder = new Builder(BuildLog.to(out), docker, null);
BuildRequest request = getTestRequest().withBuilder(ImageReference.of("gcr.io/paketo-buildpacks/builder"));
@@ -211,9 +223,12 @@ class BuilderTests {
DockerApi docker = mockDockerApi();
Image builderImage = loadImage("image.json");
Image runImage = loadImage("run-image.json");
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), isNull()))
.willAnswer(withPulledImage(builderImage));
given(docker.image().pull(eq(ImageReference.of("example.com/custom/run:latest")), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of("example.com/custom/run:latest")), eq(ImagePlatform.from(builderImage)), any(),
isNull()))
.willAnswer(withPulledImage(runImage));
Builder builder = new Builder(BuildLog.to(out), docker, null);
BuildRequest request = getTestRequest().withRunImage(ImageReference.of("example.com/custom/run:latest"));
@@ -231,9 +246,12 @@ class BuilderTests {
DockerApi docker = mockDockerApi();
Image builderImage = loadImage("image.json");
Image runImage = loadImage("run-image.json");
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), isNull()))
.willAnswer(withPulledImage(builderImage));
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)),
any(), isNull()))
.willAnswer(withPulledImage(runImage));
given(docker.image().inspect(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF))))
.willReturn(builderImage);
@@ -247,7 +265,7 @@ class BuilderTests {
ArgumentCaptor<ImageArchive> archive = ArgumentCaptor.forClass(ImageArchive.class);
then(docker.image()).should().load(archive.capture(), any());
then(docker.image()).should().remove(archive.getValue().getTag(), true);
then(docker.image()).should(never()).pull(any(), any());
then(docker.image()).should(never()).pull(any(), any(), any());
then(docker.image()).should(times(2)).inspect(any());
}
@@ -257,9 +275,12 @@ class BuilderTests {
DockerApi docker = mockDockerApi();
Image builderImage = loadImage("image.json");
Image runImage = loadImage("run-image.json");
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), isNull()))
.willAnswer(withPulledImage(builderImage));
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)),
any(), isNull()))
.willAnswer(withPulledImage(runImage));
given(docker.image().inspect(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF))))
.willReturn(builderImage);
@@ -273,7 +294,7 @@ class BuilderTests {
ArgumentCaptor<ImageArchive> archive = ArgumentCaptor.forClass(ImageArchive.class);
then(docker.image()).should().load(archive.capture(), any());
then(docker.image()).should().remove(archive.getValue().getTag(), true);
then(docker.image()).should(times(2)).pull(any(), any(), isNull());
then(docker.image()).should(times(2)).pull(any(), any(), any(), isNull());
then(docker.image()).should(never()).inspect(any());
}
@@ -283,9 +304,12 @@ class BuilderTests {
DockerApi docker = mockDockerApi();
Image builderImage = loadImage("image.json");
Image runImage = loadImage("run-image.json");
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), isNull()))
.willAnswer(withPulledImage(builderImage));
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)),
any(), isNull()))
.willAnswer(withPulledImage(runImage));
given(docker.image().inspect(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF))))
.willThrow(
@@ -304,7 +328,7 @@ class BuilderTests {
then(docker.image()).should().load(archive.capture(), any());
then(docker.image()).should().remove(archive.getValue().getTag(), true);
then(docker.image()).should(times(2)).inspect(any());
then(docker.image()).should(times(2)).pull(any(), any(), isNull());
then(docker.image()).should(times(2)).pull(any(), any(), any(), isNull());
}
@Test
@@ -313,9 +337,12 @@ class BuilderTests {
DockerApi docker = mockDockerApi();
Image builderImage = loadImage("image.json");
Image runImage = loadImage("run-image.json");
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), isNull()))
.willAnswer(withPulledImage(builderImage));
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)),
any(), isNull()))
.willAnswer(withPulledImage(runImage));
Builder builder = new Builder(BuildLog.to(out), docker, null);
BuildRequest request = getTestRequest().withTags(ImageReference.of("my-application:1.2.3"));
@@ -339,12 +366,12 @@ class BuilderTests {
.withBuilderRegistryTokenAuthentication("builder token")
.withPublishRegistryTokenAuthentication("publish token");
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(),
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(),
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())))
.willAnswer(withPulledImage(builderImage));
given(docker.image()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(),
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())))
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)),
any(), eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())))
.willAnswer(withPulledImage(runImage));
Builder builder = new Builder(BuildLog.to(out), docker, dockerConfiguration);
BuildRequest request = getTestRequest().withPublish(true).withTags(ImageReference.of("my-application:1.2.3"));
@@ -354,11 +381,11 @@ class BuilderTests {
assertThat(out.toString()).contains("Successfully created image tag 'docker.io/library/my-application:1.2.3'");
then(docker.image()).should()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(),
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(),
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()));
then(docker.image()).should()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(),
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()));
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)),
any(), eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()));
then(docker.image()).should()
.push(eq(request.getName()), any(),
eq(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()));
@@ -372,15 +399,46 @@ class BuilderTests {
then(docker.image()).shouldHaveNoMoreInteractions();
}
@Test
void buildInvokesBuilderWithPlatform() throws Exception {
TestPrintStream out = new TestPrintStream();
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
DockerApi docker = mockDockerApi(platform);
Image builderImage = loadImage("image-with-platform.json");
Image runImage = loadImage("run-image-with-platform.json");
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), eq(platform), any(), isNull()))
.willAnswer(withPulledImage(builderImage));
given(docker.image()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(platform), any(), isNull()))
.willAnswer(withPulledImage(runImage));
Builder builder = new Builder(BuildLog.to(out), docker, null);
BuildRequest request = getTestRequest().withImagePlatform("linux/arm64/v1");
builder.build(request);
assertThat(out.toString()).contains("Running creator");
assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
ArgumentCaptor<ImageArchive> archive = ArgumentCaptor.forClass(ImageArchive.class);
then(docker.image()).should()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), eq(platform), any(), isNull());
then(docker.image()).should()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(platform), any(), isNull());
then(docker.image()).should().load(archive.capture(), any());
then(docker.image()).should().remove(archive.getValue().getTag(), true);
then(docker.image()).shouldHaveNoMoreInteractions();
}
@Test
void buildWhenStackIdDoesNotMatchThrowsException() throws Exception {
TestPrintStream out = new TestPrintStream();
DockerApi docker = mockDockerApi();
Image builderImage = loadImage("image.json");
Image runImage = loadImage("run-image-with-bad-stack.json");
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), isNull()))
.willAnswer(withPulledImage(builderImage));
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)),
any(), isNull()))
.willAnswer(withPulledImage(runImage));
Builder builder = new Builder(BuildLog.to(out), docker, null);
BuildRequest request = getTestRequest();
@@ -395,9 +453,12 @@ class BuilderTests {
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_REF)), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), isNull()))
.willAnswer(withPulledImage(builderImage));
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)),
any(), isNull()))
.willAnswer(withPulledImage(runImage));
Builder builder = new Builder(BuildLog.to(out), docker, null);
BuildRequest request = getTestRequest();
@@ -413,7 +474,7 @@ class BuilderTests {
DockerConfiguration dockerConfiguration = new DockerConfiguration()
.withBuilderRegistryTokenAuthentication("builder token");
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(),
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), any(),
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())))
.willAnswer(withPulledImage(builderImage));
Builder builder = new Builder(BuildLog.to(out), docker, dockerConfiguration);
@@ -431,7 +492,7 @@ class BuilderTests {
DockerConfiguration dockerConfiguration = new DockerConfiguration()
.withBuilderRegistryTokenAuthentication("builder token");
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(),
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), any(),
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())))
.willAnswer(withPulledImage(builderImage));
Builder builder = new Builder(BuildLog.to(out), docker, dockerConfiguration);
@@ -447,9 +508,10 @@ class BuilderTests {
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_REF)), any(), isNull()))
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), any(), isNull()))
.willAnswer(withPulledImage(builderImage));
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull()))
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), 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");
@@ -460,9 +522,13 @@ class BuilderTests {
}
private DockerApi mockDockerApi() throws IOException {
return mockDockerApi(null);
}
private DockerApi mockDockerApi(ImagePlatform platform) throws IOException {
ContainerApi containerApi = mock(ContainerApi.class);
ContainerReference reference = ContainerReference.of("container-ref");
given(containerApi.create(any(), any())).willReturn(reference);
given(containerApi.create(any(), eq(platform), any())).willReturn(reference);
given(containerApi.wait(eq(reference))).willReturn(ContainerStatus.of(0, null));
ImageApi imageApi = mock(ImageApi.class);
VolumeApi volumeApi = mock(VolumeApi.class);
@@ -476,7 +542,7 @@ class BuilderTests {
private DockerApi mockDockerApiLifecycleError() throws IOException {
ContainerApi containerApi = mock(ContainerApi.class);
ContainerReference reference = ContainerReference.of("container-ref");
given(containerApi.create(any(), any())).willReturn(reference);
given(containerApi.create(any(), isNull(), any())).willReturn(reference);
given(containerApi.wait(eq(reference))).willReturn(ContainerStatus.of(9, null));
ImageApi imageApi = mock(ImageApi.class);
VolumeApi volumeApi = mock(VolumeApi.class);
@@ -499,7 +565,7 @@ class BuilderTests {
private Answer<Image> withPulledImage(Image image) {
return (invocation) -> {
TotalProgressPullListener listener = invocation.getArgument(1, TotalProgressPullListener.class);
TotalProgressPullListener listener = invocation.getArgument(2, TotalProgressPullListener.class);
listener.onStart();
listener.onFinish();
return image;

View File

@@ -49,6 +49,7 @@ import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig;
import org.springframework.boot.buildpack.platform.docker.type.ContainerContent;
import org.springframework.boot.buildpack.platform.docker.type.ContainerReference;
import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus;
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
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.IOConsumer;
@@ -62,6 +63,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
@@ -92,8 +94,8 @@ class LifecycleTests {
@ParameterizedTest
@BooleanValueSource
void executeExecutesPhases(boolean trustBuilder) throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
createLifecycle(trustBuilder).execute();
if (trustBuilder) {
@@ -111,8 +113,8 @@ class LifecycleTests {
@Test
void executeWithBindingsExecutesPhases() throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
BuildRequest request = getTestRequest(true).withBindings(Binding.of("/host/src/path:/container/dest/path:ro"),
Binding.of("volume-name:/container/volume/path:rw"));
@@ -123,8 +125,8 @@ class LifecycleTests {
@Test
void executeExecutesPhasesWithPlatformApi03() throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
createLifecycle(true, "builder-metadata-platform-api-0.3.json").execute();
assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-platform-api-0.3.json"));
@@ -134,8 +136,8 @@ class LifecycleTests {
@ParameterizedTest
@BooleanValueSource
void executeOnlyUploadsContentOnce(boolean trustBuilder) throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
createLifecycle(trustBuilder).execute();
assertThat(this.content).hasSize(1);
@@ -144,8 +146,8 @@ class LifecycleTests {
@ParameterizedTest
@BooleanValueSource
void executeWhenAlreadyRunThrowsException(boolean trustBuilder) throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
Lifecycle lifecycle = createLifecycle(trustBuilder);
lifecycle.execute();
@@ -156,8 +158,8 @@ class LifecycleTests {
@ParameterizedTest
@BooleanValueSource
void executeWhenBuilderReturnsErrorThrowsException(boolean trustBuilder) throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(9, null));
assertThatExceptionOfType(BuilderException.class).isThrownBy(() -> createLifecycle(trustBuilder).execute())
.withMessage(
@@ -167,8 +169,8 @@ class LifecycleTests {
@ParameterizedTest
@BooleanValueSource
void executeWhenCleanCacheClearsCache(boolean trustBuilder) throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
BuildRequest request = getTestRequest(trustBuilder).withCleanCache(true);
createLifecycle(request).execute();
@@ -188,8 +190,8 @@ class LifecycleTests {
@Test
void executeWhenPlatformApiNotSupportedThrowsException() throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
assertThatIllegalStateException()
.isThrownBy(() -> createLifecycle(true, "builder-metadata-unsupported-api.json").execute())
@@ -198,8 +200,8 @@ class LifecycleTests {
@Test
void executeWhenMultiplePlatformApisNotSupportedThrowsException() throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
assertThatIllegalStateException()
.isThrownBy(() -> createLifecycle(true, "builder-metadata-unsupported-apis.json").execute())
@@ -209,8 +211,8 @@ class LifecycleTests {
@ParameterizedTest
@BooleanValueSource
void executeWhenMultiplePlatformApisSupportedExecutesPhase(boolean trustBuilder) throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
createLifecycle(trustBuilder, "builder-metadata-supported-apis.json").execute();
if (trustBuilder) {
@@ -234,8 +236,8 @@ class LifecycleTests {
@Test
void executeWithNetworkExecutesPhases() throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
BuildRequest request = getTestRequest(true).withNetwork("test");
createLifecycle(request).execute();
@@ -246,8 +248,8 @@ class LifecycleTests {
@ParameterizedTest
@BooleanValueSource
void executeWithCacheVolumeNamesExecutesPhases(boolean trustBuilder) throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
BuildRequest request = getTestRequest(trustBuilder).withBuildWorkspace(Cache.volume("work-volume"))
.withBuildCache(Cache.volume("build-volume"))
@@ -269,8 +271,8 @@ class LifecycleTests {
@ParameterizedTest
@BooleanValueSource
void executeWithCacheBindMountsExecutesPhases(boolean trustBuilder) throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
BuildRequest request = getTestRequest(trustBuilder).withBuildWorkspace(Cache.bind("/tmp/work"))
.withBuildCache(Cache.bind("/tmp/build-cache"))
@@ -292,8 +294,8 @@ class LifecycleTests {
@ParameterizedTest
@BooleanValueSource
void executeWithCreatedDateExecutesPhases(boolean trustBuilder) throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
BuildRequest request = getTestRequest(trustBuilder).withCreatedDate("2020-07-01T12:34:56Z");
createLifecycle(request).execute();
@@ -313,8 +315,8 @@ class LifecycleTests {
@ParameterizedTest
@BooleanValueSource
void executeWithApplicationDirectoryExecutesPhases(boolean trustBuilder) throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
BuildRequest request = getTestRequest(trustBuilder).withApplicationDirectory("/application");
createLifecycle(request).execute();
@@ -334,8 +336,8 @@ class LifecycleTests {
@ParameterizedTest
@BooleanValueSource
void executeWithSecurityOptionsExecutesPhases(boolean trustBuilder) throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
BuildRequest request = getTestRequest(trustBuilder)
.withSecurityOptions(List.of("label=user:USER", "label=role:ROLE"));
@@ -356,8 +358,8 @@ class LifecycleTests {
@ParameterizedTest
@BooleanValueSource
void executeWithDockerHostAndRemoteAddressExecutesPhases(boolean trustBuilder) throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
BuildRequest request = getTestRequest(trustBuilder);
createLifecycle(request, ResolvedDockerHost.from(DockerHostConfiguration.forAddress("tcp://192.168.1.2:2376")))
@@ -378,8 +380,8 @@ class LifecycleTests {
@ParameterizedTest
@BooleanValueSource
void executeWithDockerHostAndLocalAddressExecutesPhases(boolean trustBuilder) throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
BuildRequest request = getTestRequest(trustBuilder);
createLifecycle(request, ResolvedDockerHost.from(DockerHostConfiguration.forAddress("/var/alt.sock")))
@@ -397,6 +399,29 @@ class LifecycleTests {
assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
}
@ParameterizedTest
@BooleanValueSource
void executeWithImagePlatformExecutesPhases(boolean trustBuilder) throws Exception {
given(this.docker.container().create(any(), eq(ImagePlatform.of("linux/arm64"))))
.willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), eq(ImagePlatform.of("linux/arm64")), any()))
.willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
BuildRequest request = getTestRequest(trustBuilder).withImagePlatform("linux/arm64");
createLifecycle(request).execute();
if (trustBuilder) {
assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator.json"));
}
else {
assertPhaseWasRun("analyzer", withExpectedConfig("lifecycle-analyzer.json"));
assertPhaseWasRun("detector", withExpectedConfig("lifecycle-detector.json"));
assertPhaseWasRun("restorer", withExpectedConfig("lifecycle-restorer.json"));
assertPhaseWasRun("builder", withExpectedConfig("lifecycle-builder.json"));
assertPhaseWasRun("exporter", withExpectedConfig("lifecycle-exporter.json"));
}
assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
}
private DockerApi mockDockerApi() {
DockerApi docker = mock(DockerApi.class);
ImageApi imageApi = mock(ImageApi.class);
@@ -458,8 +483,8 @@ class LifecycleTests {
ArrayNode command = getCommand(config);
String name = command.get(0).asText().substring(1).replaceAll("/", "-");
this.configs.put(name, config);
if (invocation.getArguments().length > 1) {
this.content.put(name, invocation.getArgument(1, ContainerContent.class));
if (invocation.getArguments().length > 2) {
this.content.put(name, invocation.getArgument(2, ContainerContent.class));
}
return ContainerReference.of(name);
};

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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.
@@ -28,6 +28,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.buildpack.platform.docker.LogUpdateEvent;
import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent;
import org.springframework.boot.buildpack.platform.docker.type.Image;
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.docker.type.VolumeName;
import org.springframework.util.FileCopyUtils;
@@ -51,6 +52,7 @@ class PrintStreamBuildLogTests {
BuildRequest request = mock(BuildRequest.class);
ImageReference name = ImageReference.of("my-app:latest");
ImageReference builderImageReference = ImageReference.of("cnb/builder");
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
Image builderImage = mock(Image.class);
given(builderImage.getDigests()).willReturn(Collections.singletonList("00000001"));
ImageReference runImageReference = ImageReference.of("cnb/runner");
@@ -60,11 +62,12 @@ class PrintStreamBuildLogTests {
ImageReference tag = ImageReference.of("my-app:1.0");
given(request.getTags()).willReturn(Collections.singletonList(tag));
log.start(request);
Consumer<TotalProgressEvent> pullBuildImageConsumer = log.pullingImage(builderImageReference,
Consumer<TotalProgressEvent> pullBuildImageConsumer = log.pullingImage(builderImageReference, null,
ImageType.BUILDER);
pullBuildImageConsumer.accept(new TotalProgressEvent(100));
log.pulledImage(builderImage, ImageType.BUILDER);
Consumer<TotalProgressEvent> pullRunImageConsumer = log.pullingImage(runImageReference, ImageType.RUNNER);
Consumer<TotalProgressEvent> pullRunImageConsumer = log.pullingImage(runImageReference, platform,
ImageType.RUNNER);
pullRunImageConsumer.accept(new TotalProgressEvent(100));
log.pulledImage(runImage, ImageType.RUNNER);
log.executingLifecycle(request, LifecycleVersion.parse("0.5"), Cache.volume(VolumeName.of("pack-abc.cache")));

View File

@@ -18,15 +18,19 @@ package org.springframework.boot.buildpack.platform.docker;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.message.BasicHeader;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
@@ -39,15 +43,18 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.buildpack.platform.docker.DockerApi.ContainerApi;
import org.springframework.boot.buildpack.platform.docker.DockerApi.ImageApi;
import org.springframework.boot.buildpack.platform.docker.DockerApi.SystemApi;
import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi;
import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport;
import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport.Response;
import org.springframework.boot.buildpack.platform.docker.type.ApiVersion;
import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig;
import org.springframework.boot.buildpack.platform.docker.type.ContainerContent;
import org.springframework.boot.buildpack.platform.docker.type.ContainerReference;
import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus;
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.ImagePlatform;
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.Content;
@@ -80,12 +87,18 @@ import static org.mockito.Mockito.times;
@ExtendWith(MockitoExtension.class)
class DockerApiTests {
private static final String API_URL = "/" + DockerApi.API_VERSION;
private static final String API_URL = "/v" + DockerApi.MINIMUM_API_VERSION;
public static final String PING_URL = "/_ping";
private static final String IMAGES_URL = API_URL + "/images";
private static final String IMAGES_1_41_URL = "/v" + ApiVersion.of(1, 41) + "/images";
private static final String CONTAINERS_URL = API_URL + "/containers";
private static final String CONTAINERS_1_41_URL = "/v" + ApiVersion.of(1, 41) + "/containers";
private static final String VOLUMES_URL = API_URL + "/volumes";
@Mock
@@ -124,6 +137,29 @@ class DockerApiTests {
};
}
private Response responseWithHeaders(Header... headers) {
return new Response() {
@Override
public InputStream getContent() {
return null;
}
@Override
public Header getHeader(String name) {
return Arrays.stream(headers)
.filter((header) -> header.getName().equals(name))
.findFirst()
.orElse(null);
}
@Override
public void close() {
}
};
}
@Test
void createDockerApi() {
DockerApi api = new DockerApi();
@@ -154,13 +190,14 @@ class DockerApiTests {
@Test
void pullWhenReferenceIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.api.pull(null, this.pullListener))
assertThatIllegalArgumentException().isThrownBy(() -> this.api.pull(null, null, this.pullListener))
.withMessage("Reference must not be null");
}
@Test
void pullWhenListenerIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.api.pull(ImageReference.of("ubuntu"), null))
assertThatIllegalArgumentException()
.isThrownBy(() -> this.api.pull(ImageReference.of("ubuntu"), null, null))
.withMessage("Listener must not be null");
}
@@ -171,7 +208,7 @@ class DockerApiTests {
URI imageUri = new URI(IMAGES_URL + "/gcr.io/paketo-buildpacks/builder:base/json");
given(http().post(eq(createUri), isNull())).willReturn(responseOf("pull-stream.json"));
given(http().get(imageUri)).willReturn(responseOf("type/image.json"));
Image image = this.api.pull(reference, this.pullListener);
Image image = this.api.pull(reference, null, this.pullListener);
assertThat(image.getLayers()).hasSize(46);
InOrder ordered = inOrder(this.pullListener);
ordered.verify(this.pullListener).onStart();
@@ -186,7 +223,7 @@ class DockerApiTests {
URI imageUri = new URI(IMAGES_URL + "/gcr.io/paketo-buildpacks/builder:base/json");
given(http().post(eq(createUri), eq("auth token"))).willReturn(responseOf("pull-stream.json"));
given(http().get(imageUri)).willReturn(responseOf("type/image.json"));
Image image = this.api.pull(reference, this.pullListener, "auth token");
Image image = this.api.pull(reference, null, this.pullListener, "auth token");
assertThat(image.getLayers()).hasSize(46);
InOrder ordered = inOrder(this.pullListener);
ordered.verify(this.pullListener).onStart();
@@ -194,6 +231,36 @@ class DockerApiTests {
ordered.verify(this.pullListener).onFinish();
}
@Test
void pullWithPlatformPullsImageAndProducesEvents() throws Exception {
ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base");
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
URI createUri = new URI(IMAGES_1_41_URL
+ "/create?fromImage=gcr.io%2Fpaketo-buildpacks%2Fbuilder%3Abase&platform=linux%2Farm64%2Fv1");
URI imageUri = new URI(IMAGES_1_41_URL + "/gcr.io/paketo-buildpacks/builder:base/json");
given(http().head(eq(new URI(PING_URL))))
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "1.41")));
given(http().post(eq(createUri), isNull())).willReturn(responseOf("pull-stream.json"));
given(http().get(imageUri)).willReturn(responseOf("type/image.json"));
Image image = this.api.pull(reference, platform, this.pullListener);
assertThat(image.getLayers()).hasSize(46);
InOrder ordered = inOrder(this.pullListener);
ordered.verify(this.pullListener).onStart();
ordered.verify(this.pullListener, times(595)).onUpdate(any());
ordered.verify(this.pullListener).onFinish();
}
@Test
void pullWithPlatformAndInsufficientApiVersionThrowsException() throws Exception {
ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base");
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
given(http().head(eq(new URI(PING_URL)))).willReturn(responseWithHeaders(
new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, DockerApi.MINIMUM_API_VERSION)));
assertThatIllegalArgumentException().isThrownBy(() -> this.api.pull(reference, platform, this.pullListener))
.withMessageContaining("must be at least 1.41")
.withMessageContaining("current API version is 1.24");
}
@Test
void pushWhenReferenceIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.api.push(null, this.pushListener, null))
@@ -460,7 +527,7 @@ class DockerApiTests {
@Test
void createWhenConfigIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.api.create(null))
assertThatIllegalArgumentException().isThrownBy(() -> this.api.create(null, null))
.withMessage("Config must not be null");
}
@@ -471,7 +538,7 @@ class DockerApiTests {
URI createUri = new URI(CONTAINERS_URL + "/create");
given(http().post(eq(createUri), eq("application/json"), any()))
.willReturn(responseOf("create-container-response.json"));
ContainerReference containerReference = this.api.create(config);
ContainerReference containerReference = this.api.create(config, null);
assertThat(containerReference).hasToString("e90e34656806");
then(http()).should().post(any(), any(), this.writer.capture());
ByteArrayOutputStream out = new ByteArrayOutputStream();
@@ -493,7 +560,7 @@ class DockerApiTests {
.willReturn(responseOf("create-container-response.json"));
URI uploadUri = new URI(CONTAINERS_URL + "/e90e34656806/archive?path=%2F");
given(http().put(eq(uploadUri), eq("application/x-tar"), any())).willReturn(emptyResponse());
ContainerReference containerReference = this.api.create(config, content);
ContainerReference containerReference = this.api.create(config, null, content);
assertThat(containerReference).hasToString("e90e34656806");
then(http()).should().post(any(), any(), this.writer.capture());
ByteArrayOutputStream out = new ByteArrayOutputStream();
@@ -504,6 +571,34 @@ class DockerApiTests {
assertThat(out.toByteArray()).hasSizeGreaterThan(2000);
}
@Test
void createWithPlatformCreatesContainer() throws Exception {
ImageReference imageReference = ImageReference.of("ubuntu:bionic");
ContainerConfig config = ContainerConfig.of(imageReference, (update) -> update.withCommand("/bin/bash"));
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
given(http().head(eq(new URI(PING_URL))))
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "1.41")));
URI createUri = new URI(CONTAINERS_1_41_URL + "/create?platform=linux%2Farm64%2Fv1");
given(http().post(eq(createUri), eq("application/json"), any()))
.willReturn(responseOf("create-container-response.json"));
ContainerReference containerReference = this.api.create(config, platform);
assertThat(containerReference).hasToString("e90e34656806");
then(http()).should().post(any(), any(), this.writer.capture());
ByteArrayOutputStream out = new ByteArrayOutputStream();
this.writer.getValue().accept(out);
assertThat(out.toByteArray()).hasSize(config.toString().length());
}
@Test
void createWithPlatformAndInsufficientApiVersionThrowsException() throws Exception {
ImageReference imageReference = ImageReference.of("ubuntu:bionic");
ContainerConfig config = ContainerConfig.of(imageReference, (update) -> update.withCommand("/bin/bash"));
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
assertThatIllegalArgumentException().isThrownBy(() -> this.api.create(config, platform))
.withMessageContaining("must be at least 1.41")
.withMessageContaining("current API version is 1.24");
}
@Test
void startWhenReferenceIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.api.start(null))
@@ -621,4 +716,42 @@ class DockerApiTests {
}
@Nested
class SystemDockerApiTests {
private SystemApi api;
@BeforeEach
void setup() {
this.api = DockerApiTests.this.dockerApi.system();
}
@Test
void getApiVersionWithVersionHeaderReturnsVersion() throws Exception {
given(http().head(eq(new URI(PING_URL))))
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "1.44")));
assertThat(this.api.getApiVersion()).isEqualTo(ApiVersion.of(1, 44));
}
@Test
void getApiVersionWithEmptyVersionHeaderReturnsDefaultVersion() throws Exception {
given(http().head(eq(new URI(PING_URL))))
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "")));
assertThat(this.api.getApiVersion()).isEqualTo(DockerApi.MINIMUM_API_VERSION);
}
@Test
void getApiVersionWithNoVersionHeaderReturnsDefaultVersion() throws Exception {
given(http().head(eq(new URI(PING_URL)))).willReturn(emptyResponse());
assertThat(this.api.getApiVersion()).isEqualTo(DockerApi.MINIMUM_API_VERSION);
}
@Test
void getApiVersionWithExceptionReturnsDefaultVersion() throws Exception {
given(http().head(eq(new URI(PING_URL)))).willThrow(new IOException("simulated error"));
assertThat(this.api.getApiVersion()).isEqualTo(DockerApi.MINIMUM_API_VERSION);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.build;
package org.springframework.boot.buildpack.platform.docker.type;
import java.util.Arrays;
@@ -22,7 +22,6 @@ import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link ApiVersion}.
@@ -57,18 +56,6 @@ class ApiVersionTests {
assertThat(version.getMinor()).isEqualTo(2);
}
@Test
void assertSupportsWhenSupports() {
ApiVersion.parse("1.2").assertSupports(ApiVersion.parse("1.0"));
}
@Test
void assertSupportsWhenDoesNotSupportThrowsException() {
assertThatIllegalStateException()
.isThrownBy(() -> ApiVersion.parse("1.2").assertSupports(ApiVersion.parse("1.3")))
.withMessage("Detected platform API version '1.3' does not match supported version '1.2'");
}
@Test
void supportsWhenSame() {
assertThat(supports("0.0", "0.0")).isTrue();

View File

@@ -78,7 +78,7 @@ class ImageArchiveTests extends AbstractJsonTests {
}
private void assertExpectedConfig(TarArchiveEntry entry, byte[] content) throws Exception {
assertThat(entry.getName()).isEqualTo("682f8d24b9d9c313d1190a0e955dcb5e65ec9beea40420999839c6f0cbb38382.json");
assertThat(entry.getName()).isEqualTo("416c76dc7f691f91e80516ff039e056f32f996b59af4b1cb8114e6ae8171a374.json");
String actualJson = new String(content, StandardCharsets.UTF_8);
String expectedJson = StreamUtils.copyToString(getContent("image-archive-config.json"), StandardCharsets.UTF_8);
JSONAssert.assertEquals(expectedJson, actualJson, false);

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2012-2024 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.docker.type;
import java.io.IOException;
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;
class ImagePlatformTests extends AbstractJsonTests {
@Test
void ofWithOsParses() {
ImagePlatform platform = ImagePlatform.of("linux");
assertThat(platform.toString()).isEqualTo("linux");
}
@Test
void ofWithOsAndArchitectureParses() {
ImagePlatform platform = ImagePlatform.of("linux/amd64");
assertThat(platform.toString()).isEqualTo("linux/amd64");
}
@Test
void ofWithOsAndArchitectureAndVariantParses() {
ImagePlatform platform = ImagePlatform.of("linux/amd64/v1");
assertThat(platform.toString()).isEqualTo("linux/amd64/v1");
}
@Test
void ofWithEmptyValueFails() {
assertThatIllegalArgumentException().isThrownBy(() -> ImagePlatform.of(""))
.withMessageContaining("Value must not be empty");
}
@Test
void ofWithTooManySegmentsFails() {
assertThatIllegalArgumentException().isThrownBy(() -> ImagePlatform.of("linux/amd64/v1/extra"))
.withMessageContaining("value 'linux/amd64/v1/extra'");
}
@Test
void fromImageMatchesImage() throws IOException {
ImagePlatform platform = ImagePlatform.from(getImage());
assertThat(platform.toString()).isEqualTo("linux/amd64/v1");
}
private Image getImage() throws IOException {
return Image.of(getContent("image.json"));
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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.
@@ -67,6 +67,18 @@ class ImageTests extends AbstractJsonTests {
assertThat(image.getOs()).isEqualTo("linux");
}
@Test
void getArchitectureReturnsArchitecture() throws Exception {
Image image = getImage();
assertThat(image.getArchitecture()).isEqualTo("amd64");
}
@Test
void getVariantReturnsVariant() throws Exception {
Image image = getImage();
assertThat(image.getVariant()).isEqualTo("v1");
}
@Test
void getCreatedReturnsDate() throws Exception {
Image image = getImage();

View File

@@ -2,7 +2,7 @@ Building image 'docker.io/library/my-app:latest'
> Pulling builder image 'docker.io/cnb/builder' ..................................................
> Pulled builder image '00000001'
> Pulling run image 'docker.io/cnb/runner' ..................................................
> Pulling run image 'docker.io/cnb/runner' for platform 'linux/arm64/v1' ..................................................
> Pulled run image '00000002'
> Executing lifecycle version v0.5.0
> Using build cache volume 'pack-abc.cache'

View File

@@ -0,0 +1,98 @@
{
"Id": "sha256:1332879bc8e38793a45ebe5a750f2a1c35df07ec2aa9c18f694644a9de77359b",
"RepoTags": [
"cloudfoundry/run:base-cnb"
],
"RepoDigests": [
"cloudfoundry/run@sha256:fb5ecb90a42b2067a859aab23fc1f5e9d9c2589d07ba285608879e7baa415aad"
],
"Parent": "",
"Comment": "",
"Created": "2020-03-20T20:18:18.117972538Z",
"Container": "91d1af87c3bb6163cd9c7cb21e6891cd25f5fa3c7417779047776e288c0bc234",
"ContainerConfig": {
"Hostname": "91d1af87c3bb",
"Domainname": "",
"User": "1000:1000",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/sh",
"-c",
"#(nop) ",
"LABEL io.buildpacks.stack.id=io.buildpacks.stacks.bionic"
],
"ArgsEscaped": true,
"Image": "sha256:fbe314bcb23f15a2a09603b6620acd67c332fd08fbf2a7bc3db8fb2f5078d994",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": {
"io.buildpacks.stack.id": "io.buildpacks.stacks.bionic"
}
},
"DockerVersion": "18.09.6",
"Author": "",
"Config": {
"Hostname": "",
"Domainname": "",
"User": "1000:1000",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/bash"
],
"ArgsEscaped": true,
"Image": "sha256:fbe314bcb23f15a2a09603b6620acd67c332fd08fbf2a7bc3db8fb2f5078d994",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": {
"io.buildpacks.stack.id": "io.buildpacks.stacks.bionic"
}
},
"Architecture": "arm64",
"Os": "linux",
"Variant": "v1",
"Size": 71248531,
"VirtualSize": 71248531,
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/17f0a4530fbc3e2982f9dc8feb8c8ddc124473bdd50130dae20856ac597d82dd/diff:/var/lib/docker/overlay2/73dfd4e2075fccb239b3d5e9b33b32b8e410bdc3cd5a620b41346f44cc5c51f7/diff:/var/lib/docker/overlay2/b3924ed7c91730f6714d33c455db888604b59ab093033b3f59ac16ecdd777987/diff:/var/lib/docker/overlay2/e36a32cd0ab20b216a8db1a8a166b17464399e4d587d22504088a7a6ef0a68a4/diff:/var/lib/docker/overlay2/3334e94fe191333b65f571912c0fcfbbf31aeb090a2fb9b4cfdbc32a37c0fe5f/diff",
"MergedDir": "/var/lib/docker/overlay2/8d3f9e3c00bc5072f8051ec7884500ca394f2331d8bcc9452f68d04531f50f82/merged",
"UpperDir": "/var/lib/docker/overlay2/8d3f9e3c00bc5072f8051ec7884500ca394f2331d8bcc9452f68d04531f50f82/diff",
"WorkDir": "/var/lib/docker/overlay2/8d3f9e3c00bc5072f8051ec7884500ca394f2331d8bcc9452f68d04531f50f82/work"
},
"Name": "overlay2"
},
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:c8be1b8f4d60d99c281fc2db75e0f56df42a83ad2f0b091621ce19357e19d853",
"sha256:977183d4e9995d9cd5ffdfc0f29e911ec9de777bcb0f507895daa1068477f76f",
"sha256:6597da2e2e52f4d438ad49a14ca79324f130a9ea08745505aa174a8db51cb79d",
"sha256:16542a8fc3be1bfaff6ed1daa7922e7c3b47b6c3a8d98b7fca58b9517bb99b75",
"sha256:c1daeb79beb276c7441d9a1d7281433e9a7edb9f652b8996ecc62b51e88a47b2",
"sha256:eb195d29dc1aa6e4239f00e7868deebc5ac12bebe76104e0b774c1ef29ca78e3"
]
},
"Metadata": {
"LastTagTime": "0001-01-01T00:00:00Z"
}
}

View File

@@ -1,5 +1,5 @@
{
"config": {
"Config": {
"Hostname": "",
"Domainname": "",
"User": "vcap",
@@ -25,8 +25,8 @@
"io.buildpacks.stack.id": "org.cloudfoundry.stacks.cflinuxfs3"
}
},
"created": "1980-01-01T00:00:01Z",
"history": [
"Created": "1980-01-01T00:00:01Z",
"History": [
{
},
@@ -169,8 +169,10 @@
}
],
"os": "linux",
"rootfs": {
"Architecture": "amd64",
"Os": "linux",
"Variant": "v1",
"RootFS": {
"diff_ids": [
"sha256:733a8e5ce32984099ef675fce04730f6e2a6dcfdf5bd292fea01a8f936265342",
"sha256:7755b972f0b4f49de73ef5114fb3ba9c69d80f217e80da99f56f0d0a5dcb3d70",

View File

@@ -1,6 +1,6 @@
[
{
"Config": "682f8d24b9d9c313d1190a0e955dcb5e65ec9beea40420999839c6f0cbb38382.json",
"Config": "416c76dc7f691f91e80516ff039e056f32f996b59af4b1cb8114e6ae8171a374.json",
"Layers": [
"blank_0",
"blank_1",

View File

@@ -72,9 +72,10 @@
"io.buildpacks.stack.id": "org.cloudfoundry.stacks.cflinuxfs3"
}
},
"Architecture": "amd64",
"Os": "linux",
"Size": 1559461360,
"Architecture": "amd64",
"Variant": "v1",
"Size": 1559461360,
"VirtualSize": 1559461360,
"GraphDriver": {
"Data": {

View File

@@ -43,6 +43,7 @@ import org.junit.jupiter.api.condition.OS;
import org.springframework.boot.buildpack.platform.docker.DockerApi;
import org.springframework.boot.buildpack.platform.docker.DockerApi.ImageApi;
import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi;
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.VolumeName;
@@ -64,8 +65,6 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
@GradleCompatibility(configurationCache = true)
@DisabledIfDockerUnavailable
@DisabledOnOs(os = { OS.LINUX, OS.MAC }, architecture = "aarch64",
disabledReason = "The builder image has no ARM support")
class BootBuildImageIntegrationTests {
GradleBuild gradleBuild;
@@ -407,6 +406,57 @@ class BootBuildImageIntegrationTests {
removeImages(projectName);
}
@TestTemplate
@EnabledOnOs(value = { OS.LINUX, OS.MAC }, architectures = "aarch64",
disabledReason = "Lifecycle will only run on ARM architecture")
void buildsImageOnLinuxArmWithImagePlatformLinuxArm() throws IOException {
writeMainClass();
writeLongNameResource();
String builderImage = "ghcr.io/spring-io/spring-boot-cnb-test-builder:0.0.1";
String runImage = "docker.io/paketobuildpacks/run-jammy-tiny:latest";
String buildpackImage = "ghcr.io/spring-io/spring-boot-test-info:0.0.1";
removeImages(builderImage, runImage, buildpackImage);
BuildResult result = this.gradleBuild.build("bootBuildImage");
String projectName = this.gradleBuild.getProjectDir().getName();
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.getOutput()).contains("docker.io/library/" + projectName);
assertThat(result.getOutput())
.contains("Pulling builder image '" + builderImage + "' for platform 'linux/arm64'");
assertThat(result.getOutput())
.contains("Pulling builder image '" + builderImage + "' for platform 'linux/arm64'");
assertThat(result.getOutput()).contains("Pulling run image '" + runImage + "' for platform 'linux/arm64'");
assertThat(result.getOutput())
.contains("Pulling buildpack image '" + buildpackImage + "' for platform 'linux/arm64'");
assertThat(result.getOutput()).contains("Running detector");
assertThat(result.getOutput()).contains("Running builder");
assertThat(result.getOutput()).contains("---> Test Info buildpack building");
assertThat(result.getOutput()).contains("---> Test Info buildpack done");
removeImages(projectName, builderImage, runImage, buildpackImage);
}
@TestTemplate
@EnabledOnOs(value = { OS.LINUX, OS.MAC }, architectures = "amd64",
disabledReason = "The expected failure condition will not fail on ARM architectures")
void failsWhenBuildingOnLinuxAmdWithImagePlatformLinuxArm() throws IOException {
writeMainClass();
writeLongNameResource();
String builderImage = "ghcr.io/spring-io/spring-boot-cnb-test-builder:0.0.1";
String runImage = "docker.io/paketobuildpacks/run-jammy-tiny:latest";
String buildpackImage = "ghcr.io/spring-io/spring-boot-test-info:0.0.1";
removeImages(builderImage, runImage, buildpackImage);
BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage");
String projectName = this.gradleBuild.getProjectDir().getName();
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.FAILED);
assertThat(result.getOutput()).contains("docker.io/library/" + projectName);
assertThat(result.getOutput())
.contains("Pulling builder image '" + builderImage + "' for platform 'linux/arm64'");
assertThat(result.getOutput()).contains("Pulling run image '" + runImage + "' for platform 'linux/arm64'");
assertThat(result.getOutput())
.contains("Pulling buildpack image '" + buildpackImage + "' for platform 'linux/arm64'");
assertThat(result.getOutput()).contains("exec format error");
removeImages(builderImage, runImage, buildpackImage);
}
@TestTemplate
void failsWithInvalidCreatedDate() throws IOException {
writeMainClass();
@@ -589,7 +639,12 @@ class BootBuildImageIntegrationTests {
private void removeImages(String... names) throws IOException {
ImageApi imageApi = new DockerApi().image();
for (String name : names) {
imageApi.remove(ImageReference.of(name), false);
try {
imageApi.remove(ImageReference.of(name), false);
}
catch (DockerEngineException ex) {
// ignore image remove failures
}
}
}

View File

@@ -75,7 +75,7 @@ class BootBuildImageRegistryIntegrationTests {
.contains("Pushing image '" + imageName + ":latest" + "'")
.contains("Pushed image '" + imageName + ":latest" + "'");
ImageReference imageReference = ImageReference.of(imageName);
Image pulledImage = new DockerApi().image().pull(imageReference, UpdateListener.none());
Image pulledImage = new DockerApi().image().pull(imageReference, null, UpdateListener.none());
assertThat(pulledImage).isNotNull();
new DockerApi().image().remove(imageReference, false);
}

View File

@@ -0,0 +1,11 @@
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
}
bootBuildImage {
builder = "ghcr.io/spring-io/spring-boot-cnb-test-builder:0.0.1"
runImage = "paketobuildpacks/run-jammy-tiny"
buildpacks = ["ghcr.io/spring-io/spring-boot-test-info:0.0.1"]
imagePlatform = "linux/arm64"
}

View File

@@ -0,0 +1,11 @@
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
}
bootBuildImage {
builder = "ghcr.io/spring-io/spring-boot-cnb-test-builder:0.0.1"
runImage = "paketobuildpacks/run-jammy-tiny"
buildpacks = ["ghcr.io/spring-io/spring-boot-test-info:0.0.1"]
imagePlatform = "linux/arm64"
}

View File

@@ -124,7 +124,14 @@ The following table summarizes the available properties and their default values
| `trustBuilder`
| `--trustBuilder`
| Whether to treat the builder as https://buildpacks.io/docs/for-platform-operators/how-to/integrate-ci/pack/concepts/trusted_builders/#what-is-a-trusted-builder[trusted].
| `true` if the builder is one of `paketobuildpacks/builder-jammy-tiny`, `paketobuildpacks/builder-jammy-base`, `paketobuildpacks/builder-jammy-full`, `paketobuildpacks/builder-jammy-buildpackless-tiny`, `paketobuildpacks/builder-jammy-buildpackless-base`, `paketobuildpacks/builder-jammy-buildpackless-full`, `gcr.io/buildpacks/builder`, `heroku/builder`; false otherwise.
| `true` if the builder is one of `paketobuildpacks/builder-jammy-tiny`, `paketobuildpacks/builder-jammy-base`, `paketobuildpacks/builder-jammy-full`, `paketobuildpacks/builder-jammy-buildpackless-tiny`, `paketobuildpacks/builder-jammy-buildpackless-base`, `paketobuildpacks/builder-jammy-buildpackless-full`, `gcr.io/buildpacks/builder`, `heroku/builder`; `false` otherwise.
| `imagePlatform`
| `--image-platform`
a|The platform (operating system and architecture) of any builder, run, and buildpack images that are pulled.
Must be in the form of `OS[/architecture[/variant]]`, such as `linux/amd64`, `linux/arm64`, or `linux/arm/v5`.
Refer to documentation of the builder being used to determine the image OS and architecture options available.
| No default value, indicating that the platform of the host machine should be used.
| `runImage`
| `--runImage`

View File

@@ -326,6 +326,18 @@ public abstract class BootBuildImage extends DefaultTask {
@Option(option = "securityOptions", description = "Security options that will be applied to the builder container")
public abstract ListProperty<String> getSecurityOptions();
/**
* Returns the platform (os/architecture/variant) that will be used for all pulled
* images. When {@code null}, the system will choose a platform based on the host
* operating system and architecture.
* @return the image platform
*/
@Input
@Optional
@Option(option = "imagePlatform",
description = "The platform (os/architecture/variant) that will be used for all pulled images")
public abstract Property<String> getImagePlatform();
/**
* Returns the Docker configuration the builder will use.
* @return docker configuration.
@@ -377,6 +389,9 @@ public abstract class BootBuildImage extends DefaultTask {
request = customizeCreatedDate(request);
request = customizeApplicationDirectory(request);
request = customizeSecurityOptions(request);
if (getImagePlatform().isPresent()) {
request = request.withImagePlatform(getImagePlatform().get());
}
return request;
}

View File

@@ -31,6 +31,7 @@ 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.Binding;
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.gradle.junit.GradleProjectBuilder;
@@ -324,4 +325,15 @@ class BootBuildImageTests {
"label=role:ROLE");
}
@Test
void whenImagePlatformIsNotConfiguredThenRequestHasNoImagePlatform() {
assertThat(this.buildImage.createRequest().getImagePlatform()).isNull();
}
@Test
void whenImagePlatformIsConfiguredThenRequestHasImagePlatform() {
this.buildImage.getImagePlatform().set("linux/arm64/v1");
assertThat(this.buildImage.createRequest().getImagePlatform()).isEqualTo(ImagePlatform.of("linux/arm64/v1"));
}
}

View File

@@ -71,7 +71,7 @@ class BuildImageRegistryIntegrationTests extends AbstractArchiveIntegrationTests
.contains("Pushed image '" + imageName + ":latest" + "'");
ImageReference imageReference = ImageReference.of(imageName);
DockerApi.ImageApi imageApi = new DockerApi().image();
Image pulledImage = imageApi.pull(imageReference, UpdateListener.none());
Image pulledImage = imageApi.pull(imageReference, null, UpdateListener.none());
assertThat(pulledImage).isNotNull();
imageApi.remove(imageReference, false);
});

View File

@@ -31,13 +31,14 @@ import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.buildpack.platform.docker.DockerApi;
import org.springframework.boot.buildpack.platform.docker.DockerApi.ImageApi;
import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi;
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.ImageName;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.docker.type.VolumeName;
import org.springframework.boot.testsupport.container.DisabledIfDockerUnavailable;
import org.springframework.boot.testsupport.junit.DisabledOnOs;
import org.springframework.util.FileSystemUtils;
import static org.assertj.core.api.Assertions.assertThat;
@@ -51,8 +52,6 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
@ExtendWith(MavenBuildExtension.class)
@DisabledIfDockerUnavailable
@DisabledOnOs(os = { OS.LINUX, OS.MAC }, architecture = "aarch64",
disabledReason = "The builder image has no ARM support")
class BuildImageTests extends AbstractArchiveIntegrationTests {
@TestTemplate
@@ -460,7 +459,6 @@ class BuildImageTests extends AbstractArchiveIntegrationTests {
mavenBuild.project("dockerTest", "build-image-created-date")
.goals("package")
.systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT")
.systemProperty("test-build-id", testBuildId)
.execute((project) -> {
assertThat(buildLog(project)).contains("Building image")
.contains("docker.io/library/build-image-created-date:0.0.1.BUILD-SNAPSHOT")
@@ -474,11 +472,9 @@ class BuildImageTests extends AbstractArchiveIntegrationTests {
@TestTemplate
void whenBuildImageIsInvokedWithCurrentCreatedDate(MavenBuild mavenBuild) {
String testBuildId = randomString();
mavenBuild.project("dockerTest", "build-image-current-created-date")
.goals("package")
.systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT")
.systemProperty("test-build-id", testBuildId)
.execute((project) -> {
assertThat(buildLog(project)).contains("Building image")
.contains("docker.io/library/build-image-current-created-date:0.0.1.BUILD-SNAPSHOT")
@@ -497,11 +493,9 @@ class BuildImageTests extends AbstractArchiveIntegrationTests {
@TestTemplate
void whenBuildImageIsInvokedWithApplicationDirectory(MavenBuild mavenBuild) {
String testBuildId = randomString();
mavenBuild.project("dockerTest", "build-image-app-dir")
.goals("package")
.systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT")
.systemProperty("test-build-id", testBuildId)
.execute((project) -> {
assertThat(buildLog(project)).contains("Building image")
.contains("docker.io/library/build-image-app-dir:0.0.1.BUILD-SNAPSHOT")
@@ -512,11 +506,9 @@ class BuildImageTests extends AbstractArchiveIntegrationTests {
@TestTemplate
void whenBuildImageIsInvokedWithEmptySecurityOptions(MavenBuild mavenBuild) {
String testBuildId = randomString();
mavenBuild.project("dockerTest", "build-image-security-opts")
.goals("package")
.systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT")
.systemProperty("test-build-id", testBuildId)
.execute((project) -> {
assertThat(buildLog(project)).contains("Building image")
.contains("docker.io/library/build-image-security-opts:0.0.1.BUILD-SNAPSHOT")
@@ -525,6 +517,49 @@ class BuildImageTests extends AbstractArchiveIntegrationTests {
});
}
@TestTemplate
@EnabledOnOs(value = { OS.LINUX, OS.MAC }, architectures = "aarch64",
disabledReason = "Lifecycle will only run on ARM architecture")
void whenBuildImageIsInvokedOnLinuxArmWithImagePlatformLinuxArm(MavenBuild mavenBuild) throws IOException {
String builderImage = "ghcr.io/spring-io/spring-boot-cnb-test-builder:0.0.1";
String runImage = "docker.io/paketobuildpacks/run-jammy-tiny:latest";
String buildpackImage = "ghcr.io/spring-io/spring-boot-test-info:0.0.1";
removeImages(builderImage, runImage, buildpackImage);
mavenBuild.project("dockerTest", "build-image-platform-linux-arm").goals("package").execute((project) -> {
File jar = new File(project, "target/build-image-platform-linux-arm-0.0.1.BUILD-SNAPSHOT.jar");
assertThat(jar).isFile();
assertThat(buildLog(project)).contains("Building image")
.contains("docker.io/library/build-image-platform-linux-arm:0.0.1.BUILD-SNAPSHOT")
.contains("Pulling builder image '" + builderImage + "' for platform 'linux/arm64'")
.contains("Pulling run image '" + runImage + "' for platform 'linux/arm64'")
.contains("Pulling buildpack image '" + buildpackImage + "' for platform 'linux/arm64'")
.contains("---> Test Info buildpack building")
.contains("---> Test Info buildpack done")
.contains("Successfully built image");
removeImage("docker.io/library/build-image-platform-linux-arm", "0.0.1.BUILD-SNAPSHOT");
});
removeImages(builderImage, runImage, buildpackImage);
}
@TestTemplate
@EnabledOnOs(value = { OS.LINUX, OS.MAC }, architectures = "amd64",
disabledReason = "The expected failure condition will not fail on ARM architectures")
void failsWhenBuildImageIsInvokedOnLinuxAmdWithImagePlatformLinuxArm(MavenBuild mavenBuild) throws IOException {
String builderImage = "ghcr.io/spring-io/spring-boot-cnb-test-builder:0.0.1";
String runImage = "docker.io/paketobuildpacks/run-jammy-tiny:latest";
String buildpackImage = "ghcr.io/spring-io/spring-boot-test-info:0.0.1";
removeImages(buildpackImage, runImage, buildpackImage);
mavenBuild.project("dockerTest", "build-image-platform-linux-arm")
.goals("package")
.executeAndFail((project) -> assertThat(buildLog(project)).contains("Building image")
.contains("docker.io/library/build-image-platform-linux-arm:0.0.1.BUILD-SNAPSHOT")
.contains("Pulling builder image '" + builderImage + "' for platform 'linux/arm64'")
.contains("Pulling run image '" + runImage + "' for platform 'linux/arm64'")
.contains("Pulling buildpack image '" + buildpackImage + "' for platform 'linux/arm64'")
.contains("exec format error"));
removeImages(builderImage, runImage, buildpackImage);
}
@TestTemplate
void failsWhenBuildImageIsInvokedOnMultiModuleProjectWithBuildImageGoal(MavenBuild mavenBuild) {
mavenBuild.project("dockerTest", "build-image-multi-module")
@@ -582,6 +617,18 @@ class BuildImageTests extends AbstractArchiveIntegrationTests {
}
}
private void removeImages(String... names) throws IOException {
ImageApi imageApi = new DockerApi().image();
for (String name : names) {
try {
imageApi.remove(ImageReference.of(name), false);
}
catch (DockerEngineException ex) {
// ignore image remove failures
}
}
}
private void removeImage(String name, String version) {
ImageReference imageReference = ImageReference.of(ImageName.of(name), version);
try {

View File

@@ -0,0 +1,39 @@
<?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-platform-linux-arm</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-no-fork</goal>
</goals>
<configuration>
<image>
<builder>ghcr.io/spring-io/spring-boot-cnb-test-builder:0.0.1</builder>
<runImage>paketobuildpacks/run-jammy-tiny</runImage>
<buildpacks>
<buildpack>ghcr.io/spring-io/spring-boot-test-info:0.0.1</buildpack>
</buildpacks>
<imagePlatform>linux/arm64</imagePlatform>
</image>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2012-2024 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
}
}
}

View File

@@ -140,7 +140,14 @@ The following table summarizes the available parameters and their default values
| `trustBuilder` +
(`spring-boot.build-image.trustBuilder`)
| Whether to treat the builder as https://buildpacks.io/docs/for-platform-operators/how-to/integrate-ci/pack/concepts/trusted_builders/#what-is-a-trusted-builder[trusted].
| `true` if the builder is one of `paketobuildpacks/builder-jammy-tiny`, `paketobuildpacks/builder-jammy-base`, `paketobuildpacks/builder-jammy-full`, `paketobuildpacks/builder-jammy-buildpackless-tiny`, `paketobuildpacks/builder-jammy-buildpackless-base`, `paketobuildpacks/builder-jammy-buildpackless-full`, `gcr.io/buildpacks/builder`, `heroku/builder`; false otherwise.
| `true` if the builder is one of `paketobuildpacks/builder-jammy-tiny`, `paketobuildpacks/builder-jammy-base`, `paketobuildpacks/builder-jammy-full`, `paketobuildpacks/builder-jammy-buildpackless-tiny`, `paketobuildpacks/builder-jammy-buildpackless-base`, `paketobuildpacks/builder-jammy-buildpackless-full`, `gcr.io/buildpacks/builder`, `heroku/builder`; `false` otherwise.
| `imagePlatform` +
(`spring-boot.build-image.imagePlatform`)
a|The platform (operating system and architecture) of any builder, run, and buildpack images that are pulled.
Must be in the form of `OS[/architecture[/variant]]`, such as `linux/amd64`, `linux/arm64`, or `linux/arm/v5`.
Refer to documentation of the builder being used to determine the image OS and architecture options available.
| No default value, indicating that the platform of the host machine should be used.
| `runImage` +
(`spring-boot.build-image.runImage`)

View File

@@ -179,6 +179,14 @@ public abstract class BuildImageMojo extends AbstractPackagerMojo {
@Parameter(property = "spring-boot.build-image.applicationDirectory", readonly = true)
String applicationDirectory;
/**
* Alias for {@link Image#imagePlatform} to support configuration through command-line
* property.
* @since 3.4.0
*/
@Parameter(property = "spring-boot.build-image.imagePlatform", readonly = true)
String imagePlatform;
/**
* Docker configuration options.
* @since 2.4.0
@@ -299,6 +307,9 @@ public abstract class BuildImageMojo extends AbstractPackagerMojo {
if (image.applicationDirectory == null && this.applicationDirectory != null) {
image.setApplicationDirectory(this.applicationDirectory);
}
if (image.imagePlatform == null && this.imagePlatform != null) {
image.setImagePlatform(this.imagePlatform);
}
return customize(image.getBuildRequest(this.project.getArtifact(), content));
}

View File

@@ -83,6 +83,8 @@ public class Image {
List<String> securityOptions;
String imagePlatform;
/**
* The name of the created image.
* @return the image name
@@ -219,6 +221,20 @@ public class Image {
this.applicationDirectory = applicationDirectory;
}
/**
* Returns the platform (os/architecture/variant) that will be used for all pulled
* images. When {@code null}, the system will choose a platform based on the host
* operating system and architecture.
* @return the image platform
*/
public String getImagePlatform() {
return this.imagePlatform;
}
public void setImagePlatform(String imagePlatform) {
this.imagePlatform = imagePlatform;
}
BuildRequest getBuildRequest(Artifact artifact, Function<Owner, TarArchive> applicationContent) {
return customize(BuildRequest.of(getOrDeduceName(artifact), applicationContent));
}
@@ -282,6 +298,9 @@ public class Image {
if (this.securityOptions != null) {
request = request.withSecurityOptions(this.securityOptions);
}
if (this.imagePlatform != null) {
request = request.withImagePlatform(this.imagePlatform);
}
return request;
}

View File

@@ -32,6 +32,7 @@ import org.springframework.boot.buildpack.platform.build.BuildpackReference;
import org.springframework.boot.buildpack.platform.build.Cache;
import org.springframework.boot.buildpack.platform.build.PullPolicy;
import org.springframework.boot.buildpack.platform.docker.type.Binding;
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.io.Owner;
import org.springframework.boot.buildpack.platform.io.TarArchive;
@@ -80,6 +81,7 @@ class ImageTests {
assertThat(request.getBuildpacks()).isEmpty();
assertThat(request.getBindings()).isEmpty();
assertThat(request.getNetwork()).isNull();
assertThat(request.getImagePlatform()).isNull();
}
@Test
@@ -280,6 +282,14 @@ class ImageTests {
assertThat(request.getSecurityOptions()).isEmpty();
}
@Test
void getBuildRequestWhenHasImagePlatformUsesImagePlatform() {
Image image = new Image();
image.imagePlatform = "linux/arm64";
BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent());
assertThat(request.getImagePlatform()).isEqualTo(ImagePlatform.of("linux/arm64"));
}
private Artifact createArtifact() {
return new DefaultArtifact("com.example", "my-app", VersionRange.createFromVersion("0.0.1-SNAPSHOT"), "compile",
"jar", null, new DefaultArtifactHandler());