Compare commits

...

3 Commits

Author SHA1 Message Date
8c6486c233 contributing guide update 2023-08-14 02:51:14 +03:00
a1d0e993a8 resoolve dependencies by using local cache too (#107) 2023-08-14 02:31:24 +03:00
572880eb73 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
2023-08-14 01:48:08 +03:00
8 changed files with 103 additions and 10 deletions

View File

@ -158,7 +158,7 @@ Again, the most checks can be performed by `make check` command, though some add
* One file should define only one class, exception is class satellites in case if file length remains less than 400 lines.
* It is possible to create file which contains some functions (e.g. `ahriman.core.util`), but in this case you would need to define `__all__` attribute.
* The file size mentioned above must be applicable in general. In case of big classes consider splitting them into traits. Note, however, that `pylint` includes comments and docstrings into counter, thus you need to check file size by other tools.
* No global variable is allowed outside of `ahriman.version` module. `ahriman.core.context` is also special case.
* No global variable is allowed outside of `ahriman` module. `ahriman.core.context` is also special case.
* Single quotes are not allowed. The reason behind this restriction is the fact that docstrings must be written by using double quotes only, and we would like to make style consistent.
* If your class writes anything to log, the `ahriman.core.log.LazyLogging` trait must be used.
* Web API methods must be documented by using `aiohttp_apispec` library. Schema testing mostly should be implemented in related view class tests. Recommended example for documentation (excluding comments):

View File

@ -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

View File

@ -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

View File

@ -142,8 +142,13 @@ class Application(ApplicationPackages, ApplicationRepository):
while missing := missing_dependencies(with_dependencies.values()):
for package_name, username in missing.items():
package = Package.from_aur(package_name, self.repository.pacman, username)
if (source_dir := self.repository.paths.cache_for(package_name)).is_dir():
# there is local cache, load package from it
package = Package.from_build(source_dir, self.repository.architecture, username)
else:
package = Package.from_aur(package_name, self.repository.pacman, username)
with_dependencies[package.base] = package
# register package in local database
self.database.remote_update(package)
self.repository.reporter.set_unknown(package)

View File

@ -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] = []

View File

@ -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 <http://www.gnu.org/licenses/>.
#
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)

View File

@ -86,7 +86,11 @@ def test_with_dependencies(application: Application, package_ahriman: Package, p
"python-installer": create_package_mock("python-installer"),
}
package_mock = mocker.patch("ahriman.models.package.Package.from_aur", side_effect=lambda *args: packages[args[0]])
mocker.patch("pathlib.Path.is_dir", autospec=True, side_effect=lambda p: p.name == "python")
package_aur_mock = mocker.patch("ahriman.models.package.Package.from_aur",
side_effect=lambda *args: packages[args[0]])
package_local_mock = mocker.patch("ahriman.models.package.Package.from_build",
side_effect=lambda *args: packages[args[0].name])
packages_mock = mocker.patch("ahriman.application.application.Application._known_packages",
return_value={"devtools", "python-build", "python-pytest"})
update_remote_mock = mocker.patch("ahriman.core.database.SQLite.remote_update")
@ -94,11 +98,13 @@ def test_with_dependencies(application: Application, package_ahriman: Package, p
result = application.with_dependencies([package_ahriman], process_dependencies=True)
assert {package.base: package for package in result} == packages
package_mock.assert_has_calls([
package_aur_mock.assert_has_calls([
MockCall(package_python_schedule.base, application.repository.pacman, package_ahriman.packager),
MockCall("python", application.repository.pacman, package_ahriman.packager),
MockCall("python-installer", application.repository.pacman, package_ahriman.packager),
], any_order=True)
package_local_mock.assert_has_calls([
MockCall(application.repository.paths.cache_for("python"), "x86_64", package_ahriman.packager),
], any_order=True)
packages_mock.assert_called_once_with()
update_remote_mock.assert_has_calls([

View File

@ -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"