Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/main/java/land/oras/ContainerRef.java
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,21 @@ public ContainerRef withTag(String tag) {
return new ContainerRef(registry, unqualified, namespace, repository, tag, digest);
}

/**
* Return a copy of this reference pointing at the tag used by the referrers tag schema, the
* fallback used by registries that do not implement the Referrers API.
* See <a href="https://github.com/opencontainers/distribution-spec/blob/main/spec.md#referrers-tag-schema">Referrers Tag Schema</a>
* @return The container reference for the referrers fallback tag
*/
public ContainerRef withReferrersFallbackTag() {
if (digest == null) {
throw new OrasException("Digest is required to compute the referrers fallback tag");
}
String fallbackTag = "%s-%s"
.formatted(SupportedAlgorithm.fromDigest(digest).getPrefix(), SupportedAlgorithm.getDigest(digest));
return new ContainerRef(registry, unqualified, namespace, repository, fallbackTag, null);
}

@Override
public SupportedAlgorithm getAlgorithm() {
// Default if not set
Expand Down
34 changes: 34 additions & 0 deletions src/main/java/land/oras/Registry.java
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,9 @@ public Referrers getReferrers(ContainerRef containerRef, @Nullable ArtifactType
HttpClient.ResponseWrapper<String> response = client.get(
uri, Map.of(Const.ACCEPT_HEADER, Const.DEFAULT_INDEX_MEDIA_TYPE), Scopes.of(ref), authProvider);
logResponse(response);
if (response.statusCode() == 404) {
return getReferrersFallback(ref, artifactType);
}
handleError(response);
Referrers page = JsonUtils.fromJson(response.response(), Referrers.class);
String last = getLastParamFromLink(response);
Expand All @@ -643,6 +646,37 @@ public Referrers getReferrers(ContainerRef containerRef, @Nullable ArtifactType
return Referrers.from(allManifests);
}

/**
* Fallback to the referrers tag schema when the Referrers API is unavailable (returns 404).
* See <a href="https://github.com/opencontainers/distribution-spec/blob/main/spec.md#unavailable-referrers-api">Unavailable Referrers API</a>
* @param ref The container reference (with digest) to find referrers of
* @param artifactType The optional artifact type to filter on
* @return The referrers, or an empty list if the fallback tag does not exist or is not a valid index
*/
private Referrers getReferrersFallback(ContainerRef ref, @Nullable ArtifactType artifactType) {
ContainerRef fallbackRef = ref.withReferrersFallbackTag();
URI uri = URI.create("%s://%s".formatted(getScheme(), fallbackRef.getManifestsPath(this)));
HttpClient.ResponseWrapper<String> response = client.get(
uri, Map.of(Const.ACCEPT_HEADER, Const.DEFAULT_INDEX_MEDIA_TYPE), Scopes.of(fallbackRef), authProvider);
logResponse(response);
if (response.statusCode() >= 400) {
// No referrers tag either: assume there are no referrers for this digest
return Referrers.from(List.of());
}
String contentType = response.headers().get(Const.CONTENT_TYPE_HEADER.toLowerCase());
if (contentType == null || !isIndexMediaType(contentType)) {
// Not a valid image index: assume there are no referrers for this digest
return Referrers.from(List.of());
}
List<ManifestDescriptor> manifests = Index.fromJson(response.response()).getManifests();
if (artifactType != null) {
manifests = manifests.stream()
.filter(manifest -> artifactType.getMediaType().equals(manifest.getArtifactType()))
.toList();
}
return Referrers.from(manifests);
}

/**
* Delete a manifest
* @param containerRef The artifact
Expand Down
16 changes: 14 additions & 2 deletions src/test/java/land/oras/GitHubContainerRegistryITCase.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

package land.oras;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

Expand All @@ -29,7 +30,6 @@
import land.oras.utils.ArchiveUtils;
import land.oras.utils.Const;
import land.oras.utils.ZotUnsecureContainer;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.api.parallel.Execution;
Expand Down Expand Up @@ -65,7 +65,19 @@ void shouldPullIndex() {
}

@Test
@Disabled("Disabled because Referrers on GHCR")
void shouldGetReferrersUsingLegacyTagFallback() {
Registry registry = Registry.builder().build();
ContainerRef containerRef = ContainerRef.parse(
"ghcr.io/jonesbusy/alpine-signed@sha256:9e56ed4cb843f61658fcdb17d4205a87d5e217515f23831314b2173a776174d6");
Referrers referrers = registry.getReferrers(containerRef, null);
assertFalse(referrers.getManifests().isEmpty(), "Referrers must be found through the legacy tag fallback");
assertTrue(
referrers.getManifests().stream().anyMatch(manifest -> "application/vnd.dev.sigstore.bundle.v0.3+json"
.equals(manifest.getArtifactType())),
"Sigstore bundle referrer must be found");
}

@Test
@Execution(ExecutionMode.SAME_THREAD)
void shouldPullSignedImage(@TempDir Path homeDir) throws Exception {

Expand Down
108 changes: 108 additions & 0 deletions src/test/java/land/oras/RegistryWireMockTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -1765,4 +1765,112 @@ void shouldFailOnReferrerListPaginationExceedingMaxPages(WireMockRuntimeInfo wmR
exception.getMessage().contains("Referrer listing exceeded 2 pages"),
"Unexpected message: " + exception.getMessage());
}

@Test
void shouldFallbackToReferrersTagSchemaWhenReferrersApiUnavailable(WireMockRuntimeInfo wmRuntimeInfo) {

String digest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a";
String referrersPath = "/v2/library/artifact-text/referrers/" + digest;
String fallbackTagPath = "/v2/library/artifact-text/manifests/"
+ "sha256-44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a";

String fallbackIndex =
"""
{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":\
[{"mediaType":"application/vnd.oci.image.manifest.v1+json",\
"digest":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","size":1,\
"artifactType":"application/vnd.example+json"}]}""";

WireMock wireMock = wmRuntimeInfo.getWireMock();
// Registry does not implement the Referrers API (e.g. GHCR)
wireMock.register(WireMock.get(WireMock.urlPathEqualTo(referrersPath)).willReturn(WireMock.notFound()));
// Fallback to the referrers tag schema:
wireMock.register(WireMock.get(WireMock.urlPathEqualTo(fallbackTagPath))
.willReturn(WireMock.aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/vnd.oci.image.index.v1+json")
.withBody(fallbackIndex)));

Registry registry = Registry.Builder.builder()
.withAuthProvider(authProvider)
.withInsecure(true)
.build();

ContainerRef ref = ContainerRef.parse("%s/library/artifact-text"
.formatted(wmRuntimeInfo.getHttpBaseUrl().replace("http://", "")))
.withDigest(digest);

Referrers referrers = registry.getReferrers(ref, null);
assertEquals(1, referrers.getManifests().size());
assertEquals(
"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
referrers.getManifests().get(0).getDigest());
}

@Test
void shouldFilterFallbackReferrersByArtifactType(WireMockRuntimeInfo wmRuntimeInfo) {

String digest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a";
String referrersPath = "/v2/library/artifact-text/referrers/" + digest;
String fallbackTagPath = "/v2/library/artifact-text/manifests/"
+ "sha256-44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a";

String fallbackIndex =
"""
{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":\
[{"mediaType":"application/vnd.oci.image.manifest.v1+json",\
"digest":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","size":1,\
"artifactType":"application/vnd.example+json"},\
{"mediaType":"application/vnd.oci.image.manifest.v1+json",\
"digest":"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","size":1,\
"artifactType":"application/vnd.other+json"}]}""";

WireMock wireMock = wmRuntimeInfo.getWireMock();
wireMock.register(WireMock.get(WireMock.urlPathEqualTo(referrersPath)).willReturn(WireMock.notFound()));
wireMock.register(WireMock.get(WireMock.urlPathEqualTo(fallbackTagPath))
.willReturn(WireMock.aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/vnd.oci.image.index.v1+json")
.withBody(fallbackIndex)));

Registry registry = Registry.Builder.builder()
.withAuthProvider(authProvider)
.withInsecure(true)
.build();

ContainerRef ref = ContainerRef.parse("%s/library/artifact-text"
.formatted(wmRuntimeInfo.getHttpBaseUrl().replace("http://", "")))
.withDigest(digest);

Referrers referrers = registry.getReferrers(ref, ArtifactType.from("application/vnd.example+json"));
assertEquals(1, referrers.getManifests().size());
assertEquals(
"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
referrers.getManifests().get(0).getDigest());
}

@Test
void shouldReturnEmptyReferrersWhenFallbackTagDoesNotExist(WireMockRuntimeInfo wmRuntimeInfo) {

String digest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a";
String referrersPath = "/v2/library/artifact-text/referrers/" + digest;
String fallbackTagPath = "/v2/library/artifact-text/manifests/"
+ "sha256-44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a";

WireMock wireMock = wmRuntimeInfo.getWireMock();
wireMock.register(WireMock.get(WireMock.urlPathEqualTo(referrersPath)).willReturn(WireMock.notFound()));
wireMock.register(WireMock.get(WireMock.urlPathEqualTo(fallbackTagPath)).willReturn(WireMock.notFound()));

Registry registry = Registry.Builder.builder()
.withAuthProvider(authProvider)
.withInsecure(true)
.build();

ContainerRef ref = ContainerRef.parse("%s/library/artifact-text"
.formatted(wmRuntimeInfo.getHttpBaseUrl().replace("http://", "")))
.withDigest(digest);

Referrers referrers = registry.getReferrers(ref, null);
assertTrue(referrers.getManifests().isEmpty());
}
}
Loading