Skip to content

Commit 506b552

Browse files
authored
List repositories (#275)
2 parents be53b90 + 5e6eaaf commit 506b552

9 files changed

Lines changed: 128 additions & 0 deletions

File tree

src/main/java/land/oras/ContainerRef.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,23 @@ private String getApiPrefix(@Nullable Registry target) {
205205
return "%s/v2/%s".formatted(getApiRegistry(target), repository);
206206
}
207207

208+
/**
209+
* Return the catalog repositories URL
210+
* @param target The target registry
211+
* @return The tag URL
212+
*/
213+
public String getRepositoriesPath(@Nullable Registry target) {
214+
return "%s/v2/_catalog".formatted(getApiRegistry(target));
215+
}
216+
217+
/**
218+
* Return the catalog repositories URL
219+
* @return The tag URL
220+
*/
221+
public String getRepositoriesPath() {
222+
return getRepositoriesPath(null);
223+
}
224+
208225
/**
209226
* Return the tag URL
210227
* @param target The target registry

src/main/java/land/oras/OCI.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,12 @@ public final Manifest attachArtifact(T ref, ArtifactType artifactType, LocalPath
256256
*/
257257
public abstract Tags getTags(T ref);
258258

259+
/**
260+
* Get the tags for a ref
261+
* @return The repositories
262+
*/
263+
public abstract Repositories getRepositories();
264+
259265
/**
260266
* Push an artifact
261267
* @param ref The container

src/main/java/land/oras/OCILayout.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,11 @@ public Tags getTags(LayoutRef ref) {
303303
return new Tags(name, tags);
304304
}
305305

306+
@Override
307+
public Repositories getRepositories() {
308+
return new Repositories(List.of(path.getFileName().toString()));
309+
}
310+
306311
@Override
307312
public Referrers getReferrers(LayoutRef ref, @Nullable ArtifactType artifactType) {
308313
Index index = Index.fromPath(getIndexPath());

src/main/java/land/oras/Registry.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,19 @@ public Tags getTags(ContainerRef containerRef) {
161161
return JsonUtils.fromJson(response.response(), Tags.class);
162162
}
163163

164+
@Override
165+
public Repositories getRepositories() {
166+
ContainerRef containerRef = ContainerRef.parse("default").forRegistry(this);
167+
URI uri = URI.create("%s://%s".formatted(getScheme(), containerRef.getRepositoriesPath(this)));
168+
HttpClient.ResponseWrapper<String> response = client.get(
169+
uri,
170+
Map.of(Const.ACCEPT_HEADER, Const.DEFAULT_JSON_MEDIA_TYPE),
171+
Scopes.of(this, containerRef),
172+
authProvider);
173+
handleError(response);
174+
return JsonUtils.fromJson(response.response(), Repositories.class);
175+
}
176+
164177
@Override
165178
public Referrers getReferrers(ContainerRef containerRef, @Nullable ArtifactType artifactType) {
166179
if (containerRef.getDigest() == null) {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*-
2+
* =LICENSE=
3+
* ORAS Java SDK
4+
* ===
5+
* Copyright (C) 2024 - 2025 ORAS
6+
* ===
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
* =LICENSEEND=
19+
*/
20+
21+
package land.oras;
22+
23+
import java.util.List;
24+
import org.jspecify.annotations.NullMarked;
25+
26+
/**
27+
* The repositories response object
28+
* @param repositories The repositories
29+
*/
30+
@NullMarked
31+
public record Repositories(List<String> repositories) {}

src/test/java/land/oras/ContainerRefTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ void shouldParseImageWithNoRegistry() {
166166
containerRef.getApiRegistry(
167167
Registry.builder().withRegistry("foo.io").build()));
168168
assertEquals("registry-1.docker.io/v2/library/alpine/tags/list", containerRef.getTagsPath());
169+
assertEquals("registry-1.docker.io/v2/_catalog", containerRef.getRepositoriesPath());
169170
assertEquals("library", containerRef.getNamespace());
170171
assertEquals("alpine", containerRef.getRepository());
171172
assertEquals("latest", containerRef.getTag());
@@ -243,6 +244,7 @@ void shouldHandleNoNamespace() {
243244
void shouldGetTagsPathOtherRegistry() {
244245
ContainerRef containerRef = ContainerRef.parse("demo.goharbor.io/foo/alpine:latest@sha256:1234567890abcdef");
245246
assertEquals("demo.goharbor.io/v2/foo/alpine/tags/list", containerRef.getTagsPath());
247+
assertEquals("demo.goharbor.io/v2/_catalog", containerRef.getRepositoriesPath());
246248
assertEquals("demo.goharbor.io/v2/foo/alpine/blobs/sha256:1234567890abcdef", containerRef.getBlobsPath(null));
247249
assertEquals(
248250
"foo/alpine",

src/test/java/land/oras/OCILayoutTest.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,19 @@ void shouldListTags() throws Exception {
141141
assertEquals("latest", tags.tags().get(0));
142142
}
143143

144+
@Test
145+
void shouldListRepositories() throws Exception {
146+
Path extractDir1 = extractDir.resolve("shouldListRepositories");
147+
Files.createDirectory(extractDir1);
148+
149+
LayoutRef layoutRef = LayoutRef.parse("src/test/resources/oci/subject:latest");
150+
OCILayout ociLayout =
151+
OCILayout.Builder.builder().defaults(layoutRef.getFolder()).build();
152+
Repositories repositories = ociLayout.getRepositories();
153+
assertEquals(1, repositories.repositories().size());
154+
assertEquals("subject", repositories.repositories().get(0));
155+
}
156+
144157
@Test
145158
void shouldPushConfig() throws IOException {
146159
Path path = layoutPath.resolve("shouldPushConfig");

src/test/java/land/oras/RegistryTest.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,21 @@ void before() {
6666
registry.withFollowOutput();
6767
}
6868

69+
@Test
70+
void shouldListRepositories() {
71+
72+
// Setup
73+
Registry registry = Registry.Builder.builder()
74+
.defaults("myuser", "mypass")
75+
.withRegistry(this.registry.getRegistry())
76+
.withInsecure(true)
77+
.build();
78+
79+
// Test
80+
List<String> repositories = registry.getRepositories().repositories();
81+
assertNotNull(repositories);
82+
}
83+
6984
@Test
7085
void shouldFailToPushBlobForInvalidDigest() {
7186
Registry registry = Registry.Builder.builder()

src/test/java/land/oras/RegistryWireMockTest.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,32 @@ void shouldListTags(WireMockRuntimeInfo wmRuntimeInfo) {
158158
assertEquals("0.1.1", tags.get(1));
159159
}
160160

161+
@Test
162+
void shouldListRepositories(WireMockRuntimeInfo wmRuntimeInfo) {
163+
164+
// Return data from wiremock
165+
WireMock wireMock = wmRuntimeInfo.getWireMock();
166+
wireMock.register(WireMock.get(WireMock.urlEqualTo("/v2/_catalog"))
167+
.willReturn(
168+
WireMock.okJson(JsonUtils.toJson(new Repositories(List.of("foo", "bar", "library/alpine"))))));
169+
170+
// Insecure registry
171+
Registry registry = Registry.Builder.builder()
172+
.withAuthProvider(authProvider)
173+
.withInsecure(true)
174+
.withRegistry(wmRuntimeInfo.getHttpBaseUrl().replace("http://", ""))
175+
.build();
176+
177+
// Test
178+
List<String> repositories = registry.getRepositories().repositories();
179+
180+
// Assert
181+
assertEquals(3, repositories.size());
182+
assertEquals("foo", repositories.get(0));
183+
assertEquals("bar", repositories.get(1));
184+
assertEquals("library/alpine", repositories.get(2));
185+
}
186+
161187
@Test
162188
void shouldListTagsWithFileStoreAuth(WireMockRuntimeInfo wmRuntimeInfo) throws IOException {
163189

0 commit comments

Comments
 (0)