Browse Source

Support recent Docker installs by raising the API version when possible

Update `DockerApi` so that the URL uses version `v1.50` whenever
possible. Prior to this commit, `v1.24` was often used which breaks
recent Docker installs due to the dropping of API version v1.43 and
below.

If the actual API version running is less than `v1.50`, but greater
than the minimum required for the API call, it will be used instead.
This hopefully means that older versions of Docker will continue to
work as they did previously.

Fixes gh-48050
pull/48297/head
Phillip Webb 1 month ago
parent
commit
9b387cbe77
  1. 1
      spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ApiVersions.java
  2. 67
      spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java
  3. 11
      spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ApiVersion.java
  4. 106
      spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java
  5. 11
      spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ApiVersionTests.java

1
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 { @@ -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);
}

67
spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java

@ -64,14 +64,10 @@ public class DockerApi { @@ -64,14 +64,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;
@ -127,17 +123,30 @@ public class DockerApi { @@ -127,17 +123,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) {
@ -151,13 +160,6 @@ public class DockerApi { @@ -151,13 +160,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) {
@ -226,7 +228,7 @@ public class DockerApi { @@ -226,7 +228,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();
@ -388,8 +390,8 @@ public class DockerApi { @@ -388,8 +390,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);
}
@ -435,8 +437,7 @@ public class DockerApi { @@ -435,8 +437,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
@ -634,4 +635,24 @@ public class DockerApi { @@ -634,4 +635,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
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 @@ @@ -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; @@ -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<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;
@ -135,4 +139,9 @@ public final class ApiVersion { @@ -135,4 +139,9 @@ public final class ApiVersion {
return new ApiVersion(major, minor);
}
@Override
public int compareTo(ApiVersion other) {
return COMPARATOR.compare(this, other);
}
}

106
spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java

@ -45,6 +45,7 @@ import org.mockito.Mock; @@ -45,6 +45,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;
@ -90,20 +91,14 @@ import static org.mockito.Mockito.times; @@ -90,20 +91,14 @@ import static org.mockito.Mockito.times;
@ExtendWith(MockitoExtension.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");
@ -176,6 +171,52 @@ class DockerApiTests { @@ -176,6 +171,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 {
@ -244,12 +285,11 @@ class DockerApiTests { @@ -244,12 +285,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);
@ -264,8 +304,7 @@ class DockerApiTests { @@ -264,8 +304,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");
@ -403,10 +442,9 @@ class DockerApiTests { @@ -403,10 +442,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");
@ -416,9 +454,8 @@ class DockerApiTests { @@ -416,9 +454,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");
@ -619,23 +656,27 @@ class DockerApiTests { @@ -619,23 +656,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);
@ -651,8 +692,7 @@ class DockerApiTests { @@ -651,8 +692,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");

11
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 { @@ -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));
}

Loading…
Cancel
Save