Skip to content

Commit 9b387cb

Browse files
committed
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
1 parent b6460ea commit 9b387cb

File tree

5 files changed

+138
-58
lines changed

5 files changed

+138
-58
lines changed

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ApiVersions.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ public boolean equals(Object obj) {
6969
if (obj == null || getClass() != obj.getClass()) {
7070
return false;
7171
}
72-
7372
ApiVersions other = (ApiVersions) obj;
7473
return Arrays.equals(this.apiVersions, other.apiVersions);
7574
}

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

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,10 @@ public class DockerApi {
6464

6565
private static final List<String> FORCE_PARAMS = Collections.unmodifiableList(Arrays.asList("force", "1"));
6666

67-
static final ApiVersion API_VERSION = ApiVersion.of(1, 24);
68-
69-
static final ApiVersion PLATFORM_API_VERSION = ApiVersion.of(1, 41);
70-
71-
static final ApiVersion PLATFORM_INSPECT_API_VERSION = ApiVersion.of(1, 49);
72-
7367
static final ApiVersion UNKNOWN_API_VERSION = ApiVersion.of(0, 0);
7468

69+
static final ApiVersion PREFERRED_API_VERSION = ApiVersion.of(1, 50);
70+
7571
static final String API_VERSION_HEADER_NAME = "API-Version";
7672

7773
private final HttpTransport http;
@@ -127,17 +123,30 @@ private JsonStream jsonStream() {
127123
}
128124

129125
private URI buildUrl(String path, Collection<?> params) {
130-
return buildUrl(API_VERSION, path, (params != null) ? params.toArray() : null);
126+
return buildUrl(Feature.BASELINE, path, (params != null) ? params.toArray() : null);
131127
}
132128

133129
private URI buildUrl(String path, Object... params) {
134-
return buildUrl(API_VERSION, path, params);
130+
return buildUrl(Feature.BASELINE, path, params);
135131
}
136132

137-
private URI buildUrl(ApiVersion apiVersion, String path, Object... params) {
138-
verifyApiVersion(apiVersion);
133+
URI buildUrl(Feature feature, String path, Object... params) {
134+
ApiVersion version = getApiVersion();
135+
if (version.equals(UNKNOWN_API_VERSION) || (version.compareTo(PREFERRED_API_VERSION) >= 0
136+
&& version.compareTo(feature.minimumVersion()) >= 0)) {
137+
return buildVersionedUrl(PREFERRED_API_VERSION, path, params);
138+
}
139+
if (version.compareTo(feature.minimumVersion()) >= 0) {
140+
return buildVersionedUrl(version, path, params);
141+
}
142+
throw new IllegalStateException(
143+
"Docker API version must be at least %s to support this feature, but current API version is %s"
144+
.formatted(feature.minimumVersion(), version));
145+
}
146+
147+
private URI buildVersionedUrl(ApiVersion version, String path, Object[] params) {
139148
try {
140-
URIBuilder builder = new URIBuilder("/v" + apiVersion + path);
149+
URIBuilder builder = new URIBuilder("/v" + version + path);
141150
if (params != null) {
142151
int param = 0;
143152
while (param < params.length) {
@@ -151,13 +160,6 @@ private URI buildUrl(ApiVersion apiVersion, String path, Object... params) {
151160
}
152161
}
153162

154-
private void verifyApiVersion(ApiVersion minimumVersion) {
155-
ApiVersion actualVersion = getApiVersion();
156-
Assert.state(actualVersion.equals(UNKNOWN_API_VERSION) || actualVersion.supports(minimumVersion),
157-
() -> "Docker API version must be at least " + minimumVersion
158-
+ " to support this feature, but current API version is " + actualVersion);
159-
}
160-
161163
private ApiVersion getApiVersion() {
162164
ApiVersion apiVersion = this.apiVersion;
163165
if (this.apiVersion == null) {
@@ -226,7 +228,7 @@ public Image pull(ImageReference reference, ImagePlatform platform,
226228
Assert.notNull(reference, "Reference must not be null");
227229
Assert.notNull(listener, "Listener must not be null");
228230
URI createUri = (platform != null)
229-
? buildUrl(PLATFORM_API_VERSION, "/images/create", "fromImage", reference, "platform", platform)
231+
? buildUrl(Feature.PLATFORM, "/images/create", "fromImage", reference, "platform", platform)
230232
: buildUrl("/images/create", "fromImage", reference);
231233
DigestCaptureUpdateListener digestCapture = new DigestCaptureUpdateListener();
232234
listener.onStart();
@@ -388,8 +390,8 @@ public Image inspect(ImageReference reference, ImagePlatform platform) throws IO
388390

389391
private URI inspectUrl(ImageReference reference, ImagePlatform platform) {
390392
String path = "/images/" + reference + "/json";
391-
if (platform != null && getApiVersion().supports(PLATFORM_INSPECT_API_VERSION)) {
392-
return buildUrl(PLATFORM_INSPECT_API_VERSION, path, "platform", platform.toJson());
393+
if (platform != null && getApiVersion().supports(Feature.PLATFORM_INSPECT.minimumVersion())) {
394+
return buildUrl(Feature.PLATFORM_INSPECT, path, "platform", platform.toJson());
393395
}
394396
return buildUrl(path);
395397
}
@@ -435,8 +437,7 @@ public ContainerReference create(ContainerConfig config, ImagePlatform platform,
435437
}
436438

437439
private ContainerReference createContainer(ContainerConfig config, ImagePlatform platform) throws IOException {
438-
URI createUri = (platform != null)
439-
? buildUrl(PLATFORM_API_VERSION, "/containers/create", "platform", platform)
440+
URI createUri = (platform != null) ? buildUrl(Feature.PLATFORM, "/containers/create", "platform", platform)
440441
: buildUrl("/containers/create");
441442
try (Response response = http().post(createUri, "application/json", config::writeTo)) {
442443
return ContainerReference
@@ -634,4 +635,24 @@ public void onUpdate(PushImageUpdateEvent event) {
634635

635636
}
636637

638+
enum Feature {
639+
640+
BASELINE(ApiVersion.of(1, 24)),
641+
642+
PLATFORM(ApiVersion.of(1, 41)),
643+
644+
PLATFORM_INSPECT(ApiVersion.of(1, 49));
645+
646+
private final ApiVersion minimumVersion;
647+
648+
Feature(ApiVersion minimumVersion) {
649+
this.minimumVersion = minimumVersion;
650+
}
651+
652+
ApiVersion minimumVersion() {
653+
return this.minimumVersion;
654+
}
655+
656+
}
657+
637658
}

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ApiVersion.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.boot.buildpack.platform.docker.type;
1818

19+
import java.util.Comparator;
1920
import java.util.regex.Matcher;
2021
import java.util.regex.Pattern;
2122

@@ -28,10 +29,13 @@
2829
* @author Scott Frederick
2930
* @since 3.4.0
3031
*/
31-
public final class ApiVersion {
32+
public final class ApiVersion implements Comparable<ApiVersion> {
3233

3334
private static final Pattern PATTERN = Pattern.compile("^v?(\\d+)\\.(\\d*)$");
3435

36+
private static final Comparator<ApiVersion> COMPARATOR = Comparator.comparing(ApiVersion::getMajor)
37+
.thenComparing(ApiVersion::getMinor);
38+
3539
private final int major;
3640

3741
private final int minor;
@@ -135,4 +139,9 @@ public static ApiVersion of(int major, int minor) {
135139
return new ApiVersion(major, minor);
136140
}
137141

142+
@Override
143+
public int compareTo(ApiVersion other) {
144+
return COMPARATOR.compare(this, other);
145+
}
146+
138147
}

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

Lines changed: 73 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import org.mockito.junit.jupiter.MockitoExtension;
4646

4747
import org.springframework.boot.buildpack.platform.docker.DockerApi.ContainerApi;
48+
import org.springframework.boot.buildpack.platform.docker.DockerApi.Feature;
4849
import org.springframework.boot.buildpack.platform.docker.DockerApi.ImageApi;
4950
import org.springframework.boot.buildpack.platform.docker.DockerApi.SystemApi;
5051
import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi;
@@ -90,20 +91,14 @@
9091
@ExtendWith(MockitoExtension.class)
9192
class DockerApiTests {
9293

93-
private static final String API_URL = "/v" + DockerApi.API_VERSION;
94+
private static final String API_URL = "/v" + DockerApi.PREFERRED_API_VERSION;
9495

9596
public static final String PING_URL = "/_ping";
9697

9798
private static final String IMAGES_URL = API_URL + "/images";
9899

99-
private static final String PLATFORM_IMAGES_URL = "/v" + DockerApi.PLATFORM_API_VERSION + "/images";
100-
101-
private static final String PLATFORM_INSPECT_IMAGES_URL = "/v" + DockerApi.PLATFORM_INSPECT_API_VERSION + "/images";
102-
103100
private static final String CONTAINERS_URL = API_URL + "/containers";
104101

105-
private static final String PLATFORM_CONTAINERS_URL = "/v" + DockerApi.PLATFORM_API_VERSION + "/containers";
106-
107102
private static final String VOLUMES_URL = API_URL + "/volumes";
108103

109104
private static final ImagePlatform LINUX_ARM64_PLATFORM = ImagePlatform.of("linux/arm64/v1");
@@ -176,6 +171,52 @@ void createDockerApi() {
176171
assertThat(api).isNotNull();
177172
}
178173

174+
@Test
175+
void buildUrlWhenUnknownVersionUsesPreferredVersion() throws Exception {
176+
setVersion("0.0");
177+
assertThat(this.dockerApi.buildUrl(Feature.BASELINE, "/test"))
178+
.isEqualTo(URI.create("/v" + DockerApi.PREFERRED_API_VERSION + "/test"));
179+
}
180+
181+
@Test
182+
void buildUrlWhenVersionIsGreaterThanPreferredUsesPreferred() throws Exception {
183+
setVersion("1000.0");
184+
assertThat(this.dockerApi.buildUrl(Feature.BASELINE, "/test"))
185+
.isEqualTo(URI.create("/v" + DockerApi.PREFERRED_API_VERSION + "/test"));
186+
}
187+
188+
@Test
189+
void buildUrlWhenVersionIsEqualToPreferredUsesPreferred() throws Exception {
190+
setVersion(DockerApi.PREFERRED_API_VERSION.toString());
191+
assertThat(this.dockerApi.buildUrl(Feature.BASELINE, "/test"))
192+
.isEqualTo(URI.create("/v" + DockerApi.PREFERRED_API_VERSION + "/test"));
193+
}
194+
195+
@Test
196+
void buildUrlWhenVersionIsLessThanPreferredAndGreaterThanMinimumUsesVersionVersion() throws Exception {
197+
setVersion("1.48");
198+
assertThat(this.dockerApi.buildUrl(Feature.BASELINE, "/test")).isEqualTo(URI.create("/v1.48/test"));
199+
}
200+
201+
@Test
202+
void buildUrlWhenVersionIsLessThanPreferredAndEqualToMinimumUsesVersionVersion() throws Exception {
203+
setVersion(Feature.BASELINE.minimumVersion().toString());
204+
assertThat(this.dockerApi.buildUrl(Feature.BASELINE, "/test")).isEqualTo(URI.create("/v1.24/test"));
205+
}
206+
207+
@Test
208+
void buildUrlWhenVersionIsLessThanMinimumThrowsException() throws Exception {
209+
setVersion("1.23");
210+
assertThatIllegalStateException().isThrownBy(() -> this.dockerApi.buildUrl(Feature.BASELINE, "/test"))
211+
.withMessage("Docker API version must be at least 1.24 "
212+
+ "to support this feature, but current API version is 1.23");
213+
}
214+
215+
private void setVersion(String version) throws IOException, URISyntaxException {
216+
given(http().head(eq(new URI(PING_URL))))
217+
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, version)));
218+
}
219+
179220
@Nested
180221
class ImageDockerApiTests {
181222

@@ -244,12 +285,11 @@ void pullWithRegistryAuthPullsImageAndProducesEvents() throws Exception {
244285
@Test
245286
void pullWithPlatformPullsImageAndProducesEvents() throws Exception {
246287
ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base");
247-
URI createUri = new URI(PLATFORM_IMAGES_URL
248-
+ "/create?fromImage=gcr.io%2Fpaketo-buildpacks%2Fbuilder%3Abase&platform=linux%2Farm64%2Fv1");
249-
URI imageUri = new URI(PLATFORM_INSPECT_IMAGES_URL + "/gcr.io/paketo-buildpacks/builder:base/json?platform="
288+
URI createUri = new URI(
289+
"/v1.49/images/create?fromImage=gcr.io%2Fpaketo-buildpacks%2Fbuilder%3Abase&platform=linux%2Farm64%2Fv1");
290+
URI imageUri = new URI("/v1.49/images/gcr.io/paketo-buildpacks/builder:base/json?platform="
250291
+ ENCODED_LINUX_ARM64_PLATFORM_JSON);
251-
given(http().head(eq(new URI(PING_URL))))
252-
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "1.49")));
292+
setVersion("1.49");
253293
given(http().post(eq(createUri), isNull())).willReturn(responseOf("pull-stream.json"));
254294
given(http().get(imageUri)).willReturn(responseOf("type/image.json"));
255295
Image image = this.api.pull(reference, LINUX_ARM64_PLATFORM, this.pullListener);
@@ -264,8 +304,7 @@ void pullWithPlatformPullsImageAndProducesEvents() throws Exception {
264304
void pullWithPlatformAndInsufficientApiVersionThrowsException() throws Exception {
265305
ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base");
266306
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
267-
given(http().head(eq(new URI(PING_URL)))).willReturn(
268-
responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, DockerApi.API_VERSION)));
307+
setVersion("1.24");
269308
assertThatIllegalStateException().isThrownBy(() -> this.api.pull(reference, platform, this.pullListener))
270309
.withMessageContaining("must be at least 1.41")
271310
.withMessageContaining("current API version is 1.24");
@@ -403,10 +442,9 @@ void inspectInspectImage() throws Exception {
403442
@Test
404443
void inspectWithPlatformWhenSupportedVersionInspectImage() throws Exception {
405444
ImageReference reference = ImageReference.of("docker.io/paketobuildpacks/builder:base");
406-
URI imageUri = new URI(PLATFORM_INSPECT_IMAGES_URL
407-
+ "/docker.io/paketobuildpacks/builder:base/json?platform=" + ENCODED_LINUX_ARM64_PLATFORM_JSON);
408-
given(http().head(eq(new URI(PING_URL)))).willReturn(responseWithHeaders(
409-
new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, DockerApi.PLATFORM_INSPECT_API_VERSION)));
445+
URI imageUri = new URI("/v1.49/images/docker.io/paketobuildpacks/builder:base/json?platform="
446+
+ ENCODED_LINUX_ARM64_PLATFORM_JSON);
447+
setVersion("1.49");
410448
given(http().get(imageUri)).willReturn(responseOf("type/image-platform.json"));
411449
Image image = this.api.inspect(reference, LINUX_ARM64_PLATFORM);
412450
assertThat(image.getArchitecture()).isEqualTo("arm64");
@@ -416,9 +454,8 @@ void inspectWithPlatformWhenSupportedVersionInspectImage() throws Exception {
416454
@Test
417455
void inspectWithPlatformWhenOldVersionInspectImage() throws Exception {
418456
ImageReference reference = ImageReference.of("docker.io/paketobuildpacks/builder:base");
419-
URI imageUri = new URI(IMAGES_URL + "/docker.io/paketobuildpacks/builder:base/json");
420-
given(http().head(eq(new URI(PING_URL)))).willReturn(responseWithHeaders(
421-
new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, DockerApi.PLATFORM_API_VERSION)));
457+
URI imageUri = new URI("/v1.48/images/docker.io/paketobuildpacks/builder:base/json");
458+
setVersion("1.48");
422459
given(http().get(imageUri)).willReturn(responseOf("type/image.json"));
423460
Image image = this.api.inspect(reference, LINUX_ARM64_PLATFORM);
424461
assertThat(image.getArchitecture()).isEqualTo("amd64");
@@ -619,23 +656,27 @@ void createWhenHasContentContainerWithContent() throws Exception {
619656

620657
@Test
621658
void createWithPlatformCreatesContainer() throws Exception {
622-
createWithPlatform("1.41");
659+
ImageReference imageReference = ImageReference.of("ubuntu:bionic");
660+
ContainerConfig config = ContainerConfig.of(imageReference, (update) -> update.withCommand("/bin/bash"));
661+
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
662+
setVersion("1.41");
663+
URI createUri = new URI("/v1.41/containers/create?platform=linux%2Farm64%2Fv1");
664+
given(http().post(eq(createUri), eq("application/json"), any()))
665+
.willReturn(responseOf("create-container-response.json"));
666+
ContainerReference containerReference = this.api.create(config, platform);
667+
assertThat(containerReference).hasToString("e90e34656806");
668+
then(http()).should().post(any(), any(), this.writer.capture());
669+
ByteArrayOutputStream out = new ByteArrayOutputStream();
670+
this.writer.getValue().accept(out);
671+
assertThat(out.toByteArray()).hasSize(config.toString().length());
623672
}
624673

625674
@Test
626675
void createWithPlatformAndUnknownApiVersionAttemptsCreate() throws Exception {
627-
createWithPlatform(null);
628-
}
629-
630-
private void createWithPlatform(String apiVersion) throws IOException, URISyntaxException {
631676
ImageReference imageReference = ImageReference.of("ubuntu:bionic");
632677
ContainerConfig config = ContainerConfig.of(imageReference, (update) -> update.withCommand("/bin/bash"));
633678
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
634-
if (apiVersion != null) {
635-
given(http().head(eq(new URI(PING_URL))))
636-
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, apiVersion)));
637-
}
638-
URI createUri = new URI(PLATFORM_CONTAINERS_URL + "/create?platform=linux%2Farm64%2Fv1");
679+
URI createUri = new URI(CONTAINERS_URL + "/create?platform=linux%2Farm64%2Fv1");
639680
given(http().post(eq(createUri), eq("application/json"), any()))
640681
.willReturn(responseOf("create-container-response.json"));
641682
ContainerReference containerReference = this.api.create(config, platform);
@@ -651,8 +692,7 @@ void createWithPlatformAndKnownInsufficientApiVersionThrowsException() throws Ex
651692
ImageReference imageReference = ImageReference.of("ubuntu:bionic");
652693
ContainerConfig config = ContainerConfig.of(imageReference, (update) -> update.withCommand("/bin/bash"));
653694
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
654-
given(http().head(eq(new URI(PING_URL))))
655-
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "1.24")));
695+
setVersion("1.24");
656696
assertThatIllegalStateException().isThrownBy(() -> this.api.create(config, platform))
657697
.withMessageContaining("must be at least 1.41")
658698
.withMessageContaining("current API version is 1.24");

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ApiVersionTests.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,17 @@ void equalsAndHashCode() {
110110
assertThat(v12a).isEqualTo(v12a).isEqualTo(v12b).isNotEqualTo(v13);
111111
}
112112

113+
@Test
114+
void compareTo() {
115+
assertThat(ApiVersion.of(0, 0).compareTo(ApiVersion.of(0, 0))).isZero();
116+
assertThat(ApiVersion.of(0, 1).compareTo(ApiVersion.of(0, 1))).isZero();
117+
assertThat(ApiVersion.of(1, 0).compareTo(ApiVersion.of(1, 0))).isZero();
118+
assertThat(ApiVersion.of(0, 0).compareTo(ApiVersion.of(0, 1))).isLessThan(0);
119+
assertThat(ApiVersion.of(0, 1).compareTo(ApiVersion.of(0, 0))).isGreaterThan(0);
120+
assertThat(ApiVersion.of(1, 0).compareTo(ApiVersion.of(0, 1))).isGreaterThan(0);
121+
assertThat(ApiVersion.of(0, 1).compareTo(ApiVersion.of(1, 0))).isLessThan(0);
122+
}
123+
113124
private boolean supports(String v1, String v2) {
114125
return ApiVersion.parse(v1).supports(ApiVersion.parse(v2));
115126
}

0 commit comments

Comments
 (0)