From 572880eb73bdad87645436e2f02c86d16da14296 Mon Sep 17 00:00:00 2001 From: Evgeniy Alekseev Date: Mon, 14 Aug 2023 01:46:24 +0300 Subject: [PATCH] add ability to read values from environment variables It makes sense to read some values from environment. In particular this feature is useful in case of running application in containers in ci/cd See #108 for more details --- docker/entrypoint.sh | 2 +- docs/configuration.rst | 9 ++++ .../core/configuration/configuration.py | 15 ++++-- .../core/configuration/shell_interpolator.py | 51 +++++++++++++++++++ .../configuration/test_shell_interpolator.py | 15 ++++++ 5 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 src/ahriman/core/configuration/shell_interpolator.py create mode 100644 tests/ahriman/core/configuration/test_shell_interpolator.py 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"