diff --git a/src/main/java/land/oras/Registry.java b/src/main/java/land/oras/Registry.java index ddf7f1ec..0f3fab07 100644 --- a/src/main/java/land/oras/Registry.java +++ b/src/main/java/land/oras/Registry.java @@ -536,20 +536,23 @@ public Tags getTags(ContainerRef containerRef) { logResponse(response); handleError(response); Tags page = JsonUtils.fromJson(response.response(), Tags.class) - .withLast(getLastFromLink(response).orElse(null)); + .withLast(getLastParamFromLink(response)) + .withN(getNParamFromLink(response)); if (page.last() == null) { return page; } // Follow pagination links, accumulating all tags, guarded against unbounded loops. List allTags = new ArrayList<>(page.tags()); String last = page.last(); + Integer n = page.n(); for (int pageNum = 1; last != null; pageNum++) { if (tagListMaxPages > 0 && pageNum >= tagListMaxPages) { throw new OrasException( "Tag listing exceeded %d pages: possible self-referential Link header from a rogue registry" .formatted(tagListMaxPages)); } - Tags nextPage = getTags(ref, Integer.MAX_VALUE, last); + + Tags nextPage = getTags(ref, n == null ? Integer.MAX_VALUE : n, last); allTags.addAll(nextPage.tags()); last = nextPage.last(); } @@ -571,7 +574,8 @@ public Tags getTags(ContainerRef containerRef, int n, @Nullable String last) { logResponse(response); handleError(response); return JsonUtils.fromJson(response.response(), Tags.class) - .withLast(getLastFromLink(response).orElse(null)); + .withLast(getLastParamFromLink(response)) + .withN(getNParamFromLink(response)); } @Override @@ -615,7 +619,7 @@ public Referrers getReferrers(ContainerRef containerRef, @Nullable ArtifactType logResponse(response); handleError(response); Referrers page = JsonUtils.fromJson(response.response(), Referrers.class); - String last = getLastFromLink(response).orElse(null); + String last = getLastParamFromLink(response); if (last == null) { return page; } @@ -634,7 +638,7 @@ public Referrers getReferrers(ContainerRef containerRef, @Nullable ArtifactType handleError(nextResponse); Referrers nextPage = JsonUtils.fromJson(nextResponse.response(), Referrers.class); allManifests.addAll(nextPage.getManifests()); - last = getLastFromLink(nextResponse).orElse(null); + last = getLastParamFromLink(response); } return Referrers.from(allManifests); } @@ -1716,7 +1720,19 @@ ResolvedRegistry getResolvedHeaders(ContainerRef containerRef) { return new ResolvedRegistry(ref.getRegistry(), response.headers()); } - private Optional getLastFromLink(HttpClient.ResponseWrapper response) { + private @Nullable String getLastParamFromLink(HttpClient.ResponseWrapper response) { + return getParamFromLink(response, Const.QUERY_PARAM_LAST).orElse(null); + } + + private @Nullable Integer getNParamFromLink(HttpClient.ResponseWrapper response) { + Optional n = getParamFromLink(response, Const.QUERY_PARAM_N); + if (n.isPresent()) { + return Integer.parseInt(n.get()); + } + return null; + } + + private Optional getParamFromLink(HttpClient.ResponseWrapper response, String param) { String linkHeader = response.headers().get(Const.LINK_HEADER.toLowerCase()); if (linkHeader == null) { return Optional.empty(); @@ -1735,10 +1751,10 @@ private Optional getLastFromLink(HttpClient.ResponseWrapper resp } String query = uri.substring(q + 1); - for (String param : query.split("&")) { - int eq = param.indexOf('='); - if (eq > 0 && "last".equals(param.substring(0, eq))) { - return Optional.of(param.substring(eq + 1)); + for (String p : query.split("&")) { + int eq = p.indexOf('='); + if (eq > 0 && param.equals(p.substring(0, eq))) { + return Optional.of(p.substring(eq + 1)); } } diff --git a/src/main/java/land/oras/Tags.java b/src/main/java/land/oras/Tags.java index 1e1dcfef..588f24cc 100644 --- a/src/main/java/land/oras/Tags.java +++ b/src/main/java/land/oras/Tags.java @@ -30,11 +30,12 @@ * @param name The name * @param tags The tags * @param last The last tag index, to iterate + * @param n The n parameter, to iterate. As part of link response */ @NullMarked @OrasModel @JsonInclude(JsonInclude.Include.NON_NULL) -public record Tags(String name, List tags, @Nullable String last) { +public record Tags(String name, List tags, @Nullable String last, @Nullable Integer n) { /** * Constructor without last @@ -42,7 +43,7 @@ public record Tags(String name, List tags, @Nullable String last) { * @param tags The tags */ public Tags(String name, List tags) { - this(name, tags, null); + this(name, tags, null, null); } /** @@ -51,6 +52,15 @@ public Tags(String name, List tags) { * @return A new Tags object with the last index */ public Tags withLast(@Nullable String last) { - return new Tags(this.name, this.tags, last); + return new Tags(this.name, this.tags, last, n); + } + + /** + * With n param + * @param n The n param + * @return A new Tags object with the n param + */ + public Tags withN(@Nullable Integer n) { + return new Tags(this.name, this.tags, last, n); } } diff --git a/src/main/java/land/oras/auth/HttpClient.java b/src/main/java/land/oras/auth/HttpClient.java index 3482499a..af95e1ff 100644 --- a/src/main/java/land/oras/auth/HttpClient.java +++ b/src/main/java/land/oras/auth/HttpClient.java @@ -766,7 +766,7 @@ private ResponseWrapper executeRequest( continue; } - return redoRequest(uri, response, builder, handler, newScopes, authProvider); + return redoRequest(uri, response, builder, body, handler, newScopes, authProvider); } catch (InterruptedException e) { Thread.currentThread().interrupt(); @@ -855,6 +855,7 @@ private ResponseWrapper redoRequest( URI originUri, HttpResponse response, HttpRequest.Builder builder, + byte[] body, HttpResponse.BodyHandler handler, Scopes scopes, AuthProvider authProvider) { @@ -873,7 +874,9 @@ private ResponseWrapper redoRequest( String service = token.service(); try { builder = builder.setHeader(Const.AUTHORIZATION_HEADER, "Bearer " + bearerToken); - HttpResponse newResponse = executeAndRecordRequest(builder.build(), handler); + HttpRequest request = builder.build(); + logRequest(request, body); + HttpResponse newResponse = executeAndRecordRequest(request, handler); // Follow redirect if (shouldRedirect(newResponse)) { diff --git a/src/main/java/land/oras/utils/Const.java b/src/main/java/land/oras/utils/Const.java index 2a20e1b2..e2befe8e 100644 --- a/src/main/java/land/oras/utils/Const.java +++ b/src/main/java/land/oras/utils/Const.java @@ -103,6 +103,16 @@ private Const() { */ public static final String DEFAULT_REGISTRY = "docker.io"; + /** + * Last query param for tag iteration + */ + public static final String QUERY_PARAM_LAST = "last"; + + /** + * The N query param + */ + public static final String QUERY_PARAM_N = "n"; + /** * Default tag */ diff --git a/src/test/java/land/oras/AgentGatewayDevITCase.java b/src/test/java/land/oras/AgentGatewayDevITCase.java new file mode 100644 index 00000000..2abc3ce6 --- /dev/null +++ b/src/test/java/land/oras/AgentGatewayDevITCase.java @@ -0,0 +1,42 @@ +/*- + * =LICENSE= + * ORAS Java SDK + * === + * Copyright (C) 2024 - 2026 ORAS + * === + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =LICENSEEND= + */ + +package land.oras; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers +@Execution(ExecutionMode.CONCURRENT) +class AgentGatewayDevITCase { + + @Test + void shouldGetTags() { + Registry registry = Registry.builder().build(); + ContainerRef containerRef = ContainerRef.parse("cr.agentgateway.dev/agentgateway"); + Tags tags = registry.getTags(containerRef); + assertTrue(tags.tags().contains("v1.3.0"), "Tag v1.3.0 must exists"); + assertTrue(tags.tags().size() > 400, "More that 400 must be returned"); + } +} diff --git a/src/test/java/land/oras/GitHubContainerRegistryITCase.java b/src/test/java/land/oras/GitHubContainerRegistryITCase.java index b84cc6b7..23aad07d 100644 --- a/src/test/java/land/oras/GitHubContainerRegistryITCase.java +++ b/src/test/java/land/oras/GitHubContainerRegistryITCase.java @@ -47,6 +47,15 @@ class GitHubContainerRegistryITCase { @TempDir Path tempDir; + @Test + void shouldGetTags() { + Registry registry = Registry.builder().build(); + ContainerRef containerRef = ContainerRef.parse("ghcr.io/jenkinsci/helm-charts/jenkins"); + Tags tags = registry.getTags(containerRef); + assertTrue(tags.tags().contains("5.9.40"), "Tag 5.9.40 must exists"); + assertTrue(tags.tags().size() > 50, "More that 50 must be returned"); + } + @Test void shouldPullIndex() { Registry registry = Registry.builder().build();