packagers support

This commit is contained in:
Evgenii Alekseev 2023-06-01 18:24:16 +03:00
parent d495163fdd
commit 30b108531a
89 changed files with 849 additions and 318 deletions

View File

@ -58,7 +58,7 @@ systemd-machine-id-setup &> /dev/null
# otherwise we prepend executable by sudo command # otherwise we prepend executable by sudo command
if [ -n "$AHRIMAN_FORCE_ROOT" ]; then if [ -n "$AHRIMAN_FORCE_ROOT" ]; then
AHRIMAN_EXECUTABLE=("ahriman") AHRIMAN_EXECUTABLE=("ahriman")
elif ahriman help-commands-unsafe --command="$*" &> /dev/null; then elif ahriman help-commands-unsafe -- "$@" &> /dev/null; then
AHRIMAN_EXECUTABLE=("sudo" "-u" "$AHRIMAN_USER" "--" "ahriman") AHRIMAN_EXECUTABLE=("sudo" "-u" "$AHRIMAN_USER" "--" "ahriman")
else else
AHRIMAN_EXECUTABLE=("ahriman") AHRIMAN_EXECUTABLE=("ahriman")

View File

@ -1,4 +1,4 @@
.TH AHRIMAN "1" "2023\-05\-28" "ahriman" "Generated Python Manual" .TH AHRIMAN "1" "2023\-06\-03" "ahriman" "Generated Python Manual"
.SH NAME .SH NAME
ahriman ahriman
.SH SYNOPSIS .SH SYNOPSIS
@ -199,13 +199,12 @@ show help message for application or command and exit
show help message for specific command show help message for specific command
.SH COMMAND \fI\,'ahriman help\-commands\-unsafe'\/\fR .SH COMMAND \fI\,'ahriman help\-commands\-unsafe'\/\fR
usage: ahriman help\-commands\-unsafe [\-h] [\-\-command COMMAND] usage: ahriman help\-commands\-unsafe [\-h] [command ...]
list unsafe commands as defined in default args list unsafe commands as defined in default args
.SH OPTIONS \fI\,'ahriman help\-commands\-unsafe'\/\fR
.TP .TP
\fB\-\-command\fR \fI\,COMMAND\/\fR \fBcommand\fR
instead of showing commands, just test command line for unsafe subcommand and return 0 in case if command is safe and 1 instead of showing commands, just test command line for unsafe subcommand and return 0 in case if command is safe and 1
otherwise otherwise
@ -226,7 +225,7 @@ print application and its dependencies versions
.SH COMMAND \fI\,'ahriman package\-add'\/\fR .SH COMMAND \fI\,'ahriman package\-add'\/\fR
usage: ahriman package\-add [\-h] [\-\-dependencies | \-\-no\-dependencies] [\-e] [\-n] [\-y] usage: ahriman package\-add [\-h] [\-\-dependencies | \-\-no\-dependencies] [\-e] [\-n] [\-y]
[\-s {auto,archive,aur,directory,local,remote,repository}] [\-s {auto,archive,aur,directory,local,remote,repository}] [\-u USERNAME]
package [package ...] package [package ...]
add existing or new package to the build queue add existing or new package to the build queue
@ -256,6 +255,10 @@ download fresh package databases from the mirror before actions, \-yy to force r
\fB\-s\fR \fI\,{auto,archive,aur,directory,local,remote,repository}\/\fR, \fB\-\-source\fR \fI\,{auto,archive,aur,directory,local,remote,repository}\/\fR \fB\-s\fR \fI\,{auto,archive,aur,directory,local,remote,repository}\/\fR, \fB\-\-source\fR \fI\,{auto,archive,aur,directory,local,remote,repository}\/\fR
explicitly specify the package source for this command explicitly specify the package source for this command
.TP
\fB\-u\fR \fI\,USERNAME\/\fR, \fB\-\-username\fR \fI\,USERNAME\/\fR
build as user
.SH COMMAND \fI\,'ahriman package\-remove'\/\fR .SH COMMAND \fI\,'ahriman package\-remove'\/\fR
usage: ahriman package\-remove [\-h] package [package ...] usage: ahriman package\-remove [\-h] package [package ...]
@ -457,7 +460,7 @@ download fresh package databases from the mirror before actions, \-yy to force r
.SH COMMAND \fI\,'ahriman repo\-rebuild'\/\fR .SH COMMAND \fI\,'ahriman repo\-rebuild'\/\fR
usage: ahriman repo\-rebuild [\-h] [\-\-depends\-on DEPENDS_ON] [\-\-dry\-run] [\-\-from\-database] [\-e] usage: ahriman repo\-rebuild [\-h] [\-\-depends\-on DEPENDS_ON] [\-\-dry\-run] [\-\-from\-database] [\-e]
[\-s {unknown,pending,building,failed,success}] [\-s {unknown,pending,building,failed,success}] [\-u USERNAME]
force rebuild whole repository force rebuild whole repository
@ -484,6 +487,10 @@ return non\-zero exit status if result is empty
\fB\-s\fR \fI\,{unknown,pending,building,failed,success}\/\fR, \fB\-\-status\fR \fI\,{unknown,pending,building,failed,success}\/\fR \fB\-s\fR \fI\,{unknown,pending,building,failed,success}\/\fR, \fB\-\-status\fR \fI\,{unknown,pending,building,failed,success}\/\fR
filter packages by status. Requires \-\-from\-database to be set filter packages by status. Requires \-\-from\-database to be set
.TP
\fB\-u\fR \fI\,USERNAME\/\fR, \fB\-\-username\fR \fI\,USERNAME\/\fR
build as user
.SH COMMAND \fI\,'ahriman repo\-remove\-unknown'\/\fR .SH COMMAND \fI\,'ahriman repo\-remove\-unknown'\/\fR
usage: ahriman repo\-remove\-unknown [\-h] [\-\-dry\-run] usage: ahriman repo\-remove\-unknown [\-h] [\-\-dry\-run]
@ -553,7 +560,7 @@ instead of running all triggers as set by configuration, just process specified
.SH COMMAND \fI\,'ahriman repo\-update'\/\fR .SH COMMAND \fI\,'ahriman repo\-update'\/\fR
usage: ahriman repo\-update [\-h] [\-\-aur | \-\-no\-aur] [\-\-dependencies | \-\-no\-dependencies] [\-\-dry\-run] [\-e] usage: ahriman repo\-update [\-h] [\-\-aur | \-\-no\-aur] [\-\-dependencies | \-\-no\-dependencies] [\-\-dry\-run] [\-e]
[\-\-local | \-\-no\-local] [\-\-manual | \-\-no\-manual] [\-\-vcs | \-\-no\-vcs] [\-y] [\-\-local | \-\-no\-local] [\-\-manual | \-\-no\-manual] [\-u USERNAME] [\-\-vcs | \-\-no\-vcs] [\-y]
[package ...] [package ...]
check for packages updates and run build process if requested check for packages updates and run build process if requested
@ -587,6 +594,10 @@ enable or disable checking of local packages for updates
\fB\-\-manual\fR, \fB\-\-no\-manual\fR \fB\-\-manual\fR, \fB\-\-no\-manual\fR
include or exclude manual updates include or exclude manual updates
.TP
\fB\-u\fR \fI\,USERNAME\/\fR, \fB\-\-username\fR \fI\,USERNAME\/\fR
build as user
.TP .TP
\fB\-\-vcs\fR, \fB\-\-no\-vcs\fR \fB\-\-vcs\fR, \fB\-\-no\-vcs\fR
fetch actual version of VCS packages fetch actual version of VCS packages
@ -724,7 +735,8 @@ drop into python shell while having created application
instead of dropping into shell, just execute the specified code instead of dropping into shell, just execute the specified code
.SH COMMAND \fI\,'ahriman user\-add'\/\fR .SH COMMAND \fI\,'ahriman user\-add'\/\fR
usage: ahriman user\-add [\-h] [\-p PASSWORD] [\-r {unauthorized,read,reporter,full}] [\-s] username usage: ahriman user\-add [\-h] [\-\-key KEY] [\-\-packager PACKAGER] [\-p PASSWORD] [\-r {unauthorized,read,reporter,full}] [\-s]
username
update user for web services with the given password and role. In case if password was not entered it will be asked interactively update user for web services with the given password and role. In case if password was not entered it will be asked interactively
@ -733,6 +745,14 @@ update user for web services with the given password and role. In case if passwo
username for web service username for web service
.SH OPTIONS \fI\,'ahriman user\-add'\/\fR .SH OPTIONS \fI\,'ahriman user\-add'\/\fR
.TP
\fB\-\-key\fR \fI\,KEY\/\fR
optional PGP key used by this user. The private key must be imported
.TP
\fB\-\-packager\fR \fI\,PACKAGER\/\fR
optional packager id used for build process in form of `Name Surname <mail@example.com>`
.TP .TP
\fB\-p\fR \fI\,PASSWORD\/\fR, \fB\-\-password\fR \fI\,PASSWORD\/\fR \fB\-p\fR \fI\,PASSWORD\/\fR, \fB\-\-password\fR \fI\,PASSWORD\/\fR
user password. Blank password will be treated as empty password, which is in particular must be used for OAuth2 user password. Blank password will be treated as empty password, which is in particular must be used for OAuth2

View File

@ -68,6 +68,14 @@ ahriman.core.database.migrations.m007\_check\_depends module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.core.database.migrations.m008\_packagers module
-------------------------------------------------------
.. automodule:: ahriman.core.database.migrations.m008_packagers
:members:
:no-undoc-members:
:show-inheritance:
Module contents Module contents
--------------- ---------------

View File

@ -116,6 +116,14 @@ ahriman.models.package\_source module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.models.packagers module
-------------------------------
.. automodule:: ahriman.models.packagers
:members:
:no-undoc-members:
:show-inheritance:
ahriman.models.pacman\_synchronization module ahriman.models.pacman\_synchronization module
--------------------------------------------- ---------------------------------------------

View File

