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 083d32348d0..e5659f9bce4 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 @@ -64,9 +64,11 @@ public class DockerApi { private static final List FORCE_PARAMS = Collections.unmodifiableList(Arrays.asList("force", "1")); - static final ApiVersion MINIMUM_API_VERSION = ApiVersion.of(1, 24); + static final ApiVersion API_VERSION = ApiVersion.of(1, 24); - static final ApiVersion MINIMUM_PLATFORM_API_VERSION = ApiVersion.of(1, 41); + static final ApiVersion PLATFORM_API_VERSION = ApiVersion.of(1, 41); + + static final ApiVersion UNKNOWN_API_VERSION = ApiVersion.of(0, 0); static final String API_VERSION_HEADER_NAME = "API-Version"; @@ -123,12 +125,17 @@ public class DockerApi { } private URI buildUrl(String path, Collection params) { - return buildUrl(path, (params != null) ? params.toArray() : null); + return buildUrl(API_VERSION, path, (params != null) ? params.toArray() : null); } private URI buildUrl(String path, Object... params) { + return buildUrl(API_VERSION, path, params); + } + + private URI buildUrl(ApiVersion apiVersion, String path, Object... params) { + verifyApiVersion(apiVersion); try { - URIBuilder builder = new URIBuilder("/v" + getApiVersion() + path); + URIBuilder builder = new URIBuilder("/v" + apiVersion + path); int param = 0; while (param < params.length) { builder.addParameter(Objects.toString(params[param++]), Objects.toString(params[param++])); @@ -140,10 +147,11 @@ public class DockerApi { } } - private void verifyApiVersionForPlatform(ImagePlatform platform) { - Assert.isTrue(platform == null || getApiVersion().supports(MINIMUM_PLATFORM_API_VERSION), - () -> "Docker API version must be at least " + MINIMUM_PLATFORM_API_VERSION - + " to support the 'imagePlatform' option, but current API version is " + getApiVersion()); + 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() { @@ -213,9 +221,8 @@ public class DockerApi { UpdateListener listener, String registryAuth) throws IOException { Assert.notNull(reference, "Reference must not be null"); Assert.notNull(listener, "Listener must not be null"); - verifyApiVersionForPlatform(platform); URI createUri = (platform != null) - ? buildUrl("/images/create", "fromImage", reference, "platform", platform) + ? buildUrl(PLATFORM_API_VERSION, "/images/create", "fromImage", reference, "platform", platform) : buildUrl("/images/create", "fromImage", reference); DigestCaptureUpdateListener digestCapture = new DigestCaptureUpdateListener(); listener.onStart(); @@ -226,7 +233,7 @@ public class DockerApi { listener.onUpdate(event); }); } - return inspect(reference); + return inspect((platform != null) ? PLATFORM_API_VERSION : API_VERSION, reference); } finally { listener.onFinish(); @@ -353,8 +360,12 @@ public class DockerApi { * @throws IOException on IO error */ public Image inspect(ImageReference reference) throws IOException { + return inspect(API_VERSION, reference); + } + + private Image inspect(ApiVersion apiVersion, ImageReference reference) throws IOException { Assert.notNull(reference, "Reference must not be null"); - URI imageUri = buildUrl("/images/" + reference + "/json"); + URI imageUri = buildUrl(apiVersion, "/images/" + reference + "/json"); try (Response response = http().get(imageUri)) { return Image.of(response.getContent()); } @@ -401,8 +412,8 @@ public class DockerApi { } private ContainerReference createContainer(ContainerConfig config, ImagePlatform platform) throws IOException { - verifyApiVersionForPlatform(platform); - URI createUri = (platform != null) ? buildUrl("/containers/create", "platform", platform) + URI createUri = (platform != null) + ? buildUrl(PLATFORM_API_VERSION, "/containers/create", "platform", platform) : buildUrl("/containers/create"); try (Response response = http().post(createUri, "application/json", config::writeTo)) { return ContainerReference @@ -524,7 +535,7 @@ public class DockerApi { catch (Exception ex) { // fall through to return default value } - return MINIMUM_API_VERSION; + return UNKNOWN_API_VERSION; } catch (URISyntaxException ex) { throw new IllegalStateException(ex); 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 b12bf7f2f64..2f717e44b62 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 @@ -22,6 +22,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; +import java.net.URISyntaxException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; @@ -87,17 +88,19 @@ import static org.mockito.Mockito.times; @ExtendWith(MockitoExtension.class) class DockerApiTests { - private static final String API_URL = "/v" + DockerApi.MINIMUM_API_VERSION; + private static final String API_URL = "/v" + DockerApi.API_VERSION; + + private static final String PLATFORM_API_URL = "/v" + DockerApi.PLATFORM_API_VERSION; public static final String PING_URL = "/_ping"; private static final String IMAGES_URL = API_URL + "/images"; - private static final String IMAGES_1_41_URL = "/v" + ApiVersion.of(1, 41) + "/images"; + private static final String PLATFORM_IMAGES_URL = PLATFORM_API_URL + "/images"; private static final String CONTAINERS_URL = API_URL + "/containers"; - private static final String CONTAINERS_1_41_URL = "/v" + ApiVersion.of(1, 41) + "/containers"; + private static final String PLATFORM_CONTAINERS_URL = PLATFORM_API_URL + "/containers"; private static final String VOLUMES_URL = API_URL + "/volumes"; @@ -235,9 +238,9 @@ class DockerApiTests { void pullWithPlatformPullsImageAndProducesEvents() throws Exception { ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base"); ImagePlatform platform = ImagePlatform.of("linux/arm64/v1"); - URI createUri = new URI(IMAGES_1_41_URL + URI createUri = new URI(PLATFORM_IMAGES_URL + "/create?fromImage=gcr.io%2Fpaketo-buildpacks%2Fbuilder%3Abase&platform=linux%2Farm64%2Fv1"); - URI imageUri = new URI(IMAGES_1_41_URL + "/gcr.io/paketo-buildpacks/builder:base/json"); + URI imageUri = new URI(PLATFORM_IMAGES_URL + "/gcr.io/paketo-buildpacks/builder:base/json"); given(http().head(eq(new URI(PING_URL)))) .willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "1.41"))); given(http().post(eq(createUri), isNull())).willReturn(responseOf("pull-stream.json")); @@ -254,9 +257,9 @@ 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.MINIMUM_API_VERSION))); - assertThatIllegalArgumentException().isThrownBy(() -> this.api.pull(reference, platform, this.pullListener)) + given(http().head(eq(new URI(PING_URL)))).willReturn( + responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, DockerApi.API_VERSION))); + assertThatIllegalStateException().isThrownBy(() -> this.api.pull(reference, platform, this.pullListener)) .withMessageContaining("must be at least 1.41") .withMessageContaining("current API version is 1.24"); } @@ -583,12 +586,23 @@ class DockerApiTests { @Test void createWithPlatformCreatesContainer() throws Exception { + createWithPlatform("1.41"); + } + + @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"); - given(http().head(eq(new URI(PING_URL)))) - .willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "1.41"))); - URI createUri = new URI(CONTAINERS_1_41_URL + "/create?platform=linux%2Farm64%2Fv1"); + 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"); given(http().post(eq(createUri), eq("application/json"), any())) .willReturn(responseOf("create-container-response.json")); ContainerReference containerReference = this.api.create(config, platform); @@ -600,11 +614,13 @@ class DockerApiTests { } @Test - void createWithPlatformAndInsufficientApiVersionThrowsException() { + void createWithPlatformAndKnownInsufficientApiVersionThrowsException() throws Exception { ImageReference imageReference = ImageReference.of("ubuntu:bionic"); ContainerConfig config = ContainerConfig.of(imageReference, (update) -> update.withCommand("/bin/bash")); ImagePlatform platform = ImagePlatform.of("linux/arm64/v1"); - assertThatIllegalArgumentException().isThrownBy(() -> this.api.create(config, platform)) + given(http().head(eq(new URI(PING_URL)))) + .willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "1.24"))); + assertThatIllegalStateException().isThrownBy(() -> this.api.create(config, platform)) .withMessageContaining("must be at least 1.41") .withMessageContaining("current API version is 1.24"); } @@ -744,22 +760,22 @@ class DockerApiTests { } @Test - void getApiVersionWithEmptyVersionHeaderReturnsDefaultVersion() throws Exception { + void getApiVersionWithEmptyVersionHeaderReturnsUnknownVersion() throws Exception { given(http().head(eq(new URI(PING_URL)))) .willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, ""))); - assertThat(this.api.getApiVersion()).isEqualTo(DockerApi.MINIMUM_API_VERSION); + assertThat(this.api.getApiVersion()).isEqualTo(DockerApi.UNKNOWN_API_VERSION); } @Test - void getApiVersionWithNoVersionHeaderReturnsDefaultVersion() throws Exception { + void getApiVersionWithNoVersionHeaderReturnsUnknownVersion() throws Exception { given(http().head(eq(new URI(PING_URL)))).willReturn(emptyResponse()); - assertThat(this.api.getApiVersion()).isEqualTo(DockerApi.MINIMUM_API_VERSION); + assertThat(this.api.getApiVersion()).isEqualTo(DockerApi.UNKNOWN_API_VERSION); } @Test - void getApiVersionWithExceptionReturnsDefaultVersion() throws Exception { + void getApiVersionWithExceptionReturnsUnknownVersion() throws Exception { given(http().head(eq(new URI(PING_URL)))).willThrow(new IOException("simulated error")); - assertThat(this.api.getApiVersion()).isEqualTo(DockerApi.MINIMUM_API_VERSION); + assertThat(this.api.getApiVersion()).isEqualTo(DockerApi.UNKNOWN_API_VERSION); } }