triggers implementation (#62)

This commit is contained in:
Evgenii Alekseev 2022-05-09 17:45:39 +03:00 committed by Evgeniy Alekseev
parent d98cfa3732
commit 99874845b5
50 changed files with 3859 additions and 3222 deletions

View File

@ -14,7 +14,7 @@ Wrapper for managing custom repository inspired by [repo-scripts](https://github
* Multi-architecture support.
* VCS packages support.
* Sign support with gpg (repository, package, per package settings).
* Synchronization to remote services (rsync, s3 and github) and report generation (email, html, telegram).
* Synchronization to remote services (rsync, s3 and github) and report generation (email, html, telegram) and even ability to write own extensions.
* Dependency manager.
* Ability to patch AUR packages and even create package from local PKGBUILDs.
* Repository status interface with optional authorization and control options:

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 503 KiB

After

Width:  |  Height:  |  Size: 532 KiB

View File

@ -3,7 +3,7 @@
ahriman
.SH SYNOPSIS
.B ahriman
[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--no-report] [-q] [--unsafe] [-v] {aur-search,search,help,help-commands-unsafe,key-import,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,repo-backup,repo-check,check,repo-clean,clean,repo-config,config,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-setup,init,repo-init,setup,repo-sign,sign,repo-status-update,repo-sync,sync,repo-update,update,user-add,user-list,user-remove,web} ...
[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--no-report] [-q] [--unsafe] [-v] {aur-search,search,help,help-commands-unsafe,key-import,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,repo-backup,repo-check,check,repo-clean,clean,repo-config,config,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-restore,repo-setup,init,repo-init,setup,repo-sign,sign,repo-status-update,repo-triggers,repo-update,update,user-add,user-list,user-remove,web} ...
.SH DESCRIPTION
ArcH Linux ReposItory MANager
@ -97,9 +97,6 @@ rebuild repository
\fBahriman\fR \fI\,repo-remove-unknown\/\fR
remove unknown packages
.TP
\fBahriman\fR \fI\,repo-report\/\fR
generate report
.TP
\fBahriman\fR \fI\,repo-restore\/\fR
restore repository data
.TP
@ -112,8 +109,8 @@ sign packages
\fBahriman\fR \fI\,repo-status-update\/\fR
update repository status
.TP
\fBahriman\fR \fI\,repo-sync\/\fR
sync repository
\fBahriman\fR \fI\,repo-triggers\/\fR
run triggers
.TP
\fBahriman\fR \fI\,repo-update\/\fR
update packages
@ -408,15 +405,6 @@ just perform check for packages without removal
\fB\-i\fR, \fB\-\-info\fR
show additional package information
.SH COMMAND \fI\,'ahriman repo-report'\/\fR
usage: ahriman repo-report [-h] [target ...]
generate repository report according to current settings
.TP
\fBtarget\fR
target to generate report
.SH COMMAND \fI\,'ahriman repo-restore'\/\fR
usage: ahriman repo-restore [-h] [-o OUTPUT] path
@ -497,14 +485,10 @@ update repository status on the status page
\fB\-s\fR \fI\,{BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}\/\fR, \fB\-\-status\fR \fI\,{BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}\/\fR
new status
.SH COMMAND \fI\,'ahriman repo-sync'\/\fR
usage: ahriman repo-sync [-h] [target ...]
.SH COMMAND \fI\,'ahriman repo-triggers'\/\fR
usage: ahriman repo-triggers [-h]
sync repository files to remote server according to current settings
.TP
\fBtarget\fR
target to sync
run triggers on empty build result as configured by settings
.SH COMMAND \fI\,'ahriman repo-update'\/\fR
usage: ahriman repo-update [-h] [--dry-run] [-e] [--no-aur] [--no-local] [--no-manual] [--no-vcs] [package ...]

View File

@ -92,14 +92,6 @@ ahriman.application.handlers.remove\_unknown module
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.report module
------------------------------------------
.. automodule:: ahriman.application.handlers.report
:members:
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.restore module
-------------------------------------------
@ -148,10 +140,10 @@ ahriman.application.handlers.status\_update module
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.sync module
----------------------------------------
ahriman.application.handlers.triggers module
--------------------------------------------
.. automodule:: ahriman.application.handlers.sync
.. automodule:: ahriman.application.handlers.triggers
:members:
:no-undoc-members:
:show-inheritance:

View File

@ -44,6 +44,14 @@ ahriman.core.report.report module
:no-undoc-members:
:show-inheritance:
ahriman.core.report.report\_trigger module
------------------------------------------
.. automodule:: ahriman.core.report.report_trigger
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.report.telegram module
-----------------------------------

View File

@ -16,6 +16,7 @@ Subpackages
ahriman.core.repository
ahriman.core.sign
ahriman.core.status
ahriman.core.triggers
ahriman.core.upload
Submodules

View File

@ -0,0 +1,29 @@
ahriman.core.triggers package
=============================
Submodules
----------
ahriman.core.triggers.trigger module
------------------------------------
.. automodule:: ahriman.core.triggers.trigger
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.triggers.trigger\_loader module
--------------------------------------------
.. automodule:: ahriman.core.triggers.trigger_loader
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: ahriman.core.triggers
:members:
:no-undoc-members:
:show-inheritance:

View File

@ -44,6 +44,14 @@ ahriman.core.upload.upload module
:no-undoc-members:
:show-inheritance:
ahriman.core.upload.upload\_trigger module
------------------------------------------
.. automodule:: ahriman.core.upload.upload_trigger
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------

View File

@ -7,7 +7,7 @@ Package structure
Packages have strict rules of importing:
* ``ahriman.application`` package must not be used anywhere except for itself.
* ``ahriman.core`` and ``ahriman.models`` packages don't have any import restriction. Actually we would like to totally restrict importing of ``core`` package from ``models``\ , but it is impossible at the moment.
* ``ahriman.core`` and ``ahriman.models`` packages don't have any import restriction. Actually we would like to totally restrict importing of ``core`` package from ``models``, but it is impossible at the moment.
* ``ahriman.web`` package is allowed to be imported from ``ahriman.application`` (web handler only, only ``ahriman.web.web`` methods). It also must not be imported globally, only local import is allowed.
Full dependency diagram:
@ -30,13 +30,13 @@ This package contains application (aka executable) related classes and everythin
This package contains everything which is required for any time of application run and separated to several packages:
* ``ahriman.core.alpm`` package controls pacman related functions. It provides wrappers for ``pyalpm`` library and safe calls for repository tools (\ ``repo-add`` and ``repo-remove``\ ). Also this package contains ``ahriman.core.alpm.remote`` package which provides wrapper for remote sources (e.g. AUR RPC and official repositories RPC).
* ``ahriman.core.alpm`` package controls pacman related functions. It provides wrappers for ``pyalpm`` library and safe calls for repository tools (``repo-add`` and ``repo-remove``). Also this package contains ``ahriman.core.alpm.remote`` package which provides wrapper for remote sources (e.g. AUR RPC and official repositories RPC).
* ``ahriman.core.auth`` package provides classes for authorization methods used by web mostly. Base class is ``ahriman.core.auth.auth.Auth`` which must be called by ``load`` method.
* ``ahriman.core.build_tools`` is a package which provides wrapper for ``devtools`` commands.
* ``ahriman.core.database`` is everything including data and schema migrations for database.
* ``ahriman.core.formatters`` package provides ``Printer`` sub-classes for printing data (e.g. package properties) to stdout which are used by some handlers.
* ``ahriman.core.report`` is a package with reporting classes. Usually it must be called by ``ahriman.core.report.report.Report.load`` method.
* ``ahriman.core.repository`` contains several traits and base repository (\ ``ahriman.core.repository.repository.Repository`` class) implementation.
* ``ahriman.core.repository`` contains several traits and base repository (``ahriman.core.repository.repository.Repository`` class) implementation.
* ``ahriman.core.sign`` package provides sign feature (only gpg calls are available).
* ``ahriman.core.status`` contains helpers and watcher class which are required for web application. Reporter must be initialized by using ``ahriman.core.status.client.Client.load`` method.
* ``ahriman.core.upload`` package provides sync feature, must be called by ``ahriman.core.upload.upload.Upload.load`` method.
@ -68,7 +68,7 @@ Application run
* Parse command line arguments, find command and related handler which is set by parser.
* Call ``Handler.execute`` method.
* Define list of architectures to run. In case if there is more than one architecture specified run several subprocesses or process in current process otherwise. Class attribute ``ALLOW_MULTI_ARCHITECTURE_RUN`` controls whether application can be run in multiple processes or not - this feature is required for some handlers (e.g. ``Web``\ ) which should be able to spawn child process in daemon mode (it is impossible to do for daemonic processes).
* Define list of architectures to run. In case if there is more than one architecture specified run several subprocesses or process in current process otherwise. Class attribute ``ALLOW_MULTI_ARCHITECTURE_RUN`` controls whether application can be run in multiple processes or not - this feature is required for some handlers (e.g. ``Web``) which should be able to spawn child process in daemon mode (it is impossible to do for daemonic processes).
* In each child process call lock functions.
* After success checks pass control to ``Handler.run`` method defined by specific handler class.
* Return result (success or failure) of each subprocess and exit from application.
@ -102,7 +102,7 @@ Type conversions
By default, it parses rows into python dictionary. In addition, the following pseudo-types are supported:
* ``Dict[str, Any]``\ , ``List[Any]`` - for storing JSON data structures in database (technically there is no restriction on types for dictionary keys and values, but it is recommended to use only string keys). The type is stored as ``json`` datatype and ``json.loads`` and ``json.dumps`` methods are used in order to read and write from/to database respectively.
* ``Dict[str, Any]``, ``List[Any]`` - for storing JSON data structures in database (technically there is no restriction on types for dictionary keys and values, but it is recommended to use only string keys). The type is stored as ``json`` datatype and ``json.loads`` and ``json.dumps`` methods are used in order to read and write from/to database respectively.
Basic flows
-----------
@ -113,7 +113,7 @@ Add new packages or rebuild existing
Idea is to copy package to the directory from which it will be handled at the next update run. Different variants are supported:
* If supplied argument is file then application moves the file to the directory with built packages. Same rule applies for directory, but in this case it copies every package-like file from the specified directory.
* If supplied argument is directory and there is ``PKGBUILD`` file there it will be treated as local package. In this case it will queue this package to build and copy source files (\ ``PKGBUILD`` and ``.SRCINFO``\ ) to caches.
* If supplied argument is directory and there is ``PKGBUILD`` file there it will be treated as local package. In this case it will queue this package to build and copy source files (``PKGBUILD`` and ``.SRCINFO``) to caches.
* If supplied argument iis not file then application tries to lookup for the specified name in AUR and clones it into the directory with manual updates. This scenario can also handle package dependencies which are missing in repositories.
This logic can be overwritten by specifying the ``source`` parameter, which is partially useful if you would like to add package from AUR, but there is local directory cloned from AUR.
@ -142,7 +142,7 @@ This feature is divided into to stages: check AUR for updates and run rebuild fo
#. Build every package in clean chroot.
#. Sign packages if required.
#. Add packages to database and sign database if required.
#. Process sync and report methods.
#. Process triggers.
After any step any package data is being removed.
@ -152,7 +152,7 @@ Core functions reference
Configuration
^^^^^^^^^^^^^
``ahriman.core.configuration.Configuration`` class provides some additional methods (e.g. ``getpath`` and ``getlist``\ ) and also combines multiple files into single configuration dictionary using architecture overrides. It is the recommended way to deal with settings.
``ahriman.core.configuration.Configuration`` class provides some additional methods (e.g. ``getpath`` and ``getlist``) and also combines multiple files into single configuration dictionary using architecture overrides. It is the recommended way to deal with settings.
Utils
^^^^^
@ -176,7 +176,7 @@ Mapping (aka configuration) provider uses hashed passwords with salt from the da
* ``check_credentials`` - user password validation (authentication).
* ``verify_access`` - user permission validation (authorization).
Passwords must be stored in database as ``hash(password + salt)``\ , where ``password`` is user defined password (taken from user input), ``salt`` is random string (any length) defined globally in configuration and ``hash`` is secure hash function. Thus, the following configuration
Passwords must be stored in database as ``hash(password + salt)``, where ``password`` is user defined password (taken from user input), ``salt`` is random string (any length) defined globally in configuration and ``hash`` is secure hash function. Thus, the following configuration
.. code-block::
@ -185,20 +185,31 @@ Passwords must be stored in database as ``hash(password + salt)``\ , where ``pas
means that there is user ``username`` with ``read`` access and password ``password`` hashed by ``sha512`` with salt ``salt``.
OAuth provider uses library definitions (\ ``aioauth-client``\ ) in order *authenticate* users. It still requires user permission to be set in database, thus it inherits mapping provider without any changes. Whereas we could override ``check_credentials`` (authentication method) by something custom, OAuth flow is a bit more complex than just forward request, thus we have to implement the flow in login form.
OAuth provider uses library definitions (``aioauth-client``) in order *authenticate* users. It still requires user permission to be set in database, thus it inherits mapping provider without any changes. Whereas we could override ``check_credentials`` (authentication method) by something custom, OAuth flow is a bit more complex than just forward request, thus we have to implement the flow in login form.
OAuth's implementation also allows authenticating users via username + password (in the same way as mapping does) though it is not recommended for end-users and password must be left blank. In particular this feature is used by service reporting (aka robots).
In order to configure users there are special commands.
Triggers
^^^^^^^^
Triggers are extensions which can be used in order to perform any actions after the update process. The package provides two default extensions - one is report generation and another one is remote upload feature.
The main idea is to load classes by their full path (e.g. ``ahriman.core.upload.UploadTrigger``) by using ``importlib``: get the last part of the import and treat it as class name, join remain part by ``.`` and interpret as module path, import module and extract attribute from it.
The loaded triggers will be called with ``ahriman.models.result.Result`` and ``List[Packages]`` arguments, which describes the process result and current repository packages respectively. Any exception raised will be suppressed and will generate an exception message in logs.
For more details how to deal with the triggers, refer to :doc:`documentation <triggers>` and modules descriptions.
Remote synchronization
^^^^^^^^^^^^^^^^^^^^^^
There are several supported synchronization providers, currently they are ``rsync``\ , ``s3``\ , ``github``.
There are several supported synchronization providers, currently they are ``rsync``, ``s3``, ``github``.
``rsync`` provider does not have any specific logic except for running external rsync application with configured arguments. The service does not handle SSH configuration, thus it has to be configured before running application manually.
``s3`` provider uses ``boto3`` package and implements sync feature. The files are stored in architecture directory (e.g. if bucket is ``repository``\ , packages will be stored in ``repository/x86_64`` for the ``x86_64`` architecture), bucket must be created before any action and API key must have permissions to write to the bucket. No external configuration required. In order to upload only changed files the service compares calculated hashes with the Amazon ETags, used realization is described `here <https://teppen.io/2018/10/23/aws_s3_verify_etags/>`_.
``s3`` provider uses ``boto3`` package and implements sync feature. The files are stored in architecture directory (e.g. if bucket is ``repository``, packages will be stored in ``repository/x86_64`` for the ``x86_64`` architecture), bucket must be created before any action and API key must have permissions to write to the bucket. No external configuration required. In order to upload only changed files the service compares calculated hashes with the Amazon ETags, used realization is described `here <https://teppen.io/2018/10/23/aws_s3_verify_etags/>`_.
``github`` provider authenticates through basic auth, API key with repository write permissions is required. There will be created a release with the name of the architecture in case if it does not exist; files will be uploaded to the release assets. It also stores array of files and their MD5 checksums in release body in order to upload only changed ones. According to the Github API in case if there is already uploaded asset with the same name (e.g. database files), asset will be removed first.
@ -207,7 +218,7 @@ Additional features
Some features require optional dependencies to be installed:
* Version control executables (e.g. ``git``\ , ``svn``\ ) for VCS packages.
* Version control executables (e.g. ``git``, ``svn``) for VCS packages.
* ``gnupg`` application for package and repository sign feature.
* ``rsync`` application for rsync based repository sync.
* ``boto3`` python package for ``S3`` sync.
@ -220,7 +231,7 @@ Web application requires the following python packages to be installed:
* Core part requires ``aiohttp`` (application itself), ``aiohttp_jinja2`` and ``Jinja2`` (HTML generation from templates).
* In addition, ``aiohttp_debugtoolbar`` is required for debug panel. Please note that this option does not work together with authorization and basically must not be used in production.
* In addition, authorization feature requires ``aiohttp_security``\ , ``aiohttp_session`` and ``cryptography``.
* In addition, authorization feature requires ``aiohttp_security``, ``aiohttp_session`` and ``cryptography``.
* In addition to base authorization dependencies, OAuth2 also requires ``aioauth-client`` library.
Middlewares

View File

@ -1,5 +1,5 @@
Commands help
=============
Commands reference
==================
ahriman
-------

View File

@ -6,11 +6,11 @@ Some groups can be specified for each architecture separately. E.g. if there are
There are two variable types which have been added to default ones, they are paths and lists. List values will be read in the same way as shell does:
* By default, it splits value by spaces excluding empty elements.
* In case if quotation mark (\ ``"`` or ``'``\ ) will be found, any spaces inside will be ignored.
* In case if quotation mark (``"`` or ``'``) will be found, any spaces inside will be ignored.
* In order to use quotation mark inside value it is required to put it to another quotation mark, e.g. ``wor"'"d "with quote"`` will be parsed as ``["wor'd", "with quote"]`` and vice versa.
* Unclosed quotation mark is not allowed and will rise an exception.
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``.
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``.
``settings`` group
------------------
@ -35,12 +35,12 @@ libalpm and AUR related configuration.
Base authorization settings. ``OAuth`` provider requires ``aioauth-client`` library to be installed.
* ``target`` - specifies authorization provider, string, optional, default ``disabled``. Allowed values are ``disabled``\ , ``configuration``\ , ``oauth``.
* ``target`` - specifies authorization provider, string, optional, default ``disabled``. Allowed values are ``disabled``, ``configuration``, ``oauth``.
* ``client_id`` - OAuth2 application client ID, string, required in case if ``oauth`` is used.
* ``client_secret`` - OAuth2 application client secret key, string, required in case if ``oauth`` is used.
* ``max_age`` - parameter which controls both cookie expiration and token expiration inside the service, integer, optional, default is 7 days.
* ``oauth_provider`` - OAuth2 provider class name as is in ``aioauth-client`` (e.g. ``GoogleClient``\ , ``GithubClient`` etc), string, required in case if ``oauth`` is used.
* ``oauth_scopes`` - scopes list for OAuth2 provider, which will allow retrieving user email (which is used for checking user permissions), e.g. ``https://www.googleapis.com/auth/userinfo.email`` for ``GoogleClient`` or ``user:email`` for ``GithubClient``\ , space separated list of strings, required in case if ``oauth`` is used.
* ``oauth_provider`` - OAuth2 provider class name as is in ``aioauth-client`` (e.g. ``GoogleClient``, ``GithubClient`` etc), string, required in case if ``oauth`` is used.
* ``oauth_scopes`` - scopes list for OAuth2 provider, which will allow retrieving user email (which is used for checking user permissions), e.g. ``https://www.googleapis.com/auth/userinfo.email`` for ``GoogleClient`` or ``user:email`` for ``GithubClient``, space separated list of strings, required in case if ``oauth`` is used.
* ``safe_build_status`` - allow requesting status page without authorization, boolean, required.
* ``salt`` - password hash salt, string, required in case if authorization enabled (automatically generated by ``create-user`` subcommand).
@ -56,6 +56,7 @@ Build related configuration. Group name can refer to architecture, e.g. ``build:
* ``ignore_packages`` - list packages to ignore during a regular update (manual update will still work), space separated list of strings, optional.
* ``makepkg_flags`` - additional flags passed to ``makepkg`` command, space separated list of strings, optional.
* ``makechrootpkg_flags`` - additional flags passed to ``makechrootpkg`` command, space separated list of strings, optional.
* ``triggers`` - list of trigger classes (e.g. ``ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger``) which will be loaded and run at the end of processing, space separated list of strings, optional. You can also specify triggers by their paths, e.g. ``/usr/lib/python3.10/site-packages/ahriman/core/report/report.py.ReportTrigger``. Triggers are run in the order of mention.
``repository`` group
--------------------
@ -90,14 +91,14 @@ Type will be read from several ways:
``console`` type
^^^^^^^^^^^^^^^^
Section name must be either ``console`` (plus optional architecture name, e.g. ``console:x86_64``\ ) or random name with ``type`` set.
Section name must be either ``console`` (plus optional architecture name, e.g. ``console:x86_64``) or random name with ``type`` set.
* ``use_utf`` - use utf8 symbols in output if set and ascii otherwise, boolean, optional, default ``yes``.
``email`` type
^^^^^^^^^^^^^^
Section name must be either ``email`` (plus optional architecture name, e.g. ``email:x86_64``\ ) or random name with ``type`` set.
Section name must be either ``email`` (plus optional architecture name, e.g. ``email:x86_64``) or random name with ``type`` set.
* ``type`` - type of the report, string, optional, must be set to ``email`` if exists.
* ``full_template_path`` - path to Jinja2 template for full package description index, string, optional.
@ -109,14 +110,14 @@ Section name must be either ``email`` (plus optional architecture name, e.g. ``e
* ``port`` - SMTP port for sending emails, int, required.
* ``receivers`` - SMTP receiver addresses, space separated list of strings, required.
* ``sender`` - SMTP sender address, string, required.
* ``ssl`` - SSL mode for SMTP connection, one of ``ssl``\ , ``starttls``\ , ``disabled``\ , optional, default ``disabled``.
* ``ssl`` - SSL mode for SMTP connection, one of ``ssl``, ``starttls``, ``disabled``, optional, default ``disabled``.
* ``template_path`` - path to Jinja2 template, string, required.
* ``user`` - SMTP user to authenticate, string, optional.
``html`` type
^^^^^^^^^^^^^
Section name must be either ``html`` (plus optional architecture name, e.g. ``html:x86_64``\ ) or random name with ``type`` set.
Section name must be either ``html`` (plus optional architecture name, e.g. ``html:x86_64``) or random name with ``type`` set.
* ``type`` - type of the report, string, optional, must be set to ``html`` if exists.
* ``path`` - path to html report file, string, required.
@ -127,7 +128,7 @@ Section name must be either ``html`` (plus optional architecture name, e.g. ``ht
``telegram`` type
^^^^^^^^^^^^^^^^^
Section name must be either ``telegram`` (plus optional architecture name, e.g. ``telegram:x86_64``\ ) or random name with ``type`` set.
Section name must be either ``telegram`` (plus optional architecture name, e.g. ``telegram:x86_64``) or random name with ``type`` set.
* ``type`` - type of the report, string, optional, must be set to ``telegram`` if exists.
* ``api_key`` - telegram bot API key, string, required. Please refer FAQ about how to create chat and bot
@ -135,7 +136,7 @@ Section name must be either ``telegram`` (plus optional architecture name, e.g.
* ``homepage`` - link to homepage, string, optional.
* ``link_path`` - prefix for HTML links, string, required.
* ``template_path`` - path to Jinja2 template, string, required.
* ``template_type`` - ``parse_mode`` to be passed to telegram API, one of ``MarkdownV2``\ , ``HTML``\ , ``Markdown``\ , string, optional, default ``HTML``.
* ``template_type`` - ``parse_mode`` to be passed to telegram API, one of ``MarkdownV2``, ``HTML``, ``Markdown``, string, optional, default ``HTML``.
``upload`` group
----------------
@ -153,7 +154,7 @@ Type will be read from several ways:
``github`` type
^^^^^^^^^^^^^^^
This feature requires Github key creation (see below). Section name must be either ``github`` (plus optional architecture name, e.g. ``github:x86_64``\ ) or random name with ``type`` set.
This feature requires Github key creation (see below). Section name must be either ``github`` (plus optional architecture name, e.g. ``github:x86_64``) or random name with ``type`` set.
* ``type`` - type of the upload, string, optional, must be set to ``github`` if exists.
* ``owner`` - Github repository owner, string, required.
@ -170,30 +171,30 @@ This feature requires Github key creation (see below). Section name must be eith
``rsync`` type
^^^^^^^^^^^^^^
Requires ``rsync`` package to be installed. Do not forget to configure ssh for user ``ahriman``. Section name must be either ``rsync`` (plus optional architecture name, e.g. ``rsync:x86_64``\ ) or random name with ``type`` set.
Requires ``rsync`` package to be installed. Do not forget to configure ssh for user ``ahriman``. Section name must be either ``rsync`` (plus optional architecture name, e.g. ``rsync:x86_64``) or random name with ``type`` set.
* ``type`` - type of the upload, string, optional, must be set to ``rsync`` if exists.
* ``command`` - rsync command to run, space separated list of string, required.
* ``remote`` - remote server to rsync (e.g. ``1.2.3.4:path/to/sync``\ ), string, required.
* ``remote`` - remote server to rsync (e.g. ``1.2.3.4:path/to/sync``), string, required.
``s3`` type
^^^^^^^^^^^
Requires ``boto3`` library to be installed. Section name must be either ``s3`` (plus optional architecture name, e.g. ``s3:x86_64``\ ) or random name with ``type`` set.
Requires ``boto3`` library to be installed. Section name must be either ``s3`` (plus optional architecture name, e.g. ``s3:x86_64``) or random name with ``type`` set.
* ``type`` - type of the upload, string, optional, must be set to ``github`` if exists.
* ``access_key`` - AWS access key ID, string, required.
* ``bucket`` - bucket name (e.g. ``bucket``\ ), string, required.
* ``bucket`` - bucket name (e.g. ``bucket``), string, required.
* ``chunk_size`` - chunk size for calculating entity tags, int, optional, default 8 * 1024 * 1024.
* ``region`` - bucket region (e.g. ``eu-central-1``\ ), string, required.
* ``region`` - bucket region (e.g. ``eu-central-1``), string, required.
* ``secret_key`` - AWS secret access key, string, required.
``web:*`` groups
----------------
Web server settings. If any of ``host``\ /\ ``port`` is not set, web integration will be disabled. Group name can refer to architecture, e.g. ``web:x86_64`` can be used for x86_64 architecture specific settings. This feature requires ``aiohttp`` libraries to be installed.
Web server settings. If any of ``host``/``port`` is not set, web integration will be disabled. Group name can refer to architecture, e.g. ``web:x86_64`` can be used for x86_64 architecture specific settings. This feature requires ``aiohttp`` libraries to be installed.
* ``address`` - optional address in form ``proto://host:port`` (\ ``port`` can be omitted in case of default ``proto`` ports), will be used instead of ``http://{host}:{port}`` in case if set, string, optional. This option is required in case if ``OAuth`` provider is used.
* ``address`` - optional address in form ``proto://host:port`` (``port`` can be omitted in case of default ``proto`` ports), will be used instead of ``http://{host}:{port}`` in case if set, string, optional. This option is required in case if ``OAuth`` provider is used.
* ``debug`` - enable debug toolbar, boolean, optional, default ``no``.
* ``debug_check_host`` - check hosts to access debug toolbar, boolean, optional, default ``no``.
* ``debug_allowed_hosts`` - allowed hosts to get access to debug toolbar, space separated list of string, optional.

View File

@ -118,7 +118,7 @@ But I just wanted to change PKGBUILD from AUR a bit!
Well it is supported also.
#. Clone sources from AUR.
#. Make changes you would like to (e.g. edit ``PKGBUILD``\ , add external patches).
#. Make changes you would like to (e.g. edit ``PKGBUILD``, add external patches).
#. Run ``sudo -u ahriman ahriman patch-add /path/to/local/directory/with/PKGBUILD``.
The last command will calculate diff from current tree to the ``HEAD`` and will store it locally. Patches will be applied on any package actions (e.g. it can be used for dependency management).
@ -177,7 +177,7 @@ However, note that you do not need to rebuild repository in case if you just cha
Hmm, I have packages built, but how can I use it?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Add the following lines to your ``pacman.conf``\ :
Add the following lines to your ``pacman.conf``:
.. code-block:: ini
@ -238,7 +238,7 @@ The default action (in case if no arguments provided) is ``repo-update``. Basica
docker run -v /path/to/local/repo:/var/lib/ahriman -v /etc/ahriman.ini:/etc/ahriman.ini.d/10-overrides.ini arcan1s/ahriman:latest
By default, it runs ``repo-update``\ , but it can be overwritten to any other command you would like to, e.g.:
By default, it runs ``repo-update``, but it can be overwritten to any other command you would like to, e.g.:
.. code-block:: shell
@ -255,7 +255,7 @@ The following environment variables are supported:
* ``AHRIMAN_DEBUG`` - if set all commands will be logged to console.
* ``AHRIMAN_FORCE_ROOT`` - force run ahriman as root instead of guessing by subcommand.
* ``AHRIMAN_HOST`` - host for the web interface, default is ``0.0.0.0``.
* ``AHRIMAN_OUTPUT`` - controls logging handler, e.g. ``syslog``\ , ``console``. The name must be found in logging configuration. Note that if ``syslog`` (the default) handler is used you will need to mount ``/dev/log`` inside container because it is not available there.
* ``AHRIMAN_OUTPUT`` - controls logging handler, e.g. ``syslog``, ``console``. The name must be found in logging configuration. Note that if ``syslog`` (the default) handler is used you will need to mount ``/dev/log`` inside container because it is not available there.
* ``AHRIMAN_PACKAGER`` - packager name from which packages will be built, default is ``ahriman bot <ahriman@example.com>``.
* ``AHRIMAN_PORT`` - HTTP server port if any, default is empty.
* ``AHRIMAN_REPOSITORY`` - repository name, default is ``aur-clone``.
@ -279,7 +279,7 @@ Well for that you would need to have web container instance running forever; it
Note about ``AHRIMAN_PORT`` environment variable which is required in order to enable web service. An additional port bind by ``-p 8080:8080`` is required to pass docker port outside of container.
For every next container run use arguments ``-e AHRIMAN_PORT=8080 --net=host``\ , e.g.:
For every next container run use arguments ``-e AHRIMAN_PORT=8080 --net=host``, e.g.:
.. code-block:: shell
@ -294,7 +294,7 @@ Wait I would like to use the repository from another server
There are several choices:
#.
Easy and cheap, just share your local files through the internet, e.g. for ``nginx``\ :
Easy and cheap, just share your local files through the internet, e.g. for ``nginx``:
.. code-block::
@ -316,7 +316,7 @@ There are several choices:
[rsync]
remote = 192.168.0.1:/srv/repo
After that just add ``/srv/repo`` to the ``pacman.conf`` as usual. You can also upload to S3 (e.g. ``Server = https://s3.eu-central-1.amazonaws.com/repository/x86_64``\ ) or to Github (e.g. ``Server = https://github.com/ahriman/repository/releases/download/x86_64``\ ).
After that just add ``/srv/repo`` to the ``pacman.conf`` as usual. You can also upload to S3 (e.g. ``Server = https://s3.eu-central-1.amazonaws.com/repository/x86_64``) or to Github (e.g. ``Server = https://github.com/ahriman/repository/releases/download/x86_64``).
How do I configure S3?
^^^^^^^^^^^^^^^^^^^^^^
@ -474,7 +474,7 @@ I would like to get messages to my telegram account/channel
#. Make your channel public
#.
Get chat id if you want to use by numerical id or just use id prefixed with ``@`` (e.g. ``@ahriman``\ ). If you are not using chat the chat id is your user id. If you don't want to make channel public you can use `this guide <https://stackoverflow.com/a/33862907>`_.
Get chat id if you want to use by numerical id or just use id prefixed with ``@`` (e.g. ``@ahriman``). If you are not using chat the chat id is your user id. If you don't want to make channel public you can use `this guide <https://stackoverflow.com/a/33862907>`_.
#.
Configure the service:
@ -489,7 +489,7 @@ I would like to get messages to my telegram account/channel
chat_id = @ahriman
link_path = http://example.com/x86_64
``api_key`` is the one sent by `@BotFather <https://t.me/botfather>`_\ , ``chat_id`` is the value retrieved from previous step.
``api_key`` is the one sent by `@BotFather <https://t.me/botfather>`_, ``chat_id`` is the value retrieved from previous step.
If you did everything fine you should receive the message with the next update. Quick credentials check can be done by using the following command:
@ -606,7 +606,7 @@ The service provides several commands aim to do easy repository backup and resto
sudo ahriman repo-backup /tmp/repo.tar.gz
This command will pack all configuration files together with database file into the archive specified as command line argument (i.e. ``/tmp/repo.tar.gz``\ ). In addition it will also archive ``cache`` directory (the one which contains local clones used by e.g. local packages) and ``.gnupg`` of the ``ahriman`` user.
This command will pack all configuration files together with database file into the archive specified as command line argument (i.e. ``/tmp/repo.tar.gz``). In addition it will also archive ``cache`` directory (the one which contains local clones used by e.g. local packages) and ``.gnupg`` of the ``ahriman`` user.
#.
Copy created archive from source server ``server1.example.com`` to target ``server2.example.com``.
@ -621,7 +621,7 @@ The service provides several commands aim to do easy repository backup and resto
sudo ahriman repo-restore /tmp/repo.tar.gz
An additional argument ``-o``\ /\ ``--output`` can be used to specify extraction root (\ ``/`` by default).
An additional argument ``-o``/``--output`` can be used to specify extraction root (``/`` by default).
#.
Rebuild repository:
@ -684,7 +684,7 @@ I would like to check service logs
By default, the service writes logs to ``/dev/log`` which can be accessed by using ``journalctl`` command (logs are written to the journal of the user under which command is run).
You can also edit configuration and forward logs to ``stderr``\ , just change ``handlers`` value, e.g.:
You can also edit configuration and forward logs to ``stderr``, just change ``handlers`` value, e.g.:
.. code-block:: shell

View File

@ -10,7 +10,7 @@ Features
* Multi-architecture support.
* VCS packages support.
* Sign support with gpg (repository, package, per package settings).
* Synchronization to remote services (rsync, s3 and github) and report generation (email, html, telegram).
* Synchronization to remote services (rsync, s3 and github) and report generation (email, html, telegram) and even ability to write own extensions.
* Dependency manager.
* Ability to patch AUR packages and even create package from local PKGBUILDs.
* Repository status interface with optional authorization and control options.
@ -27,6 +27,7 @@ Contents
faq
architecture
advanced-usage
triggers
modules
Indices and tables

View File

@ -15,7 +15,7 @@ Initial setup
``repo-setup`` literally does the following steps:
#.
Create ``/var/lib/ahriman/.makepkg.conf`` with ``makepkg.conf`` overrides if required (at least you might want to set ``PACKAGER``\ ):
Create ``/var/lib/ahriman/.makepkg.conf`` with ``makepkg.conf`` overrides if required (at least you might want to set ``PACKAGER``):
.. code-block:: shell
@ -25,9 +25,9 @@ Initial setup
Configure build tools (it is required for correct dependency management system):
#.
Create build command, e.g. ``ln -s /usr/bin/archbuild /usr/local/bin/ahriman-x86_64-build`` (you can choose any name for command, basically it should be ``{name}-{arch}-build``\ ).
Create build command, e.g. ``ln -s /usr/bin/archbuild /usr/local/bin/ahriman-x86_64-build`` (you can choose any name for command, basically it should be ``{name}-{arch}-build``).
#.
Create configuration file, e.g. ``cp /usr/share/devtools/pacman-{extra,ahriman}.conf`` (same as previous ``pacman-{name}.conf``\ ).
Create configuration file, e.g. ``cp /usr/share/devtools/pacman-{extra,ahriman}.conf`` (same as previous ``pacman-{name}.conf``).
#.
Change configuration file, add your own repository, add multilib repository etc;
#.
@ -55,7 +55,7 @@ Initial setup
chmod 400 /etc/sudoers.d/ahriman
#.
Start and enable ``ahriman@.timer`` via ``systemctl``\ :
Start and enable ``ahriman@.timer`` via ``systemctl``:
.. code-block:: shell

62
docs/triggers.rst Normal file
View File

@ -0,0 +1,62 @@
Triggers
========
The package provides ability to write custom extensions which will be run on (the most) actions, e.g. after updates. By default ahriman provides two types of extensions - reporting and files uploading. Each extension must derive from the ``Trigger`` class and implement ``run`` method
Trigger example
---------------
Lets consider example of reporting trigger (e.g. `slack <https://slack.com/>`_, which provides easy HTTP API for triggers for integrations).
In order to post message to slack we will need a specific trigger url (something like ``https://hooks.slack.com/services/company_id/trigger_id``), channel (e.g. ``#archrepo``) and username (``repo-bot``). It can be retrieved from the same application instance.
As it has been mentioned, our trigger must derive from specific class
.. code-block:: python
from ahriman.core.triggers import Trigger
class SlackReporter(Trigger):
def __init__(self, architecture, configuration):
Trigger.__init__(self, architecture, configuration)
self.slack_url = configuration.get("slack", "url")
self.channel = configuration.get("slack", "channel")
self.username = configuration.get("slack", "username")
By now we have class with all required variables.
Lets implement run method. Slack API requires positing data with specific payload by HTTP, thus
.. code-block:: python
import json
import requests
def notify(result, slack_url, channel, username):
text = f"""Build has been completed with packages: {", ".join([package.name for package in result.success])}"""
payload = {"channel": channel, "username": username, "text": text}
response = requests.post(slack_url, data={"payload": json.dumps(payload)})
response.raise_for_status()
Obviously you can implement the specified method in class, but for guide purpose it has been done as separated method. Now we can merge this method into the class
.. code-block:: python
class SlackReporter(Trigger):
def __init__(self, architecture, configuration):
Trigger.__init__(self, architecture, configuration)
self.slack_url = configuration.get("slack", "url")
self.channel = configuration.get("slack", "channel")
self.username = configuration.get("slack", "username")
def run(self, result, packages):
notify(result, self.slack_url, channel, username)
Setup the trigger
-----------------
First, put the trigger in any path it can be exported, e.g. by packing the resource into python package (which will lead to import path as ``package.slack_reporter.SlackReporter``) or just put file somewhere it can be accessed by application (e.g. ``/usr/local/lib/slack_reporter.py.SlackReporter``.
After that run application as usual and receive notification in your slack channel.

View File

@ -21,6 +21,7 @@ build_command = extra-x86_64-build
ignore_packages =
makechrootpkg_flags =
makepkg_flags = --nocolor
triggers = ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger
[repository]
name = aur-clone

View File

@ -98,12 +98,11 @@ def _parser() -> argparse.ArgumentParser:
_set_repo_config_parser(subparsers)
_set_repo_rebuild_parser(subparsers)
_set_repo_remove_unknown_parser(subparsers)
_set_repo_report_parser(subparsers)
_set_repo_restore_parser(subparsers)
_set_repo_setup_parser(subparsers)
_set_repo_sign_parser(subparsers)
_set_repo_status_update_parser(subparsers)
_set_repo_sync_parser(subparsers)
_set_repo_triggers_parser(subparsers)
_set_repo_update_parser(subparsers)
_set_user_add_parser(subparsers)
_set_user_list_parser(subparsers)
@ -496,25 +495,6 @@ def _set_repo_remove_unknown_parser(root: SubParserAction) -> argparse.ArgumentP
return parser
def _set_repo_report_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for report subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-report", aliases=["report"], help="generate report",
description="generate repository report according to current settings",
epilog="Create and/or update repository report as configured.",
formatter_class=_formatter)
parser.add_argument("target", help="target to generate report", nargs="*")
parser.set_defaults(handler=handlers.Report)
return parser
def _set_repo_restore_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository restore subcommand
@ -600,7 +580,7 @@ def _set_repo_status_update_parser(root: SubParserAction) -> argparse.ArgumentPa
return parser
def _set_repo_sync_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_repo_triggers_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository sync subcommand
@ -610,12 +590,10 @@ def _set_repo_sync_parser(root: SubParserAction) -> argparse.ArgumentParser:
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-sync", aliases=["sync"], help="sync repository",
description="sync repository files to remote server according to current settings",
epilog="Synchronize the repository to remote services as configured.",
parser = root.add_parser("repo-triggers", help="run triggers",
description="run triggers on empty build result as configured by settings",
formatter_class=_formatter)
parser.add_argument("target", help="target to sync", nargs="*")
parser.set_defaults(handler=handlers.Sync)
parser.set_defaults(handler=handlers.Triggers)
return parser

View File

@ -56,8 +56,7 @@ class Application(ApplicationPackages, ApplicationRepository):
Args:
result(Result): build result
"""
self.report([], result)
self.sync([], result.success)
self.repository.process_triggers(result)
def _known_packages(self) -> Set[str]:
"""

View File

@ -85,9 +85,9 @@ class ApplicationPackages(ApplicationProperties):
self.database.build_queue_insert(package)
self.database.remote_update(package)
with tmpdir() as local_path:
Sources.load(local_path, package, self.database.patches_get(package.base), self.repository.paths)
self._process_dependencies(local_path, known_packages, without_dependencies)
with tmpdir() as local_dir:
Sources.load(local_dir, package, self.database.patches_get(package.base), self.repository.paths)
self._process_dependencies(local_dir, known_packages, without_dependencies)
def _add_directory(self, source: str, *_: Any) -> None:
"""
@ -96,8 +96,8 @@ class ApplicationPackages(ApplicationProperties):
Args:
source(str): path to local directory
"""
local_path = Path(source)
for full_path in filter(package_like, local_path.iterdir()):
local_dir = Path(source)
for full_path in filter(package_like, local_dir.iterdir()):
self._add_archive(str(full_path))
def _add_local(self, source: str, known_packages: Set[str], without_dependencies: bool) -> None:
@ -146,19 +146,19 @@ class ApplicationPackages(ApplicationProperties):
self.database.remote_update(package)
# repository packages must not depend on unknown packages, thus we are not going to process dependencies
def _process_dependencies(self, local_path: Path, known_packages: Set[str], without_dependencies: bool) -> None:
def _process_dependencies(self, local_dir: Path, known_packages: Set[str], without_dependencies: bool) -> None:
"""
process package dependencies
Args:
local_path(Path): path to local package sources (i.e. cloned AUR repository)
local_dir(Path): path to local package sources (i.e. cloned AUR repository)
known_packages(Set[str]): list of packages which are known by the service
without_dependencies(bool): if set, dependency check will be disabled
"""
if without_dependencies:
return
dependencies = Package.dependencies(local_path)
dependencies = Package.dependencies(local_dir)
self.add(dependencies.difference(known_packages), PackageSource.AUR, without_dependencies)
def add(self, names: Iterable[str], source: PackageSource, without_dependencies: bool) -> None:

View File

@ -66,17 +66,6 @@ class ApplicationRepository(ApplicationProperties):
if packages:
self.repository.clear_packages()
def report(self, target: Iterable[str], result: Result) -> None:
"""
generate report
Args:
target(Iterable[str]): list of targets to run (e.g. html)
result(Result): build result
"""
targets = target or None
self.repository.process_report(targets, result)
def sign(self, packages: Iterable[str]) -> None:
"""
sign packages and repository
@ -102,17 +91,6 @@ class ApplicationRepository(ApplicationProperties):
self.repository.sign.process_sign_repository(self.repository.repo.repo_path)
self._finalize(Result())
def sync(self, target: Iterable[str], built_packages: Iterable[Package]) -> None:
"""
sync to remote server
Args:
target(Iterable[str]): list of targets to run (e.g. s3)
built_packages(Iterable[Package]): list of packages which has just been built
"""
targets = target or None
self.repository.process_sync(targets, built_packages)
def unknown(self) -> List[str]:
"""
get packages which were not found in AUR

View File

@ -29,14 +29,13 @@ from ahriman.application.handlers.patch import Patch
from ahriman.application.handlers.rebuild import Rebuild
from ahriman.application.handlers.remove import Remove
from ahriman.application.handlers.remove_unknown import RemoveUnknown
from ahriman.application.handlers.report import Report
from ahriman.application.handlers.restore import Restore
from ahriman.application.handlers.search import Search
from ahriman.application.handlers.setup import Setup
from ahriman.application.handlers.sign import Sign
from ahriman.application.handlers.status import Status
from ahriman.application.handlers.status_update import StatusUpdate
from ahriman.application.handlers.sync import Sync
from ahriman.application.handlers.triggers import Triggers
from ahriman.application.handlers.unsafe_commands import UnsafeCommands
from ahriman.application.handlers.update import Update
from ahriman.application.handlers.users import Users

View File

@ -71,10 +71,10 @@ class Handler:
if args.architecture: # architecture is specified explicitly
return sorted(set(args.architecture))
config = Configuration()
config.load(args.configuration)
configuration = Configuration()
configuration.load(args.configuration)
# wtf???
root = config.getpath("repository", "root") # pylint: disable=assignment-from-no-return
root = configuration.getpath("repository", "root") # pylint: disable=assignment-from-no-return
architectures = RepositoryPaths.known_architectures(root)
if not architectures: # well we did not find anything

View File

@ -27,9 +27,9 @@ from ahriman.core.configuration import Configuration
from ahriman.models.result import Result
class Report(Handler):
class Triggers(Handler):
"""
generate report handler
triggers handlers
"""
@classmethod
@ -45,4 +45,4 @@ class Report(Handler):
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
Application(architecture, configuration, no_report, unsafe).report(args.target, Result())
Application(architecture, configuration, no_report, unsafe).repository.process_triggers(Result())

View File

@ -125,11 +125,11 @@ class Configuration(configparser.RawConfigParser):
Returns:
Configuration: configuration instance
"""
config = cls()
config.load(path)
config.merge_sections(architecture)
config.load_logging(quiet)
return config
configuration = cls()
configuration.load(path)
configuration.merge_sections(architecture)
configuration.load_logging(quiet)
return configuration
@staticmethod
def __convert_list(value: str) -> List[str]:

View File

@ -70,6 +70,12 @@ class InitializeException(RuntimeError):
RuntimeError.__init__(self, f"Could not load service: {details}")
class InvalidExtension(RuntimeError):
"""
exception being raised by trigger load in case of errors
"""
class InvalidOption(ValueError):
"""
exception which will be raised on configuration errors

View File

@ -24,3 +24,5 @@ from ahriman.core.report.console import Console
from ahriman.core.report.email import Email
from ahriman.core.report.html import HTML
from ahriman.core.report.telegram import Telegram
from ahriman.core.report.report_trigger import ReportTrigger

View File

@ -109,13 +109,13 @@ class Report:
result(Result): build result
"""
def run(self, packages: Iterable[Package], result: Result) -> None:
def run(self, result: Result, packages: Iterable[Package]) -> None:
"""
run report generation
Args:
packages(Iterable[Package]): list of packages to generate report
result(Result): build result
packages(Iterable[Package]): list of packages to generate report
Raises:
ReportFailed: in case of any report unmatched exception

View File

@ -17,31 +17,42 @@
# 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 argparse
from typing import Iterable
from typing import Type
from ahriman.application.application import Application
from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
from ahriman.core.triggers import Trigger
from ahriman.core.report import Report
from ahriman.models.package import Package
from ahriman.models.result import Result
class Sync(Handler):
class ReportTrigger(Trigger):
"""
remote sync handler
report trigger
Attributes:
targets(List[str]): report target list
"""
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool, unsafe: bool) -> None:
def __init__(self, architecture: str, configuration: Configuration) -> None:
"""
callback for command line
default constructor
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
Application(architecture, configuration, no_report, unsafe).sync(args.target, [])
Trigger.__init__(self, architecture, configuration)
self.targets = configuration.getlist("report", "target")
def run(self, result: Result, packages: Iterable[Package]) -> None:
"""
run trigger
Args:
result(Result): build result
packages(Iterable[Package]): list of all available packages
"""
for target in self.targets:
runner = Report.load(self.architecture, self.configuration, target)
runner.run(result, packages)

View File

@ -23,9 +23,7 @@ from pathlib import Path
from typing import Iterable, List, Optional, Set
from ahriman.core.build_tools.task import Task
from ahriman.core.report import Report
from ahriman.core.repository.cleaner import Cleaner
from ahriman.core.upload import Upload
from ahriman.core.util import tmpdir
from ahriman.models.package import Package
from ahriman.models.result import Result
@ -143,35 +141,14 @@ class Executor(Cleaner):
return self.repo.repo_path
def process_report(self, targets: Optional[Iterable[str]], result: Result) -> None:
def process_triggers(self, result: Result) -> None:
"""
generate reports
process triggers setup by settings
Args:
targets(Optional[Iterable[str]]): list of targets to generate reports. Configuration option will be used
if it is not set
result(Result): build result
"""
if targets is None:
targets = self.configuration.getlist("report", "target")
for target in targets:
runner = Report.load(self.architecture, self.configuration, target)
runner.run(self.packages(), result)
def process_sync(self, targets: Optional[Iterable[str]], built_packages: Iterable[Package]) -> None:
"""
process synchronization to remote servers
Args:
targets(Optional[Iterable[str]]): list of targets to sync. Configuration option will be used
if it is not set
built_packages(Iterable[Package]): list of packages which has just been built
"""
if targets is None:
targets = self.configuration.getlist("upload", "target")
for target in targets:
runner = Upload.load(self.architecture, self.configuration, target)
runner.run(self.paths.repository, built_packages)
self.triggers.process(result, self.packages())
def process_update(self, packages: Iterable[Path]) -> Result:
"""

View File

@ -26,6 +26,7 @@ from ahriman.core.database import SQLite
from ahriman.core.exceptions import UnsafeRun
from ahriman.core.sign.gpg import GPG
from ahriman.core.status.client import Client
from ahriman.core.triggers import TriggerLoader
from ahriman.core.util import check_user
@ -45,6 +46,7 @@ class RepositoryProperties:
repo(Repo): repo commands wrapper instance
reporter(Client): build status reporter instance
sign(GPG): GPG wrapper instance
triggers(TriggerLoader): triggers holder
"""
def __init__(self, architecture: str, configuration: Configuration, database: SQLite,
@ -78,3 +80,4 @@ class RepositoryProperties:
self.sign = GPG(architecture, configuration)
self.repo = Repo(self.name, self.paths, self.sign.repository_sign_args)
self.reporter = Client() if no_report else Client.load(configuration)
self.triggers = TriggerLoader(architecture, configuration)

View File

@ -0,0 +1,21 @@
#
# Copyright (c) 2021-2022 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/>.
#
from ahriman.core.triggers.trigger import Trigger
from ahriman.core.triggers.trigger_loader import TriggerLoader

View File

@ -0,0 +1,79 @@
#
# Copyright (c) 2021-2022 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 logging
from typing import Iterable
from ahriman.core.configuration import Configuration
from ahriman.models.package import Package
from ahriman.models.result import Result
class Trigger:
"""
trigger base class
Attributes:
architecture(str): repository architecture
configuration(Configuration): configuration instance
logger(logging.Logger): application logger
Examples:
This class must be used in order to create own extension. Basically idea is the following::
>>> class CustomTrigger(Trigger):
>>> def run(self, result: Result, packages: Iterable[Package]) -> None:
>>> perform_some_action()
Having this class you can pass it to ``configuration`` and it will be run on action::
>>> from ahriman.core.triggers import TriggerLoader
>>>
>>> configuration = Configuration()
>>> configuration.set_option("build", "triggers", "my.awesome.package.CustomTrigger")
>>>
>>> loader = TriggerLoader("x86_64", configuration)
>>> loader.process(Result(), [])
"""
def __init__(self, architecture: str, configuration: Configuration) -> None:
"""
default constructor
Args:
architecture(str): repository architecture
configuration(Configuration): configuration instance
"""
self.logger = logging.getLogger("root")
self.architecture = architecture
self.configuration = configuration
def run(self, result: Result, packages: Iterable[Package]) -> None:
"""
run trigger
Args:
result(Result): build result
packages(Iterable[Package]): list of all available packages
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError

View File

@ -0,0 +1,159 @@
#
# Copyright (c) 2021-2022 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 importlib
import logging
from pathlib import Path
from types import ModuleType
from typing import Iterable
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import InvalidExtension
from ahriman.core.triggers import Trigger
from ahriman.models.package import Package
from ahriman.models.result import Result
class TriggerLoader:
"""
trigger loader class
Attributes:
architecture(str): repository architecture
configuration(Configuration): configuration instance
logger(logging.Logger): application logger
triggers(List[Trigger]): list of loaded triggers according to the configuration
Examples:
This class more likely must not be used directly, but the usual workflow is the following::
>>> configuration = Configuration() # create configuration
>>> configuration.set_option("build", "triggers", "ahriman.core.report.ReportTrigger") # set class for load
Having such configuration you can create instance of the loader::
>>> loader = TriggerLoader("x86_64", configuration)
>>> print(loader.triggers)
After that you are free to run triggers::
>>> loader.process(Result(), [])
"""
def __init__(self, architecture: str, configuration: Configuration) -> None:
"""
default constructor
Args:
architecture(str): repository architecture
configuration(Configuration): configuration instance
"""
self.logger = logging.getLogger("root")
self.architecture = architecture
self.configuration = configuration
self.triggers = [
self._load_trigger(trigger)
for trigger in configuration.getlist("build", "triggers")
]
def _load_module_from_file(self, module_path: str, implementation: str) -> ModuleType:
"""
load module by given file path
Args:
module_path(str): import package
implementation(str): specific trigger implementation, class name, required by import
Returns:
ModuleType: module loaded from the imported file
"""
self.logger.info("load module %s from path %s", implementation, module_path)
# basically this method is called only if ``module_path`` exists and is file.
# Thus, this method should never throw ``FileNotFoundError`` exception
loader = importlib.machinery.SourceFileLoader(implementation, module_path)
module = ModuleType(loader.name)
loader.exec_module(module)
return module
def _load_module_from_package(self, package: str) -> ModuleType:
"""
load module by given package name
Args:
package(str): package name to import
Returns:
ModuleType: module loaded from the imported module
"""
self.logger.info("load module from package %s", package)
try:
return importlib.import_module(package)
except ModuleNotFoundError:
raise InvalidExtension(f"Module {package} not found")
def _load_trigger(self, module_path: str) -> Trigger:
"""
load trigger by module path
Args:
module_path(str): module import path to load
Returns:
Trigger: loaded trigger based on settings
"""
*package_path_parts, class_name = module_path.split(".")
package_or_path = ".".join(package_path_parts)
if Path(package_or_path).is_file():
module = self._load_module_from_file(package_or_path, class_name)
else:
module = self._load_module_from_package(package_or_path)
trigger_type = getattr(module, class_name, None)
if not isinstance(trigger_type, type):
raise InvalidExtension(f"{class_name} of {package_or_path} is not a type")
self.logger.info("loaded type %s of package %s", class_name, package_or_path)
try:
trigger = trigger_type(self.architecture, self.configuration)
except Exception:
raise InvalidExtension(f"Could not load instance of trigger from {class_name} of {package_or_path}")
if not isinstance(trigger, Trigger):
raise InvalidExtension(f"Class {class_name} of {package_or_path} is not a Trigger")
return trigger
def process(self, result: Result, packages: Iterable[Package]) -> None:
"""
run remote sync
Args:
result(Result): build result
packages(Iterable[Package]): list of all available packages
"""
for trigger in self.triggers:
trigger_name = type(trigger).__name__
try:
self.logger.info("executing extension %s", trigger_name)
trigger.run(result, packages)
except Exception:
self.logger.exception("got exception while run trigger %s", trigger_name)

View File

@ -23,3 +23,5 @@ from ahriman.core.upload.http_upload import HttpUpload
from ahriman.core.upload.github import Github
from ahriman.core.upload.rsync import Rsync
from ahriman.core.upload.s3 import S3
from ahriman.core.upload.upload_trigger import UploadTrigger

View File

@ -68,7 +68,7 @@ class Upload:
"""
self.logger = logging.getLogger("root")
self.architecture = architecture
self.config = configuration
self.configuration = configuration
@classmethod
def load(cls: Type[Upload], architecture: str, configuration: Configuration, target: str) -> Upload:

View File

@ -0,0 +1,58 @@
#
# Copyright (c) 2021-2022 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/>.
#
from typing import Iterable
from ahriman.core.configuration import Configuration
from ahriman.core.triggers import Trigger
from ahriman.core.upload import Upload
from ahriman.models.package import Package
from ahriman.models.result import Result
class UploadTrigger(Trigger):
"""
synchronization trigger
Attributes:
targets(List[str]): upload target 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 = configuration.getlist("upload", "target")
def run(self, result: Result, packages: Iterable[Package]) -> None:
"""
run trigger
Args:
result(Result): build result
packages(Iterable[Package]): list of all available packages
"""
for target in self.targets:
runner = Upload.load(self.architecture, self.configuration, target)
runner.run(self.configuration.repository_paths.repository, result.success)

View File

@ -9,12 +9,10 @@ def test_finalize(application: Application, mocker: MockerFixture) -> None:
"""
must report and sync at the last
"""
report_mock = mocker.patch("ahriman.application.application.Application.report")
sync_mock = mocker.patch("ahriman.application.application.Application.sync")
triggers_mock = mocker.patch("ahriman.core.repository.Repository.process_triggers")
application._finalize(Result())
report_mock.assert_called_once_with([], Result())
sync_mock.assert_called_once_with([], [])
triggers_mock.assert_called_once_with(Result())
def test_known_packages(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:

View File

@ -53,15 +53,6 @@ def test_clean_packages(application_repository: ApplicationRepository, mocker: M
clear_mock.assert_called_once_with()
def test_report(application_repository: ApplicationRepository, mocker: MockerFixture) -> None:
"""
must generate report
"""
executor_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_report")
application_repository.report(["a"], Result())
executor_mock.assert_called_once_with(["a"], Result())
def test_sign(application_repository: ApplicationRepository, package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""
@ -121,15 +112,6 @@ def test_sign_specific(application_repository: ApplicationRepository, package_ah
finalize_mock.assert_called_once_with(Result())
def test_sync(application_repository: ApplicationRepository, mocker: MockerFixture) -> None:
"""
must sync to remote
"""
executor_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_sync")
application_repository.sync(["a"], [])
executor_mock.assert_called_once_with(["a"], [])
def test_unknown_no_aur(application_repository: ApplicationRepository, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""

View File

@ -1,33 +0,0 @@
import argparse
from pytest_mock import MockerFixture
from ahriman.application.handlers import Report
from ahriman.core.configuration import Configuration
from ahriman.models.result import Result
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
"""
default arguments for these test cases
Args:
args(argparse.Namespace): command line arguments fixture
Returns:
argparse.Namespace: generated arguments for these test cases
"""
args.target = []
return args
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run command
"""
args = _default_args(args)
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
application_mock = mocker.patch("ahriman.application.application.Application.report")
Report.run(args, "x86_64", configuration, True, False)
application_mock.assert_called_once_with(args.target, Result())

View File

@ -1,32 +0,0 @@
import argparse
from pytest_mock import MockerFixture
from ahriman.application.handlers import Sync
from ahriman.core.configuration import Configuration
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
"""
default arguments for these test cases
Args:
args(argparse.Namespace): command line arguments fixture
Returns:
argparse.Namespace: generated arguments for these test cases
"""
args.target = []
return args
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run command
"""
args = _default_args(args)
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
application_mock = mocker.patch("ahriman.application.application.Application.sync")
Sync.run(args, "x86_64", configuration, True, False)
application_mock.assert_called_once_with(args.target, [])

View File

@ -0,0 +1,18 @@
import argparse
from pytest_mock import MockerFixture
from ahriman.application.handlers import Triggers
from ahriman.core.configuration import Configuration
from ahriman.models.result import Result
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run command
"""
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
application_mock = mocker.patch("ahriman.core.repository.Repository.process_triggers")
Triggers.run(args, "x86_64", configuration, True, False)
application_mock.assert_called_once_with(Result())

View File

@ -348,16 +348,6 @@ def test_subparsers_repo_remove_unknown_architecture(parser: argparse.ArgumentPa
assert args.architecture == ["x86_64"]
def test_subparsers_repo_report_architecture(parser: argparse.ArgumentParser) -> None:
"""
repo-report command must correctly parse architecture list
"""
args = parser.parse_args(["repo-report"])
assert args.architecture is None
args = parser.parse_args(["-a", "x86_64", "repo-report"])
assert args.architecture == ["x86_64"]
def test_subparsers_repo_restore(parser: argparse.ArgumentParser) -> None:
"""
repo-restore command must imply architecture list, lock, no-report and unsafe
@ -446,13 +436,13 @@ def test_subparsers_repo_status_update_option_status(parser: argparse.ArgumentPa
assert isinstance(args.status, BuildStatusEnum)
def test_subparsers_repo_sync_architecture(parser: argparse.ArgumentParser) -> None:
def test_subparsers_repo_triggers_architecture(parser: argparse.ArgumentParser) -> None:
"""
repo-sync command must correctly parse architecture list
repo-triggers command must correctly parse architecture list
"""
args = parser.parse_args(["repo-sync"])
args = parser.parse_args(["repo-triggers"])
assert args.architecture is None
args = parser.parse_args(["-a", "x86_64", "repo-sync"])
args = parser.parse_args(["-a", "x86_64", "repo-triggers"])
assert args.architecture == ["x86_64"]

View File

@ -24,7 +24,7 @@ def test_report_dummy(configuration: Configuration, result: Result, mocker: Mock
"""
mocker.patch("ahriman.models.report_settings.ReportSettings.from_option", return_value=ReportSettings.Disabled)
report_mock = mocker.patch("ahriman.core.report.Report.generate")
Report.load("x86_64", configuration, "disabled").run([], result)
Report.load("x86_64", configuration, "disabled").run(result, [])
report_mock.assert_called_once_with([], result)
@ -33,7 +33,7 @@ def test_report_console(configuration: Configuration, result: Result, mocker: Mo
must generate console report
"""
report_mock = mocker.patch("ahriman.core.report.Console.generate")
Report.load("x86_64", configuration, "console").run([], result)
Report.load("x86_64", configuration, "console").run(result, [])
report_mock.assert_called_once_with([], result)
@ -42,7 +42,7 @@ def test_report_email(configuration: Configuration, result: Result, mocker: Mock
must generate email report
"""
report_mock = mocker.patch("ahriman.core.report.Email.generate")
Report.load("x86_64", configuration, "email").run([], result)
Report.load("x86_64", configuration, "email").run(result, [])
report_mock.assert_called_once_with([], result)
@ -51,7 +51,7 @@ def test_report_html(configuration: Configuration, result: Result, mocker: Mocke
must generate html report
"""
report_mock = mocker.patch("ahriman.core.report.HTML.generate")
Report.load("x86_64", configuration, "html").run([], result)
Report.load("x86_64", configuration, "html").run(result, [])
report_mock.assert_called_once_with([], result)
@ -60,5 +60,5 @@ def test_report_telegram(configuration: Configuration, result: Result, mocker: M
must generate telegram report
"""
report_mock = mocker.patch("ahriman.core.report.Telegram.generate")
Report.load("x86_64", configuration, "telegram").run([], result)
Report.load("x86_64", configuration, "telegram").run(result, [])
report_mock.assert_called_once_with([], result)

View File

@ -0,0 +1,17 @@
from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration
from ahriman.core.report import ReportTrigger
from ahriman.models.result import Result
def test_run(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run report for specified targets
"""
configuration.set_option("report", "target", "email")
run_mock = mocker.patch("ahriman.core.report.Report.run")
trigger = ReportTrigger("x86_64", configuration)
trigger.run(Result(), [])
run_mock.assert_called_once_with(Result(), [])

View File

@ -3,12 +3,11 @@ import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from unittest import mock
from unittest.mock import MagicMock
from ahriman.core.report import Report
from ahriman.core.repository.executor import Executor
from ahriman.core.upload import Upload
from ahriman.models.package import Package
from ahriman.models.result import Result
def test_load_archives(executor: Executor) -> None:
@ -145,45 +144,15 @@ def test_process_remove_nothing(executor: Executor, package_ahriman: Package, pa
repo_remove_mock.assert_not_called()
def test_process_report(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
def test_process_triggers(executor: Executor, package_ahriman: Package, result: Result, mocker: MockerFixture) -> None:
"""
must process report
"""
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
mocker.patch("ahriman.core.report.Report.load", return_value=Report("x86_64", executor.configuration))
report_mock = mocker.patch("ahriman.core.report.Report.run")
triggers_mock = mocker.patch("ahriman.core.triggers.TriggerLoader.process")
executor.process_report(["dummy"], [])
report_mock.assert_called_once_with([package_ahriman], [])
def test_process_report_auto(executor: Executor) -> None:
"""
must process report in auto mode if no targets supplied
"""
configuration_mock = executor.configuration = MagicMock()
executor.process_report(None, [])
configuration_mock.getlist.assert_called_once_with("report", "target")
def test_process_upload(executor: Executor, mocker: MockerFixture) -> None:
"""
must process sync
"""
mocker.patch("ahriman.core.upload.Upload.load", return_value=Upload("x86_64", executor.configuration))
upload_mock = mocker.patch("ahriman.core.upload.Upload.run")
executor.process_sync(["dummy"], [])
upload_mock.assert_called_once_with(executor.paths.repository, [])
def test_process_upload_auto(executor: Executor) -> None:
"""
must process sync in auto mode if no targets supplied
"""
configuration_mock = executor.configuration = MagicMock()
executor.process_sync(None, [])
configuration_mock.getlist.assert_called_once_with("upload", "target")
executor.process_triggers(result)
triggers_mock.assert_called_once_with(result, [package_ahriman])
def test_process_update(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:

View File

@ -0,0 +1,31 @@
import pytest
from ahriman.core.configuration import Configuration
from ahriman.core.triggers import Trigger, TriggerLoader
@pytest.fixture
def trigger(configuration: Configuration) -> Trigger:
"""
fixture for trigger
Args:
configuration(Configuration): configuration fixture
Returns:
Trigger: trigger test instance
"""
return Trigger("x86_64", configuration)
@pytest.fixture
def trigger_loader(configuration: Configuration) -> TriggerLoader:
"""
fixture for trigger loader
Args:
configuration(Configuration): configuration fixture
Returns:
TriggerLoader: trigger loader test instance
"""
return TriggerLoader("x86_64", configuration)

View File

@ -0,0 +1,12 @@
import pytest
from ahriman.core.triggers import Trigger
from ahriman.models.result import Result
def test_run(trigger: Trigger) -> None:
"""
must raise NotImplemented for missing rum method
"""
with pytest.raises(NotImplementedError):
trigger.run(Result(), [])

View File

@ -0,0 +1,92 @@
import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from ahriman.core.exceptions import InvalidExtension
from ahriman.core.triggers import TriggerLoader
from ahriman.models.package import Package
from ahriman.models.result import Result
def test_load_trigger_package(trigger_loader: TriggerLoader) -> None:
"""
must load trigger from package
"""
assert trigger_loader._load_trigger("ahriman.core.report.ReportTrigger")
def test_load_trigger_package_invalid_import(trigger_loader: TriggerLoader, mocker: MockerFixture) -> None:
"""
must raise InvalidExtension on invalid import
"""
mocker.patch("ahriman.core.triggers.trigger_loader.importlib.import_module", side_effect=ModuleNotFoundError())
with pytest.raises(InvalidExtension):
trigger_loader._load_trigger("random.module")
def test_load_trigger_package_not_trigger(trigger_loader: TriggerLoader) -> None:
"""
must raise InvalidExtension if imported module is not a type
"""
with pytest.raises(InvalidExtension):
trigger_loader._load_trigger("ahriman.core.util.check_output")
def test_load_trigger_package_error_on_creation(trigger_loader: TriggerLoader, mocker: MockerFixture) -> None:
"""
must raise InvalidException on trigger initialization if any exception is thrown
"""
mocker.patch("ahriman.core.triggers.trigger.Trigger.__init__", side_effect=Exception())
with pytest.raises(InvalidExtension):
trigger_loader._load_trigger("ahriman.core.report.ReportTrigger")
def test_load_trigger_package_is_not_trigger(trigger_loader: TriggerLoader) -> None:
"""
must raise InvalidExtension if loaded class is not a trigger
"""
with pytest.raises(InvalidExtension):
trigger_loader._load_trigger("ahriman.core.sign.gpg.GPG")
def test_load_trigger_path(trigger_loader: TriggerLoader, resource_path_root: Path) -> None:
"""
must load trigger from path
"""
path = resource_path_root.parent.parent / "src" / "ahriman" / "core" / "report" / "report_trigger.py"
assert trigger_loader._load_trigger(f"{path}.ReportTrigger")
def test_load_trigger_path_not_found(trigger_loader: TriggerLoader) -> None:
"""
must raise InvalidExtension if file cannot be found
"""
with pytest.raises(InvalidExtension):
trigger_loader._load_trigger("/some/random/path.py.SomeRandomModule")
def test_process(trigger_loader: TriggerLoader, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must run triggers
"""
upload_mock = mocker.patch("ahriman.core.upload.UploadTrigger.run")
report_mock = mocker.patch("ahriman.core.report.ReportTrigger.run")
trigger_loader.process(Result(), [package_ahriman])
report_mock.assert_called_once_with(Result(), [package_ahriman])
upload_mock.assert_called_once_with(Result(), [package_ahriman])
def test_process_exception(trigger_loader: TriggerLoader, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must suppress exception during trigger run
"""
upload_mock = mocker.patch("ahriman.core.upload.UploadTrigger.run", side_effect=Exception())
report_mock = mocker.patch("ahriman.core.report.ReportTrigger.run")
log_mock = mocker.patch("logging.Logger.exception")
trigger_loader.process(Result(), [package_ahriman])
report_mock.assert_called_once_with(Result(), [package_ahriman])
upload_mock.assert_called_once_with(Result(), [package_ahriman])
log_mock.assert_called_once()

View File

@ -0,0 +1,17 @@
from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration
from ahriman.core.upload import UploadTrigger
from ahriman.models.result import Result
def test_run(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run report for specified targets
"""
configuration.set_option("upload", "target", "rsync")
run_mock = mocker.patch("ahriman.core.upload.Upload.run")
trigger = UploadTrigger("x86_64", configuration)
trigger.run(Result(), [])
run_mock.assert_called_once_with(configuration.repository_paths.repository, [])

View File

@ -22,6 +22,7 @@ build_command = extra-x86_64-build
ignore_packages =
makechrootpkg_flags =
makepkg_flags = --skippgpcheck
triggers = ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger
[repository]
name = aur-clone