packagers support (#100)

This commit is contained in:
2023-06-05 02:37:19 +03:00
committed by GitHub
parent d495163fdd
commit 4b984afb64
89 changed files with 849 additions and 318 deletions

View File

@ -27,7 +27,7 @@ from typing import TypeVar
from ahriman import version
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.build_status import BuildStatusEnum
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",
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 "
"and return 0 in case if command is safe and 1 otherwise")
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", nargs="*")
parser.set_defaults(handler=handlers.UnsafeCommands, architecture=[""], lock=None, report=False, quiet=True,
unsafe=True, parser=_parser)
return parser
@ -262,6 +262,7 @@ def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
action="count", default=False)
parser.add_argument("-s", "--source", help="explicitly specify the package source for this command",
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)
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, "
"-yy to force refresh even if up to date",
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
@ -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("-s", "--status", help="filter packages by status. Requires --from-database to be set",
type=BuildStatusEnum, choices=enum_values(BuildStatusEnum))
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
parser.set_defaults(handler=handlers.Rebuild)
return parser
@ -752,6 +755,7 @@ def _set_repo_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--manual", help="include or exclude manual updates",
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",
action=argparse.BooleanOptionalAction, default=True)
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.",
formatter_class=_formatter)
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, "
"which is in particular must be used for OAuth2 authorization type.")
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("-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.set_defaults(handler=handlers.Users, action=Action.List, architecture=[""], lock=None, report=False, # nosec
password="", quiet=True, unsafe=True)
parser.set_defaults(handler=handlers.Users, action=Action.List, architecture=[""], lock=None, report=False,
quiet=True, unsafe=True)
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",
formatter_class=_formatter)
parser.add_argument("username", help="username for web service")
parser.set_defaults(handler=handlers.Users, action=Action.Remove, architecture=[""], lock=None, report=False, # nosec
password="", quiet=True)
parser.set_defaults(handler=handlers.Users, action=Action.Remove, architecture=[""], lock=None, report=False,
quiet=True)
return parser

View File

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

View File

@ -55,15 +55,15 @@ class ApplicationPackages(ApplicationProperties):
dst = self.repository.paths.packages / local_path.name
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
Args:
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.remote_update(package)
@ -81,23 +81,24 @@ class ApplicationPackages(ApplicationProperties):
for full_path in filter(package_like, local_dir.iterdir()):
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
Args:
source(str): path to directory with local source files
username(str | None): optional override of username for build process
Raises:
UnknownPackageError: if specified package is unknown or doesn't exist
"""
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)
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
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:
raise UnknownPackageError(source)
@ -122,29 +123,31 @@ class ApplicationPackages(ApplicationProperties):
for chunk in response.iter_content(chunk_size=1024):
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
Args:
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.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
Args:
names(Iterable[str]): list of package bases 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:
resolved_source = source.resolve(name)
fn = getattr(self, f"_add_{resolved_source.value}")
fn(name)
fn(name, username)
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.tree import Tree
from ahriman.models.package import Package
from ahriman.models.packagers import Packagers
from ahriman.models.result import Result
@ -83,7 +84,7 @@ class ApplicationRepository(ApplicationProperties):
if archive.filepath is None:
self.logger.warning("filepath is empty for %s", package.base)
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
self.repository.sign.process_sign_repository(self.repository.repo.repo_path)
# process triggers
@ -104,14 +105,14 @@ class ApplicationRepository(ApplicationProperties):
packages: list[str] = []
for single in probe.packages:
try:
_ = Package.from_aur(single, self.repository.pacman)
_ = Package.from_aur(single, self.repository.pacman, None)
except Exception:
packages.append(single)
return packages
def unknown_local(probe: Package) -> list[str]:
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())
return list(packages)
@ -123,12 +124,14 @@ class ApplicationRepository(ApplicationProperties):
result.extend(unknown_aur(package)) # local package not found
return result
def update(self, updates: Iterable[Package]) -> Result:
def update(self, updates: Iterable[Package], packagers: Packagers | None = None) -> Result:
"""
run package updates
Args:
updates(Iterable[Package]): list of packages to update
packagers(Packagers | None, optional): optional override of username for build process
(Default value = None)
Returns:
Result: update result
@ -136,7 +139,7 @@ class ApplicationRepository(ApplicationProperties):
def process_update(paths: Iterable[Path], result: Result) -> None:
if not paths:
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))
# process built packages
@ -148,7 +151,7 @@ class ApplicationRepository(ApplicationProperties):
tree = Tree.resolve(updates)
for num, level in enumerate(tree):
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()
process_update(packages, build_result)