@ -6,13 +6,13 @@ _shtab_ahriman_option_strings=('-h' '--help' '-a' '--architecture' '-c' '--confi
_shtab_ahriman_aur_search_option_strings=('-h' '--help' '-e' '--exit-code' '--info' '--no-info' '--sort-by') _shtab_ahriman_aur_search_option_strings=('-h' '--help' '-e' '--exit-code' '--info' '--no-info' '--sort-by')
_shtab_ahriman_search_option_strings=('-h' '--help' '-e' '--exit-code' '--info' '--no-info' '--sort-by') _shtab_ahriman_search_option_strings=('-h' '--help' '-e' '--exit-code' '--info' '--no-info' '--sort-by')
_shtab_ahriman_help_option_strings=('-h' '--help') _shtab_ahriman_help_option_strings=('-h' '--help')
_shtab_ahriman_help_commands_unsafe_option_strings=('-h' '--help' '--command') _shtab_ahriman_help_commands_unsafe_option_strings=('-h' '--help')
_shtab_ahriman_help_updates_option_strings=('-h' '--help' '-e' '--exit-code') _shtab_ahriman_help_updates_option_strings=('-h' '--help' '-e' '--exit-code')
_shtab_ahriman_help_version_option_strings=('-h' '--help') _shtab_ahriman_help_version_option_strings=('-h' '--help')
_shtab_ahriman_version_option_strings=('-h' '--help') _shtab_ahriman_version_option_strings=('-h' '--help')
_shtab_ahriman_package_add_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '-n' '--now' '-y' '--refresh' '-s' '--source') _shtab_ahriman_package_add_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '-n' '--now' '-y' '--refresh' '-s' '--source' '-u' '--username')
_shtab_ahriman_add_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '-n' '--now' '-y' '--refresh' '-s' '--source') _shtab_ahriman_add_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '-n' '--now' '-y' '--refresh' '-s' '--source' '-u' '--username')
_shtab_ahriman_package_update_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '-n' '--now' '-y' '--refresh' '-s' '--source') _shtab_ahriman_package_update_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '-n' '--now' '-y' '--refresh' '-s' '--source' '-u' '--username')
_shtab_ahriman_package_remove_option_strings=('-h' '--help') _shtab_ahriman_package_remove_option_strings=('-h' '--help')
_shtab_ahriman_remove_option_strings=('-h' '--help') _shtab_ahriman_remove_option_strings=('-h' '--help')
_shtab_ahriman_package_status_option_strings=('-h' '--help' '--ahriman' '-e' '--exit-code' '--info' '--no-info' '-s' '--status') _shtab_ahriman_package_status_option_strings=('-h' '--help' '--ahriman' '-e' '--exit-code' '--info' '--no-info' '-s' '--status')
@ -31,8 +31,8 @@ _shtab_ahriman_repo_create_keyring_option_strings=('-h' '--help')
_shtab_ahriman_repo_create_mirrorlist_option_strings=('-h' '--help') _shtab_ahriman_repo_create_mirrorlist_option_strings=('-h' '--help')
_shtab_ahriman_repo_daemon_option_strings=('-h' '--help' '-i' '--interval' '--aur' '--no-aur' '--dependencies' '--no-dependencies' '--local' '--no-local' '--manual' '--no-manual' '--vcs' '--no-vcs' '-y' '--refresh') _shtab_ahriman_repo_daemon_option_strings=('-h' '--help' '-i' '--interval' '--aur' '--no-aur' '--dependencies' '--no-dependencies' '--local' '--no-local' '--manual' '--no-manual' '--vcs' '--no-vcs' '-y' '--refresh')
_shtab_ahriman_daemon_option_strings=('-h' '--help' '-i' '--interval' '--aur' '--no-aur' '--dependencies' '--no-dependencies' '--local' '--no-local' '--manual' '--no-manual' '--vcs' '--no-vcs' '-y' '--refresh') _shtab_ahriman_daemon_option_strings=('-h' '--help' '-i' '--interval' '--aur' '--no-aur' '--dependencies' '--no-dependencies' '--local' '--no-local' '--manual' '--no-manual' '--vcs' '--no-vcs' '-y' '--refresh')
_shtab_ahriman_repo_rebuild_option_strings=('-h' '--help' '--depends-on' '--dry-run' '--from-database' '-e' '--exit-code' '-s' '--status') _shtab_ahriman_repo_rebuild_option_strings=('-h' '--help' '--depends-on' '--dry-run' '--from-database' '-e' '--exit-code' '-s' '--status' '-u' '--username')
_shtab_ahriman_rebuild_option_strings=('-h' '--help' '--depends-on' '--dry-run' '--from-database' '-e' '--exit-code' '-s' '--status') _shtab_ahriman_rebuild_option_strings=('-h' '--help' '--depends-on' '--dry-run' '--from-database' '-e' '--exit-code' '-s' '--status' '-u' '--username')
_shtab_ahriman_repo_remove_unknown_option_strings=('-h' '--help' '--dry-run') _shtab_ahriman_repo_remove_unknown_option_strings=('-h' '--help' '--dry-run')
_shtab_ahriman_remove_unknown_option_strings=('-h' '--help' '--dry-run') _shtab_ahriman_remove_unknown_option_strings=('-h' '--help' '--dry-run')
_shtab_ahriman_repo_report_option_strings=('-h' '--help') _shtab_ahriman_repo_report_option_strings=('-h' '--help')
@ -45,8 +45,8 @@ _shtab_ahriman_repo_sync_option_strings=('-h' '--help')
_shtab_ahriman_sync_option_strings=('-h' '--help') _shtab_ahriman_sync_option_strings=('-h' '--help')
_shtab_ahriman_repo_tree_option_strings=('-h' '--help') _shtab_ahriman_repo_tree_option_strings=('-h' '--help')
_shtab_ahriman_repo_triggers_option_strings=('-h' '--help') _shtab_ahriman_repo_triggers_option_strings=('-h' '--help')
_shtab_ahriman_repo_update_option_strings=('-h' '--help' '--aur' '--no-aur' '--dependencies' '--no-dependencies' '--dry-run' '-e' '--exit-code' '--local' '--no-local' '--manual' '--no-manual' '--vcs' '--no-vcs' '-y' '--refresh') _shtab_ahriman_repo_update_option_strings=('-h' '--help' '--aur' '--no-aur' '--dependencies' '--no-dependencies' '--dry-run' '-e' '--exit-code' '--local' '--no-local' '--manual' '--no-manual' '-u' '--username' '--vcs' '--no-vcs' '-y' '--refresh')
_shtab_ahriman_update_option_strings=('-h' '--help' '--aur' '--no-aur' '--dependencies' '--no-dependencies' '--dry-run' '-e' '--exit-code' '--local' '--no-local' '--manual' '--no-manual' '--vcs' '--no-vcs' '-y' '--refresh') _shtab_ahriman_update_option_strings=('-h' '--help' '--aur' '--no-aur' '--dependencies' '--no-dependencies' '--dry-run' '-e' '--exit-code' '--local' '--no-local' '--manual' '--no-manual' '-u' '--username' '--vcs' '--no-vcs' '-y' '--refresh')
_shtab_ahriman_service_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman') _shtab_ahriman_service_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman')
_shtab_ahriman_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman') _shtab_ahriman_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman')
_shtab_ahriman_repo_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman') _shtab_ahriman_repo_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman')
@ -65,7 +65,7 @@ _shtab_ahriman_repo_setup_option_strings=('-h' '--help' '--build-as-user' '--bui
_shtab_ahriman_setup_option_strings=('-h' '--help' '--build-as-user' '--build-command' '--from-configuration' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--repository' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket') _shtab_ahriman_setup_option_strings=('-h' '--help' '--build-as-user' '--build-command' '--from-configuration' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--repository' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
_shtab_ahriman_service_shell_option_strings=('-h' '--help') _shtab_ahriman_service_shell_option_strings=('-h' '--help')
_shtab_ahriman_shell_option_strings=('-h' '--help') _shtab_ahriman_shell_option_strings=('-h' '--help')
_shtab_ahriman_user_add_option_strings=('-h' '--help' '-p' '--password' '-r' '--role' '-s' '--secure') _shtab_ahriman_user_add_option_strings=('-h' '--help' '--key' '--packager' '-p' '--password' '-r' '--role' '-s' '--secure')
_shtab_ahriman_user_list_option_strings=('-h' '--help' '-e' '--exit-code' '-r' '--role') _shtab_ahriman_user_list_option_strings=('-h' '--help' '-e' '--exit-code' '-r' '--role')
_shtab_ahriman_user_remove_option_strings=('-h' '--help') _shtab_ahriman_user_remove_option_strings=('-h' '--help')
_shtab_ahriman_web_option_strings=('-h' '--help') _shtab_ahriman_web_option_strings=('-h' '--help')
@ -133,6 +133,7 @@ _shtab_ahriman_search___info_nargs=0
_shtab_ahriman_search___no_info_nargs=0 _shtab_ahriman_search___no_info_nargs=0
_shtab_ahriman_help__h_nargs=0 _shtab_ahriman_help__h_nargs=0
_shtab_ahriman_help___help_nargs=0 _shtab_ahriman_help___help_nargs=0
_shtab_ahriman_help_commands_unsafe_pos_0_nargs=*
_shtab_ahriman_help_commands_unsafe__h_nargs=0 _shtab_ahriman_help_commands_unsafe__h_nargs=0
_shtab_ahriman_help_commands_unsafe___help_nargs=0 _shtab_ahriman_help_commands_unsafe___help_nargs=0
_shtab_ahriman_help_updates__h_nargs=0 _shtab_ahriman_help_updates__h_nargs=0

View File

@ -95,6 +95,7 @@ _shtab_ahriman_add_options=(
{-n,--now}"[run update function after]" {-n,--now}"[run update function after]"
"*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date]" "*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date]"
{-s,--source}"[explicitly specify the package source for this command]:source:(auto archive aur directory local remote repository)" {-s,--source}"[explicitly specify the package source for this command]:source:(auto archive aur directory local remote repository)"
{-u,--username}"[build as user]:username:"
"(*):package source (base name, path to local files, remote URL):" "(*):package source (base name, path to local files, remote URL):"
) )
@ -151,7 +152,7 @@ _shtab_ahriman_help_options=(
_shtab_ahriman_help_commands_unsafe_options=( _shtab_ahriman_help_commands_unsafe_options=(
"(- : *)"{-h,--help}"[show this help message and exit]" "(- : *)"{-h,--help}"[show this help message and exit]"
"--command[instead of showing commands, just test command line for unsafe subcommand and return 0 in case if command is safe and 1 otherwise]:command:" "(*)::instead of showing commands, just test command line for unsafe subcommand and return 0 in case if command is safe and 1 otherwise:"
) )
_shtab_ahriman_help_updates_options=( _shtab_ahriman_help_updates_options=(
@ -192,6 +193,7 @@ _shtab_ahriman_package_add_options=(
{-n,--now}"[run update function after]" {-n,--now}"[run update function after]"
"*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date]" "*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date]"
{-s,--source}"[explicitly specify the package source for this command]:source:(auto archive aur directory local remote repository)" {-s,--source}"[explicitly specify the package source for this command]:source:(auto archive aur directory local remote repository)"
{-u,--username}"[build as user]:username:"
"(*):package source (base name, path to local files, remote URL):" "(*):package source (base name, path to local files, remote URL):"
) )
@ -227,6 +229,7 @@ _shtab_ahriman_package_update_options=(
{-n,--now}"[run update function after]" {-n,--now}"[run update function after]"
"*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date]" "*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date]"
{-s,--source}"[explicitly specify the package source for this command]:source:(auto archive aur directory local remote repository)" {-s,--source}"[explicitly specify the package source for this command]:source:(auto archive aur directory local remote repository)"
{-u,--username}"[build as user]:username:"
"(*):package source (base name, path to local files, remote URL):" "(*):package source (base name, path to local files, remote URL):"
) )
@ -263,6 +266,7 @@ _shtab_ahriman_rebuild_options=(
"--from-database[read packages from database instead of filesystem. This feature in particular is required in case if you would like to restore repository from another repository instance. Note, however, that in order to restore packages you need to have original ahriman instance run with web service and have run repo-update at least once.]" "--from-database[read packages from database instead of filesystem. This feature in particular is required in case if you would like to restore repository from another repository instance. Note, however, that in order to restore packages you need to have original ahriman instance run with web service and have run repo-update at least once.]"
{-e,--exit-code}"[return non-zero exit status if result is empty]" {-e,--exit-code}"[return non-zero exit status if result is empty]"
{-s,--status}"[filter packages by status. Requires --from-database to be set]:status:(unknown pending building failed success)" {-s,--status}"[filter packages by status. Requires --from-database to be set]:status:(unknown pending building failed success)"
{-u,--username}"[build as user]:username:"
) )
_shtab_ahriman_remove_options=( _shtab_ahriman_remove_options=(
@ -349,6 +353,7 @@ _shtab_ahriman_repo_rebuild_options=(
"--from-database[read packages from database instead of filesystem. This feature in particular is required in case if you would like to restore repository from another repository instance. Note, however, that in order to restore packages you need to have original ahriman instance run with web service and have run repo-update at least once.]" "--from-database[read packages from database instead of filesystem. This feature in particular is required in case if you would like to restore repository from another repository instance. Note, however, that in order to restore packages you need to have original ahriman instance run with web service and have run repo-update at least once.]"
{-e,--exit-code}"[return non-zero exit status if result is empty]" {-e,--exit-code}"[return non-zero exit status if result is empty]"
{-s,--status}"[filter packages by status. Requires --from-database to be set]:status:(unknown pending building failed success)" {-s,--status}"[filter packages by status. Requires --from-database to be set]:status:(unknown pending building failed success)"
{-u,--username}"[build as user]:username:"
) )
_shtab_ahriman_repo_remove_unknown_options=( _shtab_ahriman_repo_remove_unknown_options=(
@ -413,6 +418,7 @@ _shtab_ahriman_repo_update_options=(
{-e,--exit-code}"[return non-zero exit status if result is empty]" {-e,--exit-code}"[return non-zero exit status if result is empty]"
{--local,--no-local}"[enable or disable checking of local packages for updates]:local:" {--local,--no-local}"[enable or disable checking of local packages for updates]:local:"
{--manual,--no-manual}"[include or exclude manual updates]:manual:" {--manual,--no-manual}"[include or exclude manual updates]:manual:"
{-u,--username}"[build as user]:username:"
{--vcs,--no-vcs}"[fetch actual version of VCS packages]:vcs:" {--vcs,--no-vcs}"[fetch actual version of VCS packages]:vcs:"
"*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date]" "*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date]"
"(*)::filter check by package base:" "(*)::filter check by package base:"
@ -529,6 +535,7 @@ _shtab_ahriman_update_options=(
{-e,--exit-code}"[return non-zero exit status if result is empty]" {-e,--exit-code}"[return non-zero exit status if result is empty]"
{--local,--no-local}"[enable or disable checking of local packages for updates]:local:" {--local,--no-local}"[enable or disable checking of local packages for updates]:local:"
{--manual,--no-manual}"[include or exclude manual updates]:manual:" {--manual,--no-manual}"[include or exclude manual updates]:manual:"
{-u,--username}"[build as user]:username:"
{--vcs,--no-vcs}"[fetch actual version of VCS packages]:vcs:" {--vcs,--no-vcs}"[fetch actual version of VCS packages]:vcs:"
"*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date]" "*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date]"
"(*)::filter check by package base:" "(*)::filter check by package base:"
@ -536,6 +543,8 @@ _shtab_ahriman_update_options=(
_shtab_ahriman_user_add_options=( _shtab_ahriman_user_add_options=(
"(- : *)"{-h,--help}"[show this help message and exit]" "(- : *)"{-h,--help}"[show this help message and exit]"
"--key[optional PGP key used by this user. The private key must be imported]:key:"
"--packager[optional packager id used for build process in form of \`Name Surname \<mail\@example.com\>\`]:packager:"
{-p,--password}"[user password. Blank password will be treated as empty password, which is in particular must be used for OAuth2 authorization type.]:password:" {-p,--password}"[user password. Blank password will be treated as empty password, which is in particular must be used for OAuth2 authorization type.]:password:"
{-r,--role}"[user access level]:role:(unauthorized read reporter full)" {-r,--role}"[user access level]:role:(unauthorized read reporter full)"
{-s,--secure}"[set file permissions to user-only]" {-s,--secure}"[set file permissions to user-only]"

View File

@ -87,7 +87,6 @@ Settings for signing packages or repository. Group name can refer to architectur
* ``target`` - configuration flag to enable signing, space separated list of strings, required. Allowed values are ``package`` (sign each package separately), ``repository`` (sign repository database file). * ``target`` - configuration flag to enable signing, space separated list of strings, required. Allowed values are ``package`` (sign each package separately), ``repository`` (sign repository database file).
* ``key`` - default PGP key, string, required. This key will also be used for database signing if enabled. * ``key`` - default PGP key, string, required. This key will also be used for database signing if enabled.
* ``key_*`` settings - PGP key which will be used for specific packages, string, optional. For example, if there is ``key_yay`` option the specified key will be used for yay package and default key for others.
``web:*`` groups ``web:*`` groups
---------------- ----------------

View File

@ -64,7 +64,7 @@ Initial setup
.. code-block:: shell .. code-block:: shell
echo 'Cmnd_Alias CARCHBUILD_CMD = /usr/local/bin/ahriman-x86_64-build *' | tee -a /etc/sudoers.d/ahriman echo 'Cmnd_Alias CARCHBUILD_CMD = /usr/local/bin/ahriman-x86_64-build *' | tee -a /etc/sudoers.d/ahriman
echo 'ahriman ALL=(ALL) NOPASSWD: CARCHBUILD_CMD' | tee -a /etc/sudoers.d/ahriman echo 'ahriman ALL=(ALL) NOPASSWD:SETENV: CARCHBUILD_CMD' | tee -a /etc/sudoers.d/ahriman
chmod 400 /etc/sudoers.d/ahriman chmod 400 /etc/sudoers.d/ahriman
This command supports several arguments, kindly refer to its help message. This command supports several arguments, kindly refer to its help message.

View File

@ -87,6 +87,7 @@
<th data-sortable="true" data-field="packages">packages</th> <th data-sortable="true" data-field="packages">packages</th>
<th data-sortable="true" data-visible="false" data-field="groups">groups</th> <th data-sortable="true" data-visible="false" data-field="groups">groups</th>
<th data-sortable="true" data-visible="false" data-field="licenses">licenses</th> <th data-sortable="true" data-visible="false" data-field="licenses">licenses</th>
<th data-sortable="true" data-visible="false" data-field="packager">packager</th>
<th data-sortable="true" data-field="timestamp">last update</th> <th data-sortable="true" data-field="timestamp">last update</th>
<th data-sortable="true" data-cell-style="statusFormat" data-field="status">status</th> <th data-sortable="true" data-cell-style="statusFormat" data-field="status">status</th>
</tr> </tr>

View File

@ -98,6 +98,7 @@
id: package_base, id: package_base,
base: web_url ? `<a href="${safe(web_url)}" title="${safe(package_base)}">${safe(package_base)}</a>` : safe(package_base), base: web_url ? `<a href="${safe(web_url)}" title="${safe(package_base)}">${safe(package_base)}</a>` : safe(package_base),
version: safe(description.package.version), version: safe(description.package.version),
packager: description.package.packager ? safe(description.package.packager) : "",
packages: listToTable(Object.keys(description.package.packages)), packages: listToTable(Object.keys(description.package.packages)),
groups: listToTable(extractListProperties(description.package, "groups")), groups: listToTable(extractListProperties(description.package, "groups")),
licenses: listToTable(extractListProperties(description.package, "licenses")), licenses: listToTable(extractListProperties(description.package, "licenses")),
@ -120,8 +121,8 @@
table.bootstrapTable("hideLoading"); table.bootstrapTable("hideLoading");
} else { } else {
// other errors // other errors
const messaga = error => { return `Could not load list of packages: ${error}`; }; const message = error => { return `Could not load list of packages: ${error}`; };
showFailure("Load failure", messaga, jqXHR, errorThrown); showFailure("Load failure", message, jqXHR, errorThrown);
} }
hideControls(true); hideControls(true);
}, },

View File

@ -27,7 +27,7 @@ from typing import TypeVar
from ahriman import version from ahriman import version
from ahriman.application import handlers from ahriman.application import handlers
from ahriman.core.util import enum_values from ahriman.core.util import enum_values, extract_user
from ahriman.models.action import Action from ahriman.models.action import Action
from ahriman.models.build_status import BuildStatusEnum from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.log_handler import LogHandler from ahriman.models.log_handler import LogHandler
@ -187,8 +187,8 @@ def _set_help_commands_unsafe_parser(root: SubParserAction) -> argparse.Argument
""" """
parser = root.add_parser("help-commands-unsafe", help="list unsafe commands", parser = root.add_parser("help-commands-unsafe", help="list unsafe commands",
description="list unsafe commands as defined in default args", formatter_class=_formatter) description="list unsafe commands as defined in default args", formatter_class=_formatter)
parser.add_argument("--command", help="instead of showing commands, just test command line for unsafe subcommand " parser.add_argument("command", help="instead of showing commands, just test command line for unsafe subcommand "
"and return 0 in case if command is safe and 1 otherwise") "and return 0 in case if command is safe and 1 otherwise", nargs="*")
parser.set_defaults(handler=handlers.UnsafeCommands, architecture=[""], lock=None, report=False, quiet=True, parser.set_defaults(handler=handlers.UnsafeCommands, architecture=[""], lock=None, report=False, quiet=True,
unsafe=True, parser=_parser) unsafe=True, parser=_parser)
return parser return parser
@ -262,6 +262,7 @@ def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
action="count", default=False) action="count", default=False)
parser.add_argument("-s", "--source", help="explicitly specify the package source for this command", parser.add_argument("-s", "--source", help="explicitly specify the package source for this command",
type=PackageSource, choices=enum_values(PackageSource), default=PackageSource.Auto) type=PackageSource, choices=enum_values(PackageSource), default=PackageSource.Auto)
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
parser.set_defaults(handler=handlers.Add) parser.set_defaults(handler=handlers.Add)
return parser return parser
@ -481,7 +482,8 @@ def _set_repo_check_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, " parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, "
"-yy to force refresh even if up to date", "-yy to force refresh even if up to date",
action="count", default=False) action="count", default=False)
parser.set_defaults(handler=handlers.Update, dependencies=False, dry_run=True, aur=True, local=True, manual=False) parser.set_defaults(handler=handlers.Update, dependencies=False, dry_run=True, aur=True, local=True, manual=False,
username=None)
return parser return parser
@ -578,6 +580,7 @@ def _set_repo_rebuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true") parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.add_argument("-s", "--status", help="filter packages by status. Requires --from-database to be set", parser.add_argument("-s", "--status", help="filter packages by status. Requires --from-database to be set",
type=BuildStatusEnum, choices=enum_values(BuildStatusEnum)) type=BuildStatusEnum, choices=enum_values(BuildStatusEnum))
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
parser.set_defaults(handler=handlers.Rebuild) parser.set_defaults(handler=handlers.Rebuild)
return parser return parser
@ -752,6 +755,7 @@ def _set_repo_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
action=argparse.BooleanOptionalAction, default=True) action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--manual", help="include or exclude manual updates", parser.add_argument("--manual", help="include or exclude manual updates",
action=argparse.BooleanOptionalAction, default=True) action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
parser.add_argument("--vcs", help="fetch actual version of VCS packages", parser.add_argument("--vcs", help="fetch actual version of VCS packages",
action=argparse.BooleanOptionalAction, default=True) action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, " parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, "
@ -923,6 +927,9 @@ def _set_user_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
"root privileges because it performs write to filesystem configuration.", "root privileges because it performs write to filesystem configuration.",
formatter_class=_formatter) formatter_class=_formatter)
parser.add_argument("username", help="username for web service") parser.add_argument("username", help="username for web service")
parser.add_argument("--key", help="optional PGP key used by this user. The private key must be imported")
parser.add_argument("--packager", help="optional packager id used for build process in form of "
"`Name Surname <mail@example.com>`")
parser.add_argument("-p", "--password", help="user password. Blank password will be treated as empty password, " parser.add_argument("-p", "--password", help="user password. Blank password will be treated as empty password, "
"which is in particular must be used for OAuth2 authorization type.") "which is in particular must be used for OAuth2 authorization type.")
parser.add_argument("-r", "--role", help="user access level", parser.add_argument("-r", "--role", help="user access level",
@ -949,8 +956,8 @@ def _set_user_list_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("username", help="filter users by username", nargs="?") parser.add_argument("username", help="filter users by username", nargs="?")
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true") parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.add_argument("-r", "--role", help="filter users by role", type=UserAccess, choices=enum_values(UserAccess)) parser.add_argument("-r", "--role", help="filter users by role", type=UserAccess, choices=enum_values(UserAccess))
parser.set_defaults(handler=handlers.Users, action=Action.List, architecture=[""], lock=None, report=False, # nosec parser.set_defaults(handler=handlers.Users, action=Action.List, architecture=[""], lock=None, report=False,
password="", quiet=True, unsafe=True) quiet=True, unsafe=True)
return parser return parser
@ -968,8 +975,8 @@ def _set_user_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
description="remove user from the user mapping and update the configuration", description="remove user from the user mapping and update the configuration",
formatter_class=_formatter) formatter_class=_formatter)
parser.add_argument("username", help="username for web service") parser.add_argument("username", help="username for web service")
parser.set_defaults(handler=handlers.Users, action=Action.Remove, architecture=[""], lock=None, report=False, # nosec parser.set_defaults(handler=handlers.Users, action=Action.Remove, architecture=[""], lock=None, report=False,
password="", quiet=True) quiet=True)
return parser return parser

View File

@ -39,7 +39,7 @@ class Application(ApplicationPackages, ApplicationRepository):
>>> configuration = Configuration() >>> configuration = Configuration()
>>> application = Application("x86_64", configuration, report=True, unsafe=False) >>> application = Application("x86_64", configuration, report=True, unsafe=False)
>>> # add packages to build queue >>> # add packages to build queue
>>> application.add(["ahriman"], PackageSource.AUR, without_dependencies=False) >>> application.add(["ahriman"], PackageSource.AUR)
>>> >>>
>>> # check for updates >>> # check for updates
>>> updates = application.updates([], aur=True, local=True, manual=True, vcs=True, log_fn=print) >>> updates = application.updates([], aur=True, local=True, manual=True, vcs=True, log_fn=print)
@ -96,21 +96,25 @@ class Application(ApplicationPackages, ApplicationRepository):
Args: Args:
packages(list[Package]): list of source packages of which dependencies have to be processed packages(list[Package]): list of source packages of which dependencies have to be processed
process_dependencies(bool): if no set, dependencies will not be processed process_dependencies(bool): if no set, dependencies will not be processed
Returns:
list[Package]: updated packages list. Packager for dependencies will be copied from
original package
""" """
def missing_dependencies(source: Iterable[Package]) -> set[str]: def missing_dependencies(source: Iterable[Package]) -> dict[str, str | None]:
# build initial list of dependencies # append list of known packages with packages which are in current sources
result = set() satisfied_packages = known_packages | {
for package in source: single
result.update(package.depends_build) for package in source
for single in package.packages_full
}
# remove ones which are already well-known return {
result = result.difference(known_packages) dependency: package.packager
for package in source
# remove ones which are in this list already for dependency in package.depends_build
for package in source: if dependency not in satisfied_packages
result = result.difference(package.packages_full) }
return result
if not process_dependencies or not packages: if not process_dependencies or not packages:
return packages return packages
@ -119,8 +123,8 @@ class Application(ApplicationPackages, ApplicationRepository):
with_dependencies = {package.base: package for package in packages} with_dependencies = {package.base: package for package in packages}
while missing := missing_dependencies(with_dependencies.values()): while missing := missing_dependencies(with_dependencies.values()):
for package_name in missing: for package_name, username in missing.items():
package = Package.from_aur(package_name, self.repository.pacman) package = Package.from_aur(package_name, self.repository.pacman, username)
with_dependencies[package.base] = package with_dependencies[package.base] = package
return list(with_dependencies.values()) return list(with_dependencies.values())

View File

@ -55,15 +55,15 @@ class ApplicationPackages(ApplicationProperties):
dst = self.repository.paths.packages / local_path.name dst = self.repository.paths.packages / local_path.name
shutil.copy(local_path, dst) shutil.copy(local_path, dst)
def _add_aur(self, source: str) -> None: def _add_aur(self, source: str, username: str | None) -> None:
""" """
add package from AUR add package from AUR
Args: Args:
source(str): package base name source(str): package base name
username(str | None): optional override of username for build process
""" """
package = Package.from_aur(source, self.repository.pacman) package = Package.from_aur(source, self.repository.pacman, username)
self.database.build_queue_insert(package) self.database.build_queue_insert(package)
self.database.remote_update(package) self.database.remote_update(package)
@ -81,23 +81,24 @@ class ApplicationPackages(ApplicationProperties):
for full_path in filter(package_like, local_dir.iterdir()): for full_path in filter(package_like, local_dir.iterdir()):
self._add_archive(str(full_path)) self._add_archive(str(full_path))
def _add_local(self, source: str) -> None: def _add_local(self, source: str, username: str | None) -> None:
""" """
add package from local PKGBUILDs add package from local PKGBUILDs
Args: Args:
source(str): path to directory with local source files source(str): path to directory with local source files
username(str | None): optional override of username for build process
Raises: Raises:
UnknownPackageError: if specified package is unknown or doesn't exist UnknownPackageError: if specified package is unknown or doesn't exist
""" """
if (source_dir := Path(source)).is_dir(): if (source_dir := Path(source)).is_dir():
package = Package.from_build(source_dir, self.architecture) package = Package.from_build(source_dir, self.architecture, username)
cache_dir = self.repository.paths.cache_for(package.base) cache_dir = self.repository.paths.cache_for(package.base)
shutil.copytree(source_dir, cache_dir) # copy package to store in caches shutil.copytree(source_dir, cache_dir) # copy package to store in caches
Sources.init(cache_dir) # we need to run init command in directory where we do have permissions Sources.init(cache_dir) # we need to run init command in directory where we do have permissions
elif (source_dir := self.repository.paths.cache_for(source)).is_dir(): elif (source_dir := self.repository.paths.cache_for(source)).is_dir():
package = Package.from_build(source_dir, self.architecture) package = Package.from_build(source_dir, self.architecture, username)
else: else:
raise UnknownPackageError(source) raise UnknownPackageError(source)
@ -122,29 +123,31 @@ class ApplicationPackages(ApplicationProperties):
for chunk in response.iter_content(chunk_size=1024): for chunk in response.iter_content(chunk_size=1024):
local_file.write(chunk) local_file.write(chunk)
def _add_repository(self, source: str, *_: Any) -> None: def _add_repository(self, source: str, username: str | None) -> None:
""" """
add package from official repository add package from official repository
Args: Args:
source(str): package base name source(str): package base name
username(str | None): optional override of username for build process
""" """
package = Package.from_official(source, self.repository.pacman) package = Package.from_official(source, self.repository.pacman, username)
self.database.build_queue_insert(package) self.database.build_queue_insert(package)
self.database.remote_update(package) self.database.remote_update(package)
def add(self, names: Iterable[str], source: PackageSource) -> None: def add(self, names: Iterable[str], source: PackageSource, username: str | None = None) -> None:
""" """
add packages for the next build add packages for the next build
Args: Args:
names(Iterable[str]): list of package bases to add names(Iterable[str]): list of package bases to add
source(PackageSource): package source to add source(PackageSource): package source to add
username(str | None, optional): optional override of username for build process (Default value = None)
""" """
for name in names: for name in names:
resolved_source = source.resolve(name) resolved_source = source.resolve(name)
fn = getattr(self, f"_add_{resolved_source.value}") fn = getattr(self, f"_add_{resolved_source.value}")
fn(name) fn(name, username)
def on_result(self, result: Result) -> None: def on_result(self, result: Result) -> None:
""" """

View File

