Skip to content

Commit

Permalink
Provider for trying OCI accepts header when manifest result returns n…
Browse files Browse the repository at this point in the history
…o config and schemaVersion less than 1.

Issue #5819
  • Loading branch information
Corneil du Plessis authored and cppwfs committed May 28, 2024
1 parent b0bf445 commit 04765e7
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.util.Map;

import org.springframework.cloud.dataflow.container.registry.ContainerRegistryException;
import org.springframework.cloud.dataflow.container.registry.ContainerRegistryProperties;
import org.springframework.cloud.dataflow.container.registry.ContainerRegistryRequest;
import org.springframework.cloud.dataflow.container.registry.ContainerRegistryService;
import org.springframework.util.StringUtils;
Expand All @@ -30,6 +31,7 @@
*
* @author Christian Tzolov
* @author Ilayaperumal Gopinathan
* @author Corneil du Plessis
*/
public class DefaultContainerImageMetadataResolver implements ContainerImageMetadataResolver {

Expand All @@ -39,6 +41,7 @@ public DefaultContainerImageMetadataResolver(ContainerRegistryService containerR
this.containerRegistryService = containerRegistryService;
}

@SuppressWarnings("unchecked")
@Override
public Map<String, String> getImageLabels(String imageName) {

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

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

Map manifest = this.containerRegistryService.getImageManifest(registryRequest, Map.class);

if (manifest != null && !isNotNullMap(manifest.get("config"))) {
throw new ContainerRegistryException(
String.format("Image [%s] has incorrect or missing manifest config element: %s",
imageName, manifest.toString()));
Map<String, Object> manifest = this.containerRegistryService.getImageManifest(registryRequest, Map.class);

if (manifest != null && manifest.get("config") == null) {
// when both Docker and OCI images are stored in repository the response for OCI image when using Docker manifest type will not contain config.
// In the case where we don't receive a config and schemaVersion is less than 2 we try OCI manifest type.
String manifestMediaType = registryRequest.getRegistryConf().getManifestMediaType();
if (asInt(manifest.get("schemaVersion")) < 2
&& !manifestMediaType.equals(ContainerRegistryProperties.OCI_IMAGE_MANIFEST_MEDIA_TYPE)) {
registryRequest.getRegistryConf()
.setManifestMediaType(ContainerRegistryProperties.OCI_IMAGE_MANIFEST_MEDIA_TYPE);
manifest = this.containerRegistryService.getImageManifest(registryRequest, Map.class);
}
if (manifest.get("config") == null) {
String message = String.format("Image [%s] has incorrect or missing manifest config element: %s",
imageName, manifest);
throw new ContainerRegistryException(message);
}
}
if (manifest != null) {
String configDigest = ((Map<String, String>) manifest.get("config")).get("digest");
Expand Down Expand Up @@ -85,12 +99,24 @@ public Map<String, String> getImageLabels(String imageName) {
(Map<String, String>) configElement.get("Labels") : Collections.emptyMap();
}
else {
throw new ContainerRegistryException(
String.format("Image [%s] is missing manifest", imageName));
throw new ContainerRegistryException(String.format("Image [%s] is missing manifest", imageName));
}
}

private static int asInt(Object value) {
if (value instanceof Number) {
return ((Number) value).intValue();
}
else if (value instanceof String) {
return Integer.parseInt((String) value);
}
else if (value != null) {
return Integer.parseInt(value.toString());
}
return 0;
}

private boolean isNotNullMap(Object object) {
return object != null && (object instanceof Map);
private static boolean isNotNullMap(Object object) {
return object instanceof Map;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,15 @@

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

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentMatcher;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

Expand All @@ -35,6 +34,7 @@
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
Expand All @@ -46,6 +46,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
Expand Down Expand Up @@ -210,6 +211,25 @@ public void getImageLabelsWithInvalidLabels() throws JsonProcessingException {
assertThat(labels).isEmpty();
}

@Test
public void getImageLabelsWithMixedOCIResponses() throws JsonProcessingException {
DefaultContainerImageMetadataResolver resolver = new MockedDefaultContainerImageMetadataResolver(
this.containerRegistryService);
String ociInCompatible = "{\"schemaVersion\": 1,\"name\": \"test/image\"}";
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}}";
mockManifestRestTemplateCallAccepts(ociInCompatible, "my-private-repository.com", "5000", "test/image",
"latest", ContainerRegistryProperties.DOCKER_IMAGE_MANIFEST_MEDIA_TYPE);
mockManifestRestTemplateCallAccepts(ociCompatible, "my-private-repository.com", "5000", "test/image", "latest",
ContainerRegistryProperties.OCI_IMAGE_MANIFEST_MEDIA_TYPE);
String blobResponse = "{\"config\": {\"Labels\": {\"boza\": \"koza\"}}}";
mockBlogRestTemplateCall(blobResponse, "my-private-repository.com", "5000", "test/image",
"sha256:efc06d6096cc88697e477abb0b3479557e1bec688c36813383f1a8581f87d9f8");

Map<String, String> labels = resolver.getImageLabels("my-private-repository.com:5000/test/image:latest");
assertThat(labels).isNotEmpty();
assertThat(labels).containsEntry("boza", "koza");
}

private void mockManifestRestTemplateCall(Map<String, Object> mapToReturn, String registryHost,
String registryPort, String repository, String tagOrDigest) {

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

private void mockManifestRestTemplateCallAccepts(String jsonResponse, String registryHost, String registryPort,
String repository, String tagOrDigest, String accepts) throws JsonProcessingException {

UriComponents blobUriComponents = UriComponentsBuilder.newInstance()
.scheme("https")
.host(registryHost)
.port(StringUtils.hasText(registryPort) ? registryPort : null)
.path("v2/{repository}/manifests/{reference}")
.build()
.expand(repository, tagOrDigest);

MediaType mediaType = new MediaType(org.apache.commons.lang3.StringUtils.substringBefore(accepts, "/"),
org.apache.commons.lang3.StringUtils.substringAfter(accepts, "/"));
when(mockRestTemplate.exchange(eq(blobUriComponents.toUri()), eq(HttpMethod.GET),
argThat(new HeaderAccepts(mediaType)), eq(Map.class)))
.thenReturn(new ResponseEntity<>(new ObjectMapper().readValue(jsonResponse, Map.class), HttpStatus.OK));
}

static class HeaderAccepts implements ArgumentMatcher<HttpEntity<?>> {

private final MediaType accepts;

public HeaderAccepts(MediaType accepts) {
this.accepts = accepts;
}

@Override
public boolean matches(HttpEntity<?> argument) {
return argument.getHeaders().getAccept().contains(accepts);
}

}

private class MockedDefaultContainerImageMetadataResolver extends DefaultContainerImageMetadataResolver {
public MockedDefaultContainerImageMetadataResolver(ContainerRegistryService containerRegistryService) {
super(containerRegistryService);
Expand Down

0 comments on commit 04765e7

Please sign in to comment.