From: Aleš Mrázek Date: Wed, 18 Jun 2025 14:05:31 +0000 (+0200) Subject: python: manager: allow multiple configuration file inputs X-Git-Tag: v6.0.15~9^2~6 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=6d3a31f63ebee07bb69de3f1c61a45cf59212691;p=thirdparty%2Fknot-resolver.git python: manager: allow multiple configuration file inputs --- diff --git a/python/knot_resolver/manager/main.py b/python/knot_resolver/manager/main.py index dac47bed4..1db2af975 100644 --- a/python/knot_resolver/manager/main.py +++ b/python/knot_resolver/manager/main.py @@ -5,7 +5,6 @@ file to allow us to exclude the __main__.py file from black's autoformatting import argparse import sys -from pathlib import Path from typing import NoReturn from knot_resolver.constants import CONFIG_FILE, VERSION @@ -26,11 +25,16 @@ def parse_args() -> argparse.Namespace: parser.add_argument( "-c", "--config", - help="Config file to load. Overrides default config location at '" + str(CONFIG_FILE) + "'", + help="One or more configuration files to load." + f" Overrides default configuration file location at '{str(CONFIG_FILE)}'" + " Files must not contain the same options." + " However, they may extend individual subsections." + " The location of the first configuration file determines" + "the prefix for every relative path in the configuration.", type=str, - nargs=1, + nargs="+", required=False, - default=None, + default=[str(CONFIG_FILE)], ) return parser.parse_args() @@ -42,11 +46,5 @@ def main() -> NoReturn: # parse arguments args = parse_args() - # where to look for config - if args.config is not None: - config_path = Path(args.config[0]) - else: - config_path = CONFIG_FILE - - exit_code = compat.asyncio.run(start_server(config=config_path)) + exit_code = compat.asyncio.run(start_server(config=args.config)) sys.exit(exit_code) diff --git a/python/knot_resolver/manager/server.py b/python/knot_resolver/manager/server.py index 41078a7ad..0f5ea74dd 100644 --- a/python/knot_resolver/manager/server.py +++ b/python/knot_resolver/manager/server.py @@ -18,7 +18,7 @@ from aiohttp.web_app import Application from aiohttp.web_response import json_response from aiohttp.web_runner import AppRunner, TCPSite, UnixSite -from knot_resolver.constants import CONFIG_FILE, USER +from knot_resolver.constants import USER from knot_resolver.controller import get_best_controller_implementation from knot_resolver.controller.exceptions import SubprocessControllerError, SubprocessControllerExecError from knot_resolver.controller.interface import SubprocessType @@ -88,7 +88,7 @@ class Server: # This is top-level class containing pretty much everything. Instead of global # variables, we use instance attributes. That's why there are so many and it's # ok. - def __init__(self, store: ConfigStore, config_path: Optional[Path], manager: KresManager): + def __init__(self, store: ConfigStore, config_path: Optional[List[Path]], manager: KresManager): # config store & server dynamic reconfiguration self.config_store = store @@ -98,7 +98,7 @@ class Server: self.listen: Optional[ManagementSchema] = None self.site: Union[NoneType, TCPSite, UnixSite] = None self.listen_lock = asyncio.Lock() - self._config_path: Optional[Path] = config_path + self._config_path: Optional[List[Path]] = config_path self._exit_code: int = 0 self._shutdown_event = asyncio.Event() self._manager = manager @@ -121,13 +121,17 @@ class Server: logger.warning("The manager was started with inlined configuration - can't reload") else: try: - data = await readfile(self._config_path) - config = KresConfig(try_to_parse(data)) + data = {} + for file in self._config_path: + file_data = try_to_parse(await readfile(file)) + data.update(file_data) + + config = KresConfig(data) await self.config_store.update(config) logger.info("Configuration file successfully reloaded") except FileNotFoundError: logger.error( - f"Configuration file was not found at '{self._config_path}'." + f"Configuration file was not found at '{file}'." " Something must have happened to it while we were running." ) logger.error("Configuration has NOT been changed.") @@ -537,7 +541,7 @@ async def _sigterm_while_shutting_down(): sys.exit(128 + signal.SIGTERM) -async def start_server(config: Path = CONFIG_FILE) -> int: # noqa: PLR0915 +async def start_server(config: List[str]) -> int: # noqa: PLR0915 # This function is quite long, but it describes how manager runs. So let's silence pylint # pylint: disable=too-many-statements @@ -559,23 +563,26 @@ async def start_server(config: Path = CONFIG_FILE) -> int: # noqa: PLR0915 # before starting server, initialize the subprocess controller, config store, etc. Any errors during inicialization # are fatal try: - # Make sure that the config path does not change meaning when we change working directory - config = config.absolute() + # Make sure that the config paths does not change meaning when we change working directory + config_absolute = [Path(path).absolute() for path in config] - # Preprocess config - load from file or in general take it to the last step before validation. - config_raw = await _load_raw_config(config) + config_data = {} + for file in config_absolute: + # Preprocess config - load from file or in general take it to the last step before validation. + config_raw = await _load_raw_config(file) + config_data.update(config_raw) # before processing any configuration, set validation context # - resolve_root: root against which all relative paths will be resolved # - strict_validation: check for path existence during configuration validation # - permissions_default: validate dirs/files rwx permissions against default user:group in constants - set_global_validation_context(Context(config.parent, True, False)) + set_global_validation_context(Context(config_absolute[0].parent, True, False)) # We want to change cwd as soon as possible. Some parts of the codebase are using os.getcwd() to get the # working directory. # # If we fail to read rundir from unparsed config, the first config validation error comes from here - _set_working_directory(config_raw) + _set_working_directory(config_data) # We don't want more than one manager in a single working directory. So we lock it with a PID file. # Warning - this does not prevent multiple managers with the same naming of kresd service. @@ -584,7 +591,7 @@ async def start_server(config: Path = CONFIG_FILE) -> int: # noqa: PLR0915 # set_global_validation_context(Context(config.parent)) # After the working directory is set, we can initialize proper config store with a newly parsed configuration. - config_store = await _init_config_store(config_raw) + config_store = await _init_config_store(config_data) # Some "constants" need to be loaded from the initial config, some need to be stored from the initial run conditions await init_user_constants(config_store, working_directory_on_startup) @@ -608,7 +615,7 @@ async def start_server(config: Path = CONFIG_FILE) -> int: # noqa: PLR0915 manager = await _init_manager(config_store) # prepare instance of the server (no side effects) - server = Server(config_store, config, manager) + server = Server(config_store, config_absolute, manager) # add Server's shutdown trigger to the manager manager.add_shutdown_trigger(server.trigger_shutdown)