Skip to content

Commit 55c3f03

Browse files
committed
Merge branch '3.5.x'
Closes gh-48103
2 parents a25bcfc + d1dc957 commit 55c3f03

File tree

5 files changed

+138
-54
lines changed

5 files changed

+138
-54
lines changed

buildpack/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
@@ -71,7 +71,6 @@ public boolean equals(@Nullable Object obj) {
7171
if (obj == null || getClass() != obj.getClass()) {
7272
return false;
7373
}
74-
7574
ApiVersions other = (ApiVersions) obj;
7675
return Arrays.equals(this.apiVersions, other.apiVersions);
7776
}

buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/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;
1818

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

@@ -30,10 +31,13 @@
3031
* @author Scott Frederick
3132
* @since 4.0.0
3233
*/
33-
public final class ApiVersion {
34+
public final class ApiVersion implements Comparable<ApiVersion> {
3435

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

38+
private static final Comparator<ApiVersion> COMPARATOR = Comparator.comparing(ApiVersion::getMajor)
39+
.thenComparing(ApiVersion::getMinor);
40+
3741
private final int major;
3842

3943
private final int minor;
@@ -138,4 +142,9 @@ public static ApiVersion of(int major, int minor) {
138142
return new ApiVersion(major, minor);
139143
}
140144

145+
@Override
146+
public int compareTo(ApiVersion other) {
147+
return COMPARATOR.compare(this, other);
148+
}
149+
141150
}

buildpack/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
@@ -62,14 +62,10 @@ public class DockerApi {
6262

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

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

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

7571
private final HttpTransport http;
@@ -129,17 +125,30 @@ private JsonStream jsonStream() {
129125
}
130126

131127
private URI buildUrl(String path, @Nullable Collection<?> params) {
132-
return buildUrl(API_VERSION, path, (params != null) ? params.toArray() : null);
128+
return buildUrl(Feature.BASELINE, path, (params != null) ? params.toArray() : null);
133129
}
134130

135131
private URI buildUrl(String path, Object... params) {
136-
return buildUrl(API_VERSION, path, params);
132+
return buildUrl(Feature.BASELINE, path, params);
137133
}
138134

139-
private URI buildUrl(ApiVersion apiVersion, String path, Object @Nullable ... params) {
140-
verifyApiVersion(apiVersion);
135+
URI buildUrl(Feature feature, String path, Object @Nullable ... params) {
136+
ApiVersion version = getApiVersion();
137+
if (version.equals(UNKNOWN_API_VERSION) || (version.compareTo(PREFERRED_API_VERSION) >= 0
138+
&& version.compareTo(feature.minimumVersion()) >= 0)) {
139+
return buildVersionedUrl(PREFERRED_API_VERSION, path, params);
140+
}
141+
if (version.compareTo(feature.minimumVersion()) >= 0) {
142+
return buildVersionedUrl(version, path, params);
143+
}
144+
throw new IllegalStateException(
145+
"Docker API version must be at least %s to support this feature, but current API version is %s"
146+
.formatted(feature.minimumVersion(), version));
147+
}
148+
149+
private URI buildVersionedUrl(ApiVersion version, String path, Object @Nullable ... params) {
141150
try {
142-
URIBuilder builder = new URIBuilder("/v" + apiVersion + path);
151+
URIBuilder builder = new URIBuilder("/v" + version + path);
143152
if (params != null) {
144153
int param = 0;
145154
while (param < params.length) {
@@ -153,13 +162,6 @@ private URI buildUrl(ApiVersion apiVersion, String path, Object @Nullable ... pa
153162
}
154163
}
155164

156-
private void verifyApiVersion(ApiVersion minimumVersion) {
157-
ApiVersion actualVersion = getApiVersion();
158-
Assert.state(actualVersion.equals(UNKNOWN_API_VERSION) || actualVersion.supports(minimumVersion),
159-
() -> "Docker API version must be at least " + minimumVersion
160-
+ " to support this feature, but current API version is " + actualVersion);
161-
}
162-
163165
private ApiVersion getApiVersion() {
164166
ApiVersion apiVersion = this.apiVersion;
165167
if (apiVersion == null) {
@@ -228,7 +230,7 @@ public Image pull(ImageReference reference, @Nullable ImagePlatform platform,
228230
Assert.notNull(reference, "'reference' must not be null");
229231
Assert.notNull(listener, "'listener' must not be null");
230232
URI createUri = (platform != null)
231-
? buildUrl(PLATFORM_API_VERSION, "/images/create", "fromImage", reference, "platform", platform)
233+
? buildUrl(Feature.PLATFORM, "/images/create", "fromImage", reference, "platform", platform)
232234
: buildUrl("/images/create", "fromImage", reference);
233235
DigestCaptureUpdateListener digestCapture = new DigestCaptureUpdateListener();
234236
listener.onStart();
@@ -365,8 +367,8 @@ public Image inspect(ImageReference reference, @Nullable ImagePlatform platform)
365367

366368
private URI inspectUrl(ImageReference reference, @Nullable ImagePlatform platform) {
367369
String path = "/images/" + reference + "/json";
368-
if (platform != null && getApiVersion().supports(PLATFORM_INSPECT_API_VERSION)) {
369-
return buildUrl(PLATFORM_INSPECT_API_VERSION, path, "platform", platform.toJson());
370+
if (platform != null && getApiVersion().supports(Feature.PLATFORM_INSPECT.minimumVersion())) {
371+
return buildUrl(Feature.PLATFORM_INSPECT, path, "platform", platform.toJson());
370372
}
371373
return buildUrl(path);
372374
}
@@ -413,8 +415,7 @@ public ContainerReference create(ContainerConfig config, @Nullable ImagePlatform
413415

414416
private ContainerReference createContainer(ContainerConfig config, @Nullable ImagePlatform platform)
415417
throws IOException {
416-
URI createUri = (platform != null)
417-
? buildUrl(PLATFORM_API_VERSION, "/containers/create", "platform", platform)
418+
URI createUri = (platform != null) ? buildUrl(Feature.PLATFORM, "/containers/create", "platform", platform)
418419
: buildUrl("/containers/create");
419420
try (Response response = http().post(createUri, "application/json", config::writeTo)) {
420421
return ContainerReference
@@ -619,4 +620,24 @@ public void onUpdate(PushImageUpdateEvent event) {
619620

620621
}
621622

623+
enum Feature {
624+
625+
BASELINE(ApiVersion.of(1, 24)),
626+
627+
PLATFORM(ApiVersion.of(1, 41)),
628+
629+
PLATFORM_INSPECT(ApiVersion.of(1, 49));
630+
631+
private final ApiVersion minimumVersion;
632+
633+
Feature(ApiVersion minimumVersion) {
634+
this.minimumVersion = minimumVersion;
635+
}
636+
637+
ApiVersion minimumVersion() {
638+
return this.minimumVersion;
639+
}
640+
641+
}
642+
622643
}

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

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

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

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

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

4545
import org.springframework.boot.buildpack.platform.docker.DockerApi.ContainerApi;
46+
import org.springframework.boot.buildpack.platform.docker.DockerApi.Feature;
4647
import org.springframework.boot.buildpack.platform.docker.DockerApi.ImageApi;
4748
import org.springframework.boot.buildpack.platform.docker.DockerApi.SystemApi;
4849
import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi;
@@ -88,20 +89,14 @@
8889
@ExtendWith({ MockitoExtension.class, OutputCaptureExtension.class })
8990
class DockerApiTests {
9091

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

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

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

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

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

107102
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

@@ -249,12 +290,11 @@ void pullWithRegistryAuthPullsImageAndProducesEvents() throws Exception {
249290
@Test
250291
void pullWithPlatformPullsImageAndProducesEvents() throws Exception {
251292
ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base");
252-
URI createUri = new URI(PLATFORM_IMAGES_URL
253-
+ "/create?fromImage=gcr.io%2Fpaketo-buildpacks%2Fbuilder%3Abase&platform=linux%2Farm64%2Fv1");
254-
URI imageUri = new URI(PLATFORM_INSPECT_IMAGES_URL + "/gcr.io/paketo-buildpacks/builder:base/json?platform="
293+
URI createUri = new URI(
294+
"/v1.49/images/create?fromImage=gcr.io%2Fpaketo-buildpacks%2Fbuilder%3Abase&platform=linux%2Farm64%2Fv1");
295+
URI imageUri = new URI("/v1.49/images/gcr.io/paketo-buildpacks/builder:base/json?platform="
255296
+ ENCODED_LINUX_ARM64_PLATFORM_JSON);
256-
given(http().head(eq(new URI(PING_URL))))
257-
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "1.49")));
297+
setVersion("1.49");
258298
given(http().post(eq(createUri), isNull())).willReturn(responseOf("pull-stream.json"));
259299
given(http().get(imageUri)).willReturn(responseOf("type/image.json"));
260300
Image image = this.api.pull(reference, LINUX_ARM64_PLATFORM, this.pullListener);
@@ -269,8 +309,7 @@ void pullWithPlatformPullsImageAndProducesEvents() throws Exception {
269309
void pullWithPlatformAndInsufficientApiVersionThrowsException() throws Exception {
270310
ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base");
271311
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
272-
given(http().head(eq(new URI(PING_URL)))).willReturn(
273-
responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, DockerApi.API_VERSION)));
312+
setVersion("1.24");
274313
assertThatIllegalStateException().isThrownBy(() -> this.api.pull(reference, platform, this.pullListener))
275314
.withMessageContaining("must be at least 1.41")
276315
.withMessageContaining("current API version is 1.24");
@@ -414,10 +453,9 @@ void inspectInspectImage() throws Exception {
414453
@Test
415454
void inspectWithPlatformWhenSupportedVersionInspectImage() throws Exception {
416455
ImageReference reference = ImageReference.of("docker.io/paketobuildpacks/builder:base");
417-
URI imageUri = new URI(PLATFORM_INSPECT_IMAGES_URL
418-
+ "/docker.io/paketobuildpacks/builder:base/json?platform=" + ENCODED_LINUX_ARM64_PLATFORM_JSON);
419-
given(http().head(eq(new URI(PING_URL)))).willReturn(responseWithHeaders(
420-
new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, DockerApi.PLATFORM_INSPECT_API_VERSION)));
456+
URI imageUri = new URI("/v1.49/images/docker.io/paketobuildpacks/builder:base/json?platform="
457+
+ ENCODED_LINUX_ARM64_PLATFORM_JSON);
458+
setVersion("1.49");
421459
given(http().get(imageUri)).willReturn(responseOf("type/image-platform.json"));
422460
Image image = this.api.inspect(reference, LINUX_ARM64_PLATFORM);
423461
assertThat(image.getArchitecture()).isEqualTo("arm64");
@@ -427,9 +465,8 @@ void inspectWithPlatformWhenSupportedVersionInspectImage() throws Exception {
427465
@Test
428466
void inspectWithPlatformWhenOldVersionInspectImage() throws Exception {
429467
ImageReference reference = ImageReference.of("docker.io/paketobuildpacks/builder:base");
430-
URI imageUri = new URI(IMAGES_URL + "/docker.io/paketobuildpacks/builder:base/json");
431-
given(http().head(eq(new URI(PING_URL)))).willReturn(responseWithHeaders(
432-
new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, DockerApi.PLATFORM_API_VERSION)));
468+
URI imageUri = new URI("/v1.48/images/docker.io/paketobuildpacks/builder:base/json");
469+
setVersion("1.48");
433470
given(http().get(imageUri)).willReturn(responseOf("type/image.json"));
434471
Image image = this.api.inspect(reference, LINUX_ARM64_PLATFORM);
435472
assertThat(image.getArchitecture()).isEqualTo("amd64");
@@ -597,7 +634,19 @@ void createWhenHasContentContainerWithContent() throws Exception {
597634

598635
@Test
599636
void createWithPlatformCreatesContainer() throws Exception {
600-
createWithPlatform("1.41");
637+
ImageReference imageReference = ImageReference.of("ubuntu:bionic");
638+
ContainerConfig config = ContainerConfig.of(imageReference, (update) -> update.withCommand("/bin/bash"));
639+
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
640+
setVersion("1.41");
641+
URI createUri = new URI("/v1.41/containers/create?platform=linux%2Farm64%2Fv1");
642+
given(http().post(eq(createUri), eq("application/json"), any()))
643+
.willReturn(responseOf("create-container-response.json"));
644+
ContainerReference containerReference = this.api.create(config, platform);
645+
assertThat(containerReference).hasToString("e90e34656806");
646+
then(http()).should().post(any(), any(), this.writer.capture());
647+
ByteArrayOutputStream out = new ByteArrayOutputStream();
648+
this.writer.getValue().accept(out);
649+
assertThat(out.toByteArray()).hasSize(config.toString().length());
601650
}
602651

603652
@Test
@@ -609,11 +658,7 @@ private void createWithPlatform(@Nullable String apiVersion) throws IOException,
609658
ImageReference imageReference = ImageReference.of("ubuntu:bionic");
610659
ContainerConfig config = ContainerConfig.of(imageReference, (update) -> update.withCommand("/bin/bash"));
611660
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
612-
if (apiVersion != null) {
613-
given(http().head(eq(new URI(PING_URL))))
614-
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, apiVersion)));
615-
}
616-
URI createUri = new URI(PLATFORM_CONTAINERS_URL + "/create?platform=linux%2Farm64%2Fv1");
661+
URI createUri = new URI(CONTAINERS_URL + "/create?platform=linux%2Farm64%2Fv1");
617662
given(http().post(eq(createUri), eq("application/json"), any()))
618663
.willReturn(responseOf("create-container-response.json"));
619664
ContainerReference containerReference = this.api.create(config, platform);
@@ -629,8 +674,7 @@ void createWithPlatformAndKnownInsufficientApiVersionThrowsException() throws Ex
629674
ImageReference imageReference = ImageReference.of("ubuntu:bionic");
630675
ContainerConfig config = ContainerConfig.of(imageReference, (update) -> update.withCommand("/bin/bash"));
631676
ImagePlatform platform = ImagePlatform.of("linux/arm64/v1");
632-
given(http().head(eq(new URI(PING_URL))))
633-
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "1.24")));
677+
setVersion("1.24");
634678
assertThatIllegalStateException().isThrownBy(() -> this.api.create(config, platform))
635679
.withMessageContaining("must be at least 1.41")
636680
.withMessageContaining("current API version is 1.24");

0 commit comments

Comments
 (0)