mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-09-17 12:09:16 +00:00
Merge branch '3.5.x'
Closes gh-48103
This commit is contained in:
-1
@@ -71,7 +71,6 @@ final class ApiVersions {
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ApiVersions other = (ApiVersions) obj;
|
||||
return Arrays.equals(this.apiVersions, other.apiVersions);
|
||||
}
|
||||
|
||||
+10
-1
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.boot.buildpack.platform.docker;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -30,10 +31,13 @@ import org.springframework.util.Assert;
|
||||
* @author Scott Frederick
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public final class ApiVersion {
|
||||
public final class ApiVersion implements Comparable<ApiVersion> {
|
||||
|
||||
private static final Pattern PATTERN = Pattern.compile("^v?(\\d+)\\.(\\d*)$");
|
||||
|
||||
private static final Comparator<ApiVersion> COMPARATOR = Comparator.comparing(ApiVersion::getMajor)
|
||||
.thenComparing(ApiVersion::getMinor);
|
||||
|
||||
private final int major;
|
||||
|
||||
private final int minor;
|
||||
@@ -138,4 +142,9 @@ public final class ApiVersion {
|
||||
return new ApiVersion(major, minor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(ApiVersion other) {
|
||||
return COMPARATOR.compare(this, other);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+44
-23
@@ -62,14 +62,10 @@ public class DockerApi {
|
||||
|
||||
private static final List<String> FORCE_PARAMS = Collections.unmodifiableList(Arrays.asList("force", "1"));
|
||||
|
||||
static final ApiVersion API_VERSION = ApiVersion.of(1, 24);
|
||||
|
||||
static final ApiVersion PLATFORM_API_VERSION = ApiVersion.of(1, 41);
|
||||
|
||||
static final ApiVersion PLATFORM_INSPECT_API_VERSION = ApiVersion.of(1, 49);
|
||||
|
||||
static final ApiVersion UNKNOWN_API_VERSION = ApiVersion.of(0, 0);
|
||||
|
||||
static final ApiVersion PREFERRED_API_VERSION = ApiVersion.of(1, 50);
|
||||
|
||||
static final String API_VERSION_HEADER_NAME = "API-Version";
|
||||
|
||||
private final HttpTransport http;
|
||||
@@ -129,17 +125,30 @@ public class DockerApi {
|
||||
}
|
||||
|
||||
private URI buildUrl(String path, @Nullable Collection<?> params) {
|
||||
return buildUrl(API_VERSION, path, (params != null) ? params.toArray() : null);
|
||||
return buildUrl(Feature.BASELINE, path, (params != null) ? params.toArray() : null);
|
||||
}
|
||||
|
||||
private URI buildUrl(String path, Object... params) {
|
||||
return buildUrl(API_VERSION, path, params);
|
||||
return buildUrl(Feature.BASELINE, path, params);
|
||||
}
|
||||
|
||||
private URI buildUrl(ApiVersion apiVersion, String path, Object @Nullable ... params) {
|
||||
verifyApiVersion(apiVersion);
|
||||
URI buildUrl(Feature feature, String path, Object @Nullable ... params) {
|
||||
ApiVersion version = getApiVersion();
|
||||
if (version.equals(UNKNOWN_API_VERSION) || (version.compareTo(PREFERRED_API_VERSION) >= 0
|
||||
&& version.compareTo(feature.minimumVersion()) >= 0)) {
|
||||
return buildVersionedUrl(PREFERRED_API_VERSION, path, params);
|
||||
}
|
||||
if (version.compareTo(feature.minimumVersion()) >= 0) {
|
||||
return buildVersionedUrl(version, path, params);
|
||||
}
|
||||
throw new IllegalStateException(
|
||||
"Docker API version must be at least %s to support this feature, but current API version is %s"
|
||||
.formatted(feature.minimumVersion(), version));
|
||||
}
|
||||
|
||||
private URI buildVersionedUrl(ApiVersion version, String path, Object @Nullable ... params) {
|
||||
try {
|
||||
URIBuilder builder = new URIBuilder("/v" + apiVersion + path);
|
||||
URIBuilder builder = new URIBuilder("/v" + version + path);
|
||||
if (params != null) {
|
||||
int param = 0;
|
||||
while (param < params.length) {
|
||||
@@ -153,13 +162,6 @@ public class DockerApi {
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyApiVersion(ApiVersion minimumVersion) {
|
||||
ApiVersion actualVersion = getApiVersion();
|
||||
Assert.state(actualVersion.equals(UNKNOWN_API_VERSION) || actualVersion.supports(minimumVersion),
|
||||
() -> "Docker API version must be at least " + minimumVersion
|
||||
+ " to support this feature, but current API version is " + actualVersion);
|
||||
}
|
||||
|
||||
private ApiVersion getApiVersion() {
|
||||
ApiVersion apiVersion = this.apiVersion;
|
||||
if (apiVersion == null) {
|
||||
@@ -228,7 +230,7 @@ public class DockerApi {
|
||||
Assert.notNull(reference, "'reference' must not be null");
|
||||
Assert.notNull(listener, "'listener' must not be null");
|
||||
URI createUri = (platform != null)
|
||||
? buildUrl(PLATFORM_API_VERSION, "/images/create", "fromImage", reference, "platform", platform)
|
||||
? buildUrl(Feature.PLATFORM, "/images/create", "fromImage", reference, "platform", platform)
|
||||
: buildUrl("/images/create", "fromImage", reference);
|
||||
DigestCaptureUpdateListener digestCapture = new DigestCaptureUpdateListener();
|
||||
listener.onStart();
|
||||
@@ -365,8 +367,8 @@ public class DockerApi {
|
||||
|
||||
private URI inspectUrl(ImageReference reference, @Nullable ImagePlatform platform) {
|
||||
String path = "/images/" + reference + "/json";
|
||||
if (platform != null && getApiVersion().supports(PLATFORM_INSPECT_API_VERSION)) {
|
||||
return buildUrl(PLATFORM_INSPECT_API_VERSION, path, "platform", platform.toJson());
|
||||
if (platform != null && getApiVersion().supports(Feature.PLATFORM_INSPECT.minimumVersion())) {
|
||||
return buildUrl(Feature.PLATFORM_INSPECT, path, "platform", platform.toJson());
|
||||
}
|
||||
return buildUrl(path);
|
||||
}
|
||||
@@ -413,8 +415,7 @@ public class DockerApi {
|
||||
|
||||
private ContainerReference createContainer(ContainerConfig config, @Nullable ImagePlatform platform)
|
||||
throws IOException {
|
||||
URI createUri = (platform != null)
|
||||
? buildUrl(PLATFORM_API_VERSION, "/containers/create", "platform", platform)
|
||||
URI createUri = (platform != null) ? buildUrl(Feature.PLATFORM, "/containers/create", "platform", platform)
|
||||
: buildUrl("/containers/create");
|
||||
try (Response response = http().post(createUri, "application/json", config::writeTo)) {
|
||||
return ContainerReference
|
||||
@@ -619,4 +620,24 @@ public class DockerApi {
|
||||
|
||||
}
|
||||
|
||||
enum Feature {
|
||||
|
||||
BASELINE(ApiVersion.of(1, 24)),
|
||||
|
||||
PLATFORM(ApiVersion.of(1, 41)),
|
||||
|
||||
PLATFORM_INSPECT(ApiVersion.of(1, 49));
|
||||
|
||||
private final ApiVersion minimumVersion;
|
||||
|
||||
Feature(ApiVersion minimumVersion) {
|
||||
this.minimumVersion = minimumVersion;
|
||||
}
|
||||
|
||||
ApiVersion minimumVersion() {
|
||||
return this.minimumVersion;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+11
@@ -111,6 +111,17 @@ class ApiVersionTests {
|
||||
assertThat(v12a).isEqualTo(v12a).isEqualTo(v12b).isNotEqualTo(v13);
|
||||
}
|
||||
|
||||
@Test
|
||||
void compareTo() {
|
||||
assertThat(ApiVersion.of(0, 0).compareTo(ApiVersion.of(0, 0))).isZero();
|
||||
assertThat(ApiVersion.of(0, 1).compareTo(ApiVersion.of(0, 1))).isZero();
|
||||
assertThat(ApiVersion.of(1, 0).compareTo(ApiVersion.of(1, 0))).isZero();
|
||||
assertThat(ApiVersion.of(0, 0).compareTo(ApiVersion.of(0, 1))).isLessThan(0);
|
||||
assertThat(ApiVersion.of(0, 1).compareTo(ApiVersion.of(0, 0))).isGreaterThan(0);
|
||||
assertThat(ApiVersion.of(1, 0).compareTo(ApiVersion.of(0, 1))).isGreaterThan(0);
|
||||
assertThat(ApiVersion.of(0, 1).compareTo(ApiVersion.of(1, 0))).isLessThan(0);
|
||||
}
|
||||
|
||||
private boolean supports(String v1, String v2) {
|
||||
return ApiVersion.parse(v1).supports(ApiVersion.parse(v2));
|
||||
}
|
||||
|
||||
+73
-29
@@ -43,6 +43,7 @@ import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.boot.buildpack.platform.docker.DockerApi.ContainerApi;
|
||||
import org.springframework.boot.buildpack.platform.docker.DockerApi.Feature;
|
||||
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;
|
||||
@@ -88,20 +89,14 @@ import static org.mockito.Mockito.times;
|
||||
@ExtendWith({ MockitoExtension.class, OutputCaptureExtension.class })
|
||||
class DockerApiTests {
|
||||
|
||||
private static final String API_URL = "/v" + DockerApi.API_VERSION;
|
||||
private static final String API_URL = "/v" + DockerApi.PREFERRED_API_VERSION;
|
||||
|
||||
public static final String PING_URL = "/_ping";
|
||||
|
||||
private static final String IMAGES_URL = API_URL + "/images";
|
||||
|
||||
private static final String PLATFORM_IMAGES_URL = "/v" + DockerApi.PLATFORM_API_VERSION + "/images";
|
||||
|
||||
private static final String PLATFORM_INSPECT_IMAGES_URL = "/v" + DockerApi.PLATFORM_INSPECT_API_VERSION + "/images";
|
||||
|
||||
private static final String CONTAINERS_URL = API_URL + "/containers";
|
||||
|
||||
private static final String PLATFORM_CONTAINERS_URL = "/v" + DockerApi.PLATFORM_API_VERSION + "/containers";
|
||||
|
||||
private static final String VOLUMES_URL = API_URL + "/volumes";
|
||||
|
||||
private static final ImagePlatform LINUX_ARM64_PLATFORM = ImagePlatform.of("linux/arm64/v1");
|
||||
@@ -175,6 +170,52 @@ class DockerApiTests {
|
||||
assertThat(api).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildUrlWhenUnknownVersionUsesPreferredVersion() throws Exception {
|
||||
setVersion("0.0");
|
||||
assertThat(this.dockerApi.buildUrl(Feature.BASELINE, "/test"))
|
||||
.isEqualTo(URI.create("/v" + DockerApi.PREFERRED_API_VERSION + "/test"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildUrlWhenVersionIsGreaterThanPreferredUsesPreferred() throws Exception {
|
||||
setVersion("1000.0");
|
||||
assertThat(this.dockerApi.buildUrl(Feature.BASELINE, "/test"))
|
||||
.isEqualTo(URI.create("/v" + DockerApi.PREFERRED_API_VERSION + "/test"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildUrlWhenVersionIsEqualToPreferredUsesPreferred() throws Exception {
|
||||
setVersion(DockerApi.PREFERRED_API_VERSION.toString());
|
||||
assertThat(this.dockerApi.buildUrl(Feature.BASELINE, "/test"))
|
||||
.isEqualTo(URI.create("/v" + DockerApi.PREFERRED_API_VERSION + "/test"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildUrlWhenVersionIsLessThanPreferredAndGreaterThanMinimumUsesVersionVersion() throws Exception {
|
||||
setVersion("1.48");
|
||||
assertThat(this.dockerApi.buildUrl(Feature.BASELINE, "/test")).isEqualTo(URI.create("/v1.48/test"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildUrlWhenVersionIsLessThanPreferredAndEqualToMinimumUsesVersionVersion() throws Exception {
|
||||
setVersion(Feature.BASELINE.minimumVersion().toString());
|
||||
assertThat(this.dockerApi.buildUrl(Feature.BASELINE, "/test")).isEqualTo(URI.create("/v1.24/test"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildUrlWhenVersionIsLessThanMinimumThrowsException() throws Exception {
|
||||
setVersion("1.23");
|
||||
assertThatIllegalStateException().isThrownBy(() -> this.dockerApi.buildUrl(Feature.BASELINE, "/test"))
|
||||
.withMessage("Docker API version must be at least 1.24 "
|
||||
+ "to support this feature, but current API version is 1.23");
|
||||
}
|
||||
|
||||
private void setVersion(String version) throws IOException, URISyntaxException {
|
||||
given(http().head(eq(new URI(PING_URL))))
|
||||
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, version)));
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ImageDockerApiTests {
|
||||
|
||||
@@ -249,12 +290,11 @@ class DockerApiTests {
|
||||
@Test
|
||||
void pullWithPlatformPullsImageAndProducesEvents() throws Exception {
|
||||
ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base");
|
||||
URI createUri = new URI(PLATFORM_IMAGES_URL
|
||||
+ "/create?fromImage=gcr.io%2Fpaketo-buildpacks%2Fbuilder%3Abase&platform=linux%2Farm64%2Fv1");
|
||||
URI imageUri = new URI(PLATFORM_INSPECT_IMAGES_URL + "/gcr.io/paketo-buildpacks/builder:base/json?platform="
|
||||
URI createUri = new URI(
|
||||
"/v1.49/images/create?fromImage=gcr.io%2Fpaketo-buildpacks%2Fbuilder%3Abase&platform=linux%2Farm64%2Fv1");
|
||||
URI imageUri = new URI("/v1.49/images/gcr.io/paketo-buildpacks/builder:base/json?platform="
|
||||
+ ENCODED_LINUX_ARM64_PLATFORM_JSON);
|
||||
given(http().head(eq(new URI(PING_URL))))
|
||||
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "1.49")));
|
||||
setVersion("1.49");
|
||||
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, LINUX_ARM64_PLATFORM, this.pullListener);
|
||||
@@ -269,8 +309,7 @@ class DockerApiTests {
|
||||
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.API_VERSION)));
|
||||
setVersion("1.24");
|
||||
assertThatIllegalStateException().isThrownBy(() -> this.api.pull(reference, platform, this.pullListener))
|
||||
.withMessageContaining("must be at least 1.41")
|
||||
.withMessageContaining("current API version is 1.24");
|
||||
@@ -414,10 +453,9 @@ class DockerApiTests {
|
||||
@Test
|
||||
void inspectWithPlatformWhenSupportedVersionInspectImage() throws Exception {
|
||||
ImageReference reference = ImageReference.of("docker.io/paketobuildpacks/builder:base");
|
||||
URI imageUri = new URI(PLATFORM_INSPECT_IMAGES_URL
|
||||
+ "/docker.io/paketobuildpacks/builder:base/json?platform=" + ENCODED_LINUX_ARM64_PLATFORM_JSON);
|
||||
given(http().head(eq(new URI(PING_URL)))).willReturn(responseWithHeaders(
|
||||
new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, DockerApi.PLATFORM_INSPECT_API_VERSION)));
|
||||
URI imageUri = new URI("/v1.49/images/docker.io/paketobuildpacks/builder:base/json?platform="
|
||||
+ ENCODED_LINUX_ARM64_PLATFORM_JSON);
|
||||
setVersion("1.49");
|
||||
given(http().get(imageUri)).willReturn(responseOf("type/image-platform.json"));
|
||||
Image image = this.api.inspect(reference, LINUX_ARM64_PLATFORM);
|
||||
assertThat(image.getArchitecture()).isEqualTo("arm64");
|
||||
@@ -427,9 +465,8 @@ class DockerApiTests {
|
||||
@Test
|
||||
void inspectWithPlatformWhenOldVersionInspectImage() throws Exception {
|
||||
ImageReference reference = ImageReference.of("docker.io/paketobuildpacks/builder:base");
|
||||
URI imageUri = new URI(IMAGES_URL + "/docker.io/paketobuildpacks/builder:base/json");
|
||||
given(http().head(eq(new URI(PING_URL)))).willReturn(responseWithHeaders(
|
||||
new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, DockerApi.PLATFORM_API_VERSION)));
|
||||
URI imageUri = new URI("/v1.48/images/docker.io/paketobuildpacks/builder:base/json");
|
||||
setVersion("1.48");
|
||||
given(http().get(imageUri)).willReturn(responseOf("type/image.json"));
|
||||
Image image = this.api.inspect(reference, LINUX_ARM64_PLATFORM);
|
||||
assertThat(image.getArchitecture()).isEqualTo("amd64");
|
||||
@@ -597,7 +634,19 @@ class DockerApiTests {
|
||||
|
||||
@Test
|
||||
void createWithPlatformCreatesContainer() throws Exception {
|
||||
createWithPlatform("1.41");
|
||||
ImageReference imageReference = ImageReference.of("ubuntu:bionic");
|
||||
ContainerConfig config = ContainerConfig.of(imageReference, (update) -> update.withCommand("/bin/bash"));
|
||||
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
|
||||
setVersion("1.41");
|
||||
URI createUri = new URI("/v1.41/containers/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
|
||||
@@ -609,11 +658,7 @@ class DockerApiTests {
|
||||
ImageReference imageReference = ImageReference.of("ubuntu:bionic");
|
||||
ContainerConfig config = ContainerConfig.of(imageReference, (update) -> update.withCommand("/bin/bash"));
|
||||
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
|
||||
if (apiVersion != null) {
|
||||
given(http().head(eq(new URI(PING_URL))))
|
||||
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, apiVersion)));
|
||||
}
|
||||
URI createUri = new URI(PLATFORM_CONTAINERS_URL + "/create?platform=linux%2Farm64%2Fv1");
|
||||
URI createUri = new URI(CONTAINERS_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);
|
||||
@@ -629,8 +674,7 @@ class DockerApiTests {
|
||||
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.24")));
|
||||
setVersion("1.24");
|
||||
assertThatIllegalStateException().isThrownBy(() -> this.api.create(config, platform))
|
||||
.withMessageContaining("must be at least 1.41")
|
||||
.withMessageContaining("current API version is 1.24");
|
||||
|
||||
Reference in New Issue
Block a user