View File

@ -22,6 +22,7 @@ import argparse
from ahriman.application.application import Application
from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
from ahriman.models.packagers import Packagers
class Add(Handler):
@ -45,12 +46,14 @@ class Add(Handler):
application = Application(architecture, configuration,
report=report, unsafe=unsafe, refresh_pacman_database=args.refresh)
application.on_start()
application.add(args.package, args.source)
application.add(args.package, args.source, args.username)
if not args.now:
return
packages = application.updates(args.package, aur=False, local=False, manual=True, vcs=False,
log_fn=application.logger.info)
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)

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
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)
return package.base, PkgbuildPatch(None, patch)

View File

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

View File

@ -49,7 +49,7 @@ class ServiceUpdates(Handler):
"""
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
local_version = f"{version.__version__}-{release}"

View File

@ -213,7 +213,7 @@ class Setup(Handler):
"""
command = Setup.build_command(paths.root, 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!
@staticmethod

View File

@ -18,7 +18,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import argparse
import shlex
from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
@ -47,14 +46,14 @@ class UnsafeCommands(Handler):
"""
parser = args.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:
StringPrinter(command).print(verbose=True)
else:
UnsafeCommands.check_unsafe(args.command, unsafe_commands, parser)
@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
@ -63,7 +62,7 @@ class UnsafeCommands(Handler):
unsafe_commands(list[str]): list of unsafe commands
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)
@staticmethod

View File

@ -24,6 +24,7 @@ from collections.abc import Callable
from ahriman.application.application import Application
from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
from ahriman.models.packagers import Packagers
class Update(Handler):
@ -54,7 +55,9 @@ class Update(Handler):
return
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)
@staticmethod

View File

@ -156,4 +156,5 @@ class Users(Handler):
if password is None:
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.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
Args:
sources_dir(Path): path to where sources are
packager(str | None, optional): optional packager override (Default value = None)
Returns:
list[Path]: paths of produced packages
@ -75,12 +76,18 @@ class Task(LazyLogging):
command.extend(["--"] + self.makepkg_flags)
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(
*command,
exception=BuildError(self.package.base),
cwd=sources_dir,
logger=self.logger,
user=self.uid)
user=self.uid,
environment=environment)
# well it is not actually correct, but we can deal with it
packages = Task._check_output(

View File

@ -191,10 +191,6 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"sign": {
"type": "dict",
"allow_unknown": True,
"keysrules": {
"type": "string",
"anyof_regex": ["^target$", "^key$", "^key_.*"],
},
"schema": {
"target": {
"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]:
return [
User(username=cursor["username"], password=cursor["password"], access=UserAccess(cursor["access"]))
for cursor in connection.execute(
User(username=row["username"], password=row["password"], access=UserAccess(row["access"]),
packager_id=row["packager_id"], key=row["key_id"])
for row in connection.execute(
"""
select * from users
where (:username is null or username = :username) and (:access is null or access = :access)
@ -91,12 +92,13 @@ class AuthOperations(Operations):
connection.execute(
"""
insert into users
(username, access, password)
(username, access, password, packager_id, key_id)
values
(:username, :access, :password)
(:username, :access, :password, :packager_id, :key_id)
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)

View File

@ -76,11 +76,12 @@ class PackageOperations(Operations):
connection.execute(
"""
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
(: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
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,
@ -90,6 +91,7 @@ class PackageOperations(Operations):
"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,
"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"],
version=row["version"],
remote=RemoteSource.from_json(row),
packages={})
for row in connection.execute("""select * from package_bases""")
packages={},
packager=row["packager"] or None,
) for row in connection.execute("""select * from package_bases""")
}
@staticmethod

