diff --git a/docs/configuration.rst b/docs/configuration.rst
index a4fcfc3a..c57e80b5 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -108,6 +108,24 @@ Web server settings. If any of ``host``/``port`` is not set, web integration wil
* ``unix_socket_unsafe`` - set unsafe (o+w) permissions to unix socket, boolean, optional, default ``yes``. This option is enabled by default, because it is supposed that unix socket is created in safe environment (only web service is supposed to be used in unsafe), but it can be disabled by configuration.
* ``username`` - username to authorize in web service in order to update service status, string, required in case if authorization enabled.
+``keyring`` group
+--------------------
+
+Keyring package generator plugin.
+
+* ``target`` - list of generator settings sections, space separated list of strings, required. It must point to valid section name.
+
+Keyring generator plugin
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+* ``description`` - keyring package description, string, optional, default is ``repo PGP keyring``, where ``repo`` is the repository name.
+* ``homepage`` - url to homepage location if any, string, optional.
+* ``license`` - list of licenses which are applied to this package, space separated list of strings, optional, default is ``Unlicense``.
+* ``package`` - keyring package name, string, optional, default is ``repo-keyring``, where ``repo`` is the repository name.
+* ``packagers`` - list of packagers keys, space separated list of strings, optional, if not set, the ``key_*`` options from ``sign`` group will be used.
+* ``revoked`` - list of revoked packagers keys, space separated list of strings, optional.
+* ``trusted`` - list of master keys, space separated list of strings, optional, if not set, the ``key`` option from ``sign`` group will be used.
+
``mirrorlist`` group
--------------------
diff --git a/package/share/ahriman/settings/ahriman.ini b/package/share/ahriman/settings/ahriman.ini
index 8ae1f406..0588bdc6 100644
--- a/package/share/ahriman/settings/ahriman.ini
+++ b/package/share/ahriman/settings/ahriman.ini
@@ -25,7 +25,7 @@ ignore_packages =
makechrootpkg_flags =
makepkg_flags = --nocolor --ignorearch
triggers = ahriman.core.gitremote.RemotePullTrigger ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger ahriman.core.gitremote.RemotePushTrigger
-triggers_known = ahriman.core.support.MirrorlistTrigger
+triggers_known = ahriman.core.support.KeyringTrigger ahriman.core.support.MirrorlistTrigger
vcs_allowed_age = 604800
[repository]
@@ -35,6 +35,9 @@ root = /var/lib/ahriman
[sign]
target =
+[keyring]
+target =
+
[mirrorlist]
target =
diff --git a/src/ahriman/application/ahriman.py b/src/ahriman/application/ahriman.py
index 52388f5e..4c8aae35 100644
--- a/src/ahriman/application/ahriman.py
+++ b/src/ahriman/application/ahriman.py
@@ -100,6 +100,7 @@ def _parser() -> argparse.ArgumentParser:
_set_patch_set_add_parser(subparsers)
_set_repo_backup_parser(subparsers)
_set_repo_check_parser(subparsers)
+ _set_repo_create_keyring_parser(subparsers)
_set_repo_create_mirrorlist_parser(subparsers)
_set_repo_daemon_parser(subparsers)
_set_repo_rebuild_parser(subparsers)
@@ -479,6 +480,25 @@ def _set_repo_check_parser(root: SubParserAction) -> argparse.ArgumentParser:
return parser
+def _set_repo_create_keyring_parser(root: SubParserAction) -> argparse.ArgumentParser:
+ """
+ add parser for create-keyring subcommand
+
+ Args:
+ root(SubParserAction): subparsers for the commands
+
+ Returns:
+ argparse.ArgumentParser: created argument parser
+ """
+ parser = root.add_parser("repo-create-keyring", help="create keyring package",
+ description="create package which contains list of trusted keys as set by "
+ "configuration. Note, that this action will only create package, the package "
+ "itself has to be built manually",
+ formatter_class=_formatter)
+ parser.set_defaults(handler=handlers.Triggers, trigger=["ahriman.core.support.KeyringTrigger"])
+ return parser
+
+
def _set_repo_create_mirrorlist_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for create-mirrorlist subcommand
diff --git a/src/ahriman/core/exceptions.py b/src/ahriman/core/exceptions.py
index 808e2bd2..e750efc9 100644
--- a/src/ahriman/core/exceptions.py
+++ b/src/ahriman/core/exceptions.py
@@ -194,6 +194,18 @@ class PasswordError(ValueError):
ValueError.__init__(self, f"Password error: {details}")
+class PkgbuildGeneratorError(RuntimeError):
+ """
+ exception class for support type triggers
+ """
+
+ def __init__(self) -> None:
+ """
+ default constructor
+ """
+ RuntimeError.__init__(self, "Could not generate package")
+
+
class ReportError(RuntimeError):
"""
report generation exception
diff --git a/src/ahriman/core/report/html.py b/src/ahriman/core/report/html.py
index 8c5edf13..ffc1ff08 100644
--- a/src/ahriman/core/report/html.py
+++ b/src/ahriman/core/report/html.py
@@ -57,4 +57,4 @@ class HTML(Report, JinjaTemplate):
result(Result): build result
"""
html = self.make_html(Result(success=packages), self.template_path)
- self.report_path.write_text(html)
+ self.report_path.write_text(html, encoding="utf8")
diff --git a/src/ahriman/core/repository/repository_properties.py b/src/ahriman/core/repository/repository_properties.py
index a07849b5..2a236f53 100644
--- a/src/ahriman/core/repository/repository_properties.py
+++ b/src/ahriman/core/repository/repository_properties.py
@@ -79,7 +79,7 @@ class RepositoryProperties(LazyLogging):
self.ignore_list = configuration.getlist("build", "ignore_packages", fallback=[])
self.pacman = Pacman(architecture, configuration, refresh_database=refresh_pacman_database)
- self.sign = GPG(architecture, configuration)
+ self.sign = GPG(configuration)
self.repo = Repo(self.name, self.paths, self.sign.repository_sign_args)
self.reporter = Client.load(configuration, report=report)
self.triggers = TriggerLoader.load(architecture, configuration)
diff --git a/src/ahriman/core/sign/gpg.py b/src/ahriman/core/sign/gpg.py
index 976fac7e..be167c2c 100644
--- a/src/ahriman/core/sign/gpg.py
+++ b/src/ahriman/core/sign/gpg.py
@@ -19,6 +19,7 @@
#
import requests
+from collections.abc import Generator
from pathlib import Path
from ahriman.core.configuration import Configuration
@@ -34,7 +35,6 @@ class GPG(LazyLogging):
Attributes:
DEFAULT_TIMEOUT(int): (class attribute) HTTP request timeout in seconds
- architecture(str): repository architecture
configuration(Configuration): configuration instance
default_key(str | None): default PGP key ID to use
targets(set[SignSettings]): list of targets to sign (repository, package etc)
@@ -43,15 +43,13 @@ class GPG(LazyLogging):
_check_output = check_output
DEFAULT_TIMEOUT = 30
- def __init__(self, architecture: str, configuration: Configuration) -> None:
+ def __init__(self, configuration: Configuration) -> None:
"""
default constructor
Args:
- architecture(str): repository architecture
configuration(Configuration): configuration instance
"""
- self.architecture = architecture
self.configuration = configuration
self.targets, self.default_key = self.sign_options(configuration)
@@ -128,6 +126,34 @@ class GPG(LazyLogging):
raise
return response.text
+ def key_export(self, key: str) -> str:
+ """
+ export public key from stored keychain
+
+ Args:
+ key(str): key ID to export
+
+ Returns:
+ str: PGP key in .asc format
+ """
+ return GPG._check_output("gpg", "--armor", "--no-emit-version", "--export", key, logger=self.logger)
+
+ def key_fingerprint(self, key: str) -> str:
+ """
+ get full key fingerprint from short key id
+
+ Args:
+ key(str): key ID to lookup
+
+ Returns:
+ str: full PGP key fingerprint
+ """
+ metadata = GPG._check_output("gpg", "--with-colons", "--fingerprint", key, logger=self.logger)
+ # fingerprint line will be like
+ # fpr:::::::::43A663569A07EE1E4ECC55CC7E3A4240CE3C45C2:
+ fingerprint = next(filter(lambda line: line[:3] == "fpr", metadata.splitlines()))
+ return fingerprint.split(":")[-2]
+
def key_import(self, server: str, key: str) -> None:
"""
import key to current user and sign it locally
@@ -139,6 +165,21 @@ class GPG(LazyLogging):
key_body = self.key_download(server, key)
GPG._check_output("gpg", "--import", input_data=key_body, logger=self.logger)
+ def keys(self) -> list[str]:
+ """
+ extract list of keys described in configuration
+
+ Returns:
+ list[str]: list of unique keys which are set in configuration
+ """
+ def generator() -> Generator[str, None, None]:
+ if self.default_key is not None:
+ yield self.default_key
+ for _, value in filter(lambda pair: pair[0].startswith("key_"), self.configuration["sign"].items()):
+ yield value
+
+ return sorted(set(generator()))
+
def process(self, path: Path, key: str) -> list[Path]:
"""
gpg command wrapper
diff --git a/src/ahriman/core/support/__init__.py b/src/ahriman/core/support/__init__.py
index d8f2df96..f607effe 100644
--- a/src/ahriman/core/support/__init__.py
+++ b/src/ahriman/core/support/__init__.py
@@ -17,4 +17,5 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
+from ahriman.core.support.keyring_trigger import KeyringTrigger
from ahriman.core.support.mirrorlist_trigger import MirrorlistTrigger
diff --git a/src/ahriman/core/support/keyring_trigger.py b/src/ahriman/core/support/keyring_trigger.py
new file mode 100644
index 00000000..6a83e889
--- /dev/null
+++ b/src/ahriman/core/support/keyring_trigger.py
@@ -0,0 +1,114 @@
+#
+# 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 .
+#
+from ahriman.core import context
+from ahriman.core.configuration import Configuration
+from ahriman.core.sign.gpg import GPG
+from ahriman.core.support.package_creator import PackageCreator
+from ahriman.core.support.pkgbuild.keyring_generator import KeyringGenerator
+from ahriman.core.triggers import Trigger
+from ahriman.models.context_key import ContextKey
+
+
+class KeyringTrigger(Trigger):
+ """
+ keyring generator trigger
+
+ Attributes:
+ targets(list[str]): git remote target list
+ """
+
+ CONFIGURATION_SCHEMA = {
+ "keyring": {
+ "type": "dict",
+ "schema": {
+ "target": {
+ "type": "list",
+ "coerce": "list",
+ "schema": {"type": "string"},
+ },
+ },
+ },
+ "keyring_generator": {
+ "type": "dict",
+ "schema": {
+ "description": {
+ "type": "string",
+ },
+ "homepage": {
+ "type": "string",
+ },
+ "license": {
+ "type": "list",
+ "coerce": "list",
+ },
+ "package": {
+ "type": "string",
+ },
+ "packagers": {
+ "type": "list",
+ "coerce": "list",
+ },
+ "revoked": {
+ "type": "list",
+ "coerce": "list",
+ },
+ "trusted": {
+ "type": "list",
+ "coerce": "list",
+ },
+ },
+ },
+ }
+
+ def __init__(self, architecture: str, configuration: Configuration) -> None:
+ """
+ default constructor
+
+ Args:
+ architecture(str): repository architecture
+ configuration(Configuration): configuration instance
+ """
+ Trigger.__init__(self, architecture, configuration)
+ self.targets = self.configuration_sections(configuration)
+
+ @classmethod
+ def configuration_sections(cls, configuration: Configuration) -> list[str]:
+ """
+ extract configuration sections from configuration
+
+ Args:
+ configuration(Configuration): configuration instance
+
+ Returns:
+ list[str]: read configuration sections belong to this trigger
+ """
+ return configuration.getlist("keyring", "target", fallback=[])
+
+ def on_start(self) -> None:
+ """
+ trigger action which will be called at the start of the application
+ """
+ ctx = context.get()
+ sign = ctx.get(ContextKey("sign", GPG))
+
+ for target in self.targets:
+ generator = KeyringGenerator(sign, self.configuration, target)
+ runner = PackageCreator(self.configuration, generator)
+ runner.run()
diff --git a/src/ahriman/core/support/package_creator.py b/src/ahriman/core/support/package_creator.py
index 61dfad73..6ab120df 100644
--- a/src/ahriman/core/support/package_creator.py
+++ b/src/ahriman/core/support/package_creator.py
@@ -23,14 +23,13 @@ from ahriman.core import context
from ahriman.core.build_tools.sources import Sources
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
-from ahriman.core.log import LazyLogging
from ahriman.core.support.pkgbuild.pkgbuild_generator import PkgbuildGenerator
from ahriman.models.build_status import BuildStatus
from ahriman.models.context_key import ContextKey
from ahriman.models.package import Package
-class PackageCreator(LazyLogging):
+class PackageCreator:
"""
helper which creates packages based on pkgbuild generator
diff --git a/src/ahriman/core/support/pkgbuild/keyring_generator.py b/src/ahriman/core/support/pkgbuild/keyring_generator.py
new file mode 100644
index 00000000..0ddb8c3f
--- /dev/null
+++ b/src/ahriman/core/support/pkgbuild/keyring_generator.py
@@ -0,0 +1,194 @@
+#
+# 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 .
+#
+from collections.abc import Callable
+from pathlib import Path
+
+from ahriman.core.configuration import Configuration
+from ahriman.core.exceptions import PkgbuildGeneratorError
+from ahriman.core.sign.gpg import GPG
+from ahriman.core.support.pkgbuild.pkgbuild_generator import PkgbuildGenerator
+
+
+class KeyringGenerator(PkgbuildGenerator):
+ """
+ generator for keyring PKGBUILD
+
+ Attributes:
+ sign(GPG): GPG wrapper instance
+ name(str): repository name
+ packagers(list[str]): list of packagers PGP keys
+ pkgbuild_license(list[str]): keyring package license
+ pkgbuild_pkgdesc(str): keyring package description
+ pkgbuild_pkgname(str): keyring package name
+ pkgbuild_url(str): keyring package home page
+ revoked(list[str]): list of revoked PGP keys
+ trusted(list[str]): lif of trusted PGP keys
+ """
+
+ def __init__(self, sign: GPG, configuration: Configuration, section: str) -> None:
+ """
+ default constructor
+
+ Args:
+ sign(GPG): GPG wrapper instance
+ configuration(Configuration): configuration instance
+ section(str): settings section name
+ """
+ self.sign = sign
+ self.name = configuration.repository_name
+
+ # configuration fields
+ self.packagers = configuration.getlist(section, "packagers", fallback=sign.keys())
+ self.revoked = configuration.getlist(section, "revoked", fallback=[])
+ self.trusted = configuration.getlist(
+ section, "trusted", fallback=[sign.default_key] if sign.default_key is not None else [])
+ # pkgbuild description fields
+ self.pkgbuild_pkgname = configuration.get(section, "package", fallback=f"{self.name}-keyring")
+ self.pkgbuild_pkgdesc = configuration.get(section, "description", fallback=f"{self.name} PGP keyring")
+ self.pkgbuild_license = configuration.getlist(section, "license", fallback=["Unlicense"])
+ self.pkgbuild_url = configuration.get(section, "homepage", fallback="")
+
+ @property
+ def license(self) -> list[str]:
+ """
+ package licenses list
+
+ Returns:
+ list[str]: package licenses as PKGBUILD property
+ """
+ return self.pkgbuild_license
+
+ @property
+ def pkgdesc(self) -> str:
+ """
+ package description
+
+ Returns:
+ str: package description as PKGBUILD property
+ """
+ return self.pkgbuild_pkgdesc
+
+ @property
+ def pkgname(self) -> str:
+ """
+ package name
+
+ Returns:
+ str: package name as PKGBUILD property
+ """
+ return self.pkgbuild_pkgname
+
+ @property
+ def url(self) -> str:
+ """
+ package upstream url
+
+ Returns:
+ str: package upstream url as PKGBUILD property
+ """
+ return self.pkgbuild_url
+
+ def _generate_gpg(self, source_path: Path) -> None:
+ """
+ generate GPG keychain
+
+ Args:
+ source_path(Path): destination of the file content
+ """
+ with source_path.open("w") as source_file:
+ for key in sorted(set(self.trusted + self.packagers + self.revoked)):
+ public_key = self.sign.key_export(key)
+ source_file.write(public_key)
+ source_file.write("\n")
+
+ def _generate_revoked(self, source_path: Path) -> None:
+ """
+ generate revoked PGP keys
+
+ Args:
+ source_path(Path): destination of the file content
+ """
+ with source_path.open("w") as source_file:
+ for key in sorted(set(self.revoked)):
+ fingerprint = self.sign.key_fingerprint(key)
+ source_file.write(fingerprint)
+ source_file.write("\n")
+
+ def _generate_trusted(self, source_path: Path) -> None:
+ """
+ generate trusted PGP keys
+
+ Args:
+ source_path(Path): destination of the file content
+ """
+ if not self.trusted:
+ raise PkgbuildGeneratorError
+ with source_path.open("w") as source_file:
+ for key in sorted(set(self.trusted)):
+ fingerprint = self.sign.key_fingerprint(key)
+ source_file.write(fingerprint)
+ source_file.write(":4:\n")
+
+ def install(self) -> str | None:
+ """
+ content of the install functions
+
+ Returns:
+ str | None: content of the install functions if any
+ """
+ # copy-paste from archlinux-keyring
+ return f"""post_upgrade() {{
+ if usr/bin/pacman-key -l >/dev/null 2>&1; then
+ usr/bin/pacman-key --populate {self.name}
+ usr/bin/pacman-key --updatedb
+ fi
+}}
+
+post_install() {{
+ if [ -x usr/bin/pacman-key ]; then
+ post_upgrade
+ fi
+}}"""
+
+ def package(self) -> str:
+ """
+ package function generator
+
+ Returns:
+ str: package() function for PKGBUILD
+ """
+ return f"""{{
+ install -Dm644 "{Path("$srcdir") / f"{self.name}.gpg"}" "{Path("$pkgdir") / "usr" / "share" / "pacman" / "keyrings" / f"{self.name}.gpg"}"
+ install -Dm644 "{Path("$srcdir") / f"{self.name}-revoked"}" "{Path("$pkgdir") / "usr" / "share" / "pacman" / "keyrings" / f"{self.name}-revoked"}"
+ install -Dm644 "{Path("$srcdir") / f"{self.name}-trusted"}" "{Path("$pkgdir") / "usr" / "share" / "pacman" / "keyrings" / f"{self.name}-trusted"}"
+}}"""
+
+ def sources(self) -> dict[str, Callable[[Path], None]]:
+ """
+ return list of sources for the package
+
+ Returns:
+ dict[str, Callable[[Path], None]]: map of source identifier (e.g. filename) to its generator function
+ """
+ return {
+ f"{self.name}.gpg": self._generate_gpg,
+ f"{self.name}-revoked": self._generate_revoked,
+ f"{self.name}-trusted": self._generate_trusted,
+ }
diff --git a/src/ahriman/core/support/pkgbuild/mirrorlist_generator.py b/src/ahriman/core/support/pkgbuild/mirrorlist_generator.py
index 9f8983b5..36f3df08 100644
--- a/src/ahriman/core/support/pkgbuild/mirrorlist_generator.py
+++ b/src/ahriman/core/support/pkgbuild/mirrorlist_generator.py
@@ -106,8 +106,8 @@ class MirrorlistGenerator(PkgbuildGenerator):
Args:
source_path(Path): destination of the mirrorlist content
"""
- with source_path.open("w") as source_file:
- source_file.writelines([f"Server = {server}\n" for server in self.servers])
+ content = "".join([f"Server = {server}\n" for server in self.servers])
+ source_path.write_text(content, encoding="utf8")
def package(self) -> str:
"""
@@ -139,5 +139,5 @@ class MirrorlistGenerator(PkgbuildGenerator):
dict[str, Callable[[Path], None]]: map of source identifier (e.g. filename) to its generator function
"""
return {
- "mirrorlist": self._generate_mirrorlist
+ "mirrorlist": self._generate_mirrorlist,
}
diff --git a/src/ahriman/core/support/pkgbuild/pkgbuild_generator.py b/src/ahriman/core/support/pkgbuild/pkgbuild_generator.py
index 60cbfdcb..42787e90 100644
--- a/src/ahriman/core/support/pkgbuild/pkgbuild_generator.py
+++ b/src/ahriman/core/support/pkgbuild/pkgbuild_generator.py
@@ -23,12 +23,11 @@ import itertools
from collections.abc import Callable, Generator
from pathlib import Path
-from ahriman.core.log import LazyLogging
from ahriman.core.util import utcnow
from ahriman.models.pkgbuild_patch import PkgbuildPatch
-class PkgbuildGenerator(LazyLogging):
+class PkgbuildGenerator:
"""
main class for generating PKGBUILDs
@@ -97,6 +96,14 @@ class PkgbuildGenerator(LazyLogging):
"""
return ""
+ def install(self) -> str | None:
+ """
+ content of the install functions
+
+ Returns:
+ str | None: content of the install functions if any
+ """
+
def package(self) -> str:
"""
package function generator
@@ -127,6 +134,24 @@ class PkgbuildGenerator(LazyLogging):
"""
return {}
+ def write_install(self, source_dir: Path) -> list[PkgbuildPatch]:
+ """
+ generate content of install file
+
+ Args:
+ source_dir(Path): path to directory in which sources must be generated
+
+ Returns:
+ list[PkgbuildPatch]: patch for the pkgbuild if install file exists and empty list otherwise
+ """
+ content: str | None = self.install()
+ if content is None:
+ return []
+
+ source_path = source_dir / f"{self.pkgname}.install"
+ source_path.write_text(content)
+ return [PkgbuildPatch("install", source_path.name)]
+
def write_pkgbuild(self, source_dir: Path) -> None:
"""
generate PKGBUILD content to the specified path
@@ -143,6 +168,7 @@ class PkgbuildGenerator(LazyLogging):
PkgbuildPatch("url", self.url),
]) # ...main properties as defined by derived class...
patches.extend(self.patches()) # ...optional properties as defined by derived class...
+ patches.extend(self.write_install(source_dir)) # ...install function...
patches.append(PkgbuildPatch("package()", self.package())) # ...package function...
patches.extend(self.write_sources(source_dir)) # ...and finally source files
diff --git a/tests/ahriman/application/handlers/test_handler_validate.py b/tests/ahriman/application/handlers/test_handler_validate.py
index 237d0bdd..3bfb7d0d 100644
--- a/tests/ahriman/application/handlers/test_handler_validate.py
+++ b/tests/ahriman/application/handlers/test_handler_validate.py
@@ -62,6 +62,8 @@ def test_schema(configuration: Configuration) -> None:
assert schema.pop("email")
assert schema.pop("github")
assert schema.pop("html")
+ assert schema.pop("keyring")
+ assert schema.pop("keyring_generator")
assert schema.pop("mirrorlist")
assert schema.pop("mirrorlist_generator")
assert schema.pop("report")
diff --git a/tests/ahriman/application/test_ahriman.py b/tests/ahriman/application/test_ahriman.py
index a03caae8..f5af76fd 100644
--- a/tests/ahriman/application/test_ahriman.py
+++ b/tests/ahriman/application/test_ahriman.py
@@ -373,6 +373,24 @@ def test_subparsers_repo_check_option_refresh(parser: argparse.ArgumentParser) -
assert args.refresh == 2
+def test_subparsers_repo_create_keyring(parser: argparse.ArgumentParser) -> None:
+ """
+ repo-create-keyring command must imply trigger
+ """
+ args = parser.parse_args(["repo-create-keyring"])
+ assert args.trigger == ["ahriman.core.support.KeyringTrigger"]
+
+
+def test_subparsers_repo_create_keyring_architecture(parser: argparse.ArgumentParser) -> None:
+ """
+ repo-create-keyring command must correctly parse architecture list
+ """
+ args = parser.parse_args(["repo-create-keyring"])
+ assert args.architecture is None
+ args = parser.parse_args(["-a", "x86_64", "repo-create-keyring"])
+ assert args.architecture == ["x86_64"]
+
+
def test_subparsers_repo_create_mirrorlist(parser: argparse.ArgumentParser) -> None:
"""
repo-create-mirrorlist command must imply trigger
diff --git a/tests/ahriman/core/conftest.py b/tests/ahriman/core/conftest.py
index 515b8c16..53cfcd1d 100644
--- a/tests/ahriman/core/conftest.py
+++ b/tests/ahriman/core/conftest.py
@@ -4,6 +4,7 @@ import pytest
from ahriman.core.alpm.repo import Repo
from ahriman.core.build_tools.task import Task
from ahriman.core.configuration import Configuration
+from ahriman.core.sign.gpg import GPG
from ahriman.core.tree import Leaf
from ahriman.models.package import Package
from ahriman.models.repository_paths import RepositoryPaths
@@ -63,6 +64,20 @@ def repo(configuration: Configuration, repository_paths: RepositoryPaths) -> Rep
return Repo(configuration.get("repository", "name"), repository_paths, [])
+@pytest.fixture
+def gpg(configuration: Configuration) -> GPG:
+ """
+ fixture for empty GPG
+
+ Args:
+ configuration(Configuration): configuration fixture
+
+ Returns:
+ GPG: GPG test instance
+ """
+ return GPG(configuration)
+
+
@pytest.fixture
def task_ahriman(package_ahriman: Package, configuration: Configuration, repository_paths: RepositoryPaths) -> Task:
"""
diff --git a/tests/ahriman/core/report/test_html.py b/tests/ahriman/core/report/test_html.py
index 1c4fc996..fa790b6c 100644
--- a/tests/ahriman/core/report/test_html.py
+++ b/tests/ahriman/core/report/test_html.py
@@ -16,4 +16,4 @@ def test_generate(configuration: Configuration, package_ahriman: Package, mocker
report = HTML("x86_64", configuration, "html")
report.generate([package_ahriman], Result())
- write_mock.assert_called_once_with(pytest.helpers.anyvar(int))
+ write_mock.assert_called_once_with(pytest.helpers.anyvar(int), encoding="utf8")
diff --git a/tests/ahriman/core/sign/conftest.py b/tests/ahriman/core/sign/conftest.py
index 6351b062..e5bc6e7e 100644
--- a/tests/ahriman/core/sign/conftest.py
+++ b/tests/ahriman/core/sign/conftest.py
@@ -1,23 +1,8 @@
import pytest
-from ahriman.core.configuration import Configuration
from ahriman.core.sign.gpg import GPG
-@pytest.fixture
-def gpg(configuration: Configuration) -> GPG:
- """
- fixture for empty GPG
-
- Args:
- configuration(Configuration): configuration fixture
-
- Returns:
- GPG: GPG test instance
- """
- return GPG("x86_64", configuration)
-
-
@pytest.fixture
def gpg_with_key(gpg: GPG) -> GPG:
"""
diff --git a/tests/ahriman/core/sign/test_gpg.py b/tests/ahriman/core/sign/test_gpg.py
index 9a36755a..e5427ffd 100644
--- a/tests/ahriman/core/sign/test_gpg.py
+++ b/tests/ahriman/core/sign/test_gpg.py
@@ -97,6 +97,33 @@ def test_key_download_failure(gpg: GPG, mocker: MockerFixture) -> None:
gpg.key_download("keyserver.ubuntu.com", "0xE989490C")
+def test_key_export(gpg: GPG, mocker: MockerFixture) -> None:
+ """
+ must export gpg key correctly
+ """
+ check_output_mock = mocker.patch("ahriman.core.sign.gpg.GPG._check_output", return_value="key")
+ assert gpg.key_export("k") == "key"
+ check_output_mock.assert_called_once_with("gpg", "--armor", "--no-emit-version", "--export", "k",
+ logger=pytest.helpers.anyvar(int))
+
+
+def test_key_fingerprint(gpg: GPG, mocker: MockerFixture) -> None:
+ """
+ must extract fingerprint
+ """
+ check_output_mock = mocker.patch(
+ "ahriman.core.sign.gpg.GPG._check_output",
+ return_value="""tru::1:1576103830:0:3:1:5
+fpr:::::::::C6EBB9222C3C8078631A0DE4BD2AC8C5E989490C:
+sub:-:4096:1:7E3A4240CE3C45C2:1615121387::::::e::::::23:
+fpr:::::::::43A663569A07EE1E4ECC55CC7E3A4240CE3C45C2:""")
+
+ key = "0xCE3C45C2"
+ assert gpg.key_fingerprint(key) == "C6EBB9222C3C8078631A0DE4BD2AC8C5E989490C"
+ check_output_mock.assert_called_once_with("gpg", "--with-colons", "--fingerprint", key,
+ logger=pytest.helpers.anyvar(int))
+
+
def test_key_import(gpg: GPG, mocker: MockerFixture) -> None:
"""
must import PGP key from the server
@@ -108,6 +135,21 @@ def test_key_import(gpg: GPG, mocker: MockerFixture) -> None:
check_output_mock.assert_called_once_with("gpg", "--import", input_data="key", logger=pytest.helpers.anyvar(int))
+def test_keys(gpg: GPG) -> None:
+ """
+ must extract keys
+ """
+ assert gpg.keys() == []
+
+ gpg.default_key = "key"
+ assert gpg.keys() == [gpg.default_key]
+
+ gpg.configuration.set_option("sign", "key_a", "key1")
+ gpg.configuration.set_option("sign", "key_b", "key1")
+ gpg.configuration.set_option("sign", "key_c", "key2")
+ assert gpg.keys() == ["key", "key1", "key2"]
+
+
def test_process(gpg_with_key: GPG, mocker: MockerFixture) -> None:
"""
must call process method correctly
diff --git a/tests/ahriman/core/support/pkgbuild/conftest.py b/tests/ahriman/core/support/pkgbuild/conftest.py
index f7e87b07..af061f1d 100644
--- a/tests/ahriman/core/support/pkgbuild/conftest.py
+++ b/tests/ahriman/core/support/pkgbuild/conftest.py
@@ -1,8 +1,26 @@
import pytest
+from ahriman.core.configuration import Configuration
+from ahriman.core.sign.gpg import GPG
+from ahriman.core.support.pkgbuild.keyring_generator import KeyringGenerator
from ahriman.core.support.pkgbuild.pkgbuild_generator import PkgbuildGenerator
+@pytest.fixture
+def keyring_generator(gpg: GPG, configuration: Configuration) -> KeyringGenerator:
+ """
+ fixture for keyring pkgbuild generator
+
+ Args:
+ gpg(GPG): empty GPG fixture
+ configuration(Configuration): configuration fixture
+
+ Returns:
+ KeyringGenerator: keyring generator test instance
+ """
+ return KeyringGenerator(gpg, configuration, "keyring")
+
+
@pytest.fixture
def pkgbuild_generator() -> PkgbuildGenerator:
"""
diff --git a/tests/ahriman/core/support/pkgbuild/test_keyring_generator.py b/tests/ahriman/core/support/pkgbuild/test_keyring_generator.py
new file mode 100644
index 00000000..5ae8b70d
--- /dev/null
+++ b/tests/ahriman/core/support/pkgbuild/test_keyring_generator.py
@@ -0,0 +1,185 @@
+import pytest
+
+from pathlib import Path
+from pytest_mock import MockerFixture
+from unittest.mock import MagicMock, call as MockCall
+
+from ahriman.core.configuration import Configuration
+from ahriman.core.exceptions import PkgbuildGeneratorError
+from ahriman.core.sign.gpg import GPG
+from ahriman.core.support.pkgbuild.keyring_generator import KeyringGenerator
+
+
+def test_init_packagers(gpg: GPG, configuration: Configuration, mocker: MockerFixture) -> None:
+ """
+ must extract packagers keys
+ """
+ mocker.patch("ahriman.core.sign.gpg.GPG.keys", return_value=["key"])
+
+ assert KeyringGenerator(gpg, configuration, "keyring").packagers == ["key"]
+
+ configuration.set_option("keyring", "packagers", "key1")
+ assert KeyringGenerator(gpg, configuration, "keyring").packagers == ["key1"]
+
+
+def test_init_revoked(gpg: GPG, configuration: Configuration) -> None:
+ """
+ must extract revoked keys
+ """
+ assert KeyringGenerator(gpg, configuration, "keyring").revoked == []
+
+ configuration.set_option("keyring", "revoked", "key1")
+ assert KeyringGenerator(gpg, configuration, "keyring").revoked == ["key1"]
+
+
+def test_init_trusted(gpg: GPG, configuration: Configuration) -> None:
+ """
+ must extract trusted keys
+ """
+ assert KeyringGenerator(gpg, configuration, "keyring").trusted == []
+
+ gpg.default_key = "key"
+ assert KeyringGenerator(gpg, configuration, "keyring").trusted == ["key"]
+
+ configuration.set_option("keyring", "trusted", "key1")
+ assert KeyringGenerator(gpg, configuration, "keyring").trusted == ["key1"]
+
+
+def test_license(gpg: GPG, configuration: Configuration) -> None:
+ """
+ must generate correct licenses list
+ """
+ assert KeyringGenerator(gpg, configuration, "keyring").license == ["Unlicense"]
+
+ configuration.set_option("keyring", "license", "GPL MPL")
+ assert KeyringGenerator(gpg, configuration, "keyring").license == ["GPL", "MPL"]
+
+
+def test_pkgdesc(gpg: GPG, configuration: Configuration) -> None:
+ """
+ must generate correct pkgdesc property
+ """
+ assert KeyringGenerator(gpg, configuration, "keyring").pkgdesc == "aur-clone PGP keyring"
+
+ configuration.set_option("keyring", "description", "description")
+ assert KeyringGenerator(gpg, configuration, "keyring").pkgdesc == "description"
+
+
+def test_pkgname(gpg: GPG, configuration: Configuration) -> None:
+ """
+ must generate correct pkgname property
+ """
+ assert KeyringGenerator(gpg, configuration, "keyring").pkgname == "aur-clone-keyring"
+
+ configuration.set_option("keyring", "package", "keyring")
+ assert KeyringGenerator(gpg, configuration, "keyring").pkgname == "keyring"
+
+
+def test_url(gpg: GPG, configuration: Configuration) -> None:
+ """
+ must generate correct url property
+ """
+ assert KeyringGenerator(gpg, configuration, "keyring").url == ""
+
+ configuration.set_option("keyring", "homepage", "homepage")
+ assert KeyringGenerator(gpg, configuration, "keyring").url == "homepage"
+
+
+def test_generate_gpg(keyring_generator: KeyringGenerator, mocker: MockerFixture) -> None:
+ """
+ must correctly generate file with all PGP keys
+ """
+ file_mock = MagicMock()
+ export_mock = mocker.patch("ahriman.core.sign.gpg.GPG.key_export", side_effect=lambda key: key)
+ open_mock = mocker.patch("pathlib.Path.open")
+ open_mock.return_value.__enter__.return_value = file_mock
+ keyring_generator.packagers = ["key"]
+ keyring_generator.revoked = ["revoked"]
+ keyring_generator.trusted = ["trusted", "key"]
+
+ keyring_generator._generate_gpg(Path("local"))
+ open_mock.assert_called_once_with("w")
+ export_mock.assert_has_calls([MockCall("key"), MockCall("revoked"), MockCall("trusted")])
+ file_mock.write.assert_has_calls([
+ MockCall("key"), MockCall("\n"),
+ MockCall("revoked"), MockCall("\n"),
+ MockCall("trusted"), MockCall("\n"),
+ ])
+
+
+def test_generate_revoked(keyring_generator: KeyringGenerator, mocker: MockerFixture) -> None:
+ """
+ must correctly generate file with revoked keys
+ """
+ file_mock = MagicMock()
+ fingerprint_mock = mocker.patch("ahriman.core.sign.gpg.GPG.key_fingerprint", side_effect=lambda key: key)
+ open_mock = mocker.patch("pathlib.Path.open")
+ open_mock.return_value.__enter__.return_value = file_mock
+ keyring_generator.revoked = ["revoked"]
+
+ keyring_generator._generate_revoked(Path("local"))
+ open_mock.assert_called_once_with("w")
+ fingerprint_mock.assert_called_once_with("revoked")
+ file_mock.write.assert_has_calls([MockCall("revoked"), MockCall("\n")])
+
+
+def test_generate_trusted(keyring_generator: KeyringGenerator, mocker: MockerFixture) -> None:
+ """
+ must correctly generate file with trusted keys
+ """
+ file_mock = MagicMock()
+ fingerprint_mock = mocker.patch("ahriman.core.sign.gpg.GPG.key_fingerprint", side_effect=lambda key: key)
+ open_mock = mocker.patch("pathlib.Path.open")
+ open_mock.return_value.__enter__.return_value = file_mock
+ keyring_generator.trusted = ["trusted", "trusted"]
+
+ keyring_generator._generate_trusted(Path("local"))
+ open_mock.assert_called_once_with("w")
+ fingerprint_mock.assert_called_once_with("trusted")
+ file_mock.write.assert_has_calls([MockCall("trusted"), MockCall(":4:\n")])
+
+
+def test_generate_trusted_empty(keyring_generator: KeyringGenerator) -> None:
+ """
+ must raise PkgbuildGeneratorError if no trusted keys set
+ """
+ with pytest.raises(PkgbuildGeneratorError):
+ keyring_generator._generate_trusted(Path("local"))
+
+
+def test_install(keyring_generator: KeyringGenerator) -> None:
+ """
+ must return install functions
+ """
+ assert keyring_generator.install() == """post_upgrade() {
+ if usr/bin/pacman-key -l >/dev/null 2>&1; then
+ usr/bin/pacman-key --populate aur-clone
+ usr/bin/pacman-key --updatedb
+ fi
+}
+
+post_install() {
+ if [ -x usr/bin/pacman-key ]; then
+ post_upgrade
+ fi
+}"""
+
+
+def test_package(keyring_generator: KeyringGenerator) -> None:
+ """
+ must generate package function correctly
+ """
+ assert keyring_generator.package() == """{
+ install -Dm644 "$srcdir/aur-clone.gpg" "$pkgdir/usr/share/pacman/keyrings/aur-clone.gpg"
+ install -Dm644 "$srcdir/aur-clone-revoked" "$pkgdir/usr/share/pacman/keyrings/aur-clone-revoked"
+ install -Dm644 "$srcdir/aur-clone-trusted" "$pkgdir/usr/share/pacman/keyrings/aur-clone-trusted"
+}"""
+
+
+def test_sources(keyring_generator: KeyringGenerator) -> None:
+ """
+ must return valid sources files list
+ """
+ assert keyring_generator.sources().get("aur-clone.gpg")
+ assert keyring_generator.sources().get("aur-clone-revoked")
+ assert keyring_generator.sources().get("aur-clone-trusted")
diff --git a/tests/ahriman/core/support/pkgbuild/test_mirrorlist_generator.py b/tests/ahriman/core/support/pkgbuild/test_mirrorlist_generator.py
index 59e764bd..6e5c3553 100644
--- a/tests/ahriman/core/support/pkgbuild/test_mirrorlist_generator.py
+++ b/tests/ahriman/core/support/pkgbuild/test_mirrorlist_generator.py
@@ -1,6 +1,4 @@
from pathlib import Path
-from unittest.mock import MagicMock
-
from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration
@@ -61,14 +59,9 @@ def test_generate_mirrorlist(mirrorlist_generator: MirrorlistGenerator, mocker:
"""
must correctly generate mirrorlist file
"""
- path = Path("local")
- file_mock = MagicMock()
- open_mock = mocker.patch("pathlib.Path.open")
- open_mock.return_value.__enter__.return_value = file_mock
-
- mirrorlist_generator._generate_mirrorlist(path)
- open_mock.assert_called_once_with("w")
- file_mock.writelines.assert_called_once_with(["Server = http://localhost\n"])
+ write_mock = mocker.patch("pathlib.Path.write_text")
+ mirrorlist_generator._generate_mirrorlist(Path("local"))
+ write_mock.assert_called_once_with("Server = http://localhost\n", encoding="utf8")
def test_package(mirrorlist_generator: MirrorlistGenerator) -> None:
diff --git a/tests/ahriman/core/support/pkgbuild/test_pkgbuild_generator.py b/tests/ahriman/core/support/pkgbuild/test_pkgbuild_generator.py
index 77e38a90..d4857c71 100644
--- a/tests/ahriman/core/support/pkgbuild/test_pkgbuild_generator.py
+++ b/tests/ahriman/core/support/pkgbuild/test_pkgbuild_generator.py
@@ -48,6 +48,13 @@ def test_url(pkgbuild_generator: PkgbuildGenerator) -> None:
assert pkgbuild_generator.url == ""
+def test_install(pkgbuild_generator: PkgbuildGenerator) -> None:
+ """
+ must return empty install function
+ """
+ assert pkgbuild_generator.install() is None
+
+
def test_package(pkgbuild_generator: PkgbuildGenerator) -> None:
"""
must raise NotImplementedError on missing package function
@@ -70,6 +77,25 @@ def test_sources(pkgbuild_generator: PkgbuildGenerator) -> None:
assert pkgbuild_generator.sources() == {}
+def test_write_install(pkgbuild_generator: PkgbuildGenerator, mocker: MockerFixture) -> None:
+ """
+ must write install file
+ """
+ mocker.patch.object(PkgbuildGenerator, "pkgname", "package")
+ mocker.patch("ahriman.core.support.pkgbuild.pkgbuild_generator.PkgbuildGenerator.install", return_value="content")
+ write_mock = mocker.patch("pathlib.Path.write_text")
+
+ assert pkgbuild_generator.write_install(Path("local")) == [PkgbuildPatch("install", "package.install")]
+ write_mock.assert_called_once_with("content")
+
+
+def test_write_install_empty(pkgbuild_generator: PkgbuildGenerator) -> None:
+ """
+ must return empty patch list for missing install function
+ """
+ assert pkgbuild_generator.write_install(Path("local")) == []
+
+
def test_write_pkgbuild(pkgbuild_generator: PkgbuildGenerator, mocker: MockerFixture) -> None:
"""
must write PKGBUILD content to file
@@ -80,14 +106,17 @@ def test_write_pkgbuild(pkgbuild_generator: PkgbuildGenerator, mocker: MockerFix
mocker.patch("ahriman.core.support.pkgbuild.pkgbuild_generator.PkgbuildGenerator.package", return_value="{}")
patches_mock = mocker.patch("ahriman.core.support.pkgbuild.pkgbuild_generator.PkgbuildGenerator.patches",
return_value=[PkgbuildPatch("property", "value")])
+ install_mock = mocker.patch("ahriman.core.support.pkgbuild.pkgbuild_generator.PkgbuildGenerator.write_install",
+ return_value=[PkgbuildPatch("install", "pkgname.install")])
sources_mock = mocker.patch("ahriman.core.support.pkgbuild.pkgbuild_generator.PkgbuildGenerator.write_sources",
return_value=[PkgbuildPatch("source", []), PkgbuildPatch("sha512sums", [])])
write_mock = mocker.patch("ahriman.models.pkgbuild_patch.PkgbuildPatch.write")
pkgbuild_generator.write_pkgbuild(path)
patches_mock.assert_called_once_with()
+ install_mock.assert_called_once_with(path)
sources_mock.assert_called_once_with(path)
- write_mock.assert_has_calls([MockCall(path / "PKGBUILD")] * 11)
+ write_mock.assert_has_calls([MockCall(path / "PKGBUILD")] * 12)
def test_write_sources(pkgbuild_generator: PkgbuildGenerator, mocker: MockerFixture) -> None:
diff --git a/tests/ahriman/core/support/test_keyring_trigger.py b/tests/ahriman/core/support/test_keyring_trigger.py
new file mode 100644
index 00000000..8c82703f
--- /dev/null
+++ b/tests/ahriman/core/support/test_keyring_trigger.py
@@ -0,0 +1,30 @@
+from pytest_mock import MockerFixture
+
+from ahriman.core.configuration import Configuration
+from ahriman.core.sign.gpg import GPG
+from ahriman.core.support import KeyringTrigger
+from ahriman.models.context_key import ContextKey
+
+
+def test_configuration_sections(configuration: Configuration) -> None:
+ """
+ must correctly parse target list
+ """
+ configuration.set_option("keyring", "target", "a b c")
+ assert KeyringTrigger.configuration_sections(configuration) == ["a", "b", "c"]
+
+ configuration.remove_option("keyring", "target")
+ assert KeyringTrigger.configuration_sections(configuration) == []
+
+
+def test_on_start(configuration: Configuration, mocker: MockerFixture) -> None:
+ """
+ must run report for specified targets
+ """
+ gpg_mock = mocker.patch("ahriman.core._Context.get")
+ run_mock = mocker.patch("ahriman.core.support.package_creator.PackageCreator.run")
+
+ trigger = KeyringTrigger("x86_64", configuration)
+ trigger.on_start()
+ gpg_mock.assert_called_once_with(ContextKey("sign", GPG))
+ run_mock.assert_called_once_with()
diff --git a/tests/ahriman/core/support/test_mirrorlist_trigger.py b/tests/ahriman/core/support/test_mirrorlist_trigger.py
index 25abbcb6..e5e79013 100644
--- a/tests/ahriman/core/support/test_mirrorlist_trigger.py
+++ b/tests/ahriman/core/support/test_mirrorlist_trigger.py
@@ -19,7 +19,6 @@ def test_on_start(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run report for specified targets
"""
- configuration.set_option("mirrorlist", "target", "mirrorlist")
run_mock = mocker.patch("ahriman.core.support.package_creator.PackageCreator.run")
trigger = MirrorlistTrigger("x86_64", configuration)
diff --git a/tests/testresources/core/ahriman.ini b/tests/testresources/core/ahriman.ini
index ff7da9fb..ad35c8aa 100644
--- a/tests/testresources/core/ahriman.ini
+++ b/tests/testresources/core/ahriman.ini
@@ -25,7 +25,7 @@ ignore_packages =
makechrootpkg_flags =
makepkg_flags = --skippgpcheck
triggers = ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger
-triggers_known = ahriman.core.support.MirrorlistTrigger
+triggers_known = ahriman.core.support.KeyringTrigger ahriman.core.support.MirrorlistTrigger
[repository]
name = aur-clone
@@ -34,6 +34,9 @@ root = ../../../
[sign]
target =
+[keyring]
+target = keyring
+
[mirrorlist]
target = mirrorlist
servers = http://localhost