Skip to content

Commit 9a83af5

Browse files
authored
Add support for registry table prefix and location rewrite (without *, tag or digest yet) (#542)
2 parents 9fff41e + 3e234f0 commit 9a83af5

8 files changed

Lines changed: 429 additions & 78 deletions

File tree

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

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,10 @@ public String getEffectiveRegistry(Registry target) {
400400
? target.getRegistry()
401401
: determineFirstUnqualifiedSearchRegistry(target);
402402
}
403-
return registry;
403+
// The effective registry can we rewrotten by the registry configuration.
404+
// Ensure to return it
405+
ContainerRef rewrite = target.getRegistriesConf().rewrite(this);
406+
return rewrite.getRegistry();
404407
}
405408
/**
406409
* Return a copy of reference for a registry other registry
@@ -418,7 +421,8 @@ public ContainerRef forRegistry(String registry) {
418421
*/
419422
public boolean isInsecure(Registry registry) {
420423
String effectiveRegistry = getEffectiveRegistry(registry);
421-
if (registry.getRegistriesConf().isInsecure(effectiveRegistry)) {
424+
ContainerRef effectiveRef = forRegistry(effectiveRegistry);
425+
if (registry.getRegistriesConf().isInsecure(effectiveRef)) {
422426
LOG.info(
423427
"Access to container reference {} is insecure by location configuration for registry {}",
424428
this,
@@ -434,24 +438,16 @@ public boolean isInsecure(Registry registry) {
434438
* @return True if access to this container reference is blocked, false otherwise
435439
*/
436440
public boolean isBlocked(Registry registry) {
437-
boolean blocked = false;
438441
String effectiveRegistry = getEffectiveRegistry(registry);
439-
if (registry.getRegistriesConf().isBlocked(effectiveRegistry)) {
442+
ContainerRef effectiveRef = forRegistry(effectiveRegistry);
443+
if (registry.getRegistriesConf().isBlocked(effectiveRef)) {
440444
LOG.info(
441-
"Access to container reference {} is blocked by location configuration for registry {}",
445+
"Access to container reference {} is blocked by location/prefix configuration for registry {}",
442446
this,
443-
effectiveRegistry);
444-
blocked = true;
445-
}
446-
String location = "%s/%s".formatted(effectiveRegistry, getFullRepository(registry));
447-
if (registry.getRegistriesConf().isBlocked(location)) {
448-
LOG.info(
449-
"Access to container reference {} is blocked by location configuration for registry/repository {}",
450-
this,
451-
location);
452-
blocked = true;
447+
effectiveRef);
448+
return true;
453449
}
454-
return blocked;
450+
return false;
455451
}
456452

457453
/**
@@ -487,8 +483,11 @@ public ContainerRef forRegistry(Registry registry) {
487483
return new ContainerRef(
488484
determineFirstUnqualifiedSearchRegistry(registry), false, namespace, repository, tag, digest);
489485
}
486+
if (registry.getRegistry() == null) {
487+
return registry.getRegistriesConf().rewrite(this);
488+
}
490489
return new ContainerRef(
491-
registry.getRegistry() != null ? registry.getRegistry() : this.registry,
490+
registry.getRegistry(),
492491
false, // not unqualified if registry is set
493492
namespace,
494493
repository,

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,9 @@ public Tags getTags(ContainerRef containerRef) {
204204

205205
@Override
206206
public Repositories getRepositories() {
207-
if (registry != null && getRegistriesConf().isInsecure(registry) && !this.isInsecure()) {
207+
if (registry != null
208+
&& getRegistriesConf().isInsecure(ContainerRef.parse(registry).forRegistry(registry))
209+
&& !this.isInsecure()) {
208210
return asInsecure().getRepositories();
209211
}
210212
ContainerRef ref = ContainerRef.parse("default").forRegistry(this);

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

Lines changed: 115 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,14 @@
2424
import java.nio.file.Files;
2525
import java.nio.file.Path;
2626
import java.util.Collections;
27+
import java.util.Comparator;
2728
import java.util.HashMap;
2829
import java.util.LinkedList;
2930
import java.util.List;
3031
import java.util.Map;
3132
import java.util.Objects;
33+
import java.util.Optional;
34+
import land.oras.ContainerRef;
3235
import land.oras.exception.OrasException;
3336
import land.oras.utils.TomlUtils;
3437
import org.jspecify.annotations.NullMarked;
@@ -94,12 +97,14 @@ public static RegistriesConf newConf() {
9497

9598
/**
9699
* The model of the registry configuration
100+
* @param prefix The prefix to match against container references.
97101
* @param location The registry location
98102
* @param blocked Whether the registry is blocked. If true, the registry is blocked and cannot be used for pulling or pushing images.
99103
* @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.
100104
*/
101105
record RegistryConfig(
102-
@JsonProperty("location") String location,
106+
@Nullable @JsonProperty("prefix") String prefix,
107+
@Nullable @JsonProperty("location") String location,
103108
@Nullable @JsonProperty("blocked") Boolean blocked,
104109
@Nullable @JsonProperty("insecure") Boolean insecure) {
105110
public boolean isBlocked() {
@@ -112,8 +117,26 @@ public boolean isInsecure() {
112117
}
113118

114119
/**
115-
* The model of the config file
116-
*
120+
* The model of the parsed prefix, which contains the host and path components of the prefix.
121+
* @param host The host component of the prefix, which can be a specific hostname or a wildcard pattern (e.g., *.example.com).
122+
* @param path The path component of the prefix, which can be a specific path or a path prefix (e.g., namespace/repo).
123+
*/
124+
record ParsedPrefix(String host, String path) {
125+
126+
static ParsedPrefix parse(String prefix) {
127+
int slash = prefix.indexOf('/');
128+
if (slash < 0) {
129+
return new ParsedPrefix(prefix, "");
130+
}
131+
return new ParsedPrefix(prefix.substring(0, slash), prefix.substring(slash + 1));
132+
}
133+
}
134+
135+
/**
136+
* The model of the configuration file, which contains the list of registry configurations, aliases, and unqualified registries.
137+
* @param registries The list of registry configurations, each containing the registry location, whether it is blocked, and whether it is insecure.
138+
* @param aliases The map of registry aliases, where the key is the alias and the value is the actual registry URL.
139+
* @param unqualifiedRegistries The list of unqualified registries, which are registries that can be used without specifying a registry.
117140
*/
118141
record ConfigFile(
119142
@JsonProperty("registry") @Nullable List<RegistryConfig> registries,
@@ -150,21 +173,95 @@ public boolean hasAlias(String alias) {
150173
* @param location the registry location to check for blocking.
151174
* @return true if the registry is marked as blocked, false otherwise.
152175
*/
153-
public boolean isBlocked(String location) {
154-
return config.registries.stream()
155-
.filter(registry -> registry.location.equals(location))
156-
.anyMatch(RegistryConfig::isBlocked);
176+
public boolean isBlocked(ContainerRef location) {
177+
return selectMatchingTable(location).map(RegistryConfig::isBlocked).orElse(false);
157178
}
158179

159180
/**
160181
* Check if the given registry is marked as insecure in the configuration.
161182
* @param location the registry location to check for insecurity.
162183
* @return true if the registry is marked as insecure, false otherwise.
163184
*/
164-
public boolean isInsecure(String location) {
185+
public boolean isInsecure(ContainerRef location) {
186+
return selectMatchingTable(location).map(RegistryConfig::isInsecure).orElse(false);
187+
}
188+
189+
/**
190+
* Rewrite the given container reference according to the matching registry configuration.
191+
* @param ref the container reference to rewrite.
192+
* @return the rewritten container reference.
193+
*/
194+
public ContainerRef rewrite(ContainerRef ref) {
195+
Optional<RegistryConfig> matchingConfig = selectMatchingTable(ref);
196+
if (matchingConfig.isEmpty()) {
197+
return ref;
198+
}
199+
// No rewrite possible if location and prefix are not set
200+
String registry = matchingConfig.get().location();
201+
String prefix = matchingConfig.get().prefix();
202+
if (registry == null || registry.isBlank() || prefix == null || prefix.isBlank()) {
203+
return ref;
204+
}
205+
String currentRefString = ref.toString();
206+
String rewrittenRefString = currentRefString.replaceFirst(prefix, registry);
207+
LOG.debug(
208+
"Rewriting container reference from '{}' to '{}' using registry config with prefix '{}' and location '{}'",
209+
currentRefString,
210+
rewrittenRefString,
211+
prefix,
212+
registry);
213+
return ContainerRef.parse(rewrittenRefString);
214+
}
215+
216+
/**
217+
* Select the matching registry configuration table for the container reference.
218+
* @param ref the container reference to find the matching registry configuration for.
219+
* @return an Optional containing the matching RegistryConfig if found, or an empty Optional if no matching configuration is found.
220+
*/
221+
private Optional<RegistryConfig> selectMatchingTable(ContainerRef ref) {
165222
return config.registries.stream()
166-
.filter(registry -> registry.location.equals(location))
167-
.anyMatch(RegistryConfig::isInsecure);
223+
.filter(cfg -> matches(ref, effectivePrefix(cfg)))
224+
.max(Comparator.comparingInt(cfg -> effectivePrefix(cfg).length()));
225+
}
226+
227+
private @Nullable String effectivePrefix(RegistryConfig cfg) {
228+
return cfg.prefix() != null ? cfg.prefix() : cfg.location();
229+
}
230+
231+
/**
232+
* Check if the given container reference matches the specified prefix.
233+
* @param ref the container reference to check for a match against the prefix.
234+
* @param prefix the prefix to match against the container reference, which can be a specific hostname or a wildcard pattern (e.g., *.example.com) and an optional path component (e.g., namespace/repo).
235+
* @return true if the container reference matches the prefix, false otherwise.
236+
*/
237+
private boolean matches(ContainerRef ref, @Nullable String prefix) {
238+
if (prefix == null || prefix.isBlank()) {
239+
return false;
240+
}
241+
242+
ParsedPrefix p = ParsedPrefix.parse(prefix);
243+
244+
// Host match (supports *.example.com)
245+
if (!hostMatches(ref.getRegistry(), p.host())) {
246+
return false;
247+
}
248+
249+
// No path restriction → host-only match
250+
if (p.path().isEmpty()) {
251+
return true;
252+
}
253+
254+
// Path prefix match (namespace/repo)
255+
String refPath = String.join("/", ref.getNamespace()) + "/" + ref.getRepository();
256+
return refPath.equals(p.path()) || refPath.startsWith(p.path() + "/");
257+
}
258+
259+
private boolean hostMatches(String host, String prefixHost) {
260+
if (prefixHost.startsWith("*.")) {
261+
String domain = prefixHost.substring(2);
262+
return host.endsWith("." + domain);
263+
}
264+
return host.equals(prefixHost);
168265
}
169266

170267
/**
@@ -177,6 +274,14 @@ static class Config {
177274
*/
178275
private Config() {}
179276

277+
/**
278+
* Constructor for Config that takes a RegistryConfig and adds it to the list of registries.
279+
* @param registryConfigs The registry configuration to add to the list of registries.
280+
*/
281+
Config(List<RegistryConfig> registryConfigs) {
282+
this.registries.addAll(registryConfigs);
283+
}
284+
180285
/**
181286
* List of unqualified registries.
182287
*/

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

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import land.oras.exception.OrasException;
2828
import land.oras.utils.SupportedAlgorithm;
2929
import org.junit.jupiter.api.BeforeAll;
30+
import org.junit.jupiter.api.Disabled;
3031
import org.junit.jupiter.api.Test;
3132
import org.junit.jupiter.api.io.TempDir;
3233
import org.junit.jupiter.api.parallel.Execution;
@@ -40,34 +41,29 @@
4041
class ContainerRefTest {
4142

4243
@TempDir
43-
private static Path homeDir;
44+
private static Path homeDir1;
4445

45-
@BeforeAll
46-
static void init() throws Exception {
47-
48-
// Write home registries.conf on the temp home directory
49-
Files.createDirectory(homeDir.resolve(".config"));
50-
Files.createDirectory(homeDir.resolve(".config").resolve("containers"));
51-
}
46+
@TempDir
47+
private static Path homeDir2;
5248

53-
@Test
54-
void shouldThrowIfUnableToFindOnAnyUnQualifiedSearchRegistry() throws Exception {
49+
@TempDir
50+
private static Path homeDir3;
5551

56-
// language=toml
57-
String config = """
58-
unqualified-search-registries = ["localhost"]
59-
""";
52+
@TempDir
53+
private static Path homeDir4;
6054

61-
Files.writeString(homeDir.resolve(".config").resolve("containers").resolve("registries.conf"), config);
55+
@BeforeAll
56+
static void init() throws Exception {
6257

63-
new EnvironmentVariables()
64-
.set("HOME", homeDir.toAbsolutePath().toString())
65-
.execute(() -> {
66-
Registry registry = Registry.builder().defaults().build();
67-
ContainerRef unqualifiedRef = ContainerRef.parse("docker/library/alpine:latest");
68-
assertTrue(unqualifiedRef.isUnqualified(), "ContainerRef must be unqualified");
69-
assertThrows(OrasException.class, () -> unqualifiedRef.getEffectiveRegistry(registry));
70-
});
58+
// Write home registries.conf on the temp home directory
59+
Files.createDirectory(homeDir1.resolve(".config"));
60+
Files.createDirectory(homeDir1.resolve(".config").resolve("containers"));
61+
Files.createDirectory(homeDir2.resolve(".config"));
62+
Files.createDirectory(homeDir2.resolve(".config").resolve("containers"));
63+
Files.createDirectory(homeDir3.resolve(".config"));
64+
Files.createDirectory(homeDir3.resolve(".config").resolve("containers"));
65+
Files.createDirectory(homeDir4.resolve(".config"));
66+
Files.createDirectory(homeDir4.resolve(".config").resolve("containers"));
7167
}
7268

7369
@Test
@@ -88,10 +84,10 @@ void shouldReadRegistriesConfig() throws Exception {
8884
insecure = true
8985
""";
9086

91-
Files.writeString(homeDir.resolve(".config").resolve("containers").resolve("registries.conf"), config);
87+
Files.writeString(homeDir1.resolve(".config").resolve("containers").resolve("registries.conf"), config);
9288

9389
new EnvironmentVariables()
94-
.set("HOME", homeDir.toAbsolutePath().toString())
90+
.set("HOME", homeDir1.toAbsolutePath().toString())
9591
.execute(() -> {
9692
Registry registry = Registry.builder().defaults().build();
9793
assertEquals("https", registry.getScheme());
@@ -126,10 +122,10 @@ void shouldDetermineFromAlias() throws Exception {
126122
"my-library"="localhost/test2"
127123
""";
128124

129-
Files.writeString(homeDir.resolve(".config").resolve("containers").resolve("registries.conf"), config);
125+
Files.writeString(homeDir2.resolve(".config").resolve("containers").resolve("registries.conf"), config);
130126

131127
new EnvironmentVariables()
132-
.set("HOME", homeDir.toAbsolutePath().toString())
128+
.set("HOME", homeDir2.toAbsolutePath().toString())
133129
.execute(() -> {
134130
Registry registry = Registry.builder().defaults().build();
135131
ContainerRef unqualifiedRef = ContainerRef.parse("my-library/my-namespace");
@@ -139,6 +135,29 @@ void shouldDetermineFromAlias() throws Exception {
139135
});
140136
}
141137

138+
@Test
139+
@Disabled("Not implemented yet")
140+
void shouldRewriteAllSubdomainToLocalProxy() throws Exception {
141+
142+
// language=toml
143+
String config =
144+
"""
145+
[[registry]]
146+
prefix = "*.example.com"
147+
location = "localhost:5000/example-com"
148+
""";
149+
150+
Files.writeString(homeDir3.resolve(".config").resolve("containers").resolve("registries.conf"), config);
151+
152+
new EnvironmentVariables()
153+
.set("HOME", homeDir3.toAbsolutePath().toString())
154+
.execute(() -> {
155+
Registry registry = Registry.builder().defaults().build();
156+
ContainerRef containerRef = ContainerRef.parse("toto.example.com/library/alpine:latest");
157+
assertEquals("localhost:5000", containerRef.getEffectiveRegistry(registry));
158+
});
159+
}
160+
142161
@Test
143162
void shouldDetermineEffectiveRegistry() throws Exception {
144163

@@ -160,10 +179,10 @@ void shouldDetermineEffectiveRegistry() throws Exception {
160179
// Ensure empty config does not cause error with machine contains default registry
161180
String config = "";
162181

163-
Files.writeString(homeDir.resolve(".config").resolve("containers").resolve("registries.conf"), config);
182+
Files.writeString(homeDir4.resolve(".config").resolve("containers").resolve("registries.conf"), config);
164183

165184
new EnvironmentVariables()
166-
.set("HOME", homeDir.toAbsolutePath().toString())
185+
.set("HOME", homeDir4.toAbsolutePath().toString())
167186
.execute(() -> {
168187
Registry r = Registry.builder().defaults().build();
169188

0 commit comments

Comments
 (0)