diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ApiVersions.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ApiVersions.java index fcd9458c8c2..c9805e34070 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ApiVersions.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ApiVersions.java @@ -69,7 +69,6 @@ final class ApiVersions { if (obj == null || getClass() != obj.getClass()) { return false; } - ApiVersions other = (ApiVersions) obj; return Arrays.equals(this.apiVersions, other.apiVersions); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java index 730ae09f66f..2caf78259a5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java @@ -61,14 +61,10 @@ public class DockerApi { private static final List 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; @@ -143,17 +139,30 @@ public class DockerApi { } private URI buildUrl(String path, 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... params) { - verifyApiVersion(apiVersion); + URI buildUrl(Feature feature, String path, Object... 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[] 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) { @@ -167,13 +176,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 (this.apiVersion == null) { @@ -242,7 +244,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(); @@ -376,8 +378,8 @@ public class DockerApi { private URI inspectUrl(ImageReference reference, 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); } @@ -423,8 +425,7 @@ public class DockerApi { } private ContainerReference createContainer(ContainerConfig config, 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 @@ -626,4 +627,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; + } + + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ApiVersion.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ApiVersion.java index b2b77dc70ea..52110f26ea5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ApiVersion.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ApiVersion.java @@ -16,6 +16,7 @@ package org.springframework.boot.buildpack.platform.docker.type; +import java.util.Comparator; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -28,10 +29,13 @@ import org.springframework.util.Assert; * @author Scott Frederick * @since 3.4.0 */ -public final class ApiVersion { +public final class ApiVersion implements Comparable { private static final Pattern PATTERN = Pattern.compile("^v?(\\d+)\\.(\\d*)$"); + private static final Comparator COMPARATOR = Comparator.comparing(ApiVersion::getMajor) + .thenComparing(ApiVersion::getMinor); + private final int major; private final int minor; @@ -136,4 +140,9 @@ public final class ApiVersion { return new ApiVersion(major, minor); } + @Override + public int compareTo(ApiVersion other) { + return COMPARATOR.compare(this, other); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java index 619d324c25d..8232c92e5f1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java @@ -42,6 +42,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; @@ -89,20 +90,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 { @@ -243,12 +284,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); @@ -263,8 +303,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"); @@ -402,10 +441,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"); @@ -415,9 +453,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"); @@ -580,23 +617,27 @@ 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 void createWithPlatformAndUnknownApiVersionAttemptsCreate() throws Exception { - createWithPlatform(null); - } - - private void createWithPlatform(String apiVersion) throws IOException, URISyntaxException { 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); @@ -612,8 +653,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"); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ApiVersionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ApiVersionTests.java index 04fb20fb920..af6713e1385 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ApiVersionTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ApiVersionTests.java @@ -110,6 +110,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)); }