mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-04-24 15:27:17 +00:00
triggers implementation (#62)
This commit is contained in:
parent
d98cfa3732
commit
99874845b5
@ -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 |
@ -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 ...]
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
-----------------------------------
|
||||
|
||||
|
@ -16,6 +16,7 @@ Subpackages
|
||||
ahriman.core.repository
|
||||
ahriman.core.sign
|
||||
ahriman.core.status
|
||||
ahriman.core.triggers
|
||||
ahriman.core.upload
|
||||
|
||||
Submodules
|
||||
|
29
docs/ahriman.core.triggers.rst
Normal file
29
docs/ahriman.core.triggers.rst
Normal 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:
|
@ -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
|
||||
---------------
|
||||
|
||||
|
@ -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
|
||||
|
@ -1,5 +1,5 @@
|
||||
Commands help
|
||||
=============
|
||||
Commands reference
|
||||
==================
|
||||
|
||||
ahriman
|
||||
-------
|
||||
|
@ -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.
|
||||
|
24
docs/faq.rst
24
docs/faq.rst
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
62
docs/triggers.rst
Normal 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.
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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]:
|
||||
"""
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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())
|
@ -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]:
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
@ -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:
|
||||
"""
|
||||
|
@ -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)
|
||||
|
21
src/ahriman/core/triggers/__init__.py
Normal file
21
src/ahriman/core/triggers/__init__.py
Normal 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
|
79
src/ahriman/core/triggers/trigger.py
Normal file
79
src/ahriman/core/triggers/trigger.py
Normal 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
|
159
src/ahriman/core/triggers/trigger_loader.py
Normal file
159
src/ahriman/core/triggers/trigger_loader.py
Normal 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)
|
@ -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
|
||||
|
@ -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:
|
||||
|
58
src/ahriman/core/upload/upload_trigger.py
Normal file
58
src/ahriman/core/upload/upload_trigger.py
Normal 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)
|
@ -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:
|
||||
|
@ -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:
|
||||
"""
|
||||
|
@ -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())
|
@ -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, [])
|
18
tests/ahriman/application/handlers/test_handler_triggers.py
Normal file
18
tests/ahriman/application/handlers/test_handler_triggers.py
Normal 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())
|
@ -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"]
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
17
tests/ahriman/core/report/test_report_trigger.py
Normal file
17
tests/ahriman/core/report/test_report_trigger.py
Normal 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(), [])
|
@ -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:
|
||||
|
31
tests/ahriman/core/triggers/conftest.py
Normal file
31
tests/ahriman/core/triggers/conftest.py
Normal 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)
|
12
tests/ahriman/core/triggers/test_trigger.py
Normal file
12
tests/ahriman/core/triggers/test_trigger.py
Normal 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(), [])
|
92
tests/ahriman/core/triggers/test_trigger_loader.py
Normal file
92
tests/ahriman/core/triggers/test_trigger_loader.py
Normal 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()
|
17
tests/ahriman/core/upload/test_upload_trigger.py
Normal file
17
tests/ahriman/core/upload/test_upload_trigger.py
Normal 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, [])
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user