View File

@ -77,8 +77,8 @@ class PatchOperations(Operations):
"""
def run(connection: Connection) -> list[tuple[str, PkgbuildPatch]]:
return [
(cursor["package_base"], PkgbuildPatch(cursor["variable"], cursor["patch"]))
for cursor in connection.execute(
(row["package_base"], PkgbuildPatch(row["variable"], row["patch"]))
for row in connection.execute(
"""select * from patches where :package_base is null or 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)
"""
def __init__(self, configuration: Configuration, database: SQLite, section: str) -> None:
def __init__(self, database: SQLite, configuration: Configuration, section: str) -> None:
"""
default constructor
Args:
configuration(Configuration): configuration instance
database(SQLite): database instance
configuration(Configuration): configuration instance
section(str): settings section name
"""
self.database = database

View File

@ -105,5 +105,5 @@ class RemotePushTrigger(Trigger):
for target in self.targets:
section, _ = self.configuration.gettype(
target, self.architecture, fallback=self.CONFIGURATION_SCHEMA_FALLBACK)
runner = RemotePush(self.configuration, database, section)
runner = RemotePush(database, self.configuration, section)
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.models.package import Package
from ahriman.models.package_description import PackageDescription
from ahriman.models.packagers import Packagers
from ahriman.models.result import Result
@ -63,30 +64,35 @@ class Executor(Cleaner):
"""
raise NotImplementedError
def process_build(self, updates: Iterable[Package]) -> Result:
def process_build(self, updates: Iterable[Package], packagers: Packagers | None = None) -> Result:
"""
build packages
Args:
updates(Iterable[Package]): list of packages properties to build
packagers(Packagers | None, optional): optional override of username for build process
(Default value = None)
Returns:
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)
task = Task(package, self.configuration, self.paths)
task.init(local_path, self.database)
built = task.build(local_path)
built = task.build(local_path, packager_id)
for src in built:
dst = self.paths.packages / src.name
shutil.move(src, dst)
packagers = packagers or Packagers()
result = Result()
for single in updates:
with self.in_package_context(single.base), TemporaryDirectory(ignore_cleanup_errors=True) as dir_name:
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)
except Exception:
self.reporter.set_failed(single.base)
@ -158,12 +164,14 @@ class Executor(Cleaner):
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
Args:
packages(Iterable[Path]): list of filenames to run
packagers(Packagers | None, optional): optional override of username for build process
(Default value = None)
Returns:
Result: path to repository database
@ -176,13 +184,13 @@ class Executor(Cleaner):
shutil.move(self.paths.packages / archive.filename, self.paths.packages / 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:
self.logger.warning("received empty package name for base %s", package_base)
return # suppress type checking, it never can be none actually
# in theory, it might be NOT packages directory, but we suppose it is
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:
dst = self.paths.repository / safe_filename(src.name)
shutil.move(src, dst)
@ -192,14 +200,17 @@ class Executor(Cleaner):
current_packages = self.packages()
removed_packages: list[str] = [] # list of packages which have been removed from the base
updates = self.load_archives(packages)
packagers = packagers or Packagers()
result = Result()
for local in updates:
with self.in_package_context(local.base):
try:
packager = self.packager(packagers, local.base)
for description in local.packages.values():
rename(description, local.base)
update_single(description.filename, local.base)
update_single(description.filename, local.base, packager.key)
self.reporter.set_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.triggers import TriggerLoader
from ahriman.core.util import check_user
from ahriman.models.packagers import Packagers
from ahriman.models.pacman_synchronization import PacmanSynchronization
from ahriman.models.repository_paths import RepositoryPaths
from ahriman.models.user import User
from ahriman.models.user_access import UserAccess
class RepositoryProperties(LazyLogging):
@ -83,3 +86,23 @@ class RepositoryProperties(LazyLogging):
self.repo = Repo(self.name, self.paths, self.sign.repository_sign_args)
self.reporter = Client.load(configuration, report=report)
self.triggers = TriggerLoader.load(architecture, configuration)
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:
if source == PackageSource.Repository:
remote = Package.from_official(local.base, self.pacman)
remote = Package.from_official(local.base, self.pacman, None)
else:
remote = Package.from_aur(local.base, self.pacman)
remote = Package.from_aur(local.base, self.pacman, None)
if local.is_outdated(
remote, self.paths,
@ -98,7 +98,7 @@ class UpdateHandler(Cleaner):
with self.in_package_context(cache_dir.name):
try:
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)
if local is None:

View File

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

View File

@ -78,7 +78,7 @@ class Spawn(Thread, LazyLogging):
result = callback(args, architecture)
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
@ -94,6 +94,8 @@ class Spawn(Thread, LazyLogging):
arguments.extend(args)
# named command arguments
for argument, value in kwargs.items():
if value is None:
continue # skip null values
arguments.append(f"--{argument}")
if value:
arguments.append(value)
@ -122,27 +124,31 @@ class Spawn(Thread, LazyLogging):
kwargs = {} if server is None else {"key-server": server}
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
Args:
packages(Iterable[str]): packages list to add
username(str | None): optional override of username for build process
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:
kwargs["now"] = ""
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
Args:
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:
"""
@ -153,11 +159,15 @@ class Spawn(Thread, LazyLogging):
"""
self._spawn_process("package-remove", *packages)
def packages_update(self) -> None:
def packages_update(self, username: str | None) -> None:
"""
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:
"""

View File

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

View File

@ -67,5 +67,5 @@ class PackageCreator:
ctx = context.get()
database: SQLite = ctx.get(ContextKey("database", SQLite))
_, 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())

View File

@ -21,6 +21,7 @@ from collections.abc import Callable
from pathlib import Path
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.exceptions import PkgbuildGeneratorError
from ahriman.core.sign.gpg import GPG
from ahriman.core.support.pkgbuild.pkgbuild_generator import PkgbuildGenerator
@ -42,11 +43,12 @@ class KeyringGenerator(PkgbuildGenerator):
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
Args:
database(SQLite): database instance
sign(GPG): GPG wrapper instance
configuration(Configuration): configuration instance
section(str): settings section name
@ -55,7 +57,8 @@ class KeyringGenerator(PkgbuildGenerator):
self.name = configuration.repository_name
# 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.trusted = configuration.getlist(
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:
"""
content of the install functions
content of the .install functions
Returns:
str | None: content of the install functions if any
str | None: content of the .install functions if any
"""
# copy-paste from archlinux-keyring
return f"""post_upgrade() {{

View File

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

View File

@ -28,6 +28,7 @@ import requests
import subprocess
from collections.abc import Callable, Generator, Iterable
from dataclasses import asdict
from enum import Enum
from pathlib import Path
from pwd import getpwuid
@ -40,8 +41,10 @@ from ahriman.models.repository_paths import RepositoryPaths
__all__ = [
"check_output",
"check_user",
"dataclass_view",
"enum_values",
"exception_response_text",
"extract_user",
"filter_json",
"full_version",
"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,
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
@ -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)
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)
environment(dict[str, str] | None, optional): optional environment variables if any (Default value = None)
Returns:
str: command output
@ -106,7 +111,9 @@ def check_output(*args: str, exception: Exception | None = None, cwd: Path | Non
if logger is not None:
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
# pylint: disable=unexpected-keyword-arg
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)
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]:
"""
generate list of enumeration values from the source
@ -190,6 +210,17 @@ def exception_response_text(exception: requests.exceptions.RequestException) ->
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]:
"""
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
# 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 ahriman.core.util import dataclass_view
from ahriman.models.build_status import BuildStatus
from ahriman.models.counters import Counters
@ -69,4 +70,4 @@ class InternalStatus:
Returns:
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
from collections.abc import Callable, Generator, Iterable
from dataclasses import asdict, dataclass
from dataclasses import dataclass
from pathlib import Path
from pyalpm import vercmp # 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.exceptions import PackageInfoError
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_source import PackageSource
from ahriman.models.remote_source import RemoteSource
@ -48,6 +48,7 @@ class Package(LazyLogging):
Attributes:
base(str): package base name
packager(str | None): package packager if available
packages(dict[str, PackageDescription): map of package names to their properties.
Filled only on load from archive
remote(RemoteSource | None): package remote source if applicable
@ -77,6 +78,7 @@ class Package(LazyLogging):
version: str
remote: RemoteSource | None
packages: dict[str, PackageDescription]
packager: str | None = None
_check_output = check_output
@ -204,16 +206,18 @@ class Package(LazyLogging):
"""
package = pacman.handle.load_pkg(str(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
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
Args:
name(str): package name (either base or normal name)
pacman(Pacman): alpm wrapper instance
packager(str | None, optional): packager to be used for this build (Default value = None)
Returns:
Self: package properties
@ -224,16 +228,19 @@ class Package(LazyLogging):
base=package.package_base,
version=package.version,
remote=remote,
packages={package.name: PackageDescription.from_aur(package)})
packages={package.name: PackageDescription.from_aur(package)},
packager=packager,
)
@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
Args:
path(Path): path to package sources directory
architecture(str): load package for specific architecture
packager(str | None, optional): packager to be used for this build (Default value = None)
Returns:
Self: package properties
@ -265,7 +272,7 @@ class Package(LazyLogging):
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
def from_json(cls, dump: dict[str, Any]) -> Self:
@ -284,16 +291,18 @@ class Package(LazyLogging):
for key, value in packages_json.items()
}
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
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
Args:
name(str): package name (either base or normal name)
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)
Returns:
@ -305,7 +314,9 @@ class Package(LazyLogging):
base=package.package_base,
version=package.version,
remote=remote,
packages={package.name: PackageDescription.from_aur(package)})
packages={package.name: PackageDescription.from_aur(package)},
packager=packager,
)
@staticmethod
def local_files(path: Path) -> Generator[Path, None, None]:
@ -513,4 +524,4 @@ class Package(LazyLogging):
Returns:
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
# 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 pyalpm import Package # type: ignore[import]
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
@ -172,4 +172,4 @@ class PackageDescription:
Returns:
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
# 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 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
@ -118,4 +118,4 @@ class RemoteSource:
Returns:
dict[str, Any]: json-friendly dictionary
"""
return asdict(self)
return dataclass_view(self)

View File

@ -34,12 +34,14 @@ class User:
username(str): username
password(str): hashed user password with salt
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:
Simply create user from database data and perform required validation::
>>> 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::
@ -61,9 +63,18 @@ class User:
username: str
password: str
access: UserAccess
packager_id: str | None
key: str | None
_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
def from_option(cls, username: str | None, password: str | None,
access: UserAccess = UserAccess.Read) -> Self | None:
@ -80,7 +91,7 @@ class User:
"""
if username is None or password is 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
def generate_password(length: int) -> str:
@ -149,4 +160,4 @@ class User:
Returns:
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)
authorization_policy = _AuthorizationPolicy(validator)
identity_policy = aiohttp_security.SessionIdentityPolicy()
identity_policy = application["identity"] = aiohttp_security.SessionIdentityPolicy()
aiohttp_security.setup(application, identity_policy, authorization_policy)
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={
"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
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:
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()

View File

@ -68,6 +68,7 @@ class RebuildView(BaseView):
except Exception as 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()

View File

@ -67,6 +67,7 @@ class RequestView(BaseView):
except Exception as 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()

View File

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