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:
@@ -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: ")));
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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")));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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'
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[
|
||||
{
|
||||
"Config": "682f8d24b9d9c313d1190a0e955dcb5e65ec9beea40420999839c6f0cbb38382.json",
|
||||
"Config": "416c76dc7f691f91e80516ff039e056f32f996b59af4b1cb8114e6ae8171a374.json",
|
||||
"Layers": [
|
||||
"blank_0",
|
||||
"blank_1",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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`
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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`)
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user