diff --git a/.github/workflows/setup.sh b/.github/workflows/setup.sh index 153aa812..dd048942 100755 --- a/.github/workflows/setup.sh +++ b/.github/workflows/setup.sh @@ -25,25 +25,28 @@ make VERSION=1.0.0 archlinux # well, it does not really matter which version we mv ahriman-*-src.tar.xz package/archlinux chmod +777 package/archlinux # because fuck you that's why cd package/archlinux -sudo -u nobody makepkg -cf --skipchecksums --noconfirm +sudo -u nobody -- makepkg -cf --skipchecksums --noconfirm pacman --noconfirm -U ahriman-1.0.0-1-any.pkg.tar.zst +# create machine-id which is required by build tools +systemd-machine-id-setup # special thing for the container, because /dev/log interface is not available there -sed -i 's/handlers = syslog_handler/handlers = console_handler/g' /etc/ahriman.ini.d/logging.ini +sed -i "s/handlers = syslog_handler/handlers = console_handler/g" /etc/ahriman.ini.d/logging.ini # initial setup command as root -sudo -u ahriman ahriman -a x86_64 init ahriman -a x86_64 repo-setup --packager "ahriman bot " --repository "github" --web-port 8080 # enable services systemctl enable ahriman-web@x86_64 systemctl enable ahriman@x86_64.timer # run web service (detached) -sudo -u ahriman ahriman -a x86_64 web & +sudo -u ahriman -- ahriman -a x86_64 web & WEBPID=$! sleep 15s # wait for the web service activation # add the first package -# the build itself does not really work in the container because it requires procfs -sudo -u ahriman ahriman package-add yay +# the build itself does not really work in the container +sudo -u ahriman -- ahriman package-add --now yay +# check if package was actually installed +#test -n "$(find "/var/lib/ahriman/repository/x86_64" -name "yay*pkg*")" # run package check -sudo -u ahriman ahriman repo-update +sudo -u ahriman -- ahriman repo-update # stop web service lol kill $WEBPID diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index e24c7cdb..67946831 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -20,8 +20,6 @@ fi # create repository root inside the [[mounted]] directory and set correct ownership [ -d "$AHRIMAN_REPOSITORY_ROOT" ] || mkdir "$AHRIMAN_REPOSITORY_ROOT" chown "$AHRIMAN_USER":"$AHRIMAN_USER" "$AHRIMAN_REPOSITORY_ROOT" -# run initial setup -sudo -u "$AHRIMAN_USER" -- ahriman "${AHRIMAN_DEFAULT_ARGS[@]}" repo-init # run built-in setup command AHRIMAN_SETUP_ARGS=("--build-as-user" "$AHRIMAN_USER") diff --git a/docs/faq.md b/docs/faq.md index d1d41a03..efe48697 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -12,7 +12,6 @@ TL;DR ```shell yay -S ahriman -sudo -u ahriman ahriman -a x86_64 init sudo ahriman -a x86_64 repo-setup --packager "ahriman bot " --repository "repository" systemctl enable --now ahriman@x86_64.timer ``` diff --git a/docs/setup.md b/docs/setup.md index 48a29c27..60b8bda6 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -5,11 +5,10 @@ 3. TL;DR ```shell - sudo -u ahriman ahriman -a x86_64 repo-init sudo ahriman -a x86_64 repo-setup ... ``` - `repo-init` subcommand is required to create the repository tree with correct rights. `repo-setup` literally does the following steps: + `repo-setup` literally does the following steps: 1. Create `/var/lib/ahriman/.makepkg.conf` with `makepkg.conf` overrides if required (at least you might want to set `PACKAGER`): diff --git a/src/ahriman/application/ahriman.py b/src/ahriman/application/ahriman.py index a7fcab50..542d010a 100644 --- a/src/ahriman/application/ahriman.py +++ b/src/ahriman/application/ahriman.py @@ -85,7 +85,6 @@ def _parser() -> argparse.ArgumentParser: _set_repo_check_parser(subparsers) _set_repo_clean_parser(subparsers) _set_repo_config_parser(subparsers) - _set_repo_init_parser(subparsers) _set_repo_rebuild_parser(subparsers) _set_repo_remove_unknown_parser(subparsers) _set_repo_report_parser(subparsers) @@ -342,19 +341,6 @@ def _set_repo_config_parser(root: SubParserAction) -> argparse.ArgumentParser: return parser -def _set_repo_init_parser(root: SubParserAction) -> argparse.ArgumentParser: - """ - add parser for repository init subcommand - :param root: subparsers for the commands - :return: created argument parser - """ - parser = root.add_parser("repo-init", aliases=["init"], help="create repository tree", - description="create empty repository tree. Optional command for auto architecture support", - formatter_class=_formatter) - parser.set_defaults(handler=handlers.Init, no_report=True) - return parser - - def _set_repo_rebuild_parser(root: SubParserAction) -> argparse.ArgumentParser: """ add parser for repository rebuild subcommand @@ -406,7 +392,7 @@ def _set_repo_setup_parser(root: SubParserAction) -> argparse.ArgumentParser: :param root: subparsers for the commands :return: created argument parser """ - parser = root.add_parser("repo-setup", aliases=["setup"], help="initial service configuration", + parser = root.add_parser("repo-setup", aliases=["init", "repo-init", "setup"], help="initial service configuration", description="create initial service configuration, requires root", epilog="Create _minimal_ configuration for the service according to provided options.", formatter_class=_formatter) diff --git a/src/ahriman/application/handlers/__init__.py b/src/ahriman/application/handlers/__init__.py index bc61bba5..2d1f6a77 100644 --- a/src/ahriman/application/handlers/__init__.py +++ b/src/ahriman/application/handlers/__init__.py @@ -22,7 +22,6 @@ from ahriman.application.handlers.handler import Handler from ahriman.application.handlers.add import Add from ahriman.application.handlers.clean import Clean from ahriman.application.handlers.dump import Dump -from ahriman.application.handlers.init import Init from ahriman.application.handlers.key_import import KeyImport from ahriman.application.handlers.patch import Patch from ahriman.application.handlers.rebuild import Rebuild diff --git a/src/ahriman/application/handlers/handler.py b/src/ahriman/application/handlers/handler.py index 69ffa296..471210ce 100644 --- a/src/ahriman/application/handlers/handler.py +++ b/src/ahriman/application/handlers/handler.py @@ -27,7 +27,7 @@ from typing import List, Type from ahriman.application.lock import Lock from ahriman.core.configuration import Configuration -from ahriman.core.exceptions import MissingArchitecture, MultipleArchitecture +from ahriman.core.exceptions import MissingArchitecture, MultipleArchitectures from ahriman.models.repository_paths import RepositoryPaths @@ -95,7 +95,7 @@ class Handler: # actually we do not have to spawn another process if it is single-process application, do we? if len(architectures) > 1: if not cls.ALLOW_MULTI_ARCHITECTURE_RUN: - raise MultipleArchitecture(args.command) + raise MultipleArchitectures(args.command) with Pool(len(architectures)) as pool: result = pool.starmap( diff --git a/src/ahriman/application/handlers/init.py b/src/ahriman/application/handlers/init.py deleted file mode 100644 index 2b7a9bb0..00000000 --- a/src/ahriman/application/handlers/init.py +++ /dev/null @@ -1,47 +0,0 @@ -# -# Copyright (c) 2021 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 argparse - -from typing import Type - -from ahriman.application.application import Application -from ahriman.application.handlers.handler import Handler -from ahriman.core.configuration import Configuration - - -class Init(Handler): - """ - repository init handler - """ - - ALLOW_AUTO_ARCHITECTURE_RUN = False - - @classmethod - def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, - configuration: Configuration, no_report: bool, unsafe: bool) -> None: - """ - callback for command line - :param args: command line args - :param architecture: repository architecture - :param configuration: configuration instance - :param no_report: force disable reporting - :param unsafe: if set no user check will be performed before path creation - """ - Application(architecture, configuration, no_report, unsafe).repository.repo.init() diff --git a/src/ahriman/application/handlers/setup.py b/src/ahriman/application/handlers/setup.py index 2f6d7d52..3d8e29eb 100644 --- a/src/ahriman/application/handlers/setup.py +++ b/src/ahriman/application/handlers/setup.py @@ -55,14 +55,19 @@ class Setup(Handler): :param no_report: force disable reporting :param unsafe: if set no user check will be performed before path creation """ + Setup.configuration_create_ahriman(args, architecture, args.repository, configuration.include) + configuration.reload() + application = Application(architecture, configuration, no_report, unsafe) + Setup.configuration_create_makepkg(args.packager, application.repository.paths) Setup.executable_create(args.build_command, architecture) Setup.configuration_create_devtools(args.build_command, architecture, args.from_configuration, args.no_multilib, args.repository, application.repository.paths) - Setup.configuration_create_ahriman(args, architecture, args.repository, configuration.include) Setup.configuration_create_sudo(args.build_command, architecture) + application.repository.repo.init() + @staticmethod def build_command(prefix: str, architecture: str) -> Path: """ diff --git a/src/ahriman/application/lock.py b/src/ahriman/application/lock.py index 4c72425b..90e96584 100644 --- a/src/ahriman/application/lock.py +++ b/src/ahriman/application/lock.py @@ -32,6 +32,7 @@ from ahriman.core.exceptions import DuplicateRun from ahriman.core.status.client import Client from ahriman.core.util import check_user from ahriman.models.build_status import BuildStatusEnum +from ahriman.models.repository_paths import RepositoryPaths class Lock: @@ -40,7 +41,7 @@ class Lock: :ivar force: remove lock file on start if any :ivar path: path to lock file if any :ivar reporter: build status reporter instance - :ivar root: repository root (i.e. ahriman home) + :ivar paths: repository paths instance :ivar unsafe: skip user check """ @@ -55,7 +56,7 @@ class Lock: self.force = args.force self.unsafe = args.unsafe - self.root = Path(configuration.get("repository", "root")) + self.paths = RepositoryPaths(configuration.getpath("repository", "root"), architecture) self.reporter = Client() if args.no_report else Client.load(configuration) def __enter__(self) -> Lock: @@ -103,7 +104,7 @@ class Lock: """ check if current user is actually owner of ahriman root """ - check_user(self.root, self.unsafe) + check_user(self.paths, self.unsafe) def clear(self) -> None: """ diff --git a/src/ahriman/core/alpm/repo.py b/src/ahriman/core/alpm/repo.py index 077fd9ee..0f4eb478 100644 --- a/src/ahriman/core/alpm/repo.py +++ b/src/ahriman/core/alpm/repo.py @@ -34,6 +34,7 @@ class Repo: :ivar name: repository name :ivar paths: repository paths instance :ivar sign_args: additional args which have to be used to sign repository archive + :ivar uid: uid of the repository owner user """ _check_output = check_output @@ -48,6 +49,7 @@ class Repo: self.logger = logging.getLogger("build_details") self.name = name self.paths = paths + self.uid, _ = paths.root_owner self.sign_args = sign_args @property @@ -66,7 +68,8 @@ class Repo: "repo-add", *self.sign_args, "-R", str(self.repo_path), str(path), exception=BuildFailed(path.name), cwd=self.paths.repository, - logger=self.logger) + logger=self.logger, + user=self.uid) def init(self) -> None: """ @@ -76,7 +79,8 @@ class Repo: "repo-add", *self.sign_args, str(self.repo_path), exception=None, cwd=self.paths.repository, - logger=self.logger) + logger=self.logger, + user=self.uid) def remove(self, package: str, filename: Path) -> None: """ @@ -93,4 +97,5 @@ class Repo: "repo-remove", *self.sign_args, str(self.repo_path), package, exception=BuildFailed(package), cwd=self.paths.repository, - logger=self.logger) + logger=self.logger, + user=self.uid) diff --git a/src/ahriman/core/build_tools/task.py b/src/ahriman/core/build_tools/task.py index 4345f642..52857fc8 100644 --- a/src/ahriman/core/build_tools/task.py +++ b/src/ahriman/core/build_tools/task.py @@ -38,6 +38,7 @@ class Task: :ivar logger: class logger :ivar package: package definitions :ivar paths: repository paths instance + :ivar uid: uid of the repository owner user """ _check_output = check_output @@ -53,6 +54,7 @@ class Task: self.build_logger = logging.getLogger("build_details") self.package = package self.paths = paths + self.uid, _ = paths.root_owner self.archbuild_flags = configuration.getlist("build", "archbuild_flags", fallback=[]) self.build_command = configuration.get("build", "build_command") @@ -74,7 +76,8 @@ class Task: *command, exception=BuildFailed(self.package.base), cwd=self.paths.sources_for(self.package.base), - logger=self.build_logger) + logger=self.build_logger, + user=self.uid) # well it is not actually correct, but we can deal with it packages = Task._check_output("makepkg", "--packagelist", diff --git a/src/ahriman/core/exceptions.py b/src/ahriman/core/exceptions.py index cd207627..f37ea3cc 100644 --- a/src/ahriman/core/exceptions.py +++ b/src/ahriman/core/exceptions.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +from pathlib import Path from typing import Any @@ -85,6 +86,20 @@ class InvalidOption(ValueError): ValueError.__init__(self, f"Invalid or unknown option value `{value}`") +class InvalidPath(ValueError): + """ + exception which will be raised on path which is not belong to root directory + """ + + def __init__(self, path: Path, root: Path) -> None: + """ + default constructor + :param path: path which raised an exception + :param root: repository root (i.e. ahriman home) + """ + ValueError.__init__(self, f"Path `{path}` does not belong to repository root `{root}`") + + class InvalidPackageInfo(RuntimeError): """ exception which will be raised on package load errors @@ -111,7 +126,7 @@ class MissingArchitecture(ValueError): ValueError.__init__(self, f"Architecture required for subcommand {command}, but missing") -class MultipleArchitecture(ValueError): +class MultipleArchitectures(ValueError): """ exception which will be raised if multiple architectures are not supported by the handler """ diff --git a/src/ahriman/core/repository/properties.py b/src/ahriman/core/repository/properties.py index 3edbe82d..10918b37 100644 --- a/src/ahriman/core/repository/properties.py +++ b/src/ahriman/core/repository/properties.py @@ -62,7 +62,7 @@ class Properties: self.paths = RepositoryPaths(configuration.getpath("repository", "root"), architecture) try: - check_user(self.paths.root, unsafe) + check_user(self.paths, unsafe) self.paths.tree_create() except UnsafeRun: self.logger.warning("root owner differs from the current user, skipping tree creation") diff --git a/src/ahriman/core/util.py b/src/ahriman/core/util.py index f14dd7d4..bb768dd9 100644 --- a/src/ahriman/core/util.py +++ b/src/ahriman/core/util.py @@ -27,10 +27,11 @@ from pathlib import Path from typing import Any, Dict, Generator, Iterable, Optional, Union from ahriman.core.exceptions import InvalidOption, UnsafeRun +from ahriman.models.repository_paths import RepositoryPaths def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path] = None, - input_data: Optional[str] = None, logger: Optional[Logger] = None) -> str: + input_data: Optional[str] = None, logger: Optional[Logger] = None, user: Optional[int] = None) -> str: """ subprocess wrapper :param args: command line arguments @@ -38,12 +39,15 @@ def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path] :param cwd: current working directory :param input_data: data which will be written to command stdin :param logger: logger to log command result if required + :param user: run process as specified user :return: command output """ try: # universal_newlines is required to read input from string + # FIXME additional workaround for linter and type check which do not know that user arg is supported + # pylint: disable=unexpected-keyword-arg result: str = subprocess.check_output(args, cwd=cwd, input=input_data, stderr=subprocess.STDOUT, - universal_newlines=True).strip() + universal_newlines=True, user=user).strip() # type: ignore if logger is not None: for line in result.splitlines(): logger.debug(line) @@ -55,18 +59,18 @@ def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path] raise exception or e -def check_user(root: Path, unsafe: bool) -> None: +def check_user(paths: RepositoryPaths, unsafe: bool) -> None: """ check if current user is the owner of the root - :param root: root directory (i.e. ahriman home) + :param paths: repository paths object :param unsafe: if set no user check will be performed before path creation """ - if not root.exists(): + if not paths.root.exists(): return # no directory found, skip check if unsafe: return # unsafe flag is enabled, no check performed current_uid = os.getuid() - root_uid = root.stat().st_uid + root_uid, _ = paths.root_owner if current_uid != root_uid: raise UnsafeRun(current_uid, root_uid) diff --git a/src/ahriman/models/repository_paths.py b/src/ahriman/models/repository_paths.py index baeb97cf..55dc88dd 100644 --- a/src/ahriman/models/repository_paths.py +++ b/src/ahriman/models/repository_paths.py @@ -19,11 +19,14 @@ # from __future__ import annotations +import os import shutil from dataclasses import dataclass from pathlib import Path -from typing import Set, Type +from typing import Set, Tuple, Type + +from ahriman.core.exceptions import InvalidPath @dataclass @@ -80,6 +83,13 @@ class RepositoryPaths: """ return self.root / "repository" / self.architecture + @property + def root_owner(self) -> Tuple[int, int]: + """ + :return: owner user and group of the root directory + """ + return self.owner(self.root) + @property def sources(self) -> Path: """ @@ -101,6 +111,16 @@ class RepositoryPaths: if path.is_dir() } + @staticmethod + def owner(path: Path) -> Tuple[int, int]: + """ + retrieve owner information by path + :param path: path for which extract ids + :return: owner user and group ids of the directory + """ + stat = path.stat() + return stat.st_uid, stat.st_gid + def cache_for(self, package_base: str) -> Path: """ get path to cached PKGBUILD and package sources for the package base @@ -109,6 +129,28 @@ class RepositoryPaths: """ return self.cache / package_base + def chown(self, path: Path) -> None: + """ + set owner of path recursively (from root) to root owner + :param path: path to be chown + """ + def set_owner(current: Path) -> None: + """ + set owner to the specified path + :param current: path to set + """ + uid, gid = self.owner(current) + if uid == root_uid and gid == root_gid: + return + os.chown(current, root_uid, root_gid, follow_symlinks=False) + + if self.root not in path.parents: + raise InvalidPath(path, self.root) + root_uid, root_gid = self.root_owner + while path != self.root: + set_owner(path) + path = path.parent + def manual_for(self, package_base: str) -> Path: """ get manual path for specific package base @@ -158,3 +200,4 @@ class RepositoryPaths: self.repository, self.sources): directory.mkdir(mode=0o755, parents=True, exist_ok=True) + self.chown(directory) diff --git a/tests/ahriman/application/handlers/test_handler.py b/tests/ahriman/application/handlers/test_handler.py index 8acd1217..ec4fb98b 100644 --- a/tests/ahriman/application/handlers/test_handler.py +++ b/tests/ahriman/application/handlers/test_handler.py @@ -6,7 +6,7 @@ from pytest_mock import MockerFixture from ahriman.application.handlers import Handler from ahriman.core.configuration import Configuration -from ahriman.core.exceptions import MissingArchitecture, MultipleArchitecture +from ahriman.core.exceptions import MissingArchitecture, MultipleArchitectures def test_architectures_extract(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: @@ -94,7 +94,7 @@ def test_execute_multiple_not_supported(args: argparse.Namespace, mocker: Mocker args.command = "web" mocker.patch.object(Handler, "ALLOW_MULTI_ARCHITECTURE_RUN", False) - with pytest.raises(MultipleArchitecture): + with pytest.raises(MultipleArchitectures): Handler.execute(args) diff --git a/tests/ahriman/application/handlers/test_handler_init.py b/tests/ahriman/application/handlers/test_handler_init.py deleted file mode 100644 index 2737f89e..00000000 --- a/tests/ahriman/application/handlers/test_handler_init.py +++ /dev/null @@ -1,26 +0,0 @@ -import argparse - -from pytest_mock import MockerFixture - -from ahriman.application.handlers import Init -from ahriman.core.configuration import Configuration - - -def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: - """ - must run command - """ - mocker.patch("ahriman.core.repository.properties.check_user") - tree_create_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") - init_mock = mocker.patch("ahriman.core.alpm.repo.Repo.init") - - Init.run(args, "x86_64", configuration, True, False) - tree_create_mock.assert_called_once_with() - init_mock.assert_called_once_with() - - -def test_disallow_auto_architecture_run() -> None: - """ - must not allow multi architecture run - """ - assert not Init.ALLOW_AUTO_ARCHITECTURE_RUN diff --git a/tests/ahriman/application/handlers/test_handler_setup.py b/tests/ahriman/application/handlers/test_handler_setup.py index e2213fe7..50aafd26 100644 --- a/tests/ahriman/application/handlers/test_handler_setup.py +++ b/tests/ahriman/application/handlers/test_handler_setup.py @@ -40,6 +40,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc makepkg_configuration_mock = mocker.patch("ahriman.application.handlers.setup.Setup.configuration_create_makepkg") sudo_configuration_mock = mocker.patch("ahriman.application.handlers.setup.Setup.configuration_create_sudo") executable_mock = mocker.patch("ahriman.application.handlers.setup.Setup.executable_create") + init_mock = mocker.patch("ahriman.core.alpm.repo.Repo.init") paths = RepositoryPaths(configuration.getpath("repository", "root"), "x86_64") Setup.run(args, "x86_64", configuration, True, False) @@ -49,6 +50,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc makepkg_configuration_mock.assert_called_once_with(args.packager, paths) sudo_configuration_mock.assert_called_once_with(args.build_command, "x86_64") executable_mock.assert_called_once_with(args.build_command, "x86_64") + init_mock.assert_called_once() def test_build_command(args: argparse.Namespace) -> None: diff --git a/tests/ahriman/application/test_ahriman.py b/tests/ahriman/application/test_ahriman.py index f8b7b19a..ae558ade 100644 --- a/tests/ahriman/application/test_ahriman.py +++ b/tests/ahriman/application/test_ahriman.py @@ -288,15 +288,6 @@ def test_subparsers_repo_config(parser: argparse.ArgumentParser) -> None: assert args.unsafe -def test_subparsers_repo_init(parser: argparse.ArgumentParser) -> None: - """ - repo-init command must imply no_report - """ - args = parser.parse_args(["-a", "x86_64", "repo-init"]) - assert args.architecture == ["x86_64"] - assert args.no_report - - def test_subparsers_repo_rebuild_architecture(parser: argparse.ArgumentParser) -> None: """ repo-rebuild command must correctly parse architecture list diff --git a/tests/ahriman/application/test_lock.py b/tests/ahriman/application/test_lock.py index 61cc79b9..0818218e 100644 --- a/tests/ahriman/application/test_lock.py +++ b/tests/ahriman/application/test_lock.py @@ -82,7 +82,7 @@ def test_check_user(lock: Lock, mocker: MockerFixture) -> None: """ check_user_patch = mocker.patch("ahriman.application.lock.check_user") lock.check_user() - check_user_patch.assert_called_once_with(lock.root, False) + check_user_patch.assert_called_once_with(lock.paths, False) def test_check_user_exception(lock: Lock, mocker: MockerFixture) -> None: diff --git a/tests/ahriman/core/test_util.py b/tests/ahriman/core/test_util.py index dedc77da..0de8c851 100644 --- a/tests/ahriman/core/test_util.py +++ b/tests/ahriman/core/test_util.py @@ -9,6 +9,7 @@ from pytest_mock import MockerFixture from ahriman.core.exceptions import InvalidOption, UnsafeRun from ahriman.core.util import check_output, check_user, filter_json, package_like, pretty_datetime, pretty_size, walk from ahriman.models.package import Package +from ahriman.models.repository_paths import RepositoryPaths def test_check_output(mocker: MockerFixture) -> None: @@ -56,37 +57,37 @@ def test_check_user(mocker: MockerFixture) -> None: """ must check user correctly """ - cwd = Path.cwd() - mocker.patch("os.getuid", return_value=cwd.stat().st_uid) - check_user(cwd, False) + paths = RepositoryPaths(Path.cwd(), "x86_64") + mocker.patch("os.getuid", return_value=paths.root_owner[0]) + check_user(paths, False) -def test_check_user_no_directory(mocker: MockerFixture) -> None: +def test_check_user_no_directory(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: """ must not fail in case if no directory found """ mocker.patch("pathlib.Path.exists", return_value=False) - check_user(Path.cwd(), False) + check_user(repository_paths, False) def test_check_user_exception(mocker: MockerFixture) -> None: """ must raise exception if user differs """ - cwd = Path.cwd() - mocker.patch("os.getuid", return_value=cwd.stat().st_uid + 1) + paths = RepositoryPaths(Path.cwd(), "x86_64") + mocker.patch("os.getuid", return_value=paths.root_owner[0] + 1) with pytest.raises(UnsafeRun): - check_user(cwd, False) + check_user(paths, False) def test_check_unsafe(mocker: MockerFixture) -> None: """ must skip check if unsafe flag is set """ - cwd = Path.cwd() - mocker.patch("os.getuid", return_value=cwd.stat().st_uid + 1) - check_user(cwd, True) + paths = RepositoryPaths(Path.cwd(), "x86_64") + mocker.patch("os.getuid", return_value=paths.root_owner[0] + 1) + check_user(paths, True) def test_filter_json(package_ahriman: Package) -> None: diff --git a/tests/ahriman/models/test_repository_paths.py b/tests/ahriman/models/test_repository_paths.py index 8c2089f3..462cfbd6 100644 --- a/tests/ahriman/models/test_repository_paths.py +++ b/tests/ahriman/models/test_repository_paths.py @@ -1,10 +1,36 @@ -from pytest_mock import MockerFixture -from unittest import mock +import pytest +from pathlib import Path +from pytest_mock import MockerFixture +from typing import Callable, Tuple +from unittest import mock +from unittest.mock import MagicMock + +from ahriman.core.exceptions import InvalidPath from ahriman.models.package import Package from ahriman.models.repository_paths import RepositoryPaths +def _get_owner(root: Path, same: bool) -> Callable[[Path], Tuple[int, int]]: + """ + mocker function for owner definition + :param root: root directory + :param same: if True then returns the same as root directory and different otherwise + :return: function which can define ownership + """ + root_owner = (42, 42) + nonroot_owner = (42, 42) if same else (1, 1) + return lambda path: root_owner if path == root else nonroot_owner + + +def test_root_owner(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: + """ + must correctly define root directory owner + """ + mocker.patch("ahriman.models.repository_paths.RepositoryPaths.owner", return_value=(42, 142)) + assert repository_paths.root_owner == (42, 142) + + def test_known_architectures(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: """ must list available directory paths @@ -14,6 +40,18 @@ def test_known_architectures(repository_paths: RepositoryPaths, mocker: MockerFi iterdir_mock.assert_called_once_with() +def test_owner(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: + """ + must correctly retrieve owner of the path + """ + stat_mock = MagicMock() + stat_mock.st_uid = 42 + stat_mock.st_gid = 142 + mocker.patch("pathlib.Path.stat", return_value=stat_mock) + + assert RepositoryPaths.owner(repository_paths.root) == (42, 142) + + def test_cache_for(repository_paths: RepositoryPaths, package_ahriman: Package) -> None: """ must return correct path for cache directory @@ -23,6 +61,56 @@ def test_cache_for(repository_paths: RepositoryPaths, package_ahriman: Package) assert path.parent == repository_paths.cache +def test_chown(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: + """ + must correctly set owner for the directory + """ + repository_paths.owner = _get_owner(repository_paths.root, same=False) + mocker.patch.object(RepositoryPaths, "root_owner", (42, 42)) + chown_mock = mocker.patch("os.chown") + + path = repository_paths.root / "path" + repository_paths.chown(path) + chown_mock.assert_called_once_with(path, 42, 42, follow_symlinks=False) + + +def test_chown_parent(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: + """ + must correctly set owner for the directory including parents + """ + repository_paths.owner = _get_owner(repository_paths.root, same=False) + mocker.patch.object(RepositoryPaths, "root_owner", (42, 42)) + chown_mock = mocker.patch("os.chown") + + path = repository_paths.root / "parent" / "path" + repository_paths.chown(path) + chown_mock.assert_has_calls([ + mock.call(path, 42, 42, follow_symlinks=False), + mock.call(path.parent, 42, 42, follow_symlinks=False) + ]) + + +def test_chown_skip(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: + """ + must skip ownership set in case if it is same as root + """ + repository_paths.owner = _get_owner(repository_paths.root, same=True) + mocker.patch.object(RepositoryPaths, "root_owner", (42, 42)) + chown_mock = mocker.patch("os.chown") + + path = repository_paths.root / "path" + repository_paths.chown(path) + chown_mock.assert_not_called() + + +def test_chown_invalid_path(repository_paths: RepositoryPaths) -> None: + """ + must raise invalid path exception in case if directory outside the root supplied + """ + with pytest.raises(InvalidPath): + repository_paths.chown(repository_paths.root.parent) + + def test_manual_for(repository_paths: RepositoryPaths, package_ahriman: Package) -> None: """ must return correct path for manual directory @@ -76,9 +164,17 @@ def test_tree_create(repository_paths: RepositoryPaths, mocker: MockerFixture) - for prop in dir(repository_paths) if not prop.startswith("_") and not prop.endswith("_for") - and prop not in ("architecture", "known_architectures", "root", "tree_clear", "tree_create") + and prop not in ("architecture", + "chown", + "known_architectures", + "owner", + "root", + "root_owner", + "tree_clear", + "tree_create") } mkdir_mock = mocker.patch("pathlib.Path.mkdir") + chown_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.chown") repository_paths.tree_create() mkdir_mock.assert_has_calls( @@ -86,3 +182,4 @@ def test_tree_create(repository_paths: RepositoryPaths, mocker: MockerFixture) - mock.call(mode=0o755, parents=True, exist_ok=True) for _ in paths ], any_order=True) + chown_mock.assert_has_calls([mock.call(getattr(repository_paths, path)) for path in paths], any_order=True) diff --git a/tests/testresources/core/ahriman.ini b/tests/testresources/core/ahriman.ini index 0397c762..eed9a221 100644 --- a/tests/testresources/core/ahriman.ini +++ b/tests/testresources/core/ahriman.ini @@ -25,7 +25,7 @@ makepkg_flags = --skippgpcheck [repository] name = aur-clone -root = /var/lib/ahriman +root = ../../../ [sign] target =