From 9b6671be6c26f2984ac455f1a3dddc101d56ea4b Mon Sep 17 00:00:00 2001 From: Valentin Delaye Date: Mon, 13 Oct 2025 14:19:23 +0200 Subject: [PATCH] Start implementation of registries.conf Signed-off-by: Valentin Delaye --- pom.xml | 4 + .../java/land/oras/auth/RegistriesConf.java | 136 ++++++++++++++++++ src/main/java/land/oras/utils/TomlUtils.java | 101 +++++++++++++ .../java/land/oras/utils/TomlUtilsTest.java | 63 ++++++++ 4 files changed, 304 insertions(+) create mode 100644 src/main/java/land/oras/auth/RegistriesConf.java create mode 100644 src/main/java/land/oras/utils/TomlUtils.java create mode 100644 src/test/java/land/oras/utils/TomlUtilsTest.java diff --git a/pom.xml b/pom.xml index e81d2d8..0cb793e 100644 --- a/pom.xml +++ b/pom.xml @@ -221,6 +221,10 @@ tools.jackson.core jackson-databind + + tools.jackson.dataformat + jackson-dataformat-toml + diff --git a/src/main/java/land/oras/auth/RegistriesConf.java b/src/main/java/land/oras/auth/RegistriesConf.java new file mode 100644 index 0000000..1226745 --- /dev/null +++ b/src/main/java/land/oras/auth/RegistriesConf.java @@ -0,0 +1,136 @@ +/*- + * =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 java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import land.oras.exception.OrasException; +import land.oras.utils.JsonUtils; +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 configPaths) { + List files = new ArrayList<>(); + for (Path configPath : configPaths) { + if (Files.exists(configPath)) { + ConfigFile configFile = JsonUtils.fromJson(configPath, ConfigFile.class); + LOG.debug("Loaded registries config file: {}", configPath); + files.add(configFile); + } + } + return new RegistriesConf(Config.load(files)); + } + + /** + * 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 paths = List.of( + globalPath, + System.getenv("HOME") != null + ? Path.of(System.getenv("HOME"), ".config", "containers", "registries.conf") + : globalPath); + return newConf(paths); + } + + /** + * The model of the config file + * + */ + record ConfigFile(@Nullable List unqualifiedRegistries) {} + + /** + * Get the list of unclassified registries. + * @return an unmodifiable list of unclassified registries. + */ + public List getUnclassifiedRegistries() { + return Collections.unmodifiableList(config.unqualifiedRegistries); + } + + /** + * Nested Config class for configuration management. + */ + static class Config { + + /** + * Private constructor to prevent instantiation. + */ + private Config() {} + + /** + * List of unqualified registries. + */ + private final List unqualifiedRegistries = new LinkedList<>(); + + /** + * Loads the configuration from a JSON file at the specified path and populates registries configuration + * + * @param configFiles The config files + * @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(List configFiles) throws OrasException { + Config config = new Config(); + for (ConfigFile configFile : configFiles) { + if (configFile.unqualifiedRegistries != null) { + config.unqualifiedRegistries.addAll(configFile.unqualifiedRegistries); + } + } + return config; + } + } +} diff --git a/src/main/java/land/oras/utils/TomlUtils.java b/src/main/java/land/oras/utils/TomlUtils.java new file mode 100644 index 0000000..d56b019 --- /dev/null +++ b/src/main/java/land/oras/utils/TomlUtils.java @@ -0,0 +1,101 @@ +/*- + * =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.utils; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import land.oras.exception.OrasException; +import org.jspecify.annotations.NullMarked; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.dataformat.toml.TomlMapper; + +/** + * Utility class for TOML operations. + * Use Jackson 3 internally for TOML operations + */ +@NullMarked +public final class TomlUtils { + + /** + * TOML mapper instance + */ + private static final ObjectMapper tomlMapper; + + /** + * Utils class + */ + private TomlUtils() { + // Hide constructor + } + + static { + tomlMapper = TomlMapper.builder().build(); + } + + /** + * Convert an object to a TOML string + * @param object The object to convert + * @return The TOML string + */ + public static String toToml(Object object) { + try { + return tomlMapper.writeValueAsString(object); + } catch (JacksonException e) { + throw new OrasException("Unable to convert object to TOML string", e); + } + } + + /** + * Convert a TOML string to an object + * @param toml The TOML string + * @param clazz The class of the object + * @param The type of the object + * @return The object + */ + public static T fromToml(String toml, Class clazz) { + try { + return tomlMapper.readValue(toml, clazz); + } catch (JacksonException e) { + throw new OrasException("Unable to parse TOML string", e); + } + } + + /** + * Convert a TOML string to an object + * of the TOML string is not important. + * @param path The path to the TOML file + * @param clazz The class of the object + * @param The type of the object + * @return The object + */ + public static T fromToml(Path path, Class clazz) { + try { + return tomlMapper.readValue(Files.readString(path, StandardCharsets.UTF_8), clazz); + } catch (IOException e) { + throw new OrasException("Unable to read TOML file due to IO error", e); + } catch (JacksonException e) { + throw new OrasException("Unable to parse TOML file", e); + } + } +} diff --git a/src/test/java/land/oras/utils/TomlUtilsTest.java b/src/test/java/land/oras/utils/TomlUtilsTest.java new file mode 100644 index 0000000..0e4810a --- /dev/null +++ b/src/test/java/land/oras/utils/TomlUtilsTest.java @@ -0,0 +1,63 @@ +/*- + * =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.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.file.Path; +import java.util.Map; +import land.oras.exception.OrasException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.CleanupMode; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; + +@Execution(ExecutionMode.CONCURRENT) +public class TomlUtilsTest { + + /** + * Temporary dir + */ + @TempDir(cleanup = CleanupMode.ON_SUCCESS) + private static Path dir; + + @Test + void failToParseTomlString() { + assertThrows(OrasException.class, () -> TomlUtils.fromToml("not a toml", Object.class)); + } + + @Test + @SuppressWarnings("unchecked") + void shouldParseToml() { + String tomlMap = """ + key1 = "value1" + key2 = "value2" + """; + Map map = TomlUtils.fromToml(tomlMap, Map.class); + assertNotNull(map); + assertEquals(2, map.size()); + assertEquals("value1", map.get("key1")); + assertEquals("value2", map.get("key2")); + } +}