Skip to content

Commit 123502b

Browse files
committed
Restore use of fixed version when calling docker APIs
Update `DockerApi` so that calls are made using a fixed version. For most calls this will be `v1.24`, however, for calls with a platform we must use the `v1.41`. When possible, we check that the Docker version in use meets the required minimum, however, if we can't detect the running version we now proceed and let the actual API call fail. This is due to the fact that the `/_ping` endpoint may not always be available. For example, it is restricted when building from a BitBucket CI pipeline. Fixes gh-43452
1 parent 48d51bd commit 123502b

File tree

2 files changed

+61
-34
lines changed
  • spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src

2 files changed

+61
-34
lines changed

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

+26-15
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,11 @@ public class DockerApi {
6464

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

67-
static final ApiVersion MINIMUM_API_VERSION = ApiVersion.of(1, 24);
67+
static final ApiVersion API_VERSION = ApiVersion.of(1, 24);
6868

69-
static final ApiVersion MINIMUM_PLATFORM_API_VERSION = ApiVersion.of(1, 41);
69+
static final ApiVersion PLATFORM_API_VERSION = ApiVersion.of(1, 41);
70+
71+
static final ApiVersion UNKNOWN_API_VERSION = ApiVersion.of(0, 0);
7072

7173
static final String API_VERSION_HEADER_NAME = "API-Version";
7274

@@ -123,12 +125,17 @@ private JsonStream jsonStream() {
123125
}
124126

125127
private URI buildUrl(String path, Collection<?> params) {
126-
return buildUrl(path, (params != null) ? params.toArray() : null);
128+
return buildUrl(API_VERSION, path, (params != null) ? params.toArray() : null);
127129
}
128130

129131
private URI buildUrl(String path, Object... params) {
132+
return buildUrl(API_VERSION, path, params);
133+
}
134+
135+
private URI buildUrl(ApiVersion apiVersion, String path, Object... params) {
136+
verifyApiVersion(apiVersion);
130137
try {
131-
URIBuilder builder = new URIBuilder("/v" + getApiVersion() + path);
138+
URIBuilder builder = new URIBuilder("/v" + apiVersion + path);
132139
int param = 0;
133140
while (param < params.length) {
134141
builder.addParameter(Objects.toString(params[param++]), Objects.toString(params[param++]));
@@ -140,10 +147,11 @@ private URI buildUrl(String path, Object... params) {
140147
}
141148
}
142149

143-
private void verifyApiVersionForPlatform(ImagePlatform platform) {
144-
Assert.isTrue(platform == null || getApiVersion().supports(MINIMUM_PLATFORM_API_VERSION),
145-
() -> "Docker API version must be at least " + MINIMUM_PLATFORM_API_VERSION
146-
+ " to support the 'imagePlatform' option, but current API version is " + getApiVersion());
150+
private void verifyApiVersion(ApiVersion minimumVersion) {
151+
ApiVersion actualVersion = getApiVersion();
152+
Assert.state(actualVersion.equals(UNKNOWN_API_VERSION) || actualVersion.supports(minimumVersion),
153+
() -> "Docker API version must be at least " + minimumVersion
154+
+ " to support this feature, but current API version is " + actualVersion);
147155
}
148156

149157
private ApiVersion getApiVersion() {
@@ -213,9 +221,8 @@ public Image pull(ImageReference reference, ImagePlatform platform,
213221
UpdateListener<PullImageUpdateEvent> listener, String registryAuth) throws IOException {
214222
Assert.notNull(reference, "Reference must not be null");
215223
Assert.notNull(listener, "Listener must not be null");
216-
verifyApiVersionForPlatform(platform);
217224
URI createUri = (platform != null)
218-
? buildUrl("/images/create", "fromImage", reference, "platform", platform)
225+
? buildUrl(PLATFORM_API_VERSION, "/images/create", "fromImage", reference, "platform", platform)
219226
: buildUrl("/images/create", "fromImage", reference);
220227
DigestCaptureUpdateListener digestCapture = new DigestCaptureUpdateListener();
221228
listener.onStart();
@@ -226,7 +233,7 @@ public Image pull(ImageReference reference, ImagePlatform platform,
226233
listener.onUpdate(event);
227234
});
228235
}
229-
return inspect(reference);
236+
return inspect((platform != null) ? PLATFORM_API_VERSION : API_VERSION, reference);
230237
}
231238
finally {
232239
listener.onFinish();
@@ -353,8 +360,12 @@ public void remove(ImageReference reference, boolean force) throws IOException {
353360
* @throws IOException on IO error
354361
*/
355362
public Image inspect(ImageReference reference) throws IOException {
363+
return inspect(API_VERSION, reference);
364+
}
365+
366+
private Image inspect(ApiVersion apiVersion, ImageReference reference) throws IOException {
356367
Assert.notNull(reference, "Reference must not be null");
357-
URI imageUri = buildUrl("/images/" + reference + "/json");
368+
URI imageUri = buildUrl(apiVersion, "/images/" + reference + "/json");
358369
try (Response response = http().get(imageUri)) {
359370
return Image.of(response.getContent());
360371
}
@@ -401,8 +412,8 @@ public ContainerReference create(ContainerConfig config, ImagePlatform platform,
401412
}
402413

403414
private ContainerReference createContainer(ContainerConfig config, ImagePlatform platform) throws IOException {
404-
verifyApiVersionForPlatform(platform);
405-
URI createUri = (platform != null) ? buildUrl("/containers/create", "platform", platform)
415+
URI createUri = (platform != null)
416+
? buildUrl(PLATFORM_API_VERSION, "/containers/create", "platform", platform)
406417
: buildUrl("/containers/create");
407418
try (Response response = http().post(createUri, "application/json", config::writeTo)) {
408419
return ContainerReference
@@ -524,7 +535,7 @@ ApiVersion getApiVersion() {
524535
catch (Exception ex) {
525536
// fall through to return default value
526537
}
527-
return MINIMUM_API_VERSION;
538+
return UNKNOWN_API_VERSION;
528539
}
529540
catch (URISyntaxException ex) {
530541
throw new IllegalStateException(ex);

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

+35-19
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.io.InputStream;
2323
import java.io.OutputStream;
2424
import java.net.URI;
25+
import java.net.URISyntaxException;
2526
import java.nio.file.Path;
2627
import java.util.ArrayList;
2728
import java.util.Arrays;
@@ -87,17 +88,19 @@
8788
@ExtendWith(MockitoExtension.class)
8889
class DockerApiTests {
8990

90-
private static final String API_URL = "/v" + DockerApi.MINIMUM_API_VERSION;
91+
private static final String API_URL = "/v" + DockerApi.API_VERSION;
92+
93+
private static final String PLATFORM_API_URL = "/v" + DockerApi.PLATFORM_API_VERSION;
9194

9295
public static final String PING_URL = "/_ping";
9396

9497
private static final String IMAGES_URL = API_URL + "/images";
9598

96-
private static final String IMAGES_1_41_URL = "/v" + ApiVersion.of(1, 41) + "/images";
99+
private static final String PLATFORM_IMAGES_URL = PLATFORM_API_URL + "/images";
97100

98101
private static final String CONTAINERS_URL = API_URL + "/containers";
99102

100-
private static final String CONTAINERS_1_41_URL = "/v" + ApiVersion.of(1, 41) + "/containers";
103+
private static final String PLATFORM_CONTAINERS_URL = PLATFORM_API_URL + "/containers";
101104

102105
private static final String VOLUMES_URL = API_URL + "/volumes";
103106

@@ -235,9 +238,9 @@ void pullWithRegistryAuthPullsImageAndProducesEvents() throws Exception {
235238
void pullWithPlatformPullsImageAndProducesEvents() throws Exception {
236239
ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base");
237240
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
238-
URI createUri = new URI(IMAGES_1_41_URL
241+
URI createUri = new URI(PLATFORM_IMAGES_URL
239242
+ "/create?fromImage=gcr.io%2Fpaketo-buildpacks%2Fbuilder%3Abase&platform=linux%2Farm64%2Fv1");
240-
URI imageUri = new URI(IMAGES_1_41_URL + "/gcr.io/paketo-buildpacks/builder:base/json");
243+
URI imageUri = new URI(PLATFORM_IMAGES_URL + "/gcr.io/paketo-buildpacks/builder:base/json");
241244
given(http().head(eq(new URI(PING_URL))))
242245
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "1.41")));
243246
given(http().post(eq(createUri), isNull())).willReturn(responseOf("pull-stream.json"));
@@ -254,9 +257,9 @@ void pullWithPlatformPullsImageAndProducesEvents() throws Exception {
254257
void pullWithPlatformAndInsufficientApiVersionThrowsException() throws Exception {
255258
ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base");
256259
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
257-
given(http().head(eq(new URI(PING_URL)))).willReturn(responseWithHeaders(
258-
new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, DockerApi.MINIMUM_API_VERSION)));
259-
assertThatIllegalArgumentException().isThrownBy(() -> this.api.pull(reference, platform, this.pullListener))
260+
given(http().head(eq(new URI(PING_URL)))).willReturn(
261+
responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, DockerApi.API_VERSION)));
262+
assertThatIllegalStateException().isThrownBy(() -> this.api.pull(reference, platform, this.pullListener))
260263
.withMessageContaining("must be at least 1.41")
261264
.withMessageContaining("current API version is 1.24");
262265
}
@@ -583,12 +586,23 @@ void createWhenHasContentContainerWithContent() throws Exception {
583586

584587
@Test
585588
void createWithPlatformCreatesContainer() throws Exception {
589+
createWithPlatform("1.41");
590+
}
591+
592+
@Test
593+
void createWithPlatformAndUnknownApiVersionAttemptsCreate() throws Exception {
594+
createWithPlatform(null);
595+
}
596+
597+
private void createWithPlatform(String apiVersion) throws IOException, URISyntaxException {
586598
ImageReference imageReference = ImageReference.of("ubuntu:bionic");
587599
ContainerConfig config = ContainerConfig.of(imageReference, (update) -> update.withCommand("/bin/bash"));
588600
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
589-
given(http().head(eq(new URI(PING_URL))))
590-
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "1.41")));
591-
URI createUri = new URI(CONTAINERS_1_41_URL + "/create?platform=linux%2Farm64%2Fv1");
601+
if (apiVersion != null) {
602+
given(http().head(eq(new URI(PING_URL))))
603+
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, apiVersion)));
604+
}
605+
URI createUri = new URI(PLATFORM_CONTAINERS_URL + "/create?platform=linux%2Farm64%2Fv1");
592606
given(http().post(eq(createUri), eq("application/json"), any()))
593607
.willReturn(responseOf("create-container-response.json"));
594608
ContainerReference containerReference = this.api.create(config, platform);
@@ -600,11 +614,13 @@ void createWithPlatformCreatesContainer() throws Exception {
600614
}
601615

602616
@Test
603-
void createWithPlatformAndInsufficientApiVersionThrowsException() {
617+
void createWithPlatformAndKnownInsufficientApiVersionThrowsException() throws Exception {
604618
ImageReference imageReference = ImageReference.of("ubuntu:bionic");
605619
ContainerConfig config = ContainerConfig.of(imageReference, (update) -> update.withCommand("/bin/bash"));
606620
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
607-
assertThatIllegalArgumentException().isThrownBy(() -> this.api.create(config, platform))
621+
given(http().head(eq(new URI(PING_URL))))
622+
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "1.24")));
623+
assertThatIllegalStateException().isThrownBy(() -> this.api.create(config, platform))
608624
.withMessageContaining("must be at least 1.41")
609625
.withMessageContaining("current API version is 1.24");
610626
}
@@ -744,22 +760,22 @@ void getApiVersionWithVersionHeaderReturnsVersion() throws Exception {
744760
}
745761

746762
@Test
747-
void getApiVersionWithEmptyVersionHeaderReturnsDefaultVersion() throws Exception {
763+
void getApiVersionWithEmptyVersionHeaderReturnsUnknownVersion() throws Exception {
748764
given(http().head(eq(new URI(PING_URL))))
749765
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "")));
750-
assertThat(this.api.getApiVersion()).isEqualTo(DockerApi.MINIMUM_API_VERSION);
766+
assertThat(this.api.getApiVersion()).isEqualTo(DockerApi.UNKNOWN_API_VERSION);
751767
}
752768

753769
@Test
754-
void getApiVersionWithNoVersionHeaderReturnsDefaultVersion() throws Exception {
770+
void getApiVersionWithNoVersionHeaderReturnsUnknownVersion() throws Exception {
755771
given(http().head(eq(new URI(PING_URL)))).willReturn(emptyResponse());
756-
assertThat(this.api.getApiVersion()).isEqualTo(DockerApi.MINIMUM_API_VERSION);
772+
assertThat(this.api.getApiVersion()).isEqualTo(DockerApi.UNKNOWN_API_VERSION);
757773
}
758774

759775
@Test
760-
void getApiVersionWithExceptionReturnsDefaultVersion() throws Exception {
776+
void getApiVersionWithExceptionReturnsUnknownVersion() throws Exception {
761777
given(http().head(eq(new URI(PING_URL)))).willThrow(new IOException("simulated error"));
762-
assertThat(this.api.getApiVersion()).isEqualTo(DockerApi.MINIMUM_API_VERSION);
778+
assertThat(this.api.getApiVersion()).isEqualTo(DockerApi.UNKNOWN_API_VERSION);
763779
}
764780

765781
}

0 commit comments

Comments
 (0)