@ -25,6 +25,7 @@ from ahriman.core.build_tools.sources import Sources
from ahriman.core.formatters import UpdatePrinter from ahriman.core.formatters import UpdatePrinter
from ahriman.core.tree import Tree from ahriman.core.tree import Tree
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.packagers import Packagers
from ahriman.models.result import Result from ahriman.models.result import Result
@ -83,7 +84,7 @@ class ApplicationRepository(ApplicationProperties):
if archive.filepath is None: if archive.filepath is None:
self.logger.warning("filepath is empty for %s", package.base) self.logger.warning("filepath is empty for %s", package.base)
continue # avoid mypy warning continue # avoid mypy warning
self.repository.sign.process_sign_package(archive.filepath, package.base) self.repository.sign.process_sign_package(archive.filepath, None)
# sign repository database if set # sign repository database if set
self.repository.sign.process_sign_repository(self.repository.repo.repo_path) self.repository.sign.process_sign_repository(self.repository.repo.repo_path)
# process triggers # process triggers
@ -104,14 +105,14 @@ class ApplicationRepository(ApplicationProperties):
packages: list[str] = [] packages: list[str] = []
for single in probe.packages: for single in probe.packages:
try: try:
_ = Package.from_aur(single, self.repository.pacman) _ = Package.from_aur(single, self.repository.pacman, None)
except Exception: except Exception:
packages.append(single) packages.append(single)
return packages return packages
def unknown_local(probe: Package) -> list[str]: def unknown_local(probe: Package) -> list[str]:
cache_dir = self.repository.paths.cache_for(probe.base) cache_dir = self.repository.paths.cache_for(probe.base)
local = Package.from_build(cache_dir, self.architecture) local = Package.from_build(cache_dir, self.architecture, None)
packages = set(probe.packages.keys()).difference(local.packages.keys()) packages = set(probe.packages.keys()).difference(local.packages.keys())
return list(packages) return list(packages)
@ -123,12 +124,14 @@ class ApplicationRepository(ApplicationProperties):
result.extend(unknown_aur(package)) # local package not found result.extend(unknown_aur(package)) # local package not found
return result return result
def update(self, updates: Iterable[Package]) -> Result: def update(self, updates: Iterable[Package], packagers: Packagers | None = None) -> Result:
""" """
run package updates run package updates
Args: Args:
updates(Iterable[Package]): list of packages to update updates(Iterable[Package]): list of packages to update
packagers(Packagers | None, optional): optional override of username for build process
(Default value = None)
Returns: Returns:
Result: update result Result: update result
@ -136,7 +139,7 @@ class ApplicationRepository(ApplicationProperties):
def process_update(paths: Iterable[Path], result: Result) -> None: def process_update(paths: Iterable[Path], result: Result) -> None:
if not paths: if not paths:
return # don't need to process if no update supplied return # don't need to process if no update supplied
update_result = self.repository.process_update(paths) update_result = self.repository.process_update(paths, packagers)
self.on_result(result.merge(update_result)) self.on_result(result.merge(update_result))
# process built packages # process built packages
@ -148,7 +151,7 @@ class ApplicationRepository(ApplicationProperties):
tree = Tree.resolve(updates) tree = Tree.resolve(updates)
for num, level in enumerate(tree): for num, level in enumerate(tree):
self.logger.info("processing level #%i %s", num, [package.base for package in level]) self.logger.info("processing level #%i %s", num, [package.base for package in level])
build_result = self.repository.process_build(level) build_result = self.repository.process_build(level, packagers)
packages = self.repository.packages_built() packages = self.repository.packages_built()
process_update(packages, build_result) process_update(packages, build_result)

View File

@ -22,6 +22,7 @@ import argparse
from ahriman.application.application import Application from ahriman.application.application import Application
from ahriman.application.handlers import Handler from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.models.packagers import Packagers
class Add(Handler): class Add(Handler):
@ -45,12 +46,14 @@ class Add(Handler):
application = Application(architecture, configuration, application = Application(architecture, configuration,
report=report, unsafe=unsafe, refresh_pacman_database=args.refresh) report=report, unsafe=unsafe, refresh_pacman_database=args.refresh)
application.on_start() application.on_start()
application.add(args.package, args.source) application.add(args.package, args.source, args.username)
if not args.now: if not args.now:
return return
packages = application.updates(args.package, aur=False, local=False, manual=True, vcs=False, packages = application.updates(args.package, aur=False, local=False, manual=True, vcs=False,
log_fn=application.logger.info) log_fn=application.logger.info)
packages = application.with_dependencies(packages, process_dependencies=args.dependencies) packages = application.with_dependencies(packages, process_dependencies=args.dependencies)
result = application.update(packages) packagers = Packagers(args.username, {package.base: package.packager for package in packages})
result = application.update(packages, packagers)
Add.check_if_empty(args.exit_code, result.is_empty) Add.check_if_empty(args.exit_code, result.is_empty)

View File

@ -78,7 +78,7 @@ class Patch(Handler):
tuple[str, PkgbuildPatch]: package base and created PKGBUILD patch based on the diff from master HEAD tuple[str, PkgbuildPatch]: package base and created PKGBUILD patch based on the diff from master HEAD
to current files to current files
""" """
package = Package.from_build(sources_dir, architecture) package = Package.from_build(sources_dir, architecture, None)
patch = Sources.patch_create(sources_dir, *track) patch = Sources.patch_create(sources_dir, *track)
return package.base, PkgbuildPatch(None, patch) return package.base, PkgbuildPatch(None, patch)

View File

@ -57,7 +57,7 @@ class Rebuild(Handler):
UpdatePrinter(package, package.version).print(verbose=True) UpdatePrinter(package, package.version).print(verbose=True)
return return
result = application.update(updates) result = application.update(updates, args.username)
Rebuild.check_if_empty(args.exit_code, result.is_empty) Rebuild.check_if_empty(args.exit_code, result.is_empty)
@staticmethod @staticmethod

View File

@ -49,7 +49,7 @@ class ServiceUpdates(Handler):
""" """
application = Application(architecture, configuration, report=report, unsafe=unsafe) application = Application(architecture, configuration, report=report, unsafe=unsafe)
remote = Package.from_aur("ahriman", application.repository.pacman) remote = Package.from_aur("ahriman", application.repository.pacman, None)
release = remote.version.rsplit("-", 1)[-1] # we don't store pkgrel locally, so we just append it release = remote.version.rsplit("-", 1)[-1] # we don't store pkgrel locally, so we just append it
local_version = f"{version.__version__}-{release}" local_version = f"{version.__version__}-{release}"

View File

@ -213,7 +213,7 @@ class Setup(Handler):
""" """
command = Setup.build_command(paths.root, prefix, architecture) command = Setup.build_command(paths.root, prefix, architecture)
sudoers_file = Setup.build_command(Setup.SUDOERS_DIR_PATH, prefix, architecture) sudoers_file = Setup.build_command(Setup.SUDOERS_DIR_PATH, prefix, architecture)
sudoers_file.write_text(f"ahriman ALL=(ALL) NOPASSWD: {command} *\n", encoding="utf8") sudoers_file.write_text(f"ahriman ALL=(ALL) NOPASSWD:SETENV: {command} *\n", encoding="utf8")
sudoers_file.chmod(0o400) # security! sudoers_file.chmod(0o400) # security!
@staticmethod @staticmethod

View File

@ -18,7 +18,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
import argparse import argparse
import shlex
from ahriman.application.handlers import Handler from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
@ -47,14 +46,14 @@ class UnsafeCommands(Handler):
""" """
parser = args.parser() parser = args.parser()
unsafe_commands = UnsafeCommands.get_unsafe_commands(parser) unsafe_commands = UnsafeCommands.get_unsafe_commands(parser)
if args.command is None: if args.command:
UnsafeCommands.check_unsafe(args.command, unsafe_commands, parser)
else:
for command in unsafe_commands: for command in unsafe_commands:
StringPrinter(command).print(verbose=True) StringPrinter(command).print(verbose=True)
else:
UnsafeCommands.check_unsafe(args.command, unsafe_commands, parser)
@staticmethod @staticmethod
def check_unsafe(command: str, unsafe_commands: list[str], parser: argparse.ArgumentParser) -> None: def check_unsafe(command: list[str], unsafe_commands: list[str], parser: argparse.ArgumentParser) -> None:
""" """
check if command is unsafe check if command is unsafe
@ -63,7 +62,7 @@ class UnsafeCommands(Handler):
unsafe_commands(list[str]): list of unsafe commands unsafe_commands(list[str]): list of unsafe commands
parser(argparse.ArgumentParser): generated argument parser parser(argparse.ArgumentParser): generated argument parser
""" """
args = parser.parse_args(shlex.split(command)) args = parser.parse_args(command)
UnsafeCommands.check_if_empty(True, args.command in unsafe_commands) UnsafeCommands.check_if_empty(True, args.command in unsafe_commands)
@staticmethod @staticmethod

View File

@ -24,6 +24,7 @@ from collections.abc import Callable
from ahriman.application.application import Application from ahriman.application.application import Application
from ahriman.application.handlers import Handler from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.models.packagers import Packagers
class Update(Handler): class Update(Handler):
@ -54,7 +55,9 @@ class Update(Handler):
return return
packages = application.with_dependencies(packages, process_dependencies=args.dependencies) packages = application.with_dependencies(packages, process_dependencies=args.dependencies)
result = application.update(packages) packagers = Packagers(args.username, {package.base: package.packager for package in packages})
result = application.update(packages, packagers)
Update.check_if_empty(args.exit_code, result.is_empty) Update.check_if_empty(args.exit_code, result.is_empty)
@staticmethod @staticmethod

View File

@ -156,4 +156,5 @@ class Users(Handler):
if password is None: if password is None:
password = read_password() password = read_password()
return User(username=args.username, password=password, access=args.role) return User(username=args.username, password=password, access=args.role,
packager_id=args.packager, key=args.key)

View File

@ -59,12 +59,13 @@ class Task(LazyLogging):
self.makepkg_flags = configuration.getlist("build", "makepkg_flags", fallback=[]) self.makepkg_flags = configuration.getlist("build", "makepkg_flags", fallback=[])
self.makechrootpkg_flags = configuration.getlist("build", "makechrootpkg_flags", fallback=[]) self.makechrootpkg_flags = configuration.getlist("build", "makechrootpkg_flags", fallback=[])
def build(self, sources_dir: Path) -> list[Path]: def build(self, sources_dir: Path, packager: str | None = None) -> list[Path]:
""" """
run package build run package build
Args: Args:
sources_dir(Path): path to where sources are sources_dir(Path): path to where sources are
packager(str | None, optional): optional packager override (Default value = None)
Returns: Returns:
list[Path]: paths of produced packages list[Path]: paths of produced packages
@ -75,12 +76,18 @@ class Task(LazyLogging):
command.extend(["--"] + self.makepkg_flags) command.extend(["--"] + self.makepkg_flags)
self.logger.info("using %s for %s", command, self.package.base) self.logger.info("using %s for %s", command, self.package.base)
environment: dict[str, str] = {}
if packager is not None:
environment["PACKAGER"] = packager
self.logger.info("using environment variables %s", environment)
Task._check_output( Task._check_output(
*command, *command,
exception=BuildError(self.package.base), exception=BuildError(self.package.base),
cwd=sources_dir, cwd=sources_dir,
logger=self.logger, logger=self.logger,
user=self.uid) user=self.uid,
environment=environment)
# well it is not actually correct, but we can deal with it # well it is not actually correct, but we can deal with it
packages = Task._check_output( packages = Task._check_output(

View File

@ -191,10 +191,6 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"sign": { "sign": {
"type": "dict", "type": "dict",
"allow_unknown": True, "allow_unknown": True,
"keysrules": {
"type": "string",
"anyof_regex": ["^target$", "^key$", "^key_.*"],
},
"schema": { "schema": {
"target": { "target": {
"type": "list", "type": "list",

View File

@ -0,0 +1,85 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from sqlite3 import Connection
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.configuration import Configuration
from ahriman.core.util import package_like
from ahriman.models.package import Package
from ahriman.models.pacman_synchronization import PacmanSynchronization
__all__ = ["migrate_data", "steps"]
steps = [
"""
alter table users add column packager_id
""",
"""
alter table users add column key_id
""",
"""
alter table package_bases add column packager
""",
]
def migrate_data(connection: Connection, configuration: Configuration) -> None:
"""
perform data migration
Args:
connection(Connection): database connection
configuration(Configuration): configuration instance
"""
migrate_package_base_packager(connection, configuration)
def migrate_package_base_packager(connection: Connection, configuration: Configuration) -> None:
"""
migrate package packager field
Args:
connection(Connection): database connection
configuration(Configuration): configuration instance
"""
if not configuration.repository_paths.repository.is_dir():
return
_, architecture = configuration.check_loaded()
pacman = Pacman(architecture, configuration, refresh_database=PacmanSynchronization.Disabled)
package_list = []
for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()):
package = Package.from_archive(full_path, pacman, remote=None)
package_list.append({
"package_base": package.base,
"packager": package.packager,
})
connection.executemany(
"""
update package_bases set
packager = :packager
where package_base = :package_base
""",
package_list
)

View File

@ -57,8 +57,9 @@ class AuthOperations(Operations):
def run(connection: Connection) -> list[User]: def run(connection: Connection) -> list[User]:
return [ return [
User(username=cursor["username"], password=cursor["password"], access=UserAccess(cursor["access"])) User(username=row["username"], password=row["password"], access=UserAccess(row["access"]),
for cursor in connection.execute( packager_id=row["packager_id"], key=row["key_id"])
for row in connection.execute(
""" """
select * from users select * from users
where (:username is null or username = :username) and (:access is null or access = :access) where (:username is null or username = :username) and (:access is null or access = :access)
@ -91,12 +92,13 @@ class AuthOperations(Operations):
connection.execute( connection.execute(
""" """
insert into users insert into users
(username, access, password) (username, access, password, packager_id, key_id)
values values
(:username, :access, :password) (:username, :access, :password, :packager_id, :key_id)
on conflict (username) do update set on conflict (username) do update set
access = :access, password = :password access = :access, password = :password, packager_id = :packager_id, key_id = :key_id
""", """,
{"username": user.username.lower(), "access": user.access.value, "password": user.password}) {"username": user.username.lower(), "access": user.access.value, "password": user.password,
"packager_id": user.packager_id, "key_id": user.key})
self.with_connection(run, commit=True) self.with_connection(run, commit=True)

View File

@ -76,11 +76,12 @@ class PackageOperations(Operations):
connection.execute( connection.execute(
""" """
insert into package_bases insert into package_bases
(package_base, version, source, branch, git_url, path, web_url) (package_base, version, source, branch, git_url, path, web_url, packager)
values values
(:package_base, :version, :source, :branch, :git_url, :path, :web_url) (:package_base, :version, :source, :branch, :git_url, :path, :web_url, :packager)
on conflict (package_base) do update set on conflict (package_base) do update set
version = :version, branch = :branch, git_url = :git_url, path = :path, web_url = :web_url, source = :source version = :version, branch = :branch, git_url = :git_url, path = :path, web_url = :web_url,
source = :source, packager = :packager
""", """,
{ {
"package_base": package.base, "package_base": package.base,
@ -90,6 +91,7 @@ class PackageOperations(Operations):
"path": package.remote.path if package.remote is not None else None, "path": package.remote.path if package.remote is not None else None,
"web_url": package.remote.web_url if package.remote is not None else None, "web_url": package.remote.web_url if package.remote is not None else None,
"source": package.remote.source.value if package.remote is not None else None, "source": package.remote.source.value if package.remote is not None else None,
"packager": package.packager,
} }
) )
@ -163,8 +165,9 @@ class PackageOperations(Operations):
base=row["package_base"], base=row["package_base"],
version=row["version"], version=row["version"],
remote=RemoteSource.from_json(row), remote=RemoteSource.from_json(row),
packages={}) packages={},
for row in connection.execute("""select * from package_bases""") packager=row["packager"] or None,
) for row in connection.execute("""select * from package_bases""")
} }
@staticmethod @staticmethod

View File

@ -77,8 +77,8 @@ class PatchOperations(Operations):
""" """
def run(connection: Connection) -> list[tuple[str, PkgbuildPatch]]: def run(connection: Connection) -> list[tuple[str, PkgbuildPatch]]:
return [ return [
(cursor["package_base"], PkgbuildPatch(cursor["variable"], cursor["patch"])) (row["package_base"], PkgbuildPatch(row["variable"], row["patch"]))
for cursor in connection.execute( for row in connection.execute(
"""select * from patches where :package_base is null or package_base = :package_base""", """select * from patches where :package_base is null or package_base = :package_base""",
{"package_base": package_base}) {"package_base": package_base})
] ]

View File

@ -44,13 +44,13 @@ class RemotePush(LazyLogging):
remote_source(RemoteSource): repository remote source (remote pull url and branch) remote_source(RemoteSource): repository remote source (remote pull url and branch)
""" """
def __init__(self, configuration: Configuration, database: SQLite, section: str) -> None: def __init__(self, database: SQLite, configuration: Configuration, section: str) -> None:
""" """
default constructor default constructor
Args: Args:
configuration(Configuration): configuration instance
database(SQLite): database instance database(SQLite): database instance
configuration(Configuration): configuration instance
section(str): settings section name section(str): settings section name
""" """
self.database = database self.database = database

View File

@ -105,5 +105,5 @@ class RemotePushTrigger(Trigger):
for target in self.targets: for target in self.targets:
section, _ = self.configuration.gettype( section, _ = self.configuration.gettype(
target, self.architecture, fallback=self.CONFIGURATION_SCHEMA_FALLBACK) target, self.architecture, fallback=self.CONFIGURATION_SCHEMA_FALLBACK)
runner = RemotePush(self.configuration, database, section) runner = RemotePush(database, self.configuration, section)
runner.run(result) runner.run(result)

View File

@ -28,6 +28,7 @@ from ahriman.core.repository.cleaner import Cleaner
from ahriman.core.util import safe_filename from ahriman.core.util import safe_filename
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.package_description import PackageDescription from ahriman.models.package_description import PackageDescription
from ahriman.models.packagers import Packagers
from ahriman.models.result import Result from ahriman.models.result import Result
@ -63,30 +64,35 @@ class Executor(Cleaner):
""" """
raise NotImplementedError raise NotImplementedError
def process_build(self, updates: Iterable[Package]) -> Result: def process_build(self, updates: Iterable[Package], packagers: Packagers | None = None) -> Result:
""" """
build packages build packages
Args: Args:
updates(Iterable[Package]): list of packages properties to build updates(Iterable[Package]): list of packages properties to build
packagers(Packagers | None, optional): optional override of username for build process
(Default value = None)
Returns: Returns:
Result: build result Result: build result
""" """
def build_single(package: Package, local_path: Path) -> None: def build_single(package: Package, local_path: Path, packager_id: str | None) -> None:
self.reporter.set_building(package.base) self.reporter.set_building(package.base)
task = Task(package, self.configuration, self.paths) task = Task(package, self.configuration, self.paths)
task.init(local_path, self.database) task.init(local_path, self.database)
built = task.build(local_path) built = task.build(local_path, packager_id)
for src in built: for src in built:
dst = self.paths.packages / src.name dst = self.paths.packages / src.name
shutil.move(src, dst) shutil.move(src, dst)
packagers = packagers or Packagers()
result = Result() result = Result()
for single in updates: for single in updates:
with self.in_package_context(single.base), TemporaryDirectory(ignore_cleanup_errors=True) as dir_name: with self.in_package_context(single.base), TemporaryDirectory(ignore_cleanup_errors=True) as dir_name:
try: try:
build_single(single, Path(dir_name)) packager = self.packager(packagers, single.base)
build_single(single, Path(dir_name), packager.packager_id)
result.add_success(single) result.add_success(single)
except Exception: except Exception:
self.reporter.set_failed(single.base) self.reporter.set_failed(single.base)
@ -158,12 +164,14 @@ class Executor(Cleaner):
return self.repo.repo_path return self.repo.repo_path
def process_update(self, packages: Iterable[Path]) -> Result: def process_update(self, packages: Iterable[Path], packagers: Packagers | None = None) -> Result:
""" """
sign packages, add them to repository and update repository database sign packages, add them to repository and update repository database
Args: Args:
packages(Iterable[Path]): list of filenames to run packages(Iterable[Path]): list of filenames to run
packagers(Packagers | None, optional): optional override of username for build process
(Default value = None)
Returns: Returns:
Result: path to repository database Result: path to repository database
@ -176,13 +184,13 @@ class Executor(Cleaner):
shutil.move(self.paths.packages / archive.filename, self.paths.packages / safe) shutil.move(self.paths.packages / archive.filename, self.paths.packages / safe)
archive.filename = safe archive.filename = safe
def update_single(name: str | None, package_base: str) -> None: def update_single(name: str | None, package_base: str, packager_key: str | None) -> None:
if name is None: if name is None:
self.logger.warning("received empty package name for base %s", package_base) self.logger.warning("received empty package name for base %s", package_base)
return # suppress type checking, it never can be none actually return # suppress type checking, it never can be none actually
# in theory, it might be NOT packages directory, but we suppose it is # in theory, it might be NOT packages directory, but we suppose it is
full_path = self.paths.packages / name full_path = self.paths.packages / name
files = self.sign.process_sign_package(full_path, package_base) files = self.sign.process_sign_package(full_path, packager_key)
for src in files: for src in files:
dst = self.paths.repository / safe_filename(src.name) dst = self.paths.repository / safe_filename(src.name)
shutil.move(src, dst) shutil.move(src, dst)
@ -192,14 +200,17 @@ class Executor(Cleaner):
current_packages = self.packages() current_packages = self.packages()
removed_packages: list[str] = [] # list of packages which have been removed from the base removed_packages: list[str] = [] # list of packages which have been removed from the base
updates = self.load_archives(packages) updates = self.load_archives(packages)
packagers = packagers or Packagers()
result = Result() result = Result()
for local in updates: for local in updates:
with self.in_package_context(local.base): with self.in_package_context(local.base):
try: try:
packager = self.packager(packagers, local.base)
for description in local.packages.values(): for description in local.packages.values():
rename(description, local.base) rename(description, local.base)
update_single(description.filename, local.base) update_single(description.filename, local.base, packager.key)
self.reporter.set_success(local) self.reporter.set_success(local)
result.add_success(local) result.add_success(local)

View File

@ -27,8 +27,11 @@ from ahriman.core.sign.gpg import GPG
from ahriman.core.status.client import Client from ahriman.core.status.client import Client
from ahriman.core.triggers import TriggerLoader from ahriman.core.triggers import TriggerLoader
from ahriman.core.util import check_user from ahriman.core.util import check_user
from ahriman.models.packagers import Packagers
from ahriman.models.pacman_synchronization import PacmanSynchronization from ahriman.models.pacman_synchronization import PacmanSynchronization
from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.repository_paths import RepositoryPaths
from ahriman.models.user import User
from ahriman.models.user_access import UserAccess
class RepositoryProperties(LazyLogging): class RepositoryProperties(LazyLogging):
@ -83,3 +86,23 @@ class RepositoryProperties(LazyLogging):
self.repo = Repo(self.name, self.paths, self.sign.repository_sign_args) self.repo = Repo(self.name, self.paths, self.sign.repository_sign_args)
self.reporter = Client.load(configuration, report=report) self.reporter = Client.load(configuration, report=report)
self.triggers = TriggerLoader.load(architecture, configuration) self.triggers = TriggerLoader.load(architecture, configuration)
def packager(self, packagers: Packagers, package_base: str) -> User:
"""
extract packager from configuration having username
Args:
packagers(Packagers): packagers override holder
package_base(str): package base to lookup
Returns:
User | None: user found in database if any and empty object otherwise
"""
username = packagers.for_base(package_base)
if username is None: # none to search
return User(username="", password="", access=UserAccess.Read, packager_id=None, key=None) # nosec
if (user := self.database.user_get(username)) is not None: # found user
return user
# empty user with the username
return User(username=username, password="", access=UserAccess.Read, packager_id=None, key=None) # nosec

View File

@ -65,9 +65,9 @@ class UpdateHandler(Cleaner):
try: try:
if source == PackageSource.Repository: if source == PackageSource.Repository:
remote = Package.from_official(local.base, self.pacman) remote = Package.from_official(local.base, self.pacman, None)
else: else:
remote = Package.from_aur(local.base, self.pacman) remote = Package.from_aur(local.base, self.pacman, None)
if local.is_outdated( if local.is_outdated(
remote, self.paths, remote, self.paths,
@ -98,7 +98,7 @@ class UpdateHandler(Cleaner):
with self.in_package_context(cache_dir.name): with self.in_package_context(cache_dir.name):
try: try:
Sources.fetch(cache_dir, remote=None) Sources.fetch(cache_dir, remote=None)
remote = Package.from_build(cache_dir, self.architecture) remote = Package.from_build(cache_dir, self.architecture, None)
local = packages.get(remote.base) local = packages.get(remote.base)
if local is None: if local is None:

View File

@ -19,7 +19,6 @@
# #
import requests import requests
from collections.abc import Generator
from pathlib import Path from pathlib import Path
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
@ -165,21 +164,6 @@ class GPG(LazyLogging):
key_body = self.key_download(server, key) key_body = self.key_download(server, key)
GPG._check_output("gpg", "--import", input_data=key_body, logger=self.logger) GPG._check_output("gpg", "--import", input_data=key_body, logger=self.logger)
def keys(self) -> list[str]:
"""
extract list of keys described in configuration
Returns:
list[str]: list of unique keys which are set in configuration
"""
def generator() -> Generator[str, None, None]:
if self.default_key is not None:
yield self.default_key
for _, value in filter(lambda pair: pair[0].startswith("key_"), self.configuration["sign"].items()):
yield value
return sorted(set(generator()))
def process(self, path: Path, key: str) -> list[Path]: def process(self, path: Path, key: str) -> list[Path]:
""" """
gpg command wrapper gpg command wrapper
@ -197,20 +181,21 @@ class GPG(LazyLogging):
logger=self.logger) logger=self.logger)
return [path, path.parent / f"{path.name}.sig"] return [path, path.parent / f"{path.name}.sig"]
def process_sign_package(self, path: Path, package_base: str) -> list[Path]: def process_sign_package(self, path: Path, packager_key: str | None) -> list[Path]:
""" """
sign package if required by configuration sign package if required by configuration
Args: Args:
path(Path): path to file to sign path(Path): path to file to sign
package_base(str): package base required to check for key overrides packager_key(str | None): optional packager key to sign
Returns: Returns:
list[Path]: list of generated files including original file list[Path]: list of generated files including original file
""" """
if SignSettings.Packages not in self.targets: if SignSettings.Packages not in self.targets:
return [path] return [path]
key = self.configuration.get("sign", f"key_{package_base}", fallback=self.default_key)
key = packager_key or self.default_key
if key is None: if key is None:
self.logger.error("no default key set, skip package %s sign", path) self.logger.error("no default key set, skip package %s sign", path)
return [path] return [path]

View File

@ -78,7 +78,7 @@ class Spawn(Thread, LazyLogging):
result = callback(args, architecture) result = callback(args, architecture)
queue.put((process_id, result)) queue.put((process_id, result))
def _spawn_process(self, command: str, *args: str, **kwargs: str) -> None: def _spawn_process(self, command: str, *args: str, **kwargs: str | None) -> None:
""" """
spawn external ahriman process with supplied arguments spawn external ahriman process with supplied arguments
@ -94,6 +94,8 @@ class Spawn(Thread, LazyLogging):
arguments.extend(args) arguments.extend(args)
# named command arguments # named command arguments
for argument, value in kwargs.items(): for argument, value in kwargs.items():
if value is None:
continue # skip null values
arguments.append(f"--{argument}") arguments.append(f"--{argument}")
if value: if value:
arguments.append(value) arguments.append(value)
@ -122,27 +124,31 @@ class Spawn(Thread, LazyLogging):
kwargs = {} if server is None else {"key-server": server} kwargs = {} if server is None else {"key-server": server}
self._spawn_process("service-key-import", key, **kwargs) self._spawn_process("service-key-import", key, **kwargs)
def packages_add(self, packages: Iterable[str], *, now: bool) -> None: def packages_add(self, packages: Iterable[str], username: str | None, *, now: bool) -> None:
""" """
add packages add packages
Args: Args:
packages(Iterable[str]): packages list to add packages(Iterable[str]): packages list to add
username(str | None): optional override of username for build process
now(bool): build packages now now(bool): build packages now
""" """
kwargs = {"source": PackageSource.AUR.value} # avoid abusing by building non-aur packages # avoid abusing by building non-aur packages
kwargs = {"source": PackageSource.AUR.value, "username": username}
if now: if now:
kwargs["now"] = "" kwargs["now"] = ""
self._spawn_process("package-add", *packages, **kwargs) self._spawn_process("package-add", *packages, **kwargs)
def packages_rebuild(self, depends_on: str) -> None: def packages_rebuild(self, depends_on: str, username: str | None) -> None:
""" """
rebuild packages which depend on the specified package rebuild packages which depend on the specified package
Args: Args:
depends_on(str): packages dependency depends_on(str): packages dependency
username(str | None): optional override of username for build process
""" """
self._spawn_process("repo-rebuild", **{"depends-on": depends_on}) kwargs = {"depends-on": depends_on, "username": username}
self._spawn_process("repo-rebuild", **kwargs)
def packages_remove(self, packages: Iterable[str]) -> None: def packages_remove(self, packages: Iterable[str]) -> None:
""" """
@ -153,11 +159,15 @@ class Spawn(Thread, LazyLogging):
""" """
self._spawn_process("package-remove", *packages) self._spawn_process("package-remove", *packages)
def packages_update(self) -> None: def packages_update(self, username: str | None) -> None:
""" """
run full repository update run full repository update
Args:
username(str | None): optional override of username for build process
""" """
self._spawn_process("repo-update") kwargs = {"username": username}
self._spawn_process("repo-update", **kwargs)
def run(self) -> None: def run(self) -> None:
""" """

View File

@ -19,6 +19,7 @@
# #
from ahriman.core import context from ahriman.core import context
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.sign.gpg import GPG from ahriman.core.sign.gpg import GPG
from ahriman.core.support.package_creator import PackageCreator from ahriman.core.support.package_creator import PackageCreator
from ahriman.core.support.pkgbuild.keyring_generator import KeyringGenerator from ahriman.core.support.pkgbuild.keyring_generator import KeyringGenerator
@ -107,8 +108,9 @@ class KeyringTrigger(Trigger):
""" """
ctx = context.get() ctx = context.get()
sign = ctx.get(ContextKey("sign", GPG)) sign = ctx.get(ContextKey("sign", GPG))
database = ctx.get(ContextKey("database", SQLite))
for target in self.targets: for target in self.targets:
generator = KeyringGenerator(sign, self.configuration, target) generator = KeyringGenerator(database, sign, self.configuration, target)
runner = PackageCreator(self.configuration, generator) runner = PackageCreator(self.configuration, generator)
runner.run() runner.run()

View File

@ -67,5 +67,5 @@ class PackageCreator:
ctx = context.get() ctx = context.get()
database: SQLite = ctx.get(ContextKey("database", SQLite)) database: SQLite = ctx.get(ContextKey("database", SQLite))
_, architecture = self.configuration.check_loaded() _, architecture = self.configuration.check_loaded()
package = Package.from_build(local_path, architecture) package = Package.from_build(local_path, architecture, None)
database.package_update(package, BuildStatus()) database.package_update(package, BuildStatus())

View File

@ -21,6 +21,7 @@ from collections.abc import Callable
from pathlib import Path from pathlib import Path
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.exceptions import PkgbuildGeneratorError from ahriman.core.exceptions import PkgbuildGeneratorError
from ahriman.core.sign.gpg import GPG from ahriman.core.sign.gpg import GPG
from ahriman.core.support.pkgbuild.pkgbuild_generator import PkgbuildGenerator from ahriman.core.support.pkgbuild.pkgbuild_generator import PkgbuildGenerator
@ -42,11 +43,12 @@ class KeyringGenerator(PkgbuildGenerator):
trusted(list[str]): lif of trusted PGP keys trusted(list[str]): lif of trusted PGP keys
""" """
def __init__(self, sign: GPG, configuration: Configuration, section: str) -> None: def __init__(self, database: SQLite, sign: GPG, configuration: Configuration, section: str) -> None:
""" """
default constructor default constructor
Args: Args:
database(SQLite): database instance
sign(GPG): GPG wrapper instance sign(GPG): GPG wrapper instance
configuration(Configuration): configuration instance configuration(Configuration): configuration instance
section(str): settings section name section(str): settings section name
@ -55,7 +57,8 @@ class KeyringGenerator(PkgbuildGenerator):
self.name = configuration.repository_name self.name = configuration.repository_name
# configuration fields # configuration fields
self.packagers = configuration.getlist(section, "packagers", fallback=sign.keys()) packager_keys = [packager.key for packager in database.user_list(None, None) if packager.key is not None]
self.packagers = configuration.getlist(section, "packagers", fallback=packager_keys)
self.revoked = configuration.getlist(section, "revoked", fallback=[]) self.revoked = configuration.getlist(section, "revoked", fallback=[])
self.trusted = configuration.getlist( self.trusted = configuration.getlist(
section, "trusted", fallback=[sign.default_key] if sign.default_key is not None else []) section, "trusted", fallback=[sign.default_key] if sign.default_key is not None else [])
@ -148,10 +151,10 @@ class KeyringGenerator(PkgbuildGenerator):
def install(self) -> str | None: def install(self) -> str | None:
""" """
content of the install functions content of the .install functions
Returns: Returns:
str | None: content of the install functions if any str | None: content of the .install functions if any
""" """
# copy-paste from archlinux-keyring # copy-paste from archlinux-keyring
return f"""post_upgrade() {{ return f"""post_upgrade() {{

View File

@ -98,10 +98,10 @@ class PkgbuildGenerator:
def install(self) -> str | None: def install(self) -> str | None:
""" """
content of the install functions content of the .install functions
Returns: Returns:
str | None: content of the install functions if any str | None: content of the .install functions if any
""" """
def package(self) -> str: def package(self) -> str:

View File

@ -28,6 +28,7 @@ import requests
import subprocess import subprocess
from collections.abc import Callable, Generator, Iterable from collections.abc import Callable, Generator, Iterable
from dataclasses import asdict
from enum import Enum from enum import Enum
from pathlib import Path from pathlib import Path
from pwd import getpwuid from pwd import getpwuid
@ -40,8 +41,10 @@ from ahriman.models.repository_paths import RepositoryPaths
__all__ = [ __all__ = [
"check_output", "check_output",
"check_user", "check_user",
"dataclass_view",
"enum_values", "enum_values",
"exception_response_text", "exception_response_text",
"extract_user",
"filter_json", "filter_json",
"full_version", "full_version",
"package_like", "package_like",
@ -61,7 +64,8 @@ T = TypeVar("T")
def check_output(*args: str, exception: Exception | None = None, cwd: Path | None = None, input_data: str | None = None, def check_output(*args: str, exception: Exception | None = None, cwd: Path | None = None, input_data: str | None = None,
logger: logging.Logger | None = None, user: int | None = None) -> str: logger: logging.Logger | None = None, user: int | None = None,
environment: dict[str, str] | None = None) -> str:
""" """
subprocess wrapper subprocess wrapper
@ -73,6 +77,7 @@ def check_output(*args: str, exception: Exception | None = None, cwd: Path | Non
input_data(str | None, optional): data which will be written to command stdin (Default value = None) input_data(str | None, optional): data which will be written to command stdin (Default value = None)
logger(logging.Logger | None, optional): logger to log command result if required (Default value = None) logger(logging.Logger | None, optional): logger to log command result if required (Default value = None)
user(int | None, optional): run process as specified user (Default value = None) user(int | None, optional): run process as specified user (Default value = None)
environment(dict[str, str] | None, optional): optional environment variables if any (Default value = None)
Returns: Returns:
str: command output str: command output
@ -106,7 +111,9 @@ def check_output(*args: str, exception: Exception | None = None, cwd: Path | Non
if logger is not None: if logger is not None:
logger.debug(single) logger.debug(single)
environment = {"HOME": getpwuid(user).pw_dir} if user is not None else {} environment = environment or {}
if user is not None:
environment["HOME"] = getpwuid(user).pw_dir
# FIXME additional workaround for linter and type check which do not know that user arg is supported # FIXME additional workaround for linter and type check which do not know that user arg is supported
# pylint: disable=unexpected-keyword-arg # pylint: disable=unexpected-keyword-arg
with subprocess.Popen(args, cwd=cwd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, with subprocess.Popen(args, cwd=cwd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
@ -163,6 +170,19 @@ def check_user(paths: RepositoryPaths, *, unsafe: bool) -> None:
raise UnsafeRunError(current_uid, root_uid) raise UnsafeRunError(current_uid, root_uid)
def dataclass_view(instance: Any) -> dict[str, Any]:
"""
convert dataclass instance to json object
Args:
instance(Any): dataclass instance
Returns:
dict[str, Any]: json representation of the dataclass with empty field removed
"""
return asdict(instance, dict_factory=lambda fields: {key: value for key, value in fields if value is not None})
def enum_values(enum: type[Enum]) -> list[str]: def enum_values(enum: type[Enum]) -> list[str]:
""" """
generate list of enumeration values from the source generate list of enumeration values from the source
@ -190,6 +210,17 @@ def exception_response_text(exception: requests.exceptions.RequestException) ->
return result return result
def extract_user() -> str | None:
"""
extract user from system environment
Returns:
str | None: SUDO_USER in case if set and USER otherwise. It can return None in case if environment has been
cleared before application start
"""
return os.getenv("SUDO_USER") or os.getenv("DOAS_USER") or os.getenv("USER")
def filter_json(source: dict[str, Any], known_fields: Iterable[str]) -> dict[str, Any]: def filter_json(source: dict[str, Any], known_fields: Iterable[str]) -> dict[str, Any]:
""" """
filter json object by fields used for json-to-object conversion filter json object by fields used for json-to-object conversion

View File

@ -17,9 +17,10 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from dataclasses import asdict, dataclass, field from dataclasses import dataclass, field
from typing import Any, Self from typing import Any, Self
from ahriman.core.util import dataclass_view
from ahriman.models.build_status import BuildStatus from ahriman.models.build_status import BuildStatus
from ahriman.models.counters import Counters from ahriman.models.counters import Counters
@ -69,4 +70,4 @@ class InternalStatus:
Returns: Returns:
dict[str, Any]: json-friendly dictionary dict[str, Any]: json-friendly dictionary
""" """
return asdict(self) return dataclass_view(self)

View File

@ -23,7 +23,7 @@ from __future__ import annotations
import copy import copy
from collections.abc import Callable, Generator, Iterable from collections.abc import Callable, Generator, Iterable
from dataclasses import asdict, dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from pyalpm import vercmp # type: ignore[import] from pyalpm import vercmp # type: ignore[import]
from srcinfo.parse import parse_srcinfo # type: ignore[import] from srcinfo.parse import parse_srcinfo # type: ignore[import]
@ -34,7 +34,7 @@ from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.remote import AUR, Official, OfficialSyncdb from ahriman.core.alpm.remote import AUR, Official, OfficialSyncdb
from ahriman.core.exceptions import PackageInfoError from ahriman.core.exceptions import PackageInfoError
from ahriman.core.log import LazyLogging from ahriman.core.log import LazyLogging
from ahriman.core.util import check_output, full_version, srcinfo_property_list, utcnow from ahriman.core.util import check_output, dataclass_view, full_version, srcinfo_property_list, utcnow
from ahriman.models.package_description import PackageDescription from ahriman.models.package_description import PackageDescription
from ahriman.models.package_source import PackageSource from ahriman.models.package_source import PackageSource
from ahriman.models.remote_source import RemoteSource from ahriman.models.remote_source import RemoteSource
@ -48,6 +48,7 @@ class Package(LazyLogging):
Attributes: Attributes:
base(str): package base name base(str): package base name
packager(str | None): package packager if available
packages(dict[str, PackageDescription): map of package names to their properties. packages(dict[str, PackageDescription): map of package names to their properties.
Filled only on load from archive Filled only on load from archive
remote(RemoteSource | None): package remote source if applicable remote(RemoteSource | None): package remote source if applicable
@ -77,6 +78,7 @@ class Package(LazyLogging):
version: str version: str
remote: RemoteSource | None remote: RemoteSource | None
packages: dict[str, PackageDescription] packages: dict[str, PackageDescription]
packager: str | None = None
_check_output = check_output _check_output = check_output
@ -204,16 +206,18 @@ class Package(LazyLogging):
""" """
package = pacman.handle.load_pkg(str(path)) package = pacman.handle.load_pkg(str(path))
description = PackageDescription.from_package(package, path) description = PackageDescription.from_package(package, path)
return cls(base=package.base, version=package.version, remote=remote, packages={package.name: description}) return cls(base=package.base, version=package.version, remote=remote, packages={package.name: description},
packager=package.packager)
@classmethod @classmethod
def from_aur(cls, name: str, pacman: Pacman) -> Self: def from_aur(cls, name: str, pacman: Pacman, packager: str | None = None) -> Self:
""" """
construct package properties from AUR page construct package properties from AUR page
Args: Args:
name(str): package name (either base or normal name) name(str): package name (either base or normal name)
pacman(Pacman): alpm wrapper instance pacman(Pacman): alpm wrapper instance
packager(str | None, optional): packager to be used for this build (Default value = None)
Returns: Returns:
Self: package properties Self: package properties
@ -224,16 +228,19 @@ class Package(LazyLogging):
base=package.package_base, base=package.package_base,
version=package.version, version=package.version,
remote=remote, remote=remote,
packages={package.name: PackageDescription.from_aur(package)}) packages={package.name: PackageDescription.from_aur(package)},
packager=packager,
)
@classmethod @classmethod
def from_build(cls, path: Path, architecture: str) -> Self: def from_build(cls, path: Path, architecture: str, packager: str | None = None) -> Self:
""" """
construct package properties from sources directory construct package properties from sources directory
Args: Args:
path(Path): path to package sources directory path(Path): path to package sources directory
architecture(str): load package for specific architecture architecture(str): load package for specific architecture
packager(str | None, optional): packager to be used for this build (Default value = None)
Returns: Returns:
Self: package properties Self: package properties
@ -265,7 +272,7 @@ class Package(LazyLogging):
source=PackageSource.Local, source=PackageSource.Local,
) )
return cls(base=srcinfo["pkgbase"], version=version, remote=remote, packages=packages) return cls(base=srcinfo["pkgbase"], version=version, remote=remote, packages=packages, packager=packager)
@classmethod @classmethod
def from_json(cls, dump: dict[str, Any]) -> Self: def from_json(cls, dump: dict[str, Any]) -> Self:
@ -284,16 +291,18 @@ class Package(LazyLogging):
for key, value in packages_json.items() for key, value in packages_json.items()
} }
remote = dump.get("remote") or {} remote = dump.get("remote") or {}
return cls(base=dump["base"], version=dump["version"], remote=RemoteSource.from_json(remote), packages=packages) return cls(base=dump["base"], version=dump["version"], remote=RemoteSource.from_json(remote), packages=packages,
packager=dump.get("packager"))
@classmethod @classmethod
def from_official(cls, name: str, pacman: Pacman, *, use_syncdb: bool = True) -> Self: def from_official(cls, name: str, pacman: Pacman, packager: str | None = None, *, use_syncdb: bool = True) -> Self:
""" """
construct package properties from official repository page construct package properties from official repository page
Args: Args:
name(str): package name (either base or normal name) name(str): package name (either base or normal name)
pacman(Pacman): alpm wrapper instance pacman(Pacman): alpm wrapper instance
packager(str | None, optional): packager to be used for this build (Default value = None)
use_syncdb(bool, optional): use pacman databases instead of official repositories RPC (Default value = True) use_syncdb(bool, optional): use pacman databases instead of official repositories RPC (Default value = True)
Returns: Returns:
@ -305,7 +314,9 @@ class Package(LazyLogging):
base=package.package_base, base=package.package_base,
version=package.version, version=package.version,
remote=remote, remote=remote,
packages={package.name: PackageDescription.from_aur(package)}) packages={package.name: PackageDescription.from_aur(package)},
packager=packager,
)
@staticmethod @staticmethod
def local_files(path: Path) -> Generator[Path, None, None]: def local_files(path: Path) -> Generator[Path, None, None]:
@ -513,4 +524,4 @@ class Package(LazyLogging):
Returns: Returns:
dict[str, Any]: json-friendly dictionary dict[str, Any]: json-friendly dictionary
""" """
return asdict(self) return dataclass_view(self)

View File

@ -17,12 +17,12 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from dataclasses import asdict, dataclass, field, fields from dataclasses import dataclass, field, fields
from pathlib import Path from pathlib import Path
from pyalpm import Package # type: ignore[import] from pyalpm import Package # type: ignore[import]
from typing import Any, Self from typing import Any, Self
from ahriman.core.util import filter_json, trim_package from ahriman.core.util import dataclass_view, filter_json, trim_package
from ahriman.models.aur_package import AURPackage from ahriman.models.aur_package import AURPackage
@ -172,4 +172,4 @@ class PackageDescription:
Returns: Returns:
dict[str, Any]: json-friendly dictionary dict[str, Any]: json-friendly dictionary
""" """
return asdict(self) return dataclass_view(self)

View File

@ -0,0 +1,46 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from dataclasses import dataclass, field
@dataclass(frozen=True)
class Packagers:
"""
holder for packagers overrides
Attributes:
default(str | None): default packager username if any to be used if no override for the specified base was found
overrides: dict[str, str | None]: packager username override for specific package base
"""
default: str | None = None
overrides: dict[str, str | None] = field(default_factory=dict)
def for_base(self, package_base: str) -> str | None:
"""
extract username for the specified package base
Args:
package_base(str): package base to lookup
Returns:
str | None: package base override if set and default packager username otherwise
"""
return self.overrides.get(package_base) or self.default

View File

@ -17,11 +17,11 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from dataclasses import asdict, dataclass, fields from dataclasses import dataclass, fields
from pathlib import Path from pathlib import Path
from typing import Any, Self from typing import Any, Self
from ahriman.core.util import filter_json from ahriman.core.util import dataclass_view, filter_json
from ahriman.models.package_source import PackageSource from ahriman.models.package_source import PackageSource
@ -118,4 +118,4 @@ class RemoteSource:
Returns: Returns:
dict[str, Any]: json-friendly dictionary dict[str, Any]: json-friendly dictionary
""" """
return asdict(self) return dataclass_view(self)

View File

@ -34,12 +34,14 @@ class User:
username(str): username username(str): username
password(str): hashed user password with salt password(str): hashed user password with salt
access(UserAccess): user role access(UserAccess): user role
packager_id(str | None): packager id to be used. If not set, the default service packager will be used
key(str | None): personal packager key if any. If user id is empty, it is interpreted as default key
Examples: Examples:
Simply create user from database data and perform required validation:: Simply create user from database data and perform required validation::
>>> password = User.generate_password(24) >>> password = User.generate_password(24)
>>> user = User("ahriman", password, UserAccess.Full) >>> user = User(username="ahriman", password=password, access=UserAccess.Full, packager_id=None, key=None)
Since the password supplied may be plain text, the ``hash_password`` method can be used to hash the password:: Since the password supplied may be plain text, the ``hash_password`` method can be used to hash the password::
@ -61,9 +63,18 @@ class User:
username: str username: str
password: str password: str
access: UserAccess access: UserAccess
packager_id: str | None
key: str | None
_HASHER = sha512_crypt _HASHER = sha512_crypt
def __post_init__(self) -> None:
"""
remove empty fields
"""
object.__setattr__(self, "packager_id", self.packager_id or None)
object.__setattr__(self, "key", self.key or None)
@classmethod @classmethod
def from_option(cls, username: str | None, password: str | None, def from_option(cls, username: str | None, password: str | None,
access: UserAccess = UserAccess.Read) -> Self | None: access: UserAccess = UserAccess.Read) -> Self | None:
@ -80,7 +91,7 @@ class User:
""" """
if username is None or password is None: if username is None or password is None:
return None return None
return cls(username=username, password=password, access=access) return cls(username=username, password=password, access=access, packager_id=None, key=None)
@staticmethod @staticmethod
def generate_password(length: int) -> str: def generate_password(length: int) -> str:
@ -149,4 +160,4 @@ class User:
Returns: Returns:
str: unique string representation str: unique string representation
""" """
return f"User(username={self.username}, access={self.access})" return f"User(username={self.username}, access={self.access}, packager_id={self.packager_id}, key={self.key})"

View File

@ -148,7 +148,7 @@ def setup_auth(application: Application, configuration: Configuration, validator
setup_session(application, storage) setup_session(application, storage)
authorization_policy = _AuthorizationPolicy(validator) authorization_policy = _AuthorizationPolicy(validator)
identity_policy = aiohttp_security.SessionIdentityPolicy() identity_policy = application["identity"] = aiohttp_security.SessionIdentityPolicy()
aiohttp_security.setup(application, identity_policy, authorization_policy) aiohttp_security.setup(application, identity_policy, authorization_policy)
application.middlewares.append(_auth_handler(validator.allow_read_only)) application.middlewares.append(_auth_handler(validator.allow_read_only))

View File

@ -44,3 +44,7 @@ class PackageSchema(Schema):
keys=fields.String(), values=fields.Nested(PackagePropertiesSchema()), required=True, metadata={ keys=fields.String(), values=fields.Nested(PackagePropertiesSchema()), required=True, metadata={
"description": "Packages which belong to this base", "description": "Packages which belong to this base",
}) })
packager = fields.String(metadata={
"description": "packager for the last success package build",
"example": "John Doe <john@doe.com>",
})

View File

@ -183,3 +183,16 @@ class BaseView(View, CorsViewMixin):
return response return response
self._raise_allowed_methods() self._raise_allowed_methods()
async def username(self) -> str | None:
"""
extract username from request if any
Returns:
str | None: authorized username if any and None otherwise (e.g. if authorization is disabled)
"""
policy = self.request.app.get("identity")
if policy is not None:
identity: str = await policy.identify(self.request)
return identity
return None

View File

@ -67,6 +67,7 @@ class AddView(BaseView):
except Exception as e: except Exception as e:
raise HTTPBadRequest(reason=str(e)) raise HTTPBadRequest(reason=str(e))
self.spawner.packages_add(packages, now=True) username = await self.username()
self.spawner.packages_add(packages, username, now=True)
raise HTTPNoContent() raise HTTPNoContent()

View File

@ -68,6 +68,7 @@ class RebuildView(BaseView):
except Exception as e: except Exception as e:
raise HTTPBadRequest(reason=str(e)) raise HTTPBadRequest(reason=str(e))
self.spawner.packages_rebuild(depends_on) username = await self.username()
self.spawner.packages_rebuild(depends_on, username)
raise HTTPNoContent() raise HTTPNoContent()

View File

@ -67,6 +67,7 @@ class RequestView(BaseView):
except Exception as e: except Exception as e:
raise HTTPBadRequest(reason=str(e)) raise HTTPBadRequest(reason=str(e))
self.spawner.packages_add(packages, now=False) username = await self.username()
self.spawner.packages_add(packages, username, now=False)
raise HTTPNoContent() raise HTTPNoContent()

View File

@ -57,6 +57,7 @@ class UpdateView(BaseView):
Raises: Raises:
HTTPNoContent: in case of success response HTTPNoContent: in case of success response
""" """
self.spawner.packages_update() username = await self.username()
self.spawner.packages_update(username)
raise HTTPNoContent() raise HTTPNoContent()

View File

@ -72,16 +72,16 @@ def test_with_dependencies(application: Application, package_ahriman: Package, p
"python-installer": create_package_mock("python-installer"), "python-installer": create_package_mock("python-installer"),
} }
package_mock = mocker.patch("ahriman.models.package.Package.from_aur", side_effect=lambda p, _: packages[p]) package_mock = mocker.patch("ahriman.models.package.Package.from_aur", side_effect=lambda *args: packages[args[0]])
packages_mock = mocker.patch("ahriman.application.application.Application._known_packages", packages_mock = mocker.patch("ahriman.application.application.Application._known_packages",
return_value=["devtools", "python-build", "python-pytest"]) return_value={"devtools", "python-build", "python-pytest"})
result = application.with_dependencies([package_ahriman], process_dependencies=True) result = application.with_dependencies([package_ahriman], process_dependencies=True)
assert {package.base: package for package in result} == packages assert {package.base: package for package in result} == packages
package_mock.assert_has_calls([ package_mock.assert_has_calls([
MockCall(package_python_schedule.base, application.repository.pacman), MockCall(package_python_schedule.base, application.repository.pacman, package_ahriman.packager),
MockCall("python", application.repository.pacman), MockCall("python", application.repository.pacman, package_ahriman.packager),
MockCall("python-installer", application.repository.pacman), MockCall("python-installer", application.repository.pacman, package_ahriman.packager),
], any_order=True) ], any_order=True)
packages_mock.assert_called_once_with() packages_mock.assert_called_once_with()

View File

@ -43,7 +43,7 @@ def test_add_aur(application_packages: ApplicationPackages, package_ahriman: Pac
build_queue_mock = mocker.patch("ahriman.core.database.SQLite.build_queue_insert") build_queue_mock = mocker.patch("ahriman.core.database.SQLite.build_queue_insert")
update_remote_mock = mocker.patch("ahriman.core.database.SQLite.remote_update") update_remote_mock = mocker.patch("ahriman.core.database.SQLite.remote_update")
application_packages._add_aur(package_ahriman.base) application_packages._add_aur(package_ahriman.base, "packager")
build_queue_mock.assert_called_once_with(package_ahriman) build_queue_mock.assert_called_once_with(package_ahriman)
update_remote_mock.assert_called_once_with(package_ahriman) update_remote_mock.assert_called_once_with(package_ahriman)
@ -83,7 +83,7 @@ def test_add_local(application_packages: ApplicationPackages, package_ahriman: P
copytree_mock = mocker.patch("shutil.copytree") copytree_mock = mocker.patch("shutil.copytree")
build_queue_mock = mocker.patch("ahriman.core.database.SQLite.build_queue_insert") build_queue_mock = mocker.patch("ahriman.core.database.SQLite.build_queue_insert")
application_packages._add_local(package_ahriman.base) application_packages._add_local(package_ahriman.base, "packager")
is_dir_mock.assert_called_once_with() is_dir_mock.assert_called_once_with()
copytree_mock.assert_called_once_with( copytree_mock.assert_called_once_with(
Path(package_ahriman.base), application_packages.repository.paths.cache_for(package_ahriman.base)) Path(package_ahriman.base), application_packages.repository.paths.cache_for(package_ahriman.base))
@ -103,7 +103,7 @@ def test_add_local_cache(application_packages: ApplicationPackages, package_ahri
copytree_mock = mocker.patch("shutil.copytree") copytree_mock = mocker.patch("shutil.copytree")
build_queue_mock = mocker.patch("ahriman.core.database.SQLite.build_queue_insert") build_queue_mock = mocker.patch("ahriman.core.database.SQLite.build_queue_insert")
application_packages._add_local(package_ahriman.base) application_packages._add_local(package_ahriman.base, "packager")
copytree_mock.assert_not_called() copytree_mock.assert_not_called()
init_mock.assert_not_called() init_mock.assert_not_called()
build_queue_mock.assert_called_once_with(package_ahriman) build_queue_mock.assert_called_once_with(package_ahriman)
@ -115,7 +115,7 @@ def test_add_local_missing(application_packages: ApplicationPackages, mocker: Mo
""" """
mocker.patch("pathlib.Path.is_dir", return_value=False) mocker.patch("pathlib.Path.is_dir", return_value=False)
with pytest.raises(UnknownPackageError): with pytest.raises(UnknownPackageError):
application_packages._add_local("package") application_packages._add_local("package", "packager")
def test_add_remote(application_packages: ApplicationPackages, package_description_ahriman: PackageDescription, def test_add_remote(application_packages: ApplicationPackages, package_description_ahriman: PackageDescription,
@ -153,7 +153,7 @@ def test_add_repository(application_packages: ApplicationPackages, package_ahrim
build_queue_mock = mocker.patch("ahriman.core.database.SQLite.build_queue_insert") build_queue_mock = mocker.patch("ahriman.core.database.SQLite.build_queue_insert")
update_remote_mock = mocker.patch("ahriman.core.database.SQLite.remote_update") update_remote_mock = mocker.patch("ahriman.core.database.SQLite.remote_update")
application_packages._add_repository(package_ahriman.base) application_packages._add_repository(package_ahriman.base, "packager")
build_queue_mock.assert_called_once_with(package_ahriman) build_queue_mock.assert_called_once_with(package_ahriman)
update_remote_mock.assert_called_once_with(package_ahriman) update_remote_mock.assert_called_once_with(package_ahriman)
@ -165,8 +165,8 @@ def test_add_add_archive(application_packages: ApplicationPackages, package_ahri
""" """
add_mock = mocker.patch("ahriman.application.application.application_packages.ApplicationPackages._add_archive") add_mock = mocker.patch("ahriman.application.application.application_packages.ApplicationPackages._add_archive")
application_packages.add([package_ahriman.base], PackageSource.Archive) application_packages.add([package_ahriman.base], PackageSource.Archive, "packager")
add_mock.assert_called_once_with(package_ahriman.base) add_mock.assert_called_once_with(package_ahriman.base, "packager")
def test_add_add_aur(application_packages: ApplicationPackages, package_ahriman: Package, def test_add_add_aur(application_packages: ApplicationPackages, package_ahriman: Package,
@ -176,8 +176,8 @@ def test_add_add_aur(application_packages: ApplicationPackages, package_ahriman:
""" """
add_mock = mocker.patch("ahriman.application.application.application_packages.ApplicationPackages._add_aur") add_mock = mocker.patch("ahriman.application.application.application_packages.ApplicationPackages._add_aur")
application_packages.add([package_ahriman.base], PackageSource.AUR) application_packages.add([package_ahriman.base], PackageSource.AUR, "packager")
add_mock.assert_called_once_with(package_ahriman.base) add_mock.assert_called_once_with(package_ahriman.base, "packager")
def test_add_add_directory(application_packages: ApplicationPackages, package_ahriman: Package, def test_add_add_directory(application_packages: ApplicationPackages, package_ahriman: Package,
@ -187,8 +187,8 @@ def test_add_add_directory(application_packages: ApplicationPackages, package_ah
""" """
add_mock = mocker.patch("ahriman.application.application.application_packages.ApplicationPackages._add_directory") add_mock = mocker.patch("ahriman.application.application.application_packages.ApplicationPackages._add_directory")
application_packages.add([package_ahriman.base], PackageSource.Directory) application_packages.add([package_ahriman.base], PackageSource.Directory, "packager")
add_mock.assert_called_once_with(package_ahriman.base) add_mock.assert_called_once_with(package_ahriman.base, "packager")
def test_add_add_local(application_packages: ApplicationPackages, package_ahriman: Package, def test_add_add_local(application_packages: ApplicationPackages, package_ahriman: Package,
@ -198,8 +198,8 @@ def test_add_add_local(application_packages: ApplicationPackages, package_ahrima
""" """
add_mock = mocker.patch("ahriman.application.application.application_packages.ApplicationPackages._add_local") add_mock = mocker.patch("ahriman.application.application.application_packages.ApplicationPackages._add_local")
application_packages.add([package_ahriman.base], PackageSource.Local) application_packages.add([package_ahriman.base], PackageSource.Local, "packager")
add_mock.assert_called_once_with(package_ahriman.base) add_mock.assert_called_once_with(package_ahriman.base, "packager")
def test_add_add_remote(application_packages: ApplicationPackages, package_description_ahriman: PackageDescription, def test_add_add_remote(application_packages: ApplicationPackages, package_description_ahriman: PackageDescription,
@ -210,8 +210,8 @@ def test_add_add_remote(application_packages: ApplicationPackages, package_descr
add_mock = mocker.patch("ahriman.application.application.application_packages.ApplicationPackages._add_remote") add_mock = mocker.patch("ahriman.application.application.application_packages.ApplicationPackages._add_remote")
url = f"https://host/{package_description_ahriman.filename}" url = f"https://host/{package_description_ahriman.filename}"
application_packages.add([url], PackageSource.Remote) application_packages.add([url], PackageSource.Remote, "packager")
add_mock.assert_called_once_with(url) add_mock.assert_called_once_with(url, "packager")
def test_on_result(application_packages: ApplicationPackages) -> None: def test_on_result(application_packages: ApplicationPackages) -> None:

View File

@ -76,9 +76,9 @@ def test_sign(application_repository: ApplicationRepository, package_ahriman: Pa
application_repository.sign([]) application_repository.sign([])
sign_package_mock.assert_has_calls([ sign_package_mock.assert_has_calls([
MockCall(pytest.helpers.anyvar(int), package_ahriman.base), MockCall(pytest.helpers.anyvar(int), None),
MockCall(pytest.helpers.anyvar(int), package_python_schedule.base), MockCall(pytest.helpers.anyvar(int), None),
MockCall(pytest.helpers.anyvar(int), package_python_schedule.base), MockCall(pytest.helpers.anyvar(int), None),
]) ])
sign_repository_mock.assert_called_once_with(application_repository.repository.repo.repo_path) sign_repository_mock.assert_called_once_with(application_repository.repository.repo.repo_path)
on_result_mock.assert_called_once_with(Result()) on_result_mock.assert_called_once_with(Result())
@ -111,7 +111,7 @@ def test_sign_specific(application_repository: ApplicationRepository, package_ah
filename = package_ahriman.packages[package_ahriman.base].filepath filename = package_ahriman.packages[package_ahriman.base].filepath
application_repository.sign([package_ahriman.base]) application_repository.sign([package_ahriman.base])
sign_package_mock.assert_called_once_with(filename, package_ahriman.base) sign_package_mock.assert_called_once_with(filename, None)
sign_repository_mock.assert_called_once_with(application_repository.repository.repo.repo_path) sign_repository_mock.assert_called_once_with(application_repository.repository.repo.repo_path)
on_result_mock.assert_called_once_with(Result()) on_result_mock.assert_called_once_with(Result())
@ -170,9 +170,9 @@ def test_update(application_repository: ApplicationRepository, package_ahriman:
on_result_mock = mocker.patch( on_result_mock = mocker.patch(
"ahriman.application.application.application_repository.ApplicationRepository.on_result") "ahriman.application.application.application_repository.ApplicationRepository.on_result")
application_repository.update([package_ahriman]) application_repository.update([package_ahriman], "username")
build_mock.assert_called_once_with([package_ahriman]) build_mock.assert_called_once_with([package_ahriman], "username")
update_mock.assert_has_calls([MockCall(paths), MockCall(paths)]) update_mock.assert_has_calls([MockCall(paths, "username"), MockCall(paths, "username")])
on_result_mock.assert_has_calls([MockCall(result), MockCall(result)]) on_result_mock.assert_has_calls([MockCall(result), MockCall(result)])

View File

@ -8,6 +8,7 @@ from ahriman.core.configuration import Configuration
from ahriman.core.repository import Repository from ahriman.core.repository import Repository
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource from ahriman.models.package_source import PackageSource
from ahriman.models.packagers import Packagers
from ahriman.models.result import Result from ahriman.models.result import Result
@ -27,6 +28,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.refresh = 0 args.refresh = 0
args.source = PackageSource.Auto args.source = PackageSource.Auto
args.dependencies = True args.dependencies = True
args.username = "username"
return args return args
@ -42,7 +44,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository:
on_start_mock = mocker.patch("ahriman.application.application.Application.on_start") on_start_mock = mocker.patch("ahriman.application.application.Application.on_start")
Add.run(args, "x86_64", configuration, report=False, unsafe=False) Add.run(args, "x86_64", configuration, report=False, unsafe=False)
application_mock.assert_called_once_with(args.package, args.source) application_mock.assert_called_once_with(args.package, args.source, args.username)
dependencies_mock.assert_not_called() dependencies_mock.assert_not_called()
on_start_mock.assert_called_once_with() on_start_mock.assert_called_once_with()
@ -67,7 +69,8 @@ def test_run_with_updates(args: argparse.Namespace, configuration: Configuration
Add.run(args, "x86_64", configuration, report=False, unsafe=False) Add.run(args, "x86_64", configuration, report=False, unsafe=False)
updates_mock.assert_called_once_with(args.package, aur=False, local=False, manual=True, vcs=False, updates_mock.assert_called_once_with(args.package, aur=False, local=False, manual=True, vcs=False,
log_fn=pytest.helpers.anyvar(int)) log_fn=pytest.helpers.anyvar(int))
application_mock.assert_called_once_with([package_ahriman]) application_mock.assert_called_once_with([package_ahriman],
Packagers(args.username, {package_ahriman.base: "packager"}))
dependencies_mock.assert_called_once_with([package_ahriman], process_dependencies=args.dependencies) dependencies_mock.assert_called_once_with([package_ahriman], process_dependencies=args.dependencies)
check_mock.assert_called_once_with(False, False) check_mock.assert_called_once_with(False, False)

View File

@ -109,7 +109,7 @@ def test_patch_create_from_diff(package_ahriman: Package, mocker: MockerFixture)
sources_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.patch_create", return_value=patch.value) sources_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.patch_create", return_value=patch.value)
assert Patch.patch_create_from_diff(path, "x86_64", ["*.diff"]) == (package_ahriman.base, patch) assert Patch.patch_create_from_diff(path, "x86_64", ["*.diff"]) == (package_ahriman.base, patch)
package_mock.assert_called_once_with(path, "x86_64") package_mock.assert_called_once_with(path, "x86_64", None)
sources_mock.assert_called_once_with(path, "*.diff") sources_mock.assert_called_once_with(path, "*.diff")

View File

@ -28,6 +28,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.from_database = False args.from_database = False
args.exit_code = False args.exit_code = False
args.status = None args.status = None
args.username = "username"
return args return args
@ -50,7 +51,7 @@ def test_run(args: argparse.Namespace, package_ahriman: Package, configuration:
Rebuild.run(args, "x86_64", configuration, report=False, unsafe=False) Rebuild.run(args, "x86_64", configuration, report=False, unsafe=False)
extract_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.status, from_database=args.from_database) extract_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.status, from_database=args.from_database)
application_packages_mock.assert_called_once_with([package_ahriman], None) application_packages_mock.assert_called_once_with([package_ahriman], None)
application_mock.assert_called_once_with([package_ahriman]) application_mock.assert_called_once_with([package_ahriman], args.username)
check_mock.assert_has_calls([MockCall(False, False), MockCall(False, False)]) check_mock.assert_has_calls([MockCall(False, False), MockCall(False, False)])
on_start_mock.assert_called_once_with() on_start_mock.assert_called_once_with()

View File

@ -36,7 +36,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository:
check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty") check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty")
ServiceUpdates.run(args, "x86_64", configuration, report=False, unsafe=False) ServiceUpdates.run(args, "x86_64", configuration, report=False, unsafe=False)
package_mock.assert_called_once_with(package_ahriman.base, repository.pacman) package_mock.assert_called_once_with(package_ahriman.base, repository.pacman, None)
application_mock.assert_called_once_with(verbose=True, separator=" -> ") application_mock.assert_called_once_with(verbose=True, separator=" -> ")
check_mock.assert_called_once_with(args.exit_code, True) check_mock.assert_called_once_with(args.exit_code, True)

View File

@ -19,7 +19,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
argparse.Namespace: generated arguments for these test cases argparse.Namespace: generated arguments for these test cases
""" """
args.parser = _parser args.parser = _parser
args.command = None args.command = []
return args return args
@ -42,14 +42,14 @@ def test_run_check(args: argparse.Namespace, configuration: Configuration, mocke
must run command and check if command is unsafe must run command and check if command is unsafe
""" """
args = _default_args(args) args = _default_args(args)
args.command = "clean" args.command = ["clean"]
commands_mock = mocker.patch("ahriman.application.handlers.UnsafeCommands.get_unsafe_commands", commands_mock = mocker.patch("ahriman.application.handlers.UnsafeCommands.get_unsafe_commands",
return_value=["command"]) return_value=["command"])
check_mock = mocker.patch("ahriman.application.handlers.UnsafeCommands.check_unsafe") check_mock = mocker.patch("ahriman.application.handlers.UnsafeCommands.check_unsafe")
UnsafeCommands.run(args, "x86_64", configuration, report=False, unsafe=False) UnsafeCommands.run(args, "x86_64", configuration, report=False, unsafe=False)
commands_mock.assert_called_once_with(pytest.helpers.anyvar(int)) commands_mock.assert_called_once_with(pytest.helpers.anyvar(int))
check_mock.assert_called_once_with("clean", ["command"], pytest.helpers.anyvar(int)) check_mock.assert_called_once_with(["clean"], ["command"], pytest.helpers.anyvar(int))
def test_check_unsafe(mocker: MockerFixture) -> None: def test_check_unsafe(mocker: MockerFixture) -> None:
@ -57,7 +57,7 @@ def test_check_unsafe(mocker: MockerFixture) -> None:
must check if command is unsafe must check if command is unsafe
""" """
check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty") check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty")
UnsafeCommands.check_unsafe("service-clean", ["service-clean"], _parser()) UnsafeCommands.check_unsafe(["service-clean"], ["service-clean"], _parser())
check_mock.assert_called_once_with(True, True) check_mock.assert_called_once_with(True, True)
@ -66,7 +66,7 @@ def test_check_unsafe_safe(mocker: MockerFixture) -> None:
must check if command is safe must check if command is safe
""" """
check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty") check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty")
UnsafeCommands.check_unsafe("package-status", ["service-clean"], _parser()) UnsafeCommands.check_unsafe(["package-status"], ["service-clean"], _parser())
check_mock.assert_called_once_with(True, False) check_mock.assert_called_once_with(True, False)

View File

@ -9,6 +9,7 @@ from ahriman.application.handlers import Update
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.repository import Repository from ahriman.core.repository import Repository
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.packagers import Packagers
from ahriman.models.result import Result from ahriman.models.result import Result
@ -31,6 +32,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.manual = True args.manual = True
args.vcs = True args.vcs = True
args.refresh = 0 args.refresh = 0
args.username = "username"
return args return args
@ -51,7 +53,8 @@ def test_run(args: argparse.Namespace, package_ahriman: Package, configuration:
on_start_mock = mocker.patch("ahriman.application.application.Application.on_start") on_start_mock = mocker.patch("ahriman.application.application.Application.on_start")
Update.run(args, "x86_64", configuration, report=False, unsafe=False) Update.run(args, "x86_64", configuration, report=False, unsafe=False)
application_mock.assert_called_once_with([package_ahriman]) application_mock.assert_called_once_with([package_ahriman],
Packagers(args.username, {package_ahriman.base: "packager"}))
updates_mock.assert_called_once_with(args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs, updates_mock.assert_called_once_with(args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs,
log_fn=pytest.helpers.anyvar(int)) log_fn=pytest.helpers.anyvar(int))
dependencies_mock.assert_called_once_with([package_ahriman], process_dependencies=args.dependencies) dependencies_mock.assert_called_once_with([package_ahriman], process_dependencies=args.dependencies)

View File

@ -27,6 +27,8 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.username = "user" args.username = "user"
args.action = Action.Update args.action = Action.Update
args.exit_code = False args.exit_code = False
args.key = "key"
args.packager = "packager"
args.password = "pa55w0rd" args.password = "pa55w0rd"
args.role = UserAccess.Reporter args.role = UserAccess.Reporter
args.secure = False args.secure = False
@ -38,7 +40,8 @@ def test_run(args: argparse.Namespace, configuration: Configuration, database: S
must run command must run command
""" """
args = _default_args(args) args = _default_args(args)
user = User(username=args.username, password=args.password, access=args.role) user = User(username=args.username, password=args.password, access=args.role,
packager_id=args.packager, key=args.key)
mocker.patch("ahriman.core.database.SQLite.load", return_value=database) mocker.patch("ahriman.core.database.SQLite.load", return_value=database)
mocker.patch("ahriman.models.user.User.hash_password", return_value=user) mocker.patch("ahriman.models.user.User.hash_password", return_value=user)
get_auth_configuration_mock = mocker.patch("ahriman.application.handlers.Users.configuration_get") get_auth_configuration_mock = mocker.patch("ahriman.application.handlers.Users.configuration_get")
@ -61,7 +64,8 @@ def test_run_empty_salt(args: argparse.Namespace, configuration: Configuration,
must create configuration if salt was not set must create configuration if salt was not set
""" """
args = _default_args(args) args = _default_args(args)
user = User(username=args.username, password=args.password, access=args.role) user = User(username=args.username, password=args.password, access=args.role,
packager_id=args.packager, key=args.key)
mocker.patch("ahriman.core.database.SQLite.load", return_value=database) mocker.patch("ahriman.core.database.SQLite.load", return_value=database)
mocker.patch("ahriman.models.user.User.hash_password", return_value=user) mocker.patch("ahriman.models.user.User.hash_password", return_value=user)
get_auth_configuration_mock = mocker.patch("ahriman.application.handlers.Users.configuration_get") get_auth_configuration_mock = mocker.patch("ahriman.application.handlers.Users.configuration_get")

View File

@ -351,13 +351,14 @@ def test_subparsers_repo_backup_architecture(parser: argparse.ArgumentParser) ->
def test_subparsers_repo_check(parser: argparse.ArgumentParser) -> None: def test_subparsers_repo_check(parser: argparse.ArgumentParser) -> None:
""" """
repo-check command must imply dependencies, dry-run, aur and manual repo-check command must imply dependencies, dry-run, aur, manual and username
""" """
args = parser.parse_args(["repo-check"]) args = parser.parse_args(["repo-check"])
assert not args.dependencies assert not args.dependencies
assert args.dry_run assert args.dry_run
assert args.aur assert args.aur
assert not args.manual assert not args.manual
assert args.username is None
def test_subparsers_repo_check_architecture(parser: argparse.ArgumentParser) -> None: def test_subparsers_repo_check_architecture(parser: argparse.ArgumentParser) -> None:
@ -757,14 +758,13 @@ def test_subparsers_user_add_option_role(parser: argparse.ArgumentParser) -> Non
def test_subparsers_user_list(parser: argparse.ArgumentParser) -> None: def test_subparsers_user_list(parser: argparse.ArgumentParser) -> None:
""" """
user-list command must imply action, architecture, lock, report, password, quiet and unsafe user-list command must imply action, architecture, lock, report, quiet and unsafe
""" """
args = parser.parse_args(["user-list"]) args = parser.parse_args(["user-list"])
assert args.action == Action.List assert args.action == Action.List
assert args.architecture == [""] assert args.architecture == [""]
assert args.lock is None assert args.lock is None
assert not args.report assert not args.report
assert args.password is not None
assert args.quiet assert args.quiet
assert args.unsafe assert args.unsafe
@ -787,14 +787,13 @@ def test_subparsers_user_list_option_role(parser: argparse.ArgumentParser) -> No
def test_subparsers_user_remove(parser: argparse.ArgumentParser) -> None: def test_subparsers_user_remove(parser: argparse.ArgumentParser) -> None:
""" """
user-remove command must imply action, architecture, lock, report, password and quiet user-remove command must imply action, architecture, lock, report and quiet
""" """
args = parser.parse_args(["user-remove", "username"]) args = parser.parse_args(["user-remove", "username"])
assert args.action == Action.Remove assert args.action == Action.Remove
assert args.architecture == [""] assert args.architecture == [""]
assert args.lock is None assert args.lock is None
assert not args.report assert not args.report
assert args.password is not None
assert args.quiet assert args.quiet

View File

@ -265,7 +265,8 @@ def package_ahriman(package_description_ahriman: PackageDescription, remote_sour
base="ahriman", base="ahriman",
version="2.6.0-1", version="2.6.0-1",
remote=remote_source, remote=remote_source,
packages=packages) packages=packages,
packager="packager")
@pytest.fixture @pytest.fixture
@ -499,7 +500,7 @@ def user() -> User:
Returns: Returns:
User: user descriptor instance User: user descriptor instance
""" """
return User(username="user", password="pa55w0rd", access=UserAccess.Reporter) return User(username="user", password="pa55w0rd", access=UserAccess.Reporter, packager_id="packager", key="key")
@pytest.fixture @pytest.fixture

View File

@ -10,7 +10,7 @@ def test_build(task_ahriman: Task, mocker: MockerFixture) -> None:
must build package must build package
""" """
check_output_mock = mocker.patch("ahriman.core.build_tools.task.Task._check_output") check_output_mock = mocker.patch("ahriman.core.build_tools.task.Task._check_output")
task_ahriman.build(Path("ahriman")) task_ahriman.build(Path("ahriman"), "packager")
check_output_mock.assert_called() check_output_mock.assert_called()

View File

@ -27,7 +27,7 @@ def test_migrate_data(connection: Connection, configuration: Configuration, mock
def test_migrate_package_depends(connection: Connection, configuration: Configuration, package_ahriman: Package, def test_migrate_package_depends(connection: Connection, configuration: Configuration, package_ahriman: Package,
mocker: MockerFixture) -> None: mocker: MockerFixture) -> None:
""" """
must update make and opt depends list must update check depends list
""" """
mocker.patch("pathlib.Path.is_dir", return_value=True) mocker.patch("pathlib.Path.is_dir", return_value=True)
mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman.packages[package_ahriman.base].filepath]) mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman.packages[package_ahriman.base].filepath])
@ -45,7 +45,7 @@ def test_migrate_package_depends(connection: Connection, configuration: Configur
def test_migrate_package_depends_skip(connection: Connection, configuration: Configuration, def test_migrate_package_depends_skip(connection: Connection, configuration: Configuration,
mocker: MockerFixture) -> None: mocker: MockerFixture) -> None:
""" """
must skip update make and opt depends list if no repository directory found must skip update check depends list if no repository directory found
""" """
mocker.patch("pathlib.Path.is_dir", return_value=False) mocker.patch("pathlib.Path.is_dir", return_value=False)
migrate_package_check_depends(connection, configuration) migrate_package_check_depends(connection, configuration)

View File

@ -0,0 +1,52 @@
import pytest
from pytest_mock import MockerFixture
from sqlite3 import Connection
from ahriman.core.configuration import Configuration
from ahriman.core.database.migrations.m008_packagers import migrate_data, migrate_package_base_packager, steps
from ahriman.models.package import Package
def test_migration_packagers() -> None:
"""
migration must not be empty
"""
assert steps
def test_migrate_data(connection: Connection, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must perform data migration
"""
depends_mock = mocker.patch("ahriman.core.database.migrations.m008_packagers.migrate_package_base_packager")
migrate_data(connection, configuration)
depends_mock.assert_called_once_with(connection, configuration)
def test_migrate_package_base_packager(connection: Connection, configuration: Configuration, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must update packagers
"""
mocker.patch("pathlib.Path.is_dir", return_value=True)
mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman.packages[package_ahriman.base].filepath])
package_mock = mocker.patch("ahriman.models.package.Package.from_archive", return_value=package_ahriman)
migrate_package_base_packager(connection, configuration)
package_mock.assert_called_once_with(
package_ahriman.packages[package_ahriman.base].filepath, pytest.helpers.anyvar(int), remote=None)
connection.executemany.assert_called_once_with(pytest.helpers.anyvar(str, strict=True), [{
"package_base": package_ahriman.base,
"packager": package_ahriman.packager,
}])
def test_migrate_package_depends_skip(connection: Connection, configuration: Configuration,
mocker: MockerFixture) -> None:
"""
must skip update packagers if no repository directory found
"""
mocker.patch("pathlib.Path.is_dir", return_value=False)
migrate_package_base_packager(connection, configuration)
connection.executemany.assert_not_called()

View File

@ -16,21 +16,22 @@ def test_user_list(database: SQLite, user: User) -> None:
must return all users must return all users
""" """
database.user_update(user) database.user_update(user)
database.user_update(User(username=user.password, password=user.username, access=user.access)) second = User(username=user.password, password=user.username, access=user.access, packager_id=None, key=None)
database.user_update(second)
users = database.user_list(None, None) users = database.user_list(None, None)
assert len(users) == 2 assert len(users) == 2
assert user in users assert user in users
assert User(username=user.password, password=user.username, access=user.access) in users assert second in users
def test_user_list_filter_by_username(database: SQLite) -> None: def test_user_list_filter_by_username(database: SQLite) -> None:
""" """
must return users filtered by its id must return users filtered by its id
""" """
first = User(username="1", password="", access=UserAccess.Read) first = User(username="1", password="", access=UserAccess.Read, packager_id=None, key=None)
second = User(username="2", password="", access=UserAccess.Full) second = User(username="2", password="", access=UserAccess.Full, packager_id=None, key=None)
third = User(username="3", password="", access=UserAccess.Read) third = User(username="3", password="", access=UserAccess.Read, packager_id=None, key=None)
database.user_update(first) database.user_update(first)
database.user_update(second) database.user_update(second)
@ -45,9 +46,9 @@ def test_user_list_filter_by_access(database: SQLite) -> None:
""" """
must return users filtered by its access must return users filtered by its access
""" """
first = User(username="1", password="", access=UserAccess.Read) first = User(username="1", password="", access=UserAccess.Read, packager_id=None, key=None)
second = User(username="2", password="", access=UserAccess.Full) second = User(username="2", password="", access=UserAccess.Full, packager_id=None, key=None)
third = User(username="3", password="", access=UserAccess.Read) third = User(username="3", password="", access=UserAccess.Read, packager_id=None, key=None)
database.user_update(first) database.user_update(first)
database.user_update(second) database.user_update(second)
@ -63,9 +64,9 @@ def test_user_list_filter_by_username_access(database: SQLite) -> None:
""" """
must return users filtered by its access and username must return users filtered by its access and username
""" """
first = User(username="1", password="", access=UserAccess.Read) first = User(username="1", password="", access=UserAccess.Read, packager_id=None, key=None)
second = User(username="2", password="", access=UserAccess.Full) second = User(username="2", password="", access=UserAccess.Full, packager_id=None, key=None)
third = User(username="3", password="", access=UserAccess.Read) third = User(username="3", password="", access=UserAccess.Read, packager_id=None, key=None)
database.user_update(first) database.user_update(first)
database.user_update(second) database.user_update(second)
@ -91,6 +92,7 @@ def test_user_update(database: SQLite, user: User) -> None:
database.user_update(user) database.user_update(user)
assert database.user_get(user.username) == user assert database.user_get(user.username) == user
new_user = User(username=user.username, password=user.hash_password("salt").password, access=UserAccess.Full) new_user = User(username=user.username, password=user.hash_password("salt").password, access=UserAccess.Full,
packager_id=None, key="new key")
database.user_update(new_user) database.user_update(new_user)
assert database.user_get(new_user.username) == new_user assert database.user_get(new_user.username) == new_user

View File

@ -32,7 +32,7 @@ def test_package_update(database: SQLite, configuration: Configuration, package_
fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch") fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch")
patches_mock = mocker.patch("ahriman.core.database.SQLite.patches_get", return_value=[patch1, patch2]) patches_mock = mocker.patch("ahriman.core.database.SQLite.patches_get", return_value=[patch1, patch2])
patches_write_mock = mocker.patch("ahriman.models.pkgbuild_patch.PkgbuildPatch.write") patches_write_mock = mocker.patch("ahriman.models.pkgbuild_patch.PkgbuildPatch.write")
runner = RemotePush(configuration, database, "gitremote") runner = RemotePush(database, configuration, "gitremote")
assert runner.package_update(package_ahriman, local) == package_ahriman.base assert runner.package_update(package_ahriman, local) == package_ahriman.base
glob_mock.assert_called_once_with(".git*") glob_mock.assert_called_once_with(".git*")
@ -56,7 +56,7 @@ def test_packages_update(database: SQLite, configuration: Configuration, result:
""" """
update_mock = mocker.patch("ahriman.core.gitremote.remote_push.RemotePush.package_update", update_mock = mocker.patch("ahriman.core.gitremote.remote_push.RemotePush.package_update",
return_value=[package_ahriman.base]) return_value=[package_ahriman.base])
runner = RemotePush(configuration, database, "gitremote") runner = RemotePush(database, configuration, "gitremote")
local = Path("local") local = Path("local")
assert list(runner.packages_update(result, local)) assert list(runner.packages_update(result, local))
@ -71,7 +71,7 @@ def test_run(database: SQLite, configuration: Configuration, result: Result, pac
mocker.patch("ahriman.core.gitremote.remote_push.RemotePush.packages_update", return_value=[package_ahriman.base]) mocker.patch("ahriman.core.gitremote.remote_push.RemotePush.packages_update", return_value=[package_ahriman.base])
fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch") fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch")
push_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.push") push_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.push")
runner = RemotePush(configuration, database, "gitremote") runner = RemotePush(database, configuration, "gitremote")
runner.run(result) runner.run(result)
fetch_mock.assert_called_once_with(pytest.helpers.anyvar(int), runner.remote_source) fetch_mock.assert_called_once_with(pytest.helpers.anyvar(int), runner.remote_source)
@ -85,7 +85,7 @@ def test_run_failed(database: SQLite, configuration: Configuration, result: Resu
must reraise exception on error occurred must reraise exception on error occurred
""" """
mocker.patch("ahriman.core.build_tools.sources.Sources.fetch", side_effect=Exception()) mocker.patch("ahriman.core.build_tools.sources.Sources.fetch", side_effect=Exception())
runner = RemotePush(configuration, database, "gitremote") runner = RemotePush(database, configuration, "gitremote")
with pytest.raises(GitRemoteError): with pytest.raises(GitRemoteError):
runner.run(result) runner.run(result)

View File

@ -6,6 +6,8 @@ from unittest.mock import call as MockCall
from ahriman.core.repository.executor import Executor from ahriman.core.repository.executor import Executor
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.packagers import Packagers
from ahriman.models.user import User
def test_load_archives(executor: Executor) -> None: def test_load_archives(executor: Executor) -> None:
@ -33,7 +35,7 @@ def test_process_build(executor: Executor, package_ahriman: Package, mocker: Moc
move_mock = mocker.patch("shutil.move") move_mock = mocker.patch("shutil.move")
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_building") status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_building")
executor.process_build([package_ahriman]) executor.process_build([package_ahriman], Packagers("packager"))
# must move files (once) # must move files (once)
move_mock.assert_called_once_with(Path(package_ahriman.base), executor.paths.packages / package_ahriman.base) move_mock.assert_called_once_with(Path(package_ahriman.base), executor.paths.packages / package_ahriman.base)
# must update status # must update status
@ -157,7 +159,7 @@ def test_process_remove_unknown(executor: Executor, package_ahriman: Package, mo
status_client_mock.assert_called_once_with(package_ahriman.base) status_client_mock.assert_called_once_with(package_ahriman.base)
def test_process_update(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None: def test_process_update(executor: Executor, package_ahriman: Package, user: User, mocker: MockerFixture) -> None:
""" """
must run update process must run update process
""" """
@ -168,14 +170,16 @@ def test_process_update(executor: Executor, package_ahriman: Package, mocker: Mo
sign_package_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process_sign_package", side_effect=lambda fn, _: [fn]) sign_package_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process_sign_package", side_effect=lambda fn, _: [fn])
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_success") status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_success")
remove_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_remove") remove_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_remove")
packager_mock = mocker.patch("ahriman.core.repository.executor.Executor.packager", return_value=user)
filepath = next(package.filepath for package in package_ahriman.packages.values()) filepath = next(package.filepath for package in package_ahriman.packages.values())
# must return complete # must return complete
assert executor.process_update([filepath]) assert executor.process_update([filepath], Packagers("packager"))
packager_mock.assert_called_once_with(Packagers("packager"), "ahriman")
# must move files (once) # must move files (once)
move_mock.assert_called_once_with(executor.paths.packages / filepath, executor.paths.repository / filepath) move_mock.assert_called_once_with(executor.paths.packages / filepath, executor.paths.repository / filepath)
# must sign package # must sign package
sign_package_mock.assert_called_once_with(executor.paths.packages / filepath, package_ahriman.base) sign_package_mock.assert_called_once_with(executor.paths.packages / filepath, user.key)
# must add package # must add package
repo_add_mock.assert_called_once_with(executor.paths.repository / filepath) repo_add_mock.assert_called_once_with(executor.paths.repository / filepath)
# must update status # must update status

View File

@ -4,6 +4,10 @@ from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite from ahriman.core.database import SQLite
from ahriman.core.exceptions import UnsafeRunError from ahriman.core.exceptions import UnsafeRunError
from ahriman.core.repository.repository_properties import RepositoryProperties from ahriman.core.repository.repository_properties import RepositoryProperties
from ahriman.models.packagers import Packagers
from ahriman.models.pacman_synchronization import PacmanSynchronization
from ahriman.models.user import User
from ahriman.models.user_access import UserAccess
def test_create_tree_on_load(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> None: def test_create_tree_on_load(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> None:
@ -12,7 +16,8 @@ def test_create_tree_on_load(configuration: Configuration, database: SQLite, moc
""" """
mocker.patch("ahriman.core.repository.repository_properties.check_user") mocker.patch("ahriman.core.repository.repository_properties.check_user")
tree_create_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") tree_create_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
RepositoryProperties("x86_64", configuration, database, report=False, unsafe=False, refresh_pacman_database=0) RepositoryProperties("x86_64", configuration, database, report=False, unsafe=False,
refresh_pacman_database=PacmanSynchronization.Disabled)
tree_create_mock.assert_called_once_with() tree_create_mock.assert_called_once_with()
@ -23,6 +28,36 @@ def test_create_tree_on_load_unsafe(configuration: Configuration, database: SQLi
""" """
mocker.patch("ahriman.core.repository.repository_properties.check_user", side_effect=UnsafeRunError(0, 1)) mocker.patch("ahriman.core.repository.repository_properties.check_user", side_effect=UnsafeRunError(0, 1))
tree_create_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") tree_create_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
RepositoryProperties("x86_64", configuration, database, report=False, unsafe=False, refresh_pacman_database=0) RepositoryProperties("x86_64", configuration, database, report=False, unsafe=False,
refresh_pacman_database=PacmanSynchronization.Disabled)
tree_create_mock.assert_not_called() tree_create_mock.assert_not_called()
def test_packager(repository: RepositoryProperties, mocker: MockerFixture) -> None:
"""
must extract packager
"""
database_mock = mocker.patch("ahriman.core.database.SQLite.user_get")
assert repository.packager(Packagers("username", {}), "base")
database_mock.assert_called_once_with("username")
def test_packager_empty(repository: RepositoryProperties, mocker: MockerFixture) -> None:
"""
must return empty user if username was not set
"""
database_mock = mocker.patch("ahriman.core.database.SQLite.user_get")
user = User(username="", password="", access=UserAccess.Read, packager_id=None, key=None)
assert repository.packager(Packagers(), "base") == user
database_mock.assert_not_called()
def test_packager_empty_result(repository: RepositoryProperties, mocker: MockerFixture) -> None:
"""
must return empty user if it wasn't found in database
"""
database_mock = mocker.patch("ahriman.core.database.SQLite.user_get", return_value=None)
user = User(username="username", password="", access=UserAccess.Read, packager_id=None, key=None)
assert repository.packager(Packagers(user.username), "base") == user
database_mock.assert_called_once_with(user.username)

View File

@ -74,7 +74,7 @@ def test_updates_aur_filter(update_handler: UpdateHandler, package_ahriman: Pack
package_load_mock = mocker.patch("ahriman.models.package.Package.from_aur", return_value=package_ahriman) package_load_mock = mocker.patch("ahriman.models.package.Package.from_aur", return_value=package_ahriman)
assert update_handler.updates_aur([package_ahriman.base], vcs=True) == [package_ahriman] assert update_handler.updates_aur([package_ahriman.base], vcs=True) == [package_ahriman]
package_load_mock.assert_called_once_with(package_ahriman.base, update_handler.pacman) package_load_mock.assert_called_once_with(package_ahriman.base, update_handler.pacman, None)
def test_updates_aur_ignore(update_handler: UpdateHandler, package_ahriman: Package, def test_updates_aur_ignore(update_handler: UpdateHandler, package_ahriman: Package,
@ -120,7 +120,7 @@ def test_updates_local(update_handler: UpdateHandler, package_ahriman: Package,
assert update_handler.updates_local(vcs=True) == [package_ahriman] assert update_handler.updates_local(vcs=True) == [package_ahriman]
fetch_mock.assert_called_once_with(Path(package_ahriman.base), remote=None) fetch_mock.assert_called_once_with(Path(package_ahriman.base), remote=None)
package_load_mock.assert_called_once_with(Path(package_ahriman.base), "x86_64") package_load_mock.assert_called_once_with(Path(package_ahriman.base), "x86_64", None)
status_client_mock.assert_called_once_with(package_ahriman.base) status_client_mock.assert_called_once_with(package_ahriman.base)
package_is_outdated_mock.assert_called_once_with( package_is_outdated_mock.assert_called_once_with(
package_ahriman, update_handler.paths, package_ahriman, update_handler.paths,

View File

@ -135,21 +135,6 @@ def test_key_import(gpg: GPG, mocker: MockerFixture) -> None:
check_output_mock.assert_called_once_with("gpg", "--import", input_data="key", logger=pytest.helpers.anyvar(int)) check_output_mock.assert_called_once_with("gpg", "--import", input_data="key", logger=pytest.helpers.anyvar(int))
def test_keys(gpg: GPG) -> None:
"""
must extract keys
"""
assert gpg.keys() == []
gpg.default_key = "key"
assert gpg.keys() == [gpg.default_key]
gpg.configuration.set_option("sign", "key_a", "key1")
gpg.configuration.set_option("sign", "key_b", "key1")
gpg.configuration.set_option("sign", "key_c", "key2")
assert gpg.keys() == ["key", "key1", "key2"]
def test_process(gpg_with_key: GPG, mocker: MockerFixture) -> None: def test_process(gpg_with_key: GPG, mocker: MockerFixture) -> None:
""" """
must call process method correctly must call process method correctly
@ -170,7 +155,7 @@ def test_process_sign_package_1(gpg_with_key: GPG, mocker: MockerFixture) -> Non
gpg_with_key.targets = {SignSettings.Packages} gpg_with_key.targets = {SignSettings.Packages}
assert gpg_with_key.process_sign_package(Path("a"), "a") == result assert gpg_with_key.process_sign_package(Path("a"), "a") == result
process_mock.assert_called_once_with(Path("a"), "key") process_mock.assert_called_once_with(Path("a"), "a")
def test_process_sign_package_2(gpg_with_key: GPG, mocker: MockerFixture) -> None: def test_process_sign_package_2(gpg_with_key: GPG, mocker: MockerFixture) -> None:
@ -182,7 +167,19 @@ def test_process_sign_package_2(gpg_with_key: GPG, mocker: MockerFixture) -> Non
gpg_with_key.targets = {SignSettings.Packages, SignSettings.Repository} gpg_with_key.targets = {SignSettings.Packages, SignSettings.Repository}
assert gpg_with_key.process_sign_package(Path("a"), "a") == result assert gpg_with_key.process_sign_package(Path("a"), "a") == result
process_mock.assert_called_once_with(Path("a"), "key") process_mock.assert_called_once_with(Path("a"), "a")
def test_process_sign_package_3(gpg_with_key: GPG, mocker: MockerFixture) -> None:
"""
must sign package with default key if none passed
"""
result = [Path("a"), Path("a.sig")]
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process", return_value=result)
gpg_with_key.targets = {SignSettings.Packages}
assert gpg_with_key.process_sign_package(Path("a"), None) == result
process_mock.assert_called_once_with(Path("a"), gpg_with_key.default_key)
def test_process_sign_package_skip_1(gpg_with_key: GPG, mocker: MockerFixture) -> None: def test_process_sign_package_skip_1(gpg_with_key: GPG, mocker: MockerFixture) -> None:
@ -211,7 +208,7 @@ def test_process_sign_package_skip_3(gpg: GPG, mocker: MockerFixture) -> None:
""" """
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process") process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg.targets = {SignSettings.Packages} gpg.targets = {SignSettings.Packages}
gpg.process_sign_package(Path("a"), "a") gpg.process_sign_package(Path("a"), None)
process_mock.assert_not_called() process_mock.assert_not_called()
@ -221,7 +218,7 @@ def test_process_sign_package_skip_4(gpg: GPG, mocker: MockerFixture) -> None:
""" """
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process") process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg.targets = {SignSettings.Packages, SignSettings.Repository} gpg.targets = {SignSettings.Packages, SignSettings.Repository}
gpg.process_sign_package(Path("a"), "a") gpg.process_sign_package(Path("a"), None)
process_mock.assert_not_called() process_mock.assert_not_called()

View File

@ -1,24 +1,26 @@
import pytest import pytest
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.sign.gpg import GPG from ahriman.core.sign.gpg import GPG
from ahriman.core.support.pkgbuild.keyring_generator import KeyringGenerator from ahriman.core.support.pkgbuild.keyring_generator import KeyringGenerator
from ahriman.core.support.pkgbuild.pkgbuild_generator import PkgbuildGenerator from ahriman.core.support.pkgbuild.pkgbuild_generator import PkgbuildGenerator
@pytest.fixture @pytest.fixture
def keyring_generator(gpg: GPG, configuration: Configuration) -> KeyringGenerator: def keyring_generator(database: SQLite, gpg: GPG, configuration: Configuration) -> KeyringGenerator:
""" """
fixture for keyring pkgbuild generator fixture for keyring pkgbuild generator
Args: Args:
database(SQLite): database fixture
gpg(GPG): empty GPG fixture gpg(GPG): empty GPG fixture
configuration(Configuration): configuration fixture configuration(Configuration): configuration fixture
Returns: Returns:
KeyringGenerator: keyring generator test instance KeyringGenerator: keyring generator test instance
""" """
return KeyringGenerator(gpg, configuration, "keyring") return KeyringGenerator(database, gpg, configuration, "keyring")
@pytest.fixture @pytest.fixture

View File

@ -5,84 +5,87 @@ from pytest_mock import MockerFixture
from unittest.mock import MagicMock, call as MockCall from unittest.mock import MagicMock, call as MockCall
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.exceptions import PkgbuildGeneratorError from ahriman.core.exceptions import PkgbuildGeneratorError
from ahriman.core.sign.gpg import GPG from ahriman.core.sign.gpg import GPG
from ahriman.core.support.pkgbuild.keyring_generator import KeyringGenerator from ahriman.core.support.pkgbuild.keyring_generator import KeyringGenerator
from ahriman.models.user import User
def test_init_packagers(gpg: GPG, configuration: Configuration, mocker: MockerFixture) -> None: def test_init_packagers(database: SQLite, gpg: GPG, configuration: Configuration, user: User,
mocker: MockerFixture) -> None:
""" """
must extract packagers keys must extract packagers keys
""" """
mocker.patch("ahriman.core.sign.gpg.GPG.keys", return_value=["key"]) mocker.patch("ahriman.core.database.SQLite.user_list", return_value=[user])
assert KeyringGenerator(gpg, configuration, "keyring").packagers == ["key"] assert KeyringGenerator(database, gpg, configuration, "keyring").packagers == ["key"]
configuration.set_option("keyring", "packagers", "key1") configuration.set_option("keyring", "packagers", "key1")
assert KeyringGenerator(gpg, configuration, "keyring").packagers == ["key1"] assert KeyringGenerator(database, gpg, configuration, "keyring").packagers == ["key1"]
def test_init_revoked(gpg: GPG, configuration: Configuration) -> None: def test_init_revoked(database: SQLite, gpg: GPG, configuration: Configuration) -> None:
""" """
must extract revoked keys must extract revoked keys
""" """
assert KeyringGenerator(gpg, configuration, "keyring").revoked == [] assert KeyringGenerator(database, gpg, configuration, "keyring").revoked == []
configuration.set_option("keyring", "revoked", "key1") configuration.set_option("keyring", "revoked", "key1")
assert KeyringGenerator(gpg, configuration, "keyring").revoked == ["key1"] assert KeyringGenerator(database, gpg, configuration, "keyring").revoked == ["key1"]
def test_init_trusted(gpg: GPG, configuration: Configuration) -> None: def test_init_trusted(database: SQLite, gpg: GPG, configuration: Configuration) -> None:
""" """
must extract trusted keys must extract trusted keys
""" """
assert KeyringGenerator(gpg, configuration, "keyring").trusted == [] assert KeyringGenerator(database, gpg, configuration, "keyring").trusted == []
gpg.default_key = "key" gpg.default_key = "key"
assert KeyringGenerator(gpg, configuration, "keyring").trusted == ["key"] assert KeyringGenerator(database, gpg, configuration, "keyring").trusted == ["key"]
configuration.set_option("keyring", "trusted", "key1") configuration.set_option("keyring", "trusted", "key1")
assert KeyringGenerator(gpg, configuration, "keyring").trusted == ["key1"] assert KeyringGenerator(database, gpg, configuration, "keyring").trusted == ["key1"]
def test_license(gpg: GPG, configuration: Configuration) -> None: def test_license(database: SQLite, gpg: GPG, configuration: Configuration) -> None:
""" """
must generate correct licenses list must generate correct licenses list
""" """
assert KeyringGenerator(gpg, configuration, "keyring").license == ["Unlicense"] assert KeyringGenerator(database, gpg, configuration, "keyring").license == ["Unlicense"]
configuration.set_option("keyring", "license", "GPL MPL") configuration.set_option("keyring", "license", "GPL MPL")
assert KeyringGenerator(gpg, configuration, "keyring").license == ["GPL", "MPL"] assert KeyringGenerator(database, gpg, configuration, "keyring").license == ["GPL", "MPL"]
def test_pkgdesc(gpg: GPG, configuration: Configuration) -> None: def test_pkgdesc(database: SQLite, gpg: GPG, configuration: Configuration) -> None:
""" """
must generate correct pkgdesc property must generate correct pkgdesc property
""" """
assert KeyringGenerator(gpg, configuration, "keyring").pkgdesc == "aur-clone PGP keyring" assert KeyringGenerator(database, gpg, configuration, "keyring").pkgdesc == "aur-clone PGP keyring"
configuration.set_option("keyring", "description", "description") configuration.set_option("keyring", "description", "description")
assert KeyringGenerator(gpg, configuration, "keyring").pkgdesc == "description" assert KeyringGenerator(database, gpg, configuration, "keyring").pkgdesc == "description"
def test_pkgname(gpg: GPG, configuration: Configuration) -> None: def test_pkgname(database: SQLite, gpg: GPG, configuration: Configuration) -> None:
""" """
must generate correct pkgname property must generate correct pkgname property
""" """
assert KeyringGenerator(gpg, configuration, "keyring").pkgname == "aur-clone-keyring" assert KeyringGenerator(database, gpg, configuration, "keyring").pkgname == "aur-clone-keyring"
configuration.set_option("keyring", "package", "keyring") configuration.set_option("keyring", "package", "keyring")
assert KeyringGenerator(gpg, configuration, "keyring").pkgname == "keyring" assert KeyringGenerator(database, gpg, configuration, "keyring").pkgname == "keyring"
def test_url(gpg: GPG, configuration: Configuration) -> None: def test_url(database: SQLite, gpg: GPG, configuration: Configuration) -> None:
""" """
must generate correct url property must generate correct url property
""" """
assert KeyringGenerator(gpg, configuration, "keyring").url == "" assert KeyringGenerator(database, gpg, configuration, "keyring").url == ""
configuration.set_option("keyring", "homepage", "homepage") configuration.set_option("keyring", "homepage", "homepage")
assert KeyringGenerator(gpg, configuration, "keyring").url == "homepage" assert KeyringGenerator(database, gpg, configuration, "keyring").url == "homepage"
def test_generate_gpg(keyring_generator: KeyringGenerator, mocker: MockerFixture) -> None: def test_generate_gpg(keyring_generator: KeyringGenerator, mocker: MockerFixture) -> None:

View File

@ -1,6 +1,8 @@
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from unittest.mock import call as MockCall
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.sign.gpg import GPG from ahriman.core.sign.gpg import GPG
from ahriman.core.support import KeyringTrigger from ahriman.core.support import KeyringTrigger
from ahriman.models.context_key import ContextKey from ahriman.models.context_key import ContextKey
@ -21,10 +23,10 @@ def test_on_start(configuration: Configuration, mocker: MockerFixture) -> None:
""" """
must run report for specified targets must run report for specified targets
""" """
gpg_mock = mocker.patch("ahriman.core._Context.get") context_mock = mocker.patch("ahriman.core._Context.get")
run_mock = mocker.patch("ahriman.core.support.package_creator.PackageCreator.run") run_mock = mocker.patch("ahriman.core.support.package_creator.PackageCreator.run")
trigger = KeyringTrigger("x86_64", configuration) trigger = KeyringTrigger("x86_64", configuration)
trigger.on_start() trigger.on_start()
gpg_mock.assert_called_once_with(ContextKey("sign", GPG)) context_mock.assert_has_calls([MockCall(ContextKey("sign", GPG)), MockCall(ContextKey("database", SQLite))])
run_mock.assert_called_once_with() run_mock.assert_called_once_with()

View File

@ -35,6 +35,6 @@ def test_run(package_creator: PackageCreator, database: SQLite, mocker: MockerFi
write_mock.assert_called_once_with(local_path) write_mock.assert_called_once_with(local_path)
init_mock.assert_called_once_with(local_path) init_mock.assert_called_once_with(local_path)
package_mock.assert_called_once_with(local_path, "x86_64") package_mock.assert_called_once_with(local_path, "x86_64", None)
database_mock.assert_called_once_with(ContextKey("database", SQLite)) database_mock.assert_called_once_with(ContextKey("database", SQLite))
insert_mock.assert_called_once_with(package, pytest.helpers.anyvar(int)) insert_mock.assert_called_once_with(package, pytest.helpers.anyvar(int))

View File

@ -42,7 +42,7 @@ def test_spawn_process(spawner: Spawn, mocker: MockerFixture) -> None:
""" """
start_mock = mocker.patch("multiprocessing.Process.start") start_mock = mocker.patch("multiprocessing.Process.start")
spawner._spawn_process("add", "ahriman", now="", maybe="?") spawner._spawn_process("add", "ahriman", now="", maybe="?", none=None)
start_mock.assert_called_once_with() start_mock.assert_called_once_with()
spawner.args_parser.parse_args.assert_called_once_with( spawner.args_parser.parse_args.assert_called_once_with(
spawner.command_arguments + [ spawner.command_arguments + [
@ -74,8 +74,8 @@ def test_packages_add(spawner: Spawn, mocker: MockerFixture) -> None:
must call package addition must call package addition
""" """
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process") spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process")
spawner.packages_add(["ahriman", "linux"], now=False) spawner.packages_add(["ahriman", "linux"], None, now=False)
spawn_mock.assert_called_once_with("package-add", "ahriman", "linux", source="aur") spawn_mock.assert_called_once_with("package-add", "ahriman", "linux", source="aur", username=None)
def test_packages_add_with_build(spawner: Spawn, mocker: MockerFixture) -> None: def test_packages_add_with_build(spawner: Spawn, mocker: MockerFixture) -> None:
@ -83,8 +83,17 @@ def test_packages_add_with_build(spawner: Spawn, mocker: MockerFixture) -> None:
must call package addition with update must call package addition with update
""" """
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process") spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process")
spawner.packages_add(["ahriman", "linux"], now=True) spawner.packages_add(["ahriman", "linux"], None, now=True)
spawn_mock.assert_called_once_with("package-add", "ahriman", "linux", source="aur", now="") spawn_mock.assert_called_once_with("package-add", "ahriman", "linux", source="aur", username=None, now="")
def test_packages_add_with_username(spawner: Spawn, mocker: MockerFixture) -> None:
"""
must call package addition with username
"""
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process")
spawner.packages_add(["ahriman", "linux"], "username", now=False)
spawn_mock.assert_called_once_with("package-add", "ahriman", "linux", source="aur", username="username")
def test_packages_rebuild(spawner: Spawn, mocker: MockerFixture) -> None: def test_packages_rebuild(spawner: Spawn, mocker: MockerFixture) -> None:
@ -92,8 +101,8 @@ def test_packages_rebuild(spawner: Spawn, mocker: MockerFixture) -> None:
must call package rebuild must call package rebuild
""" """
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process") spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process")
spawner.packages_rebuild("python") spawner.packages_rebuild("python", "packager")
spawn_mock.assert_called_once_with("repo-rebuild", **{"depends-on": "python"}) spawn_mock.assert_called_once_with("repo-rebuild", **{"depends-on": "python", "username": "packager"})
def test_packages_remove(spawner: Spawn, mocker: MockerFixture) -> None: def test_packages_remove(spawner: Spawn, mocker: MockerFixture) -> None:
@ -110,8 +119,8 @@ def test_packages_update(spawner: Spawn, mocker: MockerFixture) -> None:
must call repo update must call repo update
""" """
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process") spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process")
spawner.packages_update() spawner.packages_update("packager")
spawn_mock.assert_called_once_with("repo-update") spawn_mock.assert_called_once_with("repo-update", username="packager")
def test_run(spawner: Spawn, mocker: MockerFixture) -> None: def test_run(spawner: Spawn, mocker: MockerFixture) -> None:

View File

@ -11,9 +11,9 @@ from typing import Any
from unittest.mock import MagicMock from unittest.mock import MagicMock
from ahriman.core.exceptions import BuildError, OptionError, UnsafeRunError from ahriman.core.exceptions import BuildError, OptionError, UnsafeRunError
from ahriman.core.util import check_output, check_user, enum_values, exception_response_text, filter_json, \ from ahriman.core.util import check_output, check_user, dataclass_view, enum_values, exception_response_text,\
full_version, package_like, partition, pretty_datetime, pretty_size, safe_filename, srcinfo_property, \ extract_user, filter_json, full_version, package_like, partition, pretty_datetime, pretty_size, safe_filename, \
srcinfo_property_list, trim_package, utcnow, walk srcinfo_property, srcinfo_property_list, trim_package, utcnow, walk
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource from ahriman.models.package_source import PackageSource
from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.repository_paths import RepositoryPaths
@ -91,6 +91,16 @@ def test_check_output_with_user(passwd: Any, mocker: MockerFixture) -> None:
getpwuid_mock.assert_called_once_with(user) getpwuid_mock.assert_called_once_with(user)
def test_check_output_with_user_and_environment(passwd: Any, mocker: MockerFixture) -> None:
"""
must run set environment if both environment and user are set
"""
mocker.patch("ahriman.core.util.getpwuid", return_value=passwd)
user = os.getuid()
assert check_output("python", "-c", """import os; print(os.getenv("HOME"), os.getenv("VAR"))""",
environment={"VAR": "VALUE"}, user=user) == f"{passwd.pw_dir} VALUE"
def test_check_output_failure(mocker: MockerFixture) -> None: def test_check_output_failure(mocker: MockerFixture) -> None:
""" """
must process exception correctly must process exception correctly
@ -155,6 +165,23 @@ def test_check_user_unsafe(mocker: MockerFixture) -> None:
check_user(paths, unsafe=True) check_user(paths, unsafe=True)
def test_dataclass_view(package_ahriman: Package) -> None:
"""
must serialize dataclasses
"""
assert Package.from_json(dataclass_view(package_ahriman)) == package_ahriman
def test_dataclass_view_without_none(package_ahriman: Package) -> None:
"""
must serialize dataclasses with None fields removed
"""
package_ahriman.packager = None
result = dataclass_view(package_ahriman)
assert "packager" not in result
assert Package.from_json(result) == package_ahriman
def test_exception_response_text() -> None: def test_exception_response_text() -> None:
""" """
must parse HTTP response to string must parse HTTP response to string
@ -174,6 +201,23 @@ def test_exception_response_text_empty() -> None:
assert exception_response_text(exception) == "" assert exception_response_text(exception) == ""
def test_extract_user() -> None:
"""
must extract user from system environment
"""
os.environ["USER"] = "user"
assert extract_user() == "user"
os.environ["SUDO_USER"] = "sudo"
assert extract_user() == "sudo"
os.environ["DOAS_USER"] = "doas"
assert extract_user() == "sudo"
del os.environ["SUDO_USER"]
assert extract_user() == "doas"
def test_filter_json(package_ahriman: Package) -> None: def test_filter_json(package_ahriman: Package) -> None:
""" """
must filter fields by known list must filter fields by known list

View File

@ -116,6 +116,7 @@ def pyalpm_package_ahriman(aur_package_ahriman: AURPackage) -> MagicMock:
type(mock).name = PropertyMock(return_value=aur_package_ahriman.name) type(mock).name = PropertyMock(return_value=aur_package_ahriman.name)
type(mock).optdepends = PropertyMock(return_value=aur_package_ahriman.opt_depends) type(mock).optdepends = PropertyMock(return_value=aur_package_ahriman.opt_depends)
type(mock).checkdepends = PropertyMock(return_value=aur_package_ahriman.check_depends) type(mock).checkdepends = PropertyMock(return_value=aur_package_ahriman.check_depends)
type(mock).packager = PropertyMock(return_value="packager")
type(mock).provides = PropertyMock(return_value=aur_package_ahriman.provides) type(mock).provides = PropertyMock(return_value=aur_package_ahriman.provides)
type(mock).version = PropertyMock(return_value=aur_package_ahriman.version) type(mock).version = PropertyMock(return_value=aur_package_ahriman.version)
type(mock).url = PropertyMock(return_value=aur_package_ahriman.url) type(mock).url = PropertyMock(return_value=aur_package_ahriman.url)

View File

@ -56,7 +56,7 @@ def test_depends_build_with_version_and_overlap(mocker: MockerFixture, resource_
srcinfo = (resource_path_root / "models" / "package_gcc10_srcinfo").read_text() srcinfo = (resource_path_root / "models" / "package_gcc10_srcinfo").read_text()
mocker.patch("ahriman.models.package.Package._check_output", return_value=srcinfo) mocker.patch("ahriman.models.package.Package._check_output", return_value=srcinfo)
package_gcc10 = Package.from_build(Path("local"), "x86_64") package_gcc10 = Package.from_build(Path("local"), "x86_64", None)
assert package_gcc10.depends_build == { assert package_gcc10.depends_build == {
"glibc", "zstd", # depends "glibc", "zstd", # depends
"doxygen", "binutils", "git", "libmpc", "python", # make depends "doxygen", "binutils", "git", "libmpc", "python", # make depends
@ -168,10 +168,11 @@ def test_from_aur(package_ahriman: Package, aur_package_ahriman: AURPackage, pac
""" """
mocker.patch("ahriman.core.alpm.remote.AUR.info", return_value=aur_package_ahriman) mocker.patch("ahriman.core.alpm.remote.AUR.info", return_value=aur_package_ahriman)
package = Package.from_aur(package_ahriman.base, pacman) package = Package.from_aur(package_ahriman.base, pacman, package_ahriman.packager)
assert package_ahriman.base == package.base assert package_ahriman.base == package.base
assert package_ahriman.version == package.version assert package_ahriman.version == package.version
assert package_ahriman.packages.keys() == package.packages.keys() assert package_ahriman.packages.keys() == package.packages.keys()
assert package_ahriman.packager == package.packager
def test_from_build(package_ahriman: Package, mocker: MockerFixture, resource_path_root: Path) -> None: def test_from_build(package_ahriman: Package, mocker: MockerFixture, resource_path_root: Path) -> None:
@ -181,7 +182,7 @@ def test_from_build(package_ahriman: Package, mocker: MockerFixture, resource_pa
srcinfo = (resource_path_root / "models" / "package_ahriman_srcinfo").read_text() srcinfo = (resource_path_root / "models" / "package_ahriman_srcinfo").read_text()
mocker.patch("ahriman.models.package.Package._check_output", return_value=srcinfo) mocker.patch("ahriman.models.package.Package._check_output", return_value=srcinfo)
package = Package.from_build(Path("path"), "x86_64") package = Package.from_build(Path("path"), "x86_64", "packager")
assert package_ahriman.packages.keys() == package.packages.keys() assert package_ahriman.packages.keys() == package.packages.keys()
package_ahriman.packages = package.packages # we are not going to test PackageDescription here package_ahriman.packages = package.packages # we are not going to test PackageDescription here
package_ahriman.remote = package.remote package_ahriman.remote = package.remote
@ -195,7 +196,7 @@ def test_from_build_multiple_packages(mocker: MockerFixture, resource_path_root:
srcinfo = (resource_path_root / "models" / "package_gcc10_srcinfo").read_text() srcinfo = (resource_path_root / "models" / "package_gcc10_srcinfo").read_text()
mocker.patch("ahriman.models.package.Package._check_output", return_value=srcinfo) mocker.patch("ahriman.models.package.Package._check_output", return_value=srcinfo)
package = Package.from_build(Path("path"), "x86_64") package = Package.from_build(Path("path"), "x86_64", None)
assert package.packages == { assert package.packages == {
"gcc10": PackageDescription( "gcc10": PackageDescription(
depends=["gcc10-libs=10.3.0-2", "binutils>=2.28", "libmpc", "zstd"], depends=["gcc10-libs=10.3.0-2", "binutils>=2.28", "libmpc", "zstd"],
@ -225,7 +226,7 @@ def test_from_build_architecture(mocker: MockerFixture, resource_path_root: Path
srcinfo = (resource_path_root / "models" / "package_jellyfin-ffmpeg5-bin_srcinfo").read_text() srcinfo = (resource_path_root / "models" / "package_jellyfin-ffmpeg5-bin_srcinfo").read_text()
mocker.patch("ahriman.models.package.Package._check_output", return_value=srcinfo) mocker.patch("ahriman.models.package.Package._check_output", return_value=srcinfo)
package = Package.from_build(Path("path"), "x86_64") package = Package.from_build(Path("path"), "x86_64", None)
assert package.packages == { assert package.packages == {
"jellyfin-ffmpeg5-bin": PackageDescription( "jellyfin-ffmpeg5-bin": PackageDescription(
depends=["glibc"], depends=["glibc"],
@ -254,7 +255,7 @@ def test_from_build_failed(package_ahriman: Package, mocker: MockerFixture) -> N
mocker.patch("ahriman.models.package.parse_srcinfo", return_value=({"packages": {}}, ["an error"])) mocker.patch("ahriman.models.package.parse_srcinfo", return_value=({"packages": {}}, ["an error"]))
with pytest.raises(PackageInfoError): with pytest.raises(PackageInfoError):
Package.from_build(Path("path"), "x86_64") Package.from_build(Path("path"), "x86_64", None)
def test_from_json_view_1(package_ahriman: Package) -> None: def test_from_json_view_1(package_ahriman: Package) -> None:
@ -285,10 +286,11 @@ def test_from_official(package_ahriman: Package, aur_package_ahriman: AURPackage
""" """
mocker.patch("ahriman.core.alpm.remote.Official.info", return_value=aur_package_ahriman) mocker.patch("ahriman.core.alpm.remote.Official.info", return_value=aur_package_ahriman)
package = Package.from_official(package_ahriman.base, pacman) package = Package.from_official(package_ahriman.base, pacman, package_ahriman.packager)
assert package_ahriman.base == package.base assert package_ahriman.base == package.base
assert package_ahriman.version == package.version assert package_ahriman.version == package.version
assert package_ahriman.packages.keys() == package.packages.keys() assert package_ahriman.packages.keys() == package.packages.keys()
assert package_ahriman.packager == package.packager
def test_local_files(mocker: MockerFixture, resource_path_root: Path) -> None: def test_local_files(mocker: MockerFixture, resource_path_root: Path) -> None:

View File

@ -0,0 +1,12 @@
from ahriman.models.package import Package
from ahriman.models.packagers import Packagers
def test_for_base(package_ahriman: Package) -> None:
"""
must return username used for base package
"""
assert Packagers(None, {package_ahriman.base: "packager"}).for_base(package_ahriman.base) == "packager"
assert Packagers("default", {package_ahriman.base: "packager"}).for_base("random") == "default"
assert Packagers("default").for_base(package_ahriman.base) == "default"
assert Packagers().for_base(package_ahriman.base) is None

View File

@ -8,7 +8,7 @@ def test_from_option(user: User) -> None:
""" """
must generate user from options must generate user from options
""" """
user = replace(user, access=UserAccess.Read) user = replace(user, access=UserAccess.Read, packager_id=None, key=None)
assert User.from_option(user.username, user.password) == user assert User.from_option(user.username, user.password) == user
# default is read access # default is read access
user = replace(user, access=UserAccess.Full) user = replace(user, access=UserAccess.Full)

View File

@ -2,6 +2,7 @@ import pytest
from aiohttp.test_utils import TestClient from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from unittest.mock import AsyncMock
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
from ahriman.web.views.service.add import AddView from ahriman.web.views.service.add import AddView
@ -21,13 +22,16 @@ async def test_post(client: TestClient, mocker: MockerFixture) -> None:
must call post request correctly must call post request correctly
""" """
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add") add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add")
user_mock = AsyncMock()
user_mock.return_value = "username"
mocker.patch("ahriman.web.views.base.BaseView.username", side_effect=user_mock)
request_schema = pytest.helpers.schema_request(AddView.post) request_schema = pytest.helpers.schema_request(AddView.post)
payload = {"packages": ["ahriman"]} payload = {"packages": ["ahriman"]}
assert not request_schema.validate(payload) assert not request_schema.validate(payload)
response = await client.post("/api/v1/service/add", json=payload) response = await client.post("/api/v1/service/add", json=payload)
assert response.ok assert response.ok
add_mock.assert_called_once_with(["ahriman"], now=True) add_mock.assert_called_once_with(["ahriman"], "username", now=True)
async def test_post_empty(client: TestClient, mocker: MockerFixture) -> None: async def test_post_empty(client: TestClient, mocker: MockerFixture) -> None:

View File

@ -2,6 +2,7 @@ import pytest
from aiohttp.test_utils import TestClient from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from unittest.mock import AsyncMock
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
from ahriman.web.views.service.rebuild import RebuildView from ahriman.web.views.service.rebuild import RebuildView
@ -21,13 +22,16 @@ async def test_post(client: TestClient, mocker: MockerFixture) -> None:
must call post request correctly must call post request correctly
""" """
rebuild_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_rebuild") rebuild_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_rebuild")
user_mock = AsyncMock()
user_mock.return_value = "username"
mocker.patch("ahriman.web.views.base.BaseView.username", side_effect=user_mock)
request_schema = pytest.helpers.schema_request(RebuildView.post) request_schema = pytest.helpers.schema_request(RebuildView.post)
payload = {"packages": ["python", "ahriman"]} payload = {"packages": ["python", "ahriman"]}
assert not request_schema.validate(payload) assert not request_schema.validate(payload)
response = await client.post("/api/v1/service/rebuild", json=payload) response = await client.post("/api/v1/service/rebuild", json=payload)
assert response.ok assert response.ok
rebuild_mock.assert_called_once_with("python") rebuild_mock.assert_called_once_with("python", "username")
async def test_post_exception(client: TestClient, mocker: MockerFixture) -> None: async def test_post_exception(client: TestClient, mocker: MockerFixture) -> None:

View File

@ -2,6 +2,7 @@ import pytest
from aiohttp.test_utils import TestClient from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from unittest.mock import AsyncMock
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
from ahriman.web.views.service.request import RequestView from ahriman.web.views.service.request import RequestView
@ -21,13 +22,16 @@ async def test_post(client: TestClient, mocker: MockerFixture) -> None:
must call post request correctly must call post request correctly
""" """
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add") add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add")
user_mock = AsyncMock()
user_mock.return_value = "username"
mocker.patch("ahriman.web.views.base.BaseView.username", side_effect=user_mock)
request_schema = pytest.helpers.schema_request(RequestView.post) request_schema = pytest.helpers.schema_request(RequestView.post)
payload = {"packages": ["ahriman"]} payload = {"packages": ["ahriman"]}
assert not request_schema.validate(payload) assert not request_schema.validate(payload)
response = await client.post("/api/v1/service/request", json=payload) response = await client.post("/api/v1/service/request", json=payload)
assert response.ok assert response.ok
add_mock.assert_called_once_with(["ahriman"], now=False) add_mock.assert_called_once_with(["ahriman"], "username", now=False)
async def test_post_exception(client: TestClient, mocker: MockerFixture) -> None: async def test_post_exception(client: TestClient, mocker: MockerFixture) -> None:

View File

@ -1,5 +1,6 @@
from aiohttp.test_utils import TestClient from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from unittest.mock import AsyncMock
async def test_post_update(client: TestClient, mocker: MockerFixture) -> None: async def test_post_update(client: TestClient, mocker: MockerFixture) -> None:
@ -7,7 +8,10 @@ async def test_post_update(client: TestClient, mocker: MockerFixture) -> None:
must call post request correctly for alias must call post request correctly for alias
""" """
update_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_update") update_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_update")
user_mock = AsyncMock()
user_mock.return_value = "username"
mocker.patch("ahriman.web.views.base.BaseView.username", side_effect=user_mock)
response = await client.post("/api/v1/service/update") response = await client.post("/api/v1/service/update")
assert response.ok assert response.ok
update_mock.assert_called_once_with() update_mock.assert_called_once_with("username")

View File

@ -2,6 +2,8 @@ import pytest
from multidict import MultiDict from multidict import MultiDict
from aiohttp.test_utils import TestClient from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from unittest.mock import AsyncMock
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView from ahriman.web.views.base import BaseView
@ -146,3 +148,22 @@ async def test_head_not_allowed(client: TestClient) -> None:
""" """
response = await client.head("/api/v1/service/add") response = await client.head("/api/v1/service/add")
assert response.status == 405 assert response.status == 405
async def test_username(base: BaseView, mocker: MockerFixture) -> None:
"""
must return identity of logged-in user
"""
policy = AsyncMock()
policy.identify.return_value = "identity"
mocker.patch("aiohttp.web.Application.get", return_value=policy)
assert await base.username() == "identity"
policy.identify.assert_called_once_with(base.request)
async def test_username_no_auth(base: BaseView) -> None:
"""
must return None in case if auth is disabled
"""
assert await base.username() is None