Skip to content

Commit

Permalink
Restore use of fixed version when calling docker APIs
Browse files Browse the repository at this point in the history
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
  • Loading branch information
philwebb committed Dec 10, 2024
1 parent 48d51bd commit 123502b
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,11 @@ public class DockerApi {

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";

Expand Down Expand Up @@ -123,12 +125,17 @@ private JsonStream jsonStream() {
}

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++]));
Expand All @@ -140,10 +147,11 @@ private URI buildUrl(String path, Object... params) {
}
}

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() {
Expand Down Expand Up @@ -213,9 +221,8 @@ public Image pull(ImageReference reference, ImagePlatform platform,
UpdateListener<PullImageUpdateEvent> 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();
Expand All @@ -226,7 +233,7 @@ public Image pull(ImageReference reference, ImagePlatform platform,
listener.onUpdate(event);
});
}
return inspect(reference);
return inspect((platform != null) ? PLATFORM_API_VERSION : API_VERSION, reference);
}
finally {
listener.onFinish();
Expand Down Expand Up @@ -353,8 +360,12 @@ public void remove(ImageReference reference, boolean force) throws IOException {
* @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());
}
Expand Down Expand Up @@ -401,8 +412,8 @@ public ContainerReference create(ContainerConfig config, ImagePlatform platform,
}

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
Expand Down Expand Up @@ -524,7 +535,7 @@ ApiVersion getApiVersion() {
catch (Exception ex) {
// fall through to return default value
}
return MINIMUM_API_VERSION;
return UNKNOWN_API_VERSION;
}
catch (URISyntaxException ex) {
throw new IllegalStateException(ex);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
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;
Expand Down Expand Up @@ -87,17 +88,19 @@
@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";

Expand Down Expand Up @@ -235,9 +238,9 @@ void pullWithRegistryAuthPullsImageAndProducesEvents() throws Exception {
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"));
Expand All @@ -254,9 +257,9 @@ void pullWithPlatformPullsImageAndProducesEvents() throws Exception {
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");
}
Expand Down Expand Up @@ -583,12 +586,23 @@ void createWhenHasContentContainerWithContent() throws Exception {

@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);
Expand All @@ -600,11 +614,13 @@ void createWithPlatformCreatesContainer() throws Exception {
}

@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");
}
Expand Down Expand Up @@ -744,22 +760,22 @@ void getApiVersionWithVersionHeaderReturnsVersion() throws Exception {
}

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

}
Expand Down

0 comments on commit 123502b

Please sign in to comment.