Skip to content
This repository was archived by the owner on May 14, 2025. It is now read-only.

Commit 04765e7

Browse files
Corneil du Plessiscppwfs
authored andcommitted
Provider for trying OCI accepts header when manifest result returns no config and schemaVersion less than 1.
Issue #5819
1 parent b0bf445 commit 04765e7

File tree

2 files changed

+91
-12
lines changed

2 files changed

+91
-12
lines changed

spring-cloud-dataflow-configuration-metadata/src/main/java/org/springframework/cloud/dataflow/configuration/metadata/container/DefaultContainerImageMetadataResolver.java

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.Map;
2121

2222
import org.springframework.cloud.dataflow.container.registry.ContainerRegistryException;
23+
import org.springframework.cloud.dataflow.container.registry.ContainerRegistryProperties;
2324
import org.springframework.cloud.dataflow.container.registry.ContainerRegistryRequest;
2425
import org.springframework.cloud.dataflow.container.registry.ContainerRegistryService;
2526
import org.springframework.util.StringUtils;
@@ -30,6 +31,7 @@
3031
*
3132
* @author Christian Tzolov
3233
* @author Ilayaperumal Gopinathan
34+
* @author Corneil du Plessis
3335
*/
3436
public class DefaultContainerImageMetadataResolver implements ContainerImageMetadataResolver {
3537

@@ -39,6 +41,7 @@ public DefaultContainerImageMetadataResolver(ContainerRegistryService containerR
3941
this.containerRegistryService = containerRegistryService;
4042
}
4143

44+
@SuppressWarnings("unchecked")
4245
@Override
4346
public Map<String, String> getImageLabels(String imageName) {
4447

@@ -48,12 +51,23 @@ public Map<String, String> getImageLabels(String imageName) {
4851

4952
ContainerRegistryRequest registryRequest = this.containerRegistryService.getRegistryRequest(imageName);
5053

51-
Map manifest = this.containerRegistryService.getImageManifest(registryRequest, Map.class);
52-
53-
if (manifest != null && !isNotNullMap(manifest.get("config"))) {
54-
throw new ContainerRegistryException(
55-
String.format("Image [%s] has incorrect or missing manifest config element: %s",
56-
imageName, manifest.toString()));
54+
Map<String, Object> manifest = this.containerRegistryService.getImageManifest(registryRequest, Map.class);
55+
56+
if (manifest != null && manifest.get("config") == null) {
57+
// when both Docker and OCI images are stored in repository the response for OCI image when using Docker manifest type will not contain config.
58+
// In the case where we don't receive a config and schemaVersion is less than 2 we try OCI manifest type.
59+
String manifestMediaType = registryRequest.getRegistryConf().getManifestMediaType();
60+
if (asInt(manifest.get("schemaVersion")) < 2
61+
&& !manifestMediaType.equals(ContainerRegistryProperties.OCI_IMAGE_MANIFEST_MEDIA_TYPE)) {
62+
registryRequest.getRegistryConf()
63+
.setManifestMediaType(ContainerRegistryProperties.OCI_IMAGE_MANIFEST_MEDIA_TYPE);
64+
manifest = this.containerRegistryService.getImageManifest(registryRequest, Map.class);
65+
}
66+
if (manifest.get("config") == null) {
67+
String message = String.format("Image [%s] has incorrect or missing manifest config element: %s",
68+
imageName, manifest);
69+
throw new ContainerRegistryException(message);
70+
}
5771
}
5872
if (manifest != null) {
5973
String configDigest = ((Map<String, String>) manifest.get("config")).get("digest");
@@ -85,12 +99,24 @@ public Map<String, String> getImageLabels(String imageName) {
8599
(Map<String, String>) configElement.get("Labels") : Collections.emptyMap();
86100
}
87101
else {
88-
throw new ContainerRegistryException(
89-
String.format("Image [%s] is missing manifest", imageName));
102+
throw new ContainerRegistryException(String.format("Image [%s] is missing manifest", imageName));
103+
}
104+
}
105+
106+
private static int asInt(Object value) {
107+
if (value instanceof Number) {
108+
return ((Number) value).intValue();
109+
}
110+
else if (value instanceof String) {
111+
return Integer.parseInt((String) value);
112+
}
113+
else if (value != null) {
114+
return Integer.parseInt(value.toString());
90115
}
116+
return 0;
91117
}
92118

93-
private boolean isNotNullMap(Object object) {
94-
return object != null && (object instanceof Map);
119+
private static boolean isNotNullMap(Object object) {
120+
return object instanceof Map;
95121
}
96122
}

spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/DefaultContainerImageMetadataResolverTest.java

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,15 @@
1616

1717
package org.springframework.cloud.dataflow.container.registry;
1818

19-
import java.util.Arrays;
2019
import java.util.Collections;
2120
import java.util.HashMap;
2221
import java.util.Map;
2322

2423
import com.fasterxml.jackson.core.JsonProcessingException;
2524
import com.fasterxml.jackson.databind.ObjectMapper;
26-
import org.junit.jupiter.api.BeforeAll;
2725
import org.junit.jupiter.api.BeforeEach;
2826
import org.junit.jupiter.api.Test;
27+
import org.mockito.ArgumentMatcher;
2928
import org.mockito.Mock;
3029
import org.mockito.MockitoAnnotations;
3130

@@ -35,6 +34,7 @@
3534
import org.springframework.http.HttpHeaders;
3635
import org.springframework.http.HttpMethod;
3736
import org.springframework.http.HttpStatus;
37+
import org.springframework.http.MediaType;
3838
import org.springframework.http.ResponseEntity;
3939
import org.springframework.util.StringUtils;
4040
import org.springframework.web.client.RestTemplate;
@@ -46,6 +46,7 @@
4646
import static org.mockito.ArgumentMatchers.any;
4747
import static org.mockito.ArgumentMatchers.anyBoolean;
4848
import static org.mockito.ArgumentMatchers.anyMap;
49+
import static org.mockito.ArgumentMatchers.argThat;
4950
import static org.mockito.ArgumentMatchers.eq;
5051
import static org.mockito.Mockito.mock;
5152
import static org.mockito.Mockito.when;
@@ -210,6 +211,25 @@ public void getImageLabelsWithInvalidLabels() throws JsonProcessingException {
210211
assertThat(labels).isEmpty();
211212
}
212213

214+
@Test
215+
public void getImageLabelsWithMixedOCIResponses() throws JsonProcessingException {
216+
DefaultContainerImageMetadataResolver resolver = new MockedDefaultContainerImageMetadataResolver(
217+
this.containerRegistryService);
218+
String ociInCompatible = "{\"schemaVersion\": 1,\"name\": \"test/image\"}";
219+
String ociCompatible = "{\"schemaVersion\": 2,\"mediaType\": \"application/vnd.oci.image.manifest.v1+json\",\"config\":{\"mediaType\": \"application/vnd.oci.image.config.v1+json\",\"digest\": \"sha256:efc06d6096cc88697e477abb0b3479557e1bec688c36813383f1a8581f87d9f8\",\"size\": 34268}}";
220+
mockManifestRestTemplateCallAccepts(ociInCompatible, "my-private-repository.com", "5000", "test/image",
221+
"latest", ContainerRegistryProperties.DOCKER_IMAGE_MANIFEST_MEDIA_TYPE);
222+
mockManifestRestTemplateCallAccepts(ociCompatible, "my-private-repository.com", "5000", "test/image", "latest",
223+
ContainerRegistryProperties.OCI_IMAGE_MANIFEST_MEDIA_TYPE);
224+
String blobResponse = "{\"config\": {\"Labels\": {\"boza\": \"koza\"}}}";
225+
mockBlogRestTemplateCall(blobResponse, "my-private-repository.com", "5000", "test/image",
226+
"sha256:efc06d6096cc88697e477abb0b3479557e1bec688c36813383f1a8581f87d9f8");
227+
228+
Map<String, String> labels = resolver.getImageLabels("my-private-repository.com:5000/test/image:latest");
229+
assertThat(labels).isNotEmpty();
230+
assertThat(labels).containsEntry("boza", "koza");
231+
}
232+
213233
private void mockManifestRestTemplateCall(Map<String, Object> mapToReturn, String registryHost,
214234
String registryPort, String repository, String tagOrDigest) {
215235

@@ -246,6 +266,39 @@ private void mockBlogRestTemplateCall(String jsonResponse, String registryHost,
246266
.thenReturn(new ResponseEntity<>(new ObjectMapper().readValue(jsonResponse, Map.class), HttpStatus.OK));
247267
}
248268

269+
private void mockManifestRestTemplateCallAccepts(String jsonResponse, String registryHost, String registryPort,
270+
String repository, String tagOrDigest, String accepts) throws JsonProcessingException {
271+
272+
UriComponents blobUriComponents = UriComponentsBuilder.newInstance()
273+
.scheme("https")
274+
.host(registryHost)
275+
.port(StringUtils.hasText(registryPort) ? registryPort : null)
276+
.path("v2/{repository}/manifests/{reference}")
277+
.build()
278+
.expand(repository, tagOrDigest);
279+
280+
MediaType mediaType = new MediaType(org.apache.commons.lang3.StringUtils.substringBefore(accepts, "/"),
281+
org.apache.commons.lang3.StringUtils.substringAfter(accepts, "/"));
282+
when(mockRestTemplate.exchange(eq(blobUriComponents.toUri()), eq(HttpMethod.GET),
283+
argThat(new HeaderAccepts(mediaType)), eq(Map.class)))
284+
.thenReturn(new ResponseEntity<>(new ObjectMapper().readValue(jsonResponse, Map.class), HttpStatus.OK));
285+
}
286+
287+
static class HeaderAccepts implements ArgumentMatcher<HttpEntity<?>> {
288+
289+
private final MediaType accepts;
290+
291+
public HeaderAccepts(MediaType accepts) {
292+
this.accepts = accepts;
293+
}
294+
295+
@Override
296+
public boolean matches(HttpEntity<?> argument) {
297+
return argument.getHeaders().getAccept().contains(accepts);
298+
}
299+
300+
}
301+
249302
private class MockedDefaultContainerImageMetadataResolver extends DefaultContainerImageMetadataResolver {
250303
public MockedDefaultContainerImageMetadataResolver(ContainerRegistryService containerRegistryService) {
251304
super(containerRegistryService);

0 commit comments

Comments
 (0)