forked from oras-project/oras-java
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRegistriesConf.java
More file actions
547 lines (489 loc) · 22.1 KB
/
Copy pathRegistriesConf.java
File metadata and controls
547 lines (489 loc) · 22.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
/*-
* =LICENSE=
* ORAS Java SDK
* ===
* Copyright (C) 2024 - 2025 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.auth;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonValue;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import land.oras.ContainerRef;
import land.oras.OrasModel;
import land.oras.Registry;
import land.oras.exception.OrasException;
import land.oras.utils.Const;
import land.oras.utils.TomlUtils;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handle registries.conf configuration
*/
@NullMarked
public class RegistriesConf {
private static final Logger LOG = LoggerFactory.getLogger(RegistriesConf.class);
/**
* The internal config
*/
private final Config config;
/**
* Constructor for RegistriesConf.
*
* @param config configuration instance.
*/
RegistriesConf(Config config) {
this.config = Objects.requireNonNull(config, "Config cannot be null");
}
/**
* Create a new RegistriesConf instance by loading configuration from the specified paths.
* @param configPaths The list of paths to load configuration from.
* @return A new RegistriesConf instance.
*/
public static RegistriesConf newConf(List<Path> configPaths) {
// Take the first path found: https://github.com/containers/image/blob/main/docs/containers-registries.conf.5.md
for (Path configPath : configPaths) {
LOG.debug("Checking for registries config file at: {}", configPath);
if (Files.exists(configPath)) {
ConfigFile configFile = TomlUtils.fromToml(configPath, ConfigFile.class);
LOG.debug("Loaded registries config file: {}", configPath);
return new RegistriesConf(Config.load(configFile));
}
}
// Empty config
return new RegistriesConf(new Config());
}
/**
* Create a new RegistriesConf instance by loading configuration from standard paths.
* @return A new RegistriesConf instance.
*/
public static RegistriesConf newConf() {
Path globalPath = Path.of("/etc/containers/registries.conf");
List<Path> paths = List.of(
System.getenv("HOME") != null
? Path.of(System.getenv("HOME"), ".config", "containers", "registries.conf")
: globalPath,
globalPath);
return newConf(paths);
}
/**
* The model of a mirror entry within a [[registry]] table.
* @param location The mirror registry location (host[:port][/path]).
* @param insecure Whether the mirror is insecure.
*/
@OrasModel
public record MirrorConfig(
@Nullable @JsonProperty("location") String location, @Nullable @JsonProperty("insecure") Boolean insecure) {
/**
* Return true if this mirror should be accessed over plain HTTP or with unverified TLS.
* @return true if insecure
*/
public boolean isInsecure() {
return insecure != null && insecure;
}
}
/**
* The model of the registry configuration
* @param prefix The prefix to match against container references.
* @param location The registry location
* @param blocked Whether the registry is blocked. If true, the registry is blocked and cannot be used for pulling or pushing images.
* @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.
* @param mirrors Ordered list of mirror entries to try before the registry location.
*/
@OrasModel
record RegistryConfig(
@Nullable @JsonProperty("prefix") String prefix,
@Nullable @JsonProperty("location") String location,
@Nullable @JsonProperty("blocked") Boolean blocked,
@Nullable @JsonProperty("insecure") Boolean insecure,
@Nullable @JsonProperty("mirror") List<MirrorConfig> mirrors) {
/**
* Return true if this registry is blocked and cannot be used for pulling or pushing images.
* @return true if blocked
*/
public boolean isBlocked() {
return blocked != null && blocked;
}
/**
* Return true if this registry should be accessed over plain HTTP or with unverified TLS.
* @return true if insecure
*/
public boolean isInsecure() {
return insecure != null && insecure;
}
}
/**
* The model of the parsed prefix, which contains the host and path components of the prefix.
* @param host The host component of the prefix, which can be a specific hostname or a wildcard pattern (e.g., *.example.com).
* @param path The path component of the prefix, which can be a specific path or a path prefix (e.g., namespace/repo).
*/
@OrasModel
record ParsedPrefix(String host, String path) {
static ParsedPrefix parse(String prefix) {
int slash = prefix.indexOf('/');
if (slash < 0) {
return new ParsedPrefix(prefix, "");
}
return new ParsedPrefix(prefix.substring(0, slash), prefix.substring(slash + 1));
}
}
/**
* The for handling short name
*/
@OrasModel
enum ShortNameMode {
/**
* Use all unqualified-search registries without any restriction
*/
DISABLED("disabled"),
/**
* If only one unqualified-search registry is set, use it as there is no ambiguity.
* If there is more than one registry this throw an error (default)
*/
ENFORCING("enforcing"),
/**
* Same as enforcing for ORAS Java SDK
*/
PERMISSIVE("permissive");
ShortNameMode(String value) {
this.value = value;
}
@JsonCreator
public static ShortNameMode fromString(String key) {
return ShortNameMode.valueOf(key.toUpperCase());
}
@JsonValue
public String getKey() {
return value;
}
private String value;
}
/**
* The model of the configuration file, which contains the list of registry configurations, aliases, and unqualified registries.
* @param registries The list of registry configurations, each containing the registry location, whether it is blocked, and whether it is insecure.
* @param aliases The map of registry aliases, where the key is the alias and the value is the actual registry URL.
* @param unqualifiedRegistries The list of unqualified registries, which are registries that can be used without specifying a registry.
*/
@OrasModel
record ConfigFile(
@JsonProperty("short-name-mode") @Nullable ShortNameMode shortNameMode,
@JsonProperty("registry") @Nullable List<RegistryConfig> registries,
@JsonProperty("aliases") @Nullable Map<String, String> aliases,
@JsonProperty("unqualified-search-registries") @Nullable List<String> unqualifiedRegistries) {}
/**
* Get the list of unqualified registries.
* @return an unmodifiable list of unqualified registries.
*/
public List<String> getUnqualifiedRegistries() {
return Collections.unmodifiableList(config.unqualifiedRegistries);
}
/**
* Return the key of the alias
* @param ref the container reference to get the alias key for.
* @return the alias key for the given container reference, which is either the repository name
*/
public String getAliasKey(ContainerRef ref) {
return ref.getRegistry().equals(Const.DEFAULT_REGISTRY) && "library".equals(ref.getNamespace())
? ref.getRepository()
: ref.getFullRepository();
}
/**
* Enforce the short name mode by checking the configuration. If the short name mode is set to ENFORCING or PERMISSIVE and there are multiple unqualified registries configured, this method throws an OrasException indicating that the configuration is invalid. If the configuration is valid, this method does nothing.
* @throws OrasException if the short name mode is set to ENFORCING or PERMISSIVE and there are multiple unqualified registries configured, indicating that the configuration is invalid.
*/
public void enforceShortNameMode() throws OrasException {
if ((config.shortNameMode == ShortNameMode.ENFORCING || config.shortNameMode == ShortNameMode.PERMISSIVE)
&& config.unqualifiedRegistries.size() > 1) {
throw new OrasException(
"Short name mode is set to ENFORCING/PERMISSION but multiple unqualified registries are configured: "
+ config.unqualifiedRegistries);
}
LOG.debug(
"Short name mode '{}' is valid with unqualified registries: {}",
config.shortNameMode,
config.unqualifiedRegistries);
}
/**
* Return the aliases
* @return an unmodifiable map of aliases, where the key is the alias and the value is the actual registry URL.
*/
public Map<String, String> getAliases() {
return Collections.unmodifiableMap(config.aliases);
}
/**
* Check if the given alias exists in the configuration.
* @param alias the alias to check for existence.
* @return true if the alias exists, false otherwise.
*/
public boolean hasAlias(String alias) {
return config.aliases.containsKey(alias);
}
/**
* Check if the given registry is marked as blocked in the configuration.
* @param location the registry location to check for blocking.
* @return true if the registry is marked as blocked, false otherwise.
*/
public boolean isBlocked(ContainerRef location) {
return selectMatchingTable(location).map(RegistryConfig::isBlocked).orElse(false);
}
/**
* Check if the given registry is marked as insecure in the configuration.
* If no entry found, we fall back to Registry configuration
* If the entry is found we use the insecure flag (or default true)
* @param registry The registry object
* @param location the registry location to check for insecurity.
* @return true if the registry is marked as insecure, false otherwise.
*/
public boolean isInsecure(Registry registry, ContainerRef location) {
return selectMatchingTable(location).map(RegistryConfig::isInsecure).orElse(registry.isInsecure());
}
/**
* Rewrite the given container reference according to the matching registry configuration.
* @param ref the container reference to rewrite.
* @return the rewritten container reference.
*/
public ContainerRef rewrite(ContainerRef ref) {
Optional<RegistryConfig> matchingConfig = selectMatchingTable(ref);
if (matchingConfig.isEmpty()) {
return ref;
}
// No rewrite possible if location and prefix are not set
String location = matchingConfig.get().location();
String prefix = matchingConfig.get().prefix();
if (location == null || location.isBlank() || prefix == null || prefix.isBlank()) {
return ref;
}
String currentRefString = ref.toString();
String rewrittenRefString;
// Replace all subdomain if prefix starts with "*." (e.g., *.example.com → my-registry.com)
if (prefix.startsWith("*.")) {
// The subdomain replacement can include an optional path
int firtSlashIndex = prefix.indexOf('/');
String prefixPath = firtSlashIndex < 0 ? "" : prefix.substring(firtSlashIndex);
// Remove matched host + optional prefixPath
String remainder = currentRefString.substring(ref.getRegistry().length());
if (!prefixPath.isEmpty() && remainder.startsWith(prefixPath)) {
remainder = remainder.substring(prefixPath.length());
}
rewrittenRefString = location + remainder;
} else {
// For unqualified references, we need to construct the rewritten reference properly
// by replacing the registry component and preserving the namespace/repository/tag structure
if (ref.isUnqualified()) {
// For unqualified references, build the new reference with the location as the new registry
String namespace = ref.getNamespace();
String repository = ref.getRepository();
String tag = ref.getTag();
String digest = ref.getDigest();
StringBuilder rewritten = new StringBuilder(location);
if (namespace != null && !namespace.isEmpty()) {
rewritten.append("/").append(namespace);
}
rewritten.append("/").append(repository);
rewritten.append(":").append(tag);
if (digest != null && !digest.isEmpty()) {
rewritten.append("@").append(digest);
}
rewrittenRefString = rewritten.toString();
}
// For qualified references, use the original string manipulation approach
else {
rewrittenRefString = location + currentRefString.substring(prefix.length());
}
}
LOG.debug(
"Rewriting container reference from '{}' to '{}' using registry config with prefix '{}' and location '{}'",
currentRefString,
rewrittenRefString,
prefix,
location);
return ContainerRef.parse(rewrittenRefString);
}
/**
* Return the ordered list of mirrors configured for the registry that matches the given reference.
* @param ref the container reference to look up mirrors for.
* @return an unmodifiable list of mirror configs (may be empty).
*/
public List<MirrorConfig> getMirrors(ContainerRef ref) {
Optional<RegistryConfig> matchingConfig = selectMatchingTable(ref);
if (matchingConfig.isEmpty() || matchingConfig.get().mirrors() == null) {
return Collections.emptyList();
}
return Collections.unmodifiableList(matchingConfig.get().mirrors());
}
/**
* Rewrite the given container reference to use the mirror's location, replacing the registry host.
* @param ref the original container reference.
* @param mirror the mirror configuration to apply.
* @return the rewritten reference pointing at the mirror.
*/
public ContainerRef rewriteForMirror(ContainerRef ref, MirrorConfig mirror) {
String mirrorLocation = mirror.location();
if (mirrorLocation == null || mirrorLocation.isBlank()) {
return ref;
}
// Strip trailing slashes to prevent double-slash segments in the rewritten ref
mirrorLocation = mirrorLocation.replaceAll("/+$", "");
if (mirrorLocation.isBlank()) {
return ref;
}
// Always build from components to correctly handle:
// - unqualified refs (toString() omits the registry)
// - digest-only refs (tag is null, toString() would produce ":null")
String namespace = ref.getNamespace();
String repository = ref.getRepository();
String tag = ref.getTag();
String digest = ref.getDigest();
StringBuilder sb = new StringBuilder(mirrorLocation);
if (namespace != null && !namespace.isEmpty()) {
sb.append("/").append(namespace);
}
sb.append("/").append(repository);
if (tag != null && !tag.isEmpty()) {
sb.append(":").append(tag);
}
if (digest != null && !digest.isEmpty()) {
sb.append("@").append(digest);
}
String rewrittenRefString = sb.toString();
LOG.debug("Rewriting '{}' to mirror '{}'", ref, rewrittenRefString);
return ContainerRef.parse(rewrittenRefString);
}
/**
* Select the matching registry configuration table for the container reference.
* @param ref the container reference to find the matching registry configuration for.
* @return an Optional containing the matching RegistryConfig if found, or an empty Optional if no matching configuration is found.
*/
private Optional<RegistryConfig> selectMatchingTable(ContainerRef ref) {
return config.registries.stream()
.filter(cfg -> matches(ref, effectivePrefix(cfg)))
.max(Comparator.comparingInt(cfg -> effectivePrefix(cfg).length()));
}
private @Nullable String effectivePrefix(RegistryConfig cfg) {
return cfg.prefix() != null ? cfg.prefix() : cfg.location();
}
/**
* Check if the given container reference matches the specified prefix.
* @param ref the container reference to check for a match against the prefix.
* @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).
* @return true if the container reference matches the prefix, false otherwise.
*/
private boolean matches(ContainerRef ref, @Nullable String prefix) {
if (prefix == null || prefix.isBlank()) {
return false;
}
ParsedPrefix p = ParsedPrefix.parse(prefix);
// Host match (supports *.example.com)
if (!hostMatches(ref.getRegistry(), p.host())) {
return false;
}
// No path restriction → host-only match
if (p.path().isEmpty()) {
LOG.trace("Found registry table '{}'", p);
return true;
}
// Path prefix match (namespace/repo)
String refPath = String.join("/", ref.getNamespace()) + "/" + ref.getRepository();
boolean result = refPath.equals(p.path()) || refPath.startsWith(p.path() + "/");
if (result) {
LOG.trace("Found registry table '{}' matching path '{}'", p, refPath);
}
return result;
}
/**
* Check if the given host matches the specified prefix host, which can be a specific hostname or a wildcard pattern (e.g., *.example.com).
* @param host the host to check for a match against the prefix host, which is the host component of the prefix.
* @param prefixHost the prefix host to match against, which can be a specific hostname or a wildcard pattern (e.g., *.example.com).
* @return true if the host matches the prefix host, false otherwise.
*/
private boolean hostMatches(String host, String prefixHost) {
if (prefixHost.startsWith("*.")) {
String domain = prefixHost.substring(2);
return host.endsWith("." + domain);
}
return host.equals(prefixHost);
}
/**
* Nested Config class for configuration management.
*/
static class Config {
/**
* Private constructor to prevent instantiation.
*/
private Config() {}
/**
* Constructor for Config that takes a RegistryConfig and adds it to the list of registries.
* @param registryConfigs The registry configuration to add to the list of registries.
*/
Config(List<RegistryConfig> registryConfigs) {
this.registries.addAll(registryConfigs);
}
/**
* Default to enforcing
*/
private ShortNameMode shortNameMode = ShortNameMode.ENFORCING;
/**
* List of unqualified registries.
*/
private final List<String> unqualifiedRegistries = new LinkedList<>();
/**
* Map of registry aliases, where the key is the alias and the value is the actual registry URL.
*/
private final Map<String, String> aliases = new HashMap<>();
/**
* List of registry configurations, each containing the registry location, whether it is blocked, and whether it is insecure.
*/
private final List<RegistryConfig> registries = new LinkedList<>();
/**
* Loads the configuration from a TOML file at the specified path and populates registries configuration
*
* @param configFile The config file
* @return A {@code Config} object populated with config
* @throws OrasException If an error occurs while reading or parsing the TOML file.
*/
public static Config load(ConfigFile configFile) throws OrasException {
Config config = new Config();
if (configFile.unqualifiedRegistries != null) {
LOG.trace("Loading unqualified registries: {}", configFile.unqualifiedRegistries);
config.unqualifiedRegistries.addAll(configFile.unqualifiedRegistries);
}
if (configFile.aliases != null) {
LOG.trace("Loading registry aliases: {}", configFile.aliases);
config.aliases.putAll(configFile.aliases);
}
if (configFile.registries != null) {
LOG.trace("Loading registry configurations: {}", configFile.registries);
config.registries.addAll(configFile.registries);
}
if (configFile.shortNameMode != null) {
LOG.trace("Loading short name mode: {}", configFile.shortNameMode);
config.shortNameMode = configFile.shortNameMode;
}
return config;
}
}
}