Browse Source

Merge branch '3.4.x' into 3.5.x

Closes gh-48102
pull/48129/head
Phillip Webb 2 months ago
parent
commit
d1dc9579eb
  1. 1
      spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ApiVersions.java
  2. 67
      spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java
  3. 11
      spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ApiVersion.java
  4. 106
      spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java
  5. 11
      spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ApiVersionTests.java

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

@ -69,7 +69,6 @@ final class ApiVersions { @@ -69,7 +69,6 @@ final class ApiVersions {
if (obj == null || getClass() != obj.getClass()) {
return false;
}
ApiVersions other = (ApiVersions) obj;
return Arrays.equals(this.apiVersions, other.apiVersions);
}

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

@ -61,14 +61,10 @@ public class DockerApi { @@ -61,14 +61,10 @@ public class DockerApi {
private static final List<String> FORCE_PARAMS = Collections.unmodifiableList(Arrays.asList("force", "1"));
static final ApiVersion API_VERSION = ApiVersion.of(1, 24);
static final ApiVersion PLATFORM_API_VERSION = ApiVersion.of(1, 41);
static final ApiVersion PLATFORM_INSPECT_API_VERSION = ApiVersion.of(1, 49);
static final ApiVersion UNKNOWN_API_VERSION = ApiVersion.of(0, 0);
static final ApiVersion PREFERRED_API_VERSION = ApiVersion.of(1, 50);
static final String API_VERSION_HEADER_NAME = "API-Version";
private final HttpTransport http;
@ -143,17 +139,30 @@ public class DockerApi { @@ -143,17 +139,30 @@ public class DockerApi {
}
private URI buildUrl(String path, Collection<?> params) {
return buildUrl(API_VERSION, path, (params != null) ? params.toArray() : null);
return buildUrl(Feature.BASELINE, path, (params != null) ? params.toArray() : null);
}
private URI buildUrl(String path, Object... params) {
return buildUrl(API_VERSION, path, params);
return buildUrl(Feature.BASELINE, path, params);
}
private URI buildUrl(ApiVersion apiVersion, String path, Object... params) {
verifyApiVersion(apiVersion);
URI buildUrl(Feature feature, String path, Object... params) {
ApiVersion version = getApiVersion();
if (version.equals(UNKNOWN_API_VERSION) || (version.compareTo(PREFERRED_API_VERSION) >= 0
&& version.compareTo(feature.minimumVersion()) >= 0)) {
return buildVersionedUrl(PREFERRED_API_VERSION, path, params);
}
if (version.compareTo(feature.minimumVersion()) >= 0) {
return buildVersionedUrl(version, path, params);
}
throw new IllegalStateException(
"Docker API version must be at least %s to support this feature, but current API version is %s"
.formatted(feature.minimumVersion(), version));
}
private URI buildVersionedUrl(ApiVersion version, String path, Object[] params) {
try {
URIBuilder builder = new URIBuilder("/v" + apiVersion + path);
URIBuilder builder = new URIBuilder("/v" + version + path);
if (params != null) {
int param = 0;
while (param < params.length) {
@ -167,13 +176,6 @@ public class DockerApi { @@ -167,13 +176,6 @@ public class DockerApi {
}
}
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() {
ApiVersion apiVersion = this.apiVersion;
if (this.apiVersion == null) {
@ -242,7 +244,7 @@ public class DockerApi { @@ -242,7 +244,7 @@ public class DockerApi {
Assert.notNull(reference, "'reference' must not be null");
Assert.notNull(listener, "'listener' must not be null");
URI createUri = (platform != null)
? buildUrl(PLATFORM_API_VERSION, "/images/create", "fromImage", reference, "platform", platform)
? buildUrl(Feature.PLATFORM, "/images/create", "fromImage", reference, "platform", platform)
: buildUrl("/images/create", "fromImage", reference);
DigestCaptureUpdateListener digestCapture = new DigestCaptureUpdateListener();
listener.onStart();
@ -376,8 +378,8 @@ public class DockerApi { @@ -376,8 +378,8 @@ public class DockerApi {
private URI inspectUrl(ImageReference reference, ImagePlatform platform) {
String path = "/images/" + reference + "/json";
if (platform != null && getApiVersion().supports(PLATFORM_INSPECT_API_VERSION)) {
return buildUrl(PLATFORM_INSPECT_API_VERSION, path, "platform", platform.toJson());
if (platform != null && getApiVersion().supports(Feature.PLATFORM_INSPECT.minimumVersion())) {
return buildUrl(Feature.PLATFORM_INSPECT, path, "platform", platform.toJson());
}
return buildUrl(path);
}
@ -423,8 +425,7 @@ public class DockerApi { @@ -423,8 +425,7 @@ public class DockerApi {
}
private ContainerReference createContainer(ContainerConfig config, ImagePlatform platform) throws IOException {
URI createUri = (platform != null)
? buildUrl(PLATFORM_API_VERSION, "/containers/create", "platform", platform)
URI createUri = (platform != null) ? buildUrl(Feature.PLATFORM, "/containers/create", "platform", platform)
: buildUrl("/containers/create");
try (Response response = http().post(createUri, "application/json", config::writeTo)) {
return ContainerReference
@ -626,4 +627,24 @@ public class DockerApi { @@ -626,4 +627,24 @@ public class DockerApi {
}
enum Feature {
BASELINE(ApiVersion.of(1, 24)),
PLATFORM(ApiVersion.of(1, 41)),
PLATFORM_INSPECT(ApiVersion.of(1, 49));
private final ApiVersion minimumVersion;
Feature(ApiVersion minimumVersion) {
this.minimumVersion = minimumVersion;
}
ApiVersion minimumVersion() {
return this.minimumVersion;
}
}
}

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

@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
package org.springframework.boot.buildpack.platform.docker.type;
import java.util.Comparator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -28,10 +29,13 @@ import org.springframework.util.Assert; @@ -28,10 +29,13 @@ import org.springframework.util.Assert;
* @author Scott Frederick
* @since 3.4.0
*/
public final class ApiVersion {
public final class ApiVersion implements Comparable<ApiVersion> {
private static final Pattern PATTERN = Pattern.compile("^v?(\\d+)\\.(\\d*)$");
private static final Comparator<ApiVersion> COMPARATOR = Comparator.comparing(ApiVersion::getMajor)
.thenComparing(ApiVersion::getMinor);
private final int major;
private final int minor;
@ -136,4 +140,9 @@ public final class ApiVersion { @@ -136,4 +140,9 @@ public final class ApiVersion {
return new ApiVersion(major, minor);
}
@Override
public int compareTo(ApiVersion other) {
return COMPARATOR.compare(this, other);
}
}

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

@ -42,6 +42,7 @@ import org.mockito.Mock; @@ -42,6 +42,7 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.buildpack.platform.docker.DockerApi.ContainerApi;
import org.springframework.boot.buildpack.platform.docker.DockerApi.Feature;
import org.springframework.boot.buildpack.platform.docker.DockerApi.ImageApi;
import org.springframework.boot.buildpack.platform.docker.DockerApi.SystemApi;
import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi;
@ -89,20 +90,14 @@ import static org.mockito.Mockito.times; @@ -89,20 +90,14 @@ import static org.mockito.Mockito.times;
@ExtendWith({ MockitoExtension.class, OutputCaptureExtension.class })
class DockerApiTests {
private static final String API_URL = "/v" + DockerApi.API_VERSION;
private static final String API_URL = "/v" + DockerApi.PREFERRED_API_VERSION;
public static final String PING_URL = "/_ping";
private static final String IMAGES_URL = API_URL + "/images";
private static final String PLATFORM_IMAGES_URL = "/v" + DockerApi.PLATFORM_API_VERSION + "/images";
private static final String PLATFORM_INSPECT_IMAGES_URL = "/v" + DockerApi.PLATFORM_INSPECT_API_VERSION + "/images";
private static final String CONTAINERS_URL = API_URL + "/containers";
private static final String PLATFORM_CONTAINERS_URL = "/v" + DockerApi.PLATFORM_API_VERSION + "/containers";
private static final String VOLUMES_URL = API_URL + "/volumes";
private static final ImagePlatform LINUX_ARM64_PLATFORM = ImagePlatform.of("linux/arm64/v1");
@ -175,6 +170,52 @@ class DockerApiTests { @@ -175,6 +170,52 @@ class DockerApiTests {
assertThat(api).isNotNull();
}
@Test
void buildUrlWhenUnknownVersionUsesPreferredVersion() throws Exception {
setVersion("0.0");
assertThat(this.dockerApi.buildUrl(Feature.BASELINE, "/test"))
.isEqualTo(URI.create("/v" + DockerApi.PREFERRED_API_VERSION + "/test"));
}
@Test
void buildUrlWhenVersionIsGreaterThanPreferredUsesPreferred() throws Exception {
setVersion("1000.0");
assertThat(this.dockerApi.buildUrl(Feature.BASELINE, "/test"))
.isEqualTo(URI.create("/v" + DockerApi.PREFERRED_API_VERSION + "/test"));
}
@Test
void buildUrlWhenVersionIsEqualToPreferredUsesPreferred() throws Exception {
setVersion(DockerApi.PREFERRED_API_VERSION.toString());
assertThat(this.dockerApi.buildUrl(Feature.BASELINE, "/test"))
.isEqualTo(URI.create("/v" + DockerApi.PREFERRED_API_VERSION + "/test"));
}
@Test
void buildUrlWhenVersionIsLessThanPreferredAndGreaterThanMinimumUsesVersionVersion() throws Exception {
setVersion("1.48");
assertThat(this.dockerApi.buildUrl(Feature.BASELINE, "/test")).isEqualTo(URI.create("/v1.48/test"));
}
@Test
void buildUrlWhenVersionIsLessThanPreferredAndEqualToMinimumUsesVersionVersion() throws Exception {
setVersion(Feature.BASELINE.minimumVersion().toString());
assertThat(this.dockerApi.buildUrl(Feature.BASELINE, "/test")).isEqualTo(URI.create("/v1.24/test"));
}
@Test
void buildUrlWhenVersionIsLessThanMinimumThrowsException() throws Exception {
setVersion("1.23");
assertThatIllegalStateException().isThrownBy(() -> this.dockerApi.buildUrl(Feature.BASELINE, "/test"))
.withMessage("Docker API version must be at least 1.24 "
+ "to support this feature, but current API version is 1.23");
}
private void setVersion(String version) throws IOException, URISyntaxException {
given(http().head(eq(new URI(PING_URL))))
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, version)));
}
@Nested
class ImageDockerApiTests {
@ -243,12 +284,11 @@ class DockerApiTests { @@ -243,12 +284,11 @@ class DockerApiTests {
@Test
void pullWithPlatformPullsImageAndProducesEvents() throws Exception {
ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base");
URI createUri = new URI(PLATFORM_IMAGES_URL
+ "/create?fromImage=gcr.io%2Fpaketo-buildpacks%2Fbuilder%3Abase&platform=linux%2Farm64%2Fv1");
URI imageUri = new URI(PLATFORM_INSPECT_IMAGES_URL + "/gcr.io/paketo-buildpacks/builder:base/json?platform="
URI createUri = new URI(
"/v1.49/images/create?fromImage=gcr.io%2Fpaketo-buildpacks%2Fbuilder%3Abase&platform=linux%2Farm64%2Fv1");
URI imageUri = new URI("/v1.49/images/gcr.io/paketo-buildpacks/builder:base/json?platform="
+ ENCODED_LINUX_ARM64_PLATFORM_JSON);
given(http().head(eq(new URI(PING_URL))))
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "1.49")));
setVersion("1.49");
given(http().post(eq(createUri), isNull())).willReturn(responseOf("pull-stream.json"));
given(http().get(imageUri)).willReturn(responseOf("type/image.json"));
Image image = this.api.pull(reference, LINUX_ARM64_PLATFORM, this.pullListener);
@ -263,8 +303,7 @@ class DockerApiTests { @@ -263,8 +303,7 @@ 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.API_VERSION)));
setVersion("1.24");
assertThatIllegalStateException().isThrownBy(() -> this.api.pull(reference, platform, this.pullListener))
.withMessageContaining("must be at least 1.41")
.withMessageContaining("current API version is 1.24");
@ -402,10 +441,9 @@ class DockerApiTests { @@ -402,10 +441,9 @@ class DockerApiTests {
@Test
void inspectWithPlatformWhenSupportedVersionInspectImage() throws Exception {
ImageReference reference = ImageReference.of("docker.io/paketobuildpacks/builder:base");
URI imageUri = new URI(PLATFORM_INSPECT_IMAGES_URL
+ "/docker.io/paketobuildpacks/builder:base/json?platform=" + ENCODED_LINUX_ARM64_PLATFORM_JSON);
given(http().head(eq(new URI(PING_URL)))).willReturn(responseWithHeaders(
new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, DockerApi.PLATFORM_INSPECT_API_VERSION)));
URI imageUri = new URI("/v1.49/images/docker.io/paketobuildpacks/builder:base/json?platform="
+ ENCODED_LINUX_ARM64_PLATFORM_JSON);
setVersion("1.49");
given(http().get(imageUri)).willReturn(responseOf("type/image-platform.json"));
Image image = this.api.inspect(reference, LINUX_ARM64_PLATFORM);
assertThat(image.getArchitecture()).isEqualTo("arm64");
@ -415,9 +453,8 @@ class DockerApiTests { @@ -415,9 +453,8 @@ class DockerApiTests {
@Test
void inspectWithPlatformWhenOldVersionInspectImage() throws Exception {
ImageReference reference = ImageReference.of("docker.io/paketobuildpacks/builder:base");
URI imageUri = new URI(IMAGES_URL + "/docker.io/paketobuildpacks/builder:base/json");
given(http().head(eq(new URI(PING_URL)))).willReturn(responseWithHeaders(
new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, DockerApi.PLATFORM_API_VERSION)));
URI imageUri = new URI("/v1.48/images/docker.io/paketobuildpacks/builder:base/json");
setVersion("1.48");
given(http().get(imageUri)).willReturn(responseOf("type/image.json"));
Image image = this.api.inspect(reference, LINUX_ARM64_PLATFORM);
assertThat(image.getArchitecture()).isEqualTo("amd64");
@ -580,23 +617,27 @@ class DockerApiTests { @@ -580,23 +617,27 @@ class DockerApiTests {
@Test
void createWithPlatformCreatesContainer() throws Exception {
createWithPlatform("1.41");
ImageReference imageReference = ImageReference.of("ubuntu:bionic");
ContainerConfig config = ContainerConfig.of(imageReference, (update) -> update.withCommand("/bin/bash"));
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
setVersion("1.41");
URI createUri = new URI("/v1.41/containers/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);
assertThat(containerReference).hasToString("e90e34656806");
then(http()).should().post(any(), any(), this.writer.capture());
ByteArrayOutputStream out = new ByteArrayOutputStream();
this.writer.getValue().accept(out);
assertThat(out.toByteArray()).hasSize(config.toString().length());
}
@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");
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");
URI createUri = new URI(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);
@ -612,8 +653,7 @@ class DockerApiTests { @@ -612,8 +653,7 @@ class DockerApiTests {
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.24")));
setVersion("1.24");
assertThatIllegalStateException().isThrownBy(() -> this.api.create(config, platform))
.withMessageContaining("must be at least 1.41")
.withMessageContaining("current API version is 1.24");

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

@ -110,6 +110,17 @@ class ApiVersionTests { @@ -110,6 +110,17 @@ class ApiVersionTests {
assertThat(v12a).isEqualTo(v12a).isEqualTo(v12b).isNotEqualTo(v13);
}
@Test
void compareTo() {
assertThat(ApiVersion.of(0, 0).compareTo(ApiVersion.of(0, 0))).isZero();
assertThat(ApiVersion.of(0, 1).compareTo(ApiVersion.of(0, 1))).isZero();
assertThat(ApiVersion.of(1, 0).compareTo(ApiVersion.of(1, 0))).isZero();
assertThat(ApiVersion.of(0, 0).compareTo(ApiVersion.of(0, 1))).isLessThan(0);
assertThat(ApiVersion.of(0, 1).compareTo(ApiVersion.of(0, 0))).isGreaterThan(0);
assertThat(ApiVersion.of(1, 0).compareTo(ApiVersion.of(0, 1))).isGreaterThan(0);
assertThat(ApiVersion.of(0, 1).compareTo(ApiVersion.of(1, 0))).isLessThan(0);
}
private boolean supports(String v1, String v2) {
return ApiVersion.parse(v1).supports(ApiVersion.parse(v2));
}

Loading…
Cancel
Save