diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 53d5f984..a9b9a1c4 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -59,7 +59,7 @@ systemd-machine-id-setup &> /dev/null if [ -n "$AHRIMAN_FORCE_ROOT" ]; then AHRIMAN_EXECUTABLE=("ahriman") elif ahriman help-commands-unsafe -- "$@" &> /dev/null; then - AHRIMAN_EXECUTABLE=("sudo" "-u" "$AHRIMAN_USER" "--" "ahriman") + AHRIMAN_EXECUTABLE=("sudo" "-E" "-u" "$AHRIMAN_USER" "--" "ahriman") else AHRIMAN_EXECUTABLE=("ahriman") fi diff --git a/docs/configuration.rst b/docs/configuration.rst index b354be36..eecfd6b4 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -12,6 +12,15 @@ There are two variable types which have been added to default ones, they are pat Path values, except for casting to ``pathlib.Path`` type, will be also expanded to absolute paths relative to the configuration path. E.g. if path is set to ``ahriman.ini.d/logging.ini`` and root configuration path is ``/etc/ahriman.ini``, the value will be expanded to ``/etc/ahriman.ini.d/logging.ini``. In order to disable path expand, use the full path, e.g. ``/etc/ahriman.ini.d/logging.ini``. +Configuration allows string interpolation from environment variables, e.g.: + +.. code-block:: ini + + [section] + key = $SECRET + +will try to read value from ``SECRET`` environment variable. In case if the required environment variable wasn't found, it will keep original value (i.e. ``$SECRET`` in the example). Dollar sign can be set as ``$$``. + There is also additional subcommand which will allow to validate configuration and print found errors. In order to do so, run ``service-config-validate`` subcommand, e.g.: .. code-block:: shell diff --git a/src/ahriman/core/configuration/configuration.py b/src/ahriman/core/configuration/configuration.py index ca316cbc..85ad33df 100644 --- a/src/ahriman/core/configuration/configuration.py +++ b/src/ahriman/core/configuration/configuration.py @@ -25,6 +25,7 @@ from collections.abc import Callable from pathlib import Path from typing import Any, Self +from ahriman.core.configuration.shell_interpolator import ShellInterpolator from ahriman.core.exceptions import InitializeError from ahriman.models.repository_paths import RepositoryPaths @@ -73,10 +74,16 @@ class Configuration(configparser.RawConfigParser): allow_no_value(bool, optional): copies ``configparser.RawConfigParser`` behaviour. In case if it is set to ``True``, the keys without values will be allowed (Default value = False) """ - configparser.RawConfigParser.__init__(self, allow_no_value=allow_no_value, converters={ - "list": shlex.split, - "path": self._convert_path, - }) + configparser.RawConfigParser.__init__( + self, + allow_no_value=allow_no_value, + interpolation=ShellInterpolator(), + converters={ + "list": shlex.split, + "path": self._convert_path, + } + ) + self.architecture: str | None = None self.path: Path | None = None self.includes: list[Path] = [] diff --git a/src/ahriman/core/configuration/shell_interpolator.py b/src/ahriman/core/configuration/shell_interpolator.py new file mode 100644 index 00000000..e9192867 --- /dev/null +++ b/src/ahriman/core/configuration/shell_interpolator.py @@ -0,0 +1,51 @@ +# +# Copyright (c) 2021-2023 ahriman team. +# +# This file is part of ahriman +# (see https://github.com/arcan1s/ahriman). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +import configparser +import os + +from collections.abc import Mapping, MutableMapping +from string import Template + + +class ShellInterpolator(configparser.Interpolation): + """ + custom string interpolator, because we cannot use defaults argument due to config validation + """ + + def before_get(self, parser: MutableMapping[str, Mapping[str, str]], section: str, option: str, value: str, + defaults: Mapping[str, str]) -> str: + """ + interpolate option value + + Args: + parser(MutableMapping[str, Mapping[str, str]]): option parser + section(str): section name + option(str): option name + value(str): source (not-converted) value + defaults(Mapping[str, str]): default values + + Returns: + str: substituted value + """ + # At the moment it seems that it is the most legit way to handle environment variables + # Template behaviour is literally the same as shell + # In addition, we are using shell-like variables in some cases (see ``alpm.mirror`` option), thus we would like + # to keep them alive + return Template(value).safe_substitute(os.environ) diff --git a/tests/ahriman/core/configuration/test_shell_interpolator.py b/tests/ahriman/core/configuration/test_shell_interpolator.py new file mode 100644 index 00000000..92029a3d --- /dev/null +++ b/tests/ahriman/core/configuration/test_shell_interpolator.py @@ -0,0 +1,15 @@ +import os + +from ahriman.core.configuration.shell_interpolator import ShellInterpolator + + +def test_before_get() -> None: + """ + must correctly extract environment variables + """ + interpolator = ShellInterpolator() + + assert interpolator.before_get({}, "", "", "value", {}) == "value" + assert interpolator.before_get({}, "", "", "$value", {}) == "$value" + assert interpolator.before_get({}, "", "", "$HOME", {}) == os.environ["HOME"] + assert interpolator.before_get({}, "", "", "$$HOME", {}) == "$HOME"