Skip to content

Commit d1dc957

Browse files
committed
Merge branch '3.4.x' into 3.5.x
Closes gh-48102
2 parents 25a5d43 + 9b387cb commit d1dc957

File tree

5 files changed

+138
-58
lines changed

5 files changed

+138
-58
lines changed

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ public boolean equals(Object obj) {
6969
if (obj == null || getClass() != obj.getClass()) {
7070
return false;
7171
}
72-
7372
ApiVersions other = (ApiVersions) obj;
7473
return Arrays.equals(this.apiVersions, other.apiVersions);
7574
}

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

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,10 @@ public class DockerApi {
6161

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

64-
static final ApiVersion API_VERSION = ApiVersion.of(1, 24);
65-
66-
static final ApiVersion PLATFORM_API_VERSION = ApiVersion.of(1, 41);
67-
68-
static final ApiVersion PLATFORM_INSPECT_API_VERSION = ApiVersion.of(1, 49);
69-
7064
static final ApiVersion UNKNOWN_API_VERSION = ApiVersion.of(0, 0);
7165

66+
static final ApiVersion PREFERRED_API_VERSION = ApiVersion.of(1, 50);
67+
7268
static final String API_VERSION_HEADER_NAME = "API-Version";
7369

7470
private final HttpTransport http;
@@ -143,17 +139,30 @@ private JsonStream jsonStream() {
143139
}
144140

145141
private URI buildUrl(String path, Collection<?> params) {
146-
return buildUrl(API_VERSION, path, (params != null) ? params.toArray() : null);
142+
return buildUrl(Feature.BASELINE, path, (params != null) ? params.toArray() : null);
147143
}
148144

149145
private URI buildUrl(String path, Object... params) {
150-
return buildUrl(API_VERSION, path, params);
146+
return buildUrl(Feature.BASELINE, path, params);
151147
}
152148

153-
private URI buildUrl(ApiVersion apiVersion, String path, Object... params) {
154-
verifyApiVersion(apiVersion);
149+
URI buildUrl(Feature feature, String path, Object... params) {
150+
ApiVersion version = getApiVersion();
151+
if (version.equals(UNKNOWN_API_VERSION) || (version.compareTo(PREFERRED_API_VERSION) >= 0
152+
&& version.compareTo(feature.minimumVersion()) >= 0)) {
153+
return buildVersionedUrl(PREFERRED_API_VERSION, path, params);
154+
}
155+
if (version.compareTo(feature.minimumVersion()) >= 0) {
156+
return buildVersionedUrl(version, path, params);
157+
}
158+
throw new IllegalStateException(
159+
"Docker API version must be at least %s to support this feature, but current API version is %s"
160+
.formatted(feature.minimumVersion(), version));
161+
}
162+
163+
private URI buildVersionedUrl(ApiVersion version, String path, Object[] params) {
155164
try {
156-
URIBuilder builder = new URIBuilder("/v" + apiVersion + path);
165+
URIBuilder builder = new URIBuilder("/v" + version + path);
157166
if (params != null) {
158167
int param = 0;
159168
while (param < params.length) {
@@ -167,13 +176,6 @@ private URI buildUrl(ApiVersion apiVersion, String path, Object... params) {
167176
}
168177
}
169178

170-
private void verifyApiVersion(ApiVersion minimumVersion) {
171-
ApiVersion actualVersion = getApiVersion();
172-
Assert.state(actualVersion.equals(UNKNOWN_API_VERSION) || actualVersion.supports(minimumVersion),
173-
() -> "Docker API version must be at least " + minimumVersion
174-
+ " to support this feature, but current API version is " + actualVersion);
175-
}
176-
177179
private ApiVersion getApiVersion() {
178180
ApiVersion apiVersion = this.apiVersion;
179181
if (this.apiVersion == null) {
@@ -242,7 +244,7 @@ public Image pull(ImageReference reference, ImagePlatform platform,
242244
Assert.notNull(reference, "'reference' must not be null");
243245
Assert.notNull(listener, "'listener' must not be null");
244246
URI createUri = (platform != null)
245-
? buildUrl(PLATFORM_API_VERSION, "/images/create", "fromImage", reference, "platform", platform)
247+
? buildUrl(Feature.PLATFORM, "/images/create", "fromImage", reference, "platform", platform)
246248
: buildUrl("/images/create", "fromImage", reference);
247249
DigestCaptureUpdateListener digestCapture = new DigestCaptureUpdateListener();
248250
listener.onStart();
@@ -376,8 +378,8 @@ public Image inspect(ImageReference reference, ImagePlatform platform) throws IO
376378

377379
private URI inspectUrl(ImageReference reference, ImagePlatform platform) {
378380
String path = "/images/" + reference + "/json";
379-
if (platform != null && getApiVersion().supports(PLATFORM_INSPECT_API_VERSION)) {
380-
return buildUrl(PLATFORM_INSPECT_API_VERSION, path, "platform", platform.toJson());
381+
if (platform != null && getApiVersion().supports(Feature.PLATFORM_INSPECT.minimumVersion())) {
382+
return buildUrl(Feature.PLATFORM_INSPECT, path, "platform", platform.toJson());
381383
}
382384
return buildUrl(path);
383385
}
@@ -423,8 +425,7 @@ public ContainerReference create(ContainerConfig config, ImagePlatform platform,
423425
}
424426

425427
private ContainerReference createContainer(ContainerConfig config, ImagePlatform platform) throws IOException {
426-
URI createUri = (platform != null)
427-
? buildUrl(PLATFORM_API_VERSION, "/containers/create", "platform", platform)
428+
URI createUri = (platform != null) ? buildUrl(Feature.PLATFORM, "/containers/create", "platform", platform)
428429
: buildUrl("/containers/create");
429430
try (Response response = http().post(createUri, "application/json", config::writeTo)) {
430431
return ContainerReference
@@ -626,4 +627,24 @@ public void onUpdate(PushImageUpdateEvent event) {
626627

627628
}
628629

630+
enum Feature {
631+
632+
BASELINE(ApiVersion.of(1, 24)),
633+
634+
PLATFORM(ApiVersion.of(1, 41)),
635+
636+
PLATFORM_INSPECT(ApiVersion.of(1, 49));
637+
638+
private final ApiVersion minimumVersion;
639+
640+
Feature(ApiVersion minimumVersion) {
641+
this.minimumVersion = minimumVersion;
642+
}
643+
644+
ApiVersion minimumVersion() {
645+
return this.minimumVersion;
646+
}
647+
648+
}
649+
629650
}

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.boot.buildpack.platform.docker.type;
1818

19+
import java.util.Comparator;
1920
import java.util.regex.Matcher;
2021
import java.util.regex.Pattern;
2122

@@ -28,10 +29,13 @@
2829
* @author Scott Frederick
2930
* @since 3.4.0
3031
*/
31-
public final class ApiVersion {
32+
public final class ApiVersion implements Comparable<ApiVersion> {
3233

3334
private static final Pattern PATTERN = Pattern.compile("^v?(\\d+)\\.(\\d*)$");
3435

36+
private static final Comparator<ApiVersion> COMPARATOR = Comparator.comparing(ApiVersion::getMajor)
37+
.thenComparing(ApiVersion::getMinor);
38+
3539
private final int major;
3640

3741
private final int minor;
@@ -136,4 +140,9 @@ public static ApiVersion of(int major, int minor) {
136140
return new ApiVersion(major, minor);
137141
}
138142

143+
@Override
144+
public int compareTo(ApiVersion other) {
145+
return COMPARATOR.compare(this, other);
146+
}
147+
139148
}

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

Lines changed: 73 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import org.mockito.junit.jupiter.MockitoExtension;
4343

4444
import org.springframework.boot.buildpack.platform.docker.DockerApi.ContainerApi;
45+
import org.springframework.boot.buildpack.platform.docker.DockerApi.Feature;
4546
import org.springframework.boot.buildpack.platform.docker.DockerApi.ImageApi;
4647
import org.springframework.boot.buildpack.platform.docker.DockerApi.SystemApi;
4748
import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi;
@@ -89,20 +90,14 @@
8990
@ExtendWith({ MockitoExtension.class, OutputCaptureExtension.class })
9091
class DockerApiTests {
9192

92-
private static final String API_URL = "/v" + DockerApi.API_VERSION;
93+
private static final String API_URL = "/v" + DockerApi.PREFERRED_API_VERSION;
9394

9495
public static final String PING_URL = "/_ping";
9596

9697
private static final String IMAGES_URL = API_URL + "/images";
9798

98-
private static final String PLATFORM_IMAGES_URL = "/v" + DockerApi.PLATFORM_API_VERSION + "/images";
99-
100-
private static final String PLATFORM_INSPECT_IMAGES_URL = "/v" + DockerApi.PLATFORM_INSPECT_API_VERSION + "/images";
101-
10299
private static final String CONTAINERS_URL = API_URL + "/containers";
103100

104-
private static final String PLATFORM_CONTAINERS_URL = "/v" + DockerApi.PLATFORM_API_VERSION + "/containers";
105-
106101
private static final String VOLUMES_URL = API_URL + "/volumes";
107102

108103
private static final ImagePlatform LINUX_ARM64_PLATFORM = ImagePlatform.of("linux/arm64/v1");
@@ -175,6 +170,52 @@ void createDockerApi() {
175170
assertThat(api).isNotNull();
176171
}
177172

173+
@Test
174+
void buildUrlWhenUnknownVersionUsesPreferredVersion() throws Exception {
175+
setVersion("0.0");
176+
assertThat(this.dockerApi.buildUrl(Feature.BASELINE, "/test"))
177+
.isEqualTo(URI.create("/v" + DockerApi.PREFERRED_API_VERSION + "/test"));
178+
}
179+
180+
@Test
181+
void buildUrlWhenVersionIsGreaterThanPreferredUsesPreferred() throws Exception {
182+
setVersion("1000.0");
183+
assertThat(this.dockerApi.buildUrl(Feature.BASELINE, "/test"))
184+
.isEqualTo(URI.create("/v" + DockerApi.PREFERRED_API_VERSION + "/test"));
185+
}
186+
187+
@Test
188+
void buildUrlWhenVersionIsEqualToPreferredUsesPreferred() throws Exception {
189+
setVersion(DockerApi.PREFERRED_API_VERSION.toString());
190+
assertThat(this.dockerApi.buildUrl(Feature.BASELINE, "/test"))
191+
.isEqualTo(URI.create("/v" + DockerApi.PREFERRED_API_VERSION + "/test"));
192+
}
193+
194+
@Test
195+
void buildUrlWhenVersionIsLessThanPreferredAndGreaterThanMinimumUsesVersionVersion() throws Exception {
196+
setVersion("1.48");
197+
assertThat(this.dockerApi.buildUrl(Feature.BASELINE, "/test")).isEqualTo(URI.create("/v1.48/test"));
198+
}
199+
200+
@Test
201+
void buildUrlWhenVersionIsLessThanPreferredAndEqualToMinimumUsesVersionVersion() throws Exception {
202+
setVersion(Feature.BASELINE.minimumVersion().toString());
203+
assertThat(this.dockerApi.buildUrl(Feature.BASELINE, "/test")).isEqualTo(URI.create("/v1.24/test"));
204+
}
205+
206+
@Test
207+
void buildUrlWhenVersionIsLessThanMinimumThrowsException() throws Exception {
208+
setVersion("1.23");
209+
assertThatIllegalStateException().isThrownBy(() -> this.dockerApi.buildUrl(Feature.BASELINE, "/test"))
210+
.withMessage("Docker API version must be at least 1.24 "
211+
+ "to support this feature, but current API version is 1.23");
212+
}
213+
214+
private void setVersion(String version) throws IOException, URISyntaxException {
215+
given(http().head(eq(new URI(PING_URL))))
216+
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, version)));
217+
}
218+
178219
@Nested
179220
class ImageDockerApiTests {
180221

@@ -243,12 +284,11 @@ void pullWithRegistryAuthPullsImageAndProducesEvents() throws Exception {
243284
@Test
244285
void pullWithPlatformPullsImageAndProducesEvents() throws Exception {
245286
ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base");
246-
URI createUri = new URI(PLATFORM_IMAGES_URL
247-
+ "/create?fromImage=gcr.io%2Fpaketo-buildpacks%2Fbuilder%3Abase&platform=linux%2Farm64%2Fv1");
248-
URI imageUri = new URI(PLATFORM_INSPECT_IMAGES_URL + "/gcr.io/paketo-buildpacks/builder:base/json?platform="
287+
URI createUri = new URI(
288+
"/v1.49/images/create?fromImage=gcr.io%2Fpaketo-buildpacks%2Fbuilder%3Abase&platform=linux%2Farm64%2Fv1");
289+
URI imageUri = new URI("/v1.49/images/gcr.io/paketo-buildpacks/builder:base/json?platform="
249290
+ ENCODED_LINUX_ARM64_PLATFORM_JSON);
250-
given(http().head(eq(new URI(PING_URL))))
251-
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "1.49")));
291+
setVersion("1.49");
252292
given(http().post(eq(createUri), isNull())).willReturn(responseOf("pull-stream.json"));
253293
given(http().get(imageUri)).willReturn(responseOf("type/image.json"));
254294
Image image = this.api.pull(reference, LINUX_ARM64_PLATFORM, this.pullListener);
@@ -263,8 +303,7 @@ void pullWithPlatformPullsImageAndProducesEvents() throws Exception {
263303
void pullWithPlatformAndInsufficientApiVersionThrowsException() throws Exception {
264304
ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base");
265305
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
266-
given(http().head(eq(new URI(PING_URL)))).willReturn(
267-
responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, DockerApi.API_VERSION)));
306+
setVersion("1.24");
268307
assertThatIllegalStateException().isThrownBy(() -> this.api.pull(reference, platform, this.pullListener))
269308
.withMessageContaining("must be at least 1.41")
270309
.withMessageContaining("current API version is 1.24");
@@ -402,10 +441,9 @@ void inspectInspectImage() throws Exception {
402441
@Test
403442
void inspectWithPlatformWhenSupportedVersionInspectImage() throws Exception {
404443
ImageReference reference = ImageReference.of("docker.io/paketobuildpacks/builder:base");
405-
URI imageUri = new URI(PLATFORM_INSPECT_IMAGES_URL
406-
+ "/docker.io/paketobuildpacks/builder:base/json?platform=" + ENCODED_LINUX_ARM64_PLATFORM_JSON);
407-
given(http().head(eq(new URI(PING_URL)))).willReturn(responseWithHeaders(
408-
new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, DockerApi.PLATFORM_INSPECT_API_VERSION)));
444+
URI imageUri = new URI("/v1.49/images/docker.io/paketobuildpacks/builder:base/json?platform="
445+
+ ENCODED_LINUX_ARM64_PLATFORM_JSON);
446+
setVersion("1.49");
409447
given(http().get(imageUri)).willReturn(responseOf("type/image-platform.json"));
410448
Image image = this.api.inspect(reference, LINUX_ARM64_PLATFORM);
411449
assertThat(image.getArchitecture()).isEqualTo("arm64");
@@ -415,9 +453,8 @@ void inspectWithPlatformWhenSupportedVersionInspectImage() throws Exception {
415453
@Test
416454
void inspectWithPlatformWhenOldVersionInspectImage() throws Exception {
417455
ImageReference reference = ImageReference.of("docker.io/paketobuildpacks/builder:base");
418-
URI imageUri = new URI(IMAGES_URL + "/docker.io/paketobuildpacks/builder:base/json");
419-
given(http().head(eq(new URI(PING_URL)))).willReturn(responseWithHeaders(
420-
new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, DockerApi.PLATFORM_API_VERSION)));
456+
URI imageUri = new URI("/v1.48/images/docker.io/paketobuildpacks/builder:base/json");
457+
setVersion("1.48");
421458
given(http().get(imageUri)).willReturn(responseOf("type/image.json"));
422459
Image image = this.api.inspect(reference, LINUX_ARM64_PLATFORM);
423460
assertThat(image.getArchitecture()).isEqualTo("amd64");
@@ -580,23 +617,27 @@ void createWhenHasContentContainerWithContent() throws Exception {
580617

581618
@Test
582619
void createWithPlatformCreatesContainer() throws Exception {
583-
createWithPlatform("1.41");
620+
ImageReference imageReference = ImageReference.of("ubuntu:bionic");
621+
ContainerConfig config = ContainerConfig.of(imageReference, (update) -> update.withCommand("/bin/bash"));
622+
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
623+
setVersion("1.41");
624+
URI createUri = new URI("/v1.41/containers/create?platform=linux%2Farm64%2Fv1");
625+
given(http().post(eq(createUri), eq("application/json"), any()))
626+
.willReturn(responseOf("create-container-response.json"));
627+
ContainerReference containerReference = this.api.create(config, platform);
628+
assertThat(containerReference).hasToString("e90e34656806");
629+
then(http()).should().post(any(), any(), this.writer.capture());
630+
ByteArrayOutputStream out = new ByteArrayOutputStream();
631+
this.writer.getValue().accept(out);
632+
assertThat(out.toByteArray()).hasSize(config.toString().length());
584633
}
585634

586635
@Test
587636
void createWithPlatformAndUnknownApiVersionAttemptsCreate() throws Exception {
588-
createWithPlatform(null);
589-
}
590-
591-
private void createWithPlatform(String apiVersion) throws IOException, URISyntaxException {
592637
ImageReference imageReference = ImageReference.of("ubuntu:bionic");
593638
ContainerConfig config = ContainerConfig.of(imageReference, (update) -> update.withCommand("/bin/bash"));
594639
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
595-
if (apiVersion != null) {
596-
given(http().head(eq(new URI(PING_URL))))
597-
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, apiVersion)));
598-
}
599-
URI createUri = new URI(PLATFORM_CONTAINERS_URL + "/create?platform=linux%2Farm64%2Fv1");
640+
URI createUri = new URI(CONTAINERS_URL + "/create?platform=linux%2Farm64%2Fv1");
600641
given(http().post(eq(createUri), eq("application/json"), any()))
601642
.willReturn(responseOf("create-container-response.json"));
602643
ContainerReference containerReference = this.api.create(config, platform);
@@ -612,8 +653,7 @@ void createWithPlatformAndKnownInsufficientApiVersionThrowsException() throws Ex
612653
ImageReference imageReference = ImageReference.of("ubuntu:bionic");
613654
ContainerConfig config = ContainerConfig.of(imageReference, (update) -> update.withCommand("/bin/bash"));
614655
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
615-
given(http().head(eq(new URI(PING_URL))))
616-
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "1.24")));
656+
setVersion("1.24");
617657
assertThatIllegalStateException().isThrownBy(() -> this.api.create(config, platform))
618658
.withMessageContaining("must be at least 1.41")
619659
.withMessageContaining("current API version is 1.24");

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,17 @@ void equalsAndHashCode() {
110110
assertThat(v12a).isEqualTo(v12a).isEqualTo(v12b).isNotEqualTo(v13);
111111
}
112112

113+
@Test
114+
void compareTo() {
115+
assertThat(ApiVersion.of(0, 0).compareTo(ApiVersion.of(0, 0))).isZero();
116+
assertThat(ApiVersion.of(0, 1).compareTo(ApiVersion.of(0, 1))).isZero();
117+
assertThat(ApiVersion.of(1, 0).compareTo(ApiVersion.of(1, 0))).isZero();
118+
assertThat(ApiVersion.of(0, 0).compareTo(ApiVersion.of(0, 1))).isLessThan(0);
119+
assertThat(ApiVersion.of(0, 1).compareTo(ApiVersion.of(0, 0))).isGreaterThan(0);
120+
assertThat(ApiVersion.of(1, 0).compareTo(ApiVersion.of(0, 1))).isGreaterThan(0);
121+
assertThat(ApiVersion.of(0, 1).compareTo(ApiVersion.of(1, 0))).isLessThan(0);
122+
}
123+
113124
private boolean supports(String v1, String v2) {
114125
return ApiVersion.parse(v1).supports(ApiVersion.parse(v2));
115126
}

0 commit comments

Comments
 (0)