Skip to content

Commit 6ee2fba

Browse files
committed
Add support for registries mirror
Signed-off-by: Valentin Delaye <jonesbusy@users.noreply.github.com>
1 parent df87bc6 commit 6ee2fba

5 files changed

Lines changed: 404 additions & 26 deletions

File tree

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

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import java.util.concurrent.CompletableFuture;
4040
import java.util.concurrent.ExecutorService;
4141
import java.util.concurrent.Executors;
42+
import java.util.function.BiFunction;
4243
import java.util.function.Supplier;
4344
import land.oras.auth.AuthProvider;
4445
import land.oras.auth.AuthStoreAuthenticationProvider;
@@ -526,9 +527,16 @@ public void deleteBlob(ContainerRef containerRef) {
526527

527528
@Override
528529
public void pullArtifact(ContainerRef containerRef, Path path, boolean overwrite) {
530+
withMirrorFallback(containerRef, (reg, ref) -> {
531+
reg.pullArtifactDirect(ref, path, overwrite);
532+
return null;
533+
});
534+
}
535+
536+
private void pullArtifactDirect(ContainerRef containerRef, Path path, boolean overwrite) {
529537
ContainerRef ref = containerRef.forRegistry(this).checkBlocked(this);
530538
if (ref.isInsecure(this) && !this.isInsecure()) {
531-
asInsecure().pullArtifact(containerRef, path, overwrite);
539+
asInsecure().pullArtifactDirect(containerRef, path, overwrite);
532540
return;
533541
}
534542
// Only collect layer that are files
@@ -945,9 +953,13 @@ private HttpClient.ResponseWrapper<String> headBlob(ContainerRef containerRef) {
945953
*/
946954
@Override
947955
public byte[] getBlob(ContainerRef containerRef) {
956+
return withMirrorFallback(containerRef, (reg, ref) -> reg.getBlobDirect(ref));
957+
}
958+
959+
private byte[] getBlobDirect(ContainerRef containerRef) {
948960
ContainerRef ref = containerRef.forRegistry(this).checkBlocked(this);
949961
if (ref.isInsecure(this) && !this.isInsecure()) {
950-
return asInsecure().getBlob(containerRef);
962+
return asInsecure().getBlobDirect(containerRef);
951963
}
952964
URI uri = URI.create("%s://%s".formatted(getScheme(), ref.getBlobsPath(this)));
953965
HttpClient.ResponseWrapper<String> response = client.get(
@@ -964,9 +976,16 @@ public byte[] getBlob(ContainerRef containerRef) {
964976

965977
@Override
966978
public void fetchBlob(ContainerRef containerRef, Path path) {
979+
withMirrorFallback(containerRef, (reg, ref) -> {
980+
reg.fetchBlobDirect(ref, path);
981+
return null;
982+
});
983+
}
984+
985+
private void fetchBlobDirect(ContainerRef containerRef, Path path) {
967986
ContainerRef ref = containerRef.forRegistry(this).checkBlocked(this);
968987
if (ref.isInsecure(this) && !this.isInsecure()) {
969-
asInsecure().fetchBlob(containerRef, path);
988+
asInsecure().fetchBlobDirect(containerRef, path);
970989
return;
971990
}
972991
URI uri = URI.create("%s://%s".formatted(getScheme(), ref.getBlobsPath(this)));
@@ -983,9 +1002,13 @@ public void fetchBlob(ContainerRef containerRef, Path path) {
9831002

9841003
@Override
9851004
public InputStream fetchBlob(ContainerRef containerRef) {
1005+
return withMirrorFallback(containerRef, (reg, ref) -> reg.fetchBlobDirect(ref));
1006+
}
1007+
1008+
private InputStream fetchBlobDirect(ContainerRef containerRef) {
9861009
ContainerRef ref = containerRef.forRegistry(this).checkBlocked(this);
9871010
if (ref.isInsecure(this) && !this.isInsecure()) {
988-
return asInsecure().fetchBlob(containerRef);
1011+
return asInsecure().fetchBlobDirect(containerRef);
9891012
}
9901013
URI uri = URI.create("%s://%s".formatted(getScheme(), ref.getBlobsPath(this)));
9911014
HttpClient.ResponseWrapper<InputStream> response = client.download(
@@ -1106,14 +1129,45 @@ boolean exists(ContainerRef containerRef) {
11061129
}
11071130

11081131
/**
1109-
* Get a manifest response
1132+
* Execute an operation with mirror fallback. Mirrors are tried in order; if all fail the
1133+
* operation is retried against the original registry.
1134+
* @param containerRef The original container reference used to look up mirrors
1135+
* @param operation A function (registry, ref) → result; called for each candidate
1136+
* @return The result from the first successful invocation
1137+
*/
1138+
private <T> T withMirrorFallback(ContainerRef containerRef, BiFunction<Registry, ContainerRef, T> operation) {
1139+
List<RegistriesConf.MirrorConfig> mirrors = registriesConf.getMirrors(containerRef);
1140+
for (RegistriesConf.MirrorConfig mirror : mirrors) {
1141+
String mirrorLocation = mirror.location();
1142+
if (mirrorLocation == null) continue;
1143+
ContainerRef mirrorRef = registriesConf.rewriteForMirror(containerRef, mirror);
1144+
Registry mirrorRegistry = copy(mirrorLocation);
1145+
if (mirror.isInsecure() && !isInsecure()) {
1146+
mirrorRegistry = mirrorRegistry.asInsecure();
1147+
}
1148+
try {
1149+
LOG.debug("Trying mirror {} for {}", mirrorLocation, containerRef);
1150+
return operation.apply(mirrorRegistry, mirrorRef);
1151+
} catch (OrasException e) {
1152+
LOG.warn("Mirror {} failed for {}: {}", mirrorLocation, containerRef, e.getMessage());
1153+
}
1154+
}
1155+
return operation.apply(this, containerRef);
1156+
}
1157+
1158+
/**
1159+
* Get a manifest response, trying configured mirrors before the original registry.
11101160
* @param containerRef The container
11111161
* @return The response
11121162
*/
11131163
private HttpClient.ResponseWrapper<String> getManifestResponse(ContainerRef containerRef) {
1164+
return withMirrorFallback(containerRef, (reg, ref) -> reg.getManifestResponseDirect(ref));
1165+
}
1166+
1167+
private HttpClient.ResponseWrapper<String> getManifestResponseDirect(ContainerRef containerRef) {
11141168
ContainerRef ref = containerRef.forRegistry(this).checkBlocked(this);
11151169
if (ref.isInsecure(this) && !this.isInsecure()) {
1116-
return asInsecure().getManifestResponse(containerRef);
1170+
return asInsecure().getManifestResponseDirect(containerRef);
11171171
}
11181172
URI uri = URI.create("%s://%s".formatted(getScheme(), ref.getManifestsPath(this)));
11191173
HttpClient.ResponseWrapper<String> response =

src/main/java/land/oras/auth/RegistriesConf.java

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,19 +99,34 @@ public static RegistriesConf newConf() {
9999
return newConf(paths);
100100
}
101101

102+
/**
103+
* The model of a mirror entry within a [[registry]] table.
104+
* @param location The mirror registry location (host[:port][/path]).
105+
* @param insecure Whether the mirror is insecure.
106+
*/
107+
@OrasModel
108+
public record MirrorConfig(
109+
@Nullable @JsonProperty("location") String location, @Nullable @JsonProperty("insecure") Boolean insecure) {
110+
public boolean isInsecure() {
111+
return insecure != null && insecure;
112+
}
113+
}
114+
102115
/**
103116
* The model of the registry configuration
104117
* @param prefix The prefix to match against container references.
105118
* @param location The registry location
106119
* @param blocked Whether the registry is blocked. If true, the registry is blocked and cannot be used for pulling or pushing images.
107120
* @param insecure Whether the registry is insecure. If true, the registry is considered insecure and may allow connections over HTTP or with invalid TLS certificates.
121+
* @param mirrors Ordered list of mirror entries to try before the registry location.
108122
*/
109123
@OrasModel
110-
record RegistryConfig(
124+
public record RegistryConfig(
111125
@Nullable @JsonProperty("prefix") String prefix,
112126
@Nullable @JsonProperty("location") String location,
113127
@Nullable @JsonProperty("blocked") Boolean blocked,
114-
@Nullable @JsonProperty("insecure") Boolean insecure) {
128+
@Nullable @JsonProperty("insecure") Boolean insecure,
129+
@Nullable @JsonProperty("mirror") List<MirrorConfig> mirrors) {
115130
public boolean isBlocked() {
116131
return blocked != null && blocked;
117132
}
@@ -333,6 +348,40 @@ public ContainerRef rewrite(ContainerRef ref) {
333348
return ContainerRef.parse(rewrittenRefString);
334349
}
335350

351+
/**
352+
* Return the ordered list of mirrors configured for the registry that matches the given reference.
353+
* @param ref the container reference to look up mirrors for.
354+
* @return an unmodifiable list of mirror configs (may be empty).
355+
*/
356+
public List<MirrorConfig> getMirrors(ContainerRef ref) {
357+
Optional<RegistryConfig> matchingConfig = selectMatchingTable(ref);
358+
if (matchingConfig.isEmpty() || matchingConfig.get().mirrors() == null) {
359+
return Collections.emptyList();
360+
}
361+
return Collections.unmodifiableList(matchingConfig.get().mirrors());
362+
}
363+
364+
/**
365+
* Rewrite the given container reference to use the mirror's location, replacing the registry host.
366+
* @param ref the original container reference.
367+
* @param mirror the mirror configuration to apply.
368+
* @return the rewritten reference pointing at the mirror.
369+
*/
370+
public ContainerRef rewriteForMirror(ContainerRef ref, MirrorConfig mirror) {
371+
String mirrorLocation = mirror.location();
372+
if (mirrorLocation == null || mirrorLocation.isBlank()) {
373+
return ref;
374+
}
375+
String currentRegistry = ref.getRegistry();
376+
if (currentRegistry == null) {
377+
return ref;
378+
}
379+
String currentRefString = ref.toString();
380+
String rewritten = mirrorLocation + currentRefString.substring(currentRegistry.length());
381+
LOG.debug("Rewriting '{}' to mirror '{}'", currentRefString, rewritten);
382+
return ContainerRef.parse(rewritten);
383+
}
384+
336385
/**
337386
* Select the matching registry configuration table for the container reference.
338387
* @param ref the container reference to find the matching registry configuration for.

src/test/java/land/oras/ClassAnnotationsTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ void shouldHaveAnnotationOnModel() {
4949
.loadClasses());
5050

5151
// Check number of classes
52-
assertEquals(26, modelClasses.size());
52+
assertEquals(27, modelClasses.size());
5353

5454
// Check classes
5555
assertTrue(modelClasses.contains(Annotations.class));
@@ -83,7 +83,7 @@ void shouldHaveAnnotationOnAuthPackage() {
8383
.loadClasses());
8484

8585
// Check number of classes
86-
assertEquals(8, modelClasses.size());
86+
assertEquals(9, modelClasses.size());
8787
}
8888
}
8989
}

0 commit comments

Comments
 (0)