feat: implement local reporter mode (#126)

* implement local reporter mode

* simplify watcher class

* review changes

* do not update unknown status

* allow empty key patches via api

* fix some pylint warnings in tests
This commit is contained in:
2024-05-21 16:27:17 +03:00
parent 02b13de7f4
commit 8ffc1299f0
93 changed files with 2409 additions and 935 deletions

View File

@@ -25,6 +25,7 @@ from typing import TypeVar
from ahriman.core.auth import Auth
from ahriman.core.configuration import Configuration
from ahriman.core.distributed import WorkersCache
from ahriman.core.exceptions import UnknownPackageError
from ahriman.core.sign.gpg import GPG
from ahriman.core.spawn import Spawn
from ahriman.core.status.watcher import Watcher
@@ -218,12 +219,13 @@ class BaseView(View, CorsViewMixin):
return RepositoryId(architecture, name)
return next(iter(sorted(self.services.keys())))
def service(self, repository_id: RepositoryId | None = None) -> Watcher:
def service(self, repository_id: RepositoryId | None = None, package_base: str | None = None) -> Watcher:
"""
get status watcher instance
Args:
repository_id(RepositoryId | None, optional): repository unique identifier (Default value = None)
package_base(str | None, optional): package base to validate if exists (Default value = None)
Returns:
Watcher: build status watcher instance. If no repository provided, it will return the first one
@@ -234,9 +236,11 @@ class BaseView(View, CorsViewMixin):
if repository_id is None:
repository_id = self.repository_id()
try:
return self.services[repository_id]
return self.services[repository_id](package_base)
except KeyError:
raise HTTPNotFound(reason=f"Repository {repository_id.id} is unknown")
except UnknownPackageError:
raise HTTPNotFound(reason=f"Package {package_base} is unknown")
async def username(self) -> str | None:
"""

View File

@@ -19,9 +19,8 @@
#
import aiohttp_apispec # type: ignore[import-untyped]
from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response
from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
from ahriman.core.exceptions import UnknownPackageError
from ahriman.models.changes import Changes
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ChangesSchema, ErrorSchema, PackageNameSchema, RepositoryIdSchema
@@ -70,10 +69,7 @@ class ChangesView(StatusViewGuard, BaseView):
"""
package_base = self.request.match_info["package"]
try:
changes = self.service().package_changes_get(package_base)
except UnknownPackageError:
raise HTTPNotFound(reason=f"Package {package_base} is unknown")
changes = self.service(package_base=package_base).package_changes_get(package_base)
return json_response(changes.view())
@@ -113,7 +109,6 @@ class ChangesView(StatusViewGuard, BaseView):
raise HTTPBadRequest(reason=str(ex))
changes = Changes(last_commit_sha, change)
repository_id = self.repository_id()
self.service(repository_id).database.changes_insert(package_base, changes, repository_id)
self.service().package_changes_update(package_base, changes)
raise HTTPNoContent

View File

@@ -0,0 +1,113 @@
#
# Copyright (c) 2021-2024 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import aiohttp_apispec # type: ignore[import-untyped]
from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
from ahriman.models.dependencies import Dependencies
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, DependenciesSchema, ErrorSchema, PackageNameSchema, RepositoryIdSchema
from ahriman.web.views.base import BaseView
from ahriman.web.views.status_view_guard import StatusViewGuard
class DependenciesView(StatusViewGuard, BaseView):
"""
package dependencies web view
Attributes:
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
GET_PERMISSION = UserAccess.Reporter
POST_PERMISSION = UserAccess.Full
ROUTES = ["/api/v1/packages/{package}/dependencies"]
@aiohttp_apispec.docs(
tags=["Build"],
summary="Get package dependencies",
description="Retrieve package implicit dependencies",
responses={
200: {"description": "Success response", "schema": DependenciesSchema},
401: {"description": "Authorization required", "schema": ErrorSchema},
403: {"description": "Access is forbidden", "schema": ErrorSchema},
404: {"description": "Package base and/or repository are unknown", "schema": ErrorSchema},
500: {"description": "Internal server error", "schema": ErrorSchema},
},
security=[{"token": [GET_PERMISSION]}],
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.match_info_schema(PackageNameSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
async def get(self) -> Response:
"""
get package dependencies
Returns:
Response: 200 with package implicit dependencies on success
Raises:
HTTPNotFound: if package base is unknown
"""
package_base = self.request.match_info["package"]
dependencies = self.service(package_base=package_base).package_dependencies_get(package_base)
return json_response(dependencies.view())
@aiohttp_apispec.docs(
tags=["Build"],
summary="Update package dependencies",
description="Set package implicit dependencies",
responses={
204: {"description": "Success response"},
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
401: {"description": "Authorization required", "schema": ErrorSchema},
403: {"description": "Access is forbidden", "schema": ErrorSchema},
404: {"description": "Repository is unknown", "schema": ErrorSchema},
500: {"description": "Internal server error", "schema": ErrorSchema},
},
security=[{"token": [POST_PERMISSION]}],
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.match_info_schema(PackageNameSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
@aiohttp_apispec.json_schema(DependenciesSchema)
async def post(self) -> None:
"""
insert new package dependencies
Raises:
HTTPBadRequest: if bad data is supplied
HTTPNoContent: in case of success response
"""
package_base = self.request.match_info["package"]
try:
data = await self.request.json()
data["package_base"] = package_base # read from path instead of object
dependencies = Dependencies.from_json(data)
except Exception as ex:
raise HTTPBadRequest(reason=str(ex))
self.service(package_base=package_base).package_dependencies_update(package_base, dependencies)
raise HTTPNoContent

View File

@@ -25,8 +25,8 @@ from ahriman.core.exceptions import UnknownPackageError
from ahriman.core.util import pretty_datetime
from ahriman.models.log_record_id import LogRecordId
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, LogsSchema, PackageNameSchema, RepositoryIdSchema, \
VersionedLogSchema
from ahriman.web.schemas import AuthSchema, ErrorSchema, LogsSchema, PackageNameSchema, PackageVersionSchema, \
RepositoryIdSchema, VersionedLogSchema
from ahriman.web.views.base import BaseView
from ahriman.web.views.status_view_guard import StatusViewGuard
@@ -60,7 +60,7 @@ class LogsView(StatusViewGuard, BaseView):
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.match_info_schema(PackageNameSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
@aiohttp_apispec.querystring_schema(PackageVersionSchema)
async def delete(self) -> None:
"""
delete package logs
@@ -69,7 +69,8 @@ class LogsView(StatusViewGuard, BaseView):
HTTPNoContent: on success response
"""
package_base = self.request.match_info["package"]
self.service().logs_remove(package_base, None)
version = self.request.query.get("version")
self.service().package_logs_remove(package_base, version)
raise HTTPNoContent
@@ -103,7 +104,7 @@ class LogsView(StatusViewGuard, BaseView):
try:
_, status = self.service().package_get(package_base)
logs = self.service().logs_get(package_base)
logs = self.service(package_base=package_base).package_logs_get(package_base, -1, 0)
except UnknownPackageError:
raise HTTPNotFound(reason=f"Package {package_base} is unknown")
@@ -149,6 +150,6 @@ class LogsView(StatusViewGuard, BaseView):
except Exception as ex:
raise HTTPBadRequest(reason=str(ex))
self.service().logs_update(LogRecordId(package_base, version), created, record)
self.service().package_logs_add(LogRecordId(package_base, version), created, record)
raise HTTPNoContent

View File

@@ -152,7 +152,10 @@ class PackageView(StatusViewGuard, BaseView):
raise HTTPBadRequest(reason=str(ex))
try:
self.service().package_update(package_base, status, package)
if package is None:
self.service().package_status_update(package_base, status)
else:
self.service().package_update(package, status)
except UnknownPackageError:
raise HTTPBadRequest(reason=f"Package {package_base} is unknown, but no package body set")

View File

@@ -63,7 +63,8 @@ class PatchView(StatusViewGuard, BaseView):
"""
package_base = self.request.match_info["package"]
variable = self.request.match_info["patch"]
self.service().patches_remove(package_base, variable)
self.service().package_patches_remove(package_base, variable)
raise HTTPNoContent
@@ -95,7 +96,7 @@ class PatchView(StatusViewGuard, BaseView):
package_base = self.request.match_info["package"]
variable = self.request.match_info["patch"]
patches = self.service().patches_get(package_base, variable)
patches = self.service().package_patches_get(package_base, variable)
selected = next((patch for patch in patches if patch.key == variable), None)
if selected is None:

View File

@@ -63,7 +63,7 @@ class PatchesView(StatusViewGuard, BaseView):
Response: 200 with package patches on success
"""
package_base = self.request.match_info["package"]
patches = self.service().patches_get(package_base, None)
patches = self.service().package_patches_get(package_base, None)
response = [patch.view() for patch in patches]
return json_response(response)
@@ -96,11 +96,11 @@ class PatchesView(StatusViewGuard, BaseView):
try:
data = await self.request.json()
key = data["key"]
key = data.get("key")
value = data["value"]
except Exception as ex:
raise HTTPBadRequest(reason=str(ex))
self.service().patches_update(package_base, PkgbuildPatch(key, value))
self.service().package_patches_update(package_base, PkgbuildPatch(key, value))
raise HTTPNoContent

View File

@@ -19,9 +19,8 @@
#
import aiohttp_apispec # type: ignore[import-untyped]
from aiohttp.web import HTTPNotFound, Response, json_response
from aiohttp.web import Response, json_response
from ahriman.core.exceptions import UnknownPackageError
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, LogSchema, PackageNameSchema, PaginationSchema
from ahriman.web.views.base import BaseView
@@ -68,10 +67,8 @@ class LogsView(StatusViewGuard, BaseView):
"""
package_base = self.request.match_info["package"]
limit, offset = self.page()
try:
logs = self.service().logs_get(package_base, limit, offset)
except UnknownPackageError:
raise HTTPNotFound(reason=f"Package {package_base} is unknown")
logs = self.service(package_base=package_base).package_logs_get(package_base, limit, offset)
response = [
{