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