add rebuild implementation to interface

This commit is contained in:
Evgenii Alekseev 2022-11-27 02:23:50 +02:00
parent 20e45845ba
commit 41cc58ed31
13 changed files with 202 additions and 5 deletions

View File

@ -20,6 +20,14 @@ ahriman.web.views.service.pgp module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.web.views.service.rebuild module
----------------------------------------
.. automodule:: ahriman.web.views.service.rebuild
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.views.service.remove module ahriman.web.views.service.remove module
--------------------------------------- ---------------------------------------

View File

@ -40,6 +40,11 @@
<i class="bi bi-play"></i> update <i class="bi bi-play"></i> update
</button> </button>
</li> </li>
<li>
<button id="package-rebuild-btn" class="btn dropdown-item" data-bs-toggle="modal" data-bs-target="#package-rebuild-modal" hidden>
<i class="bi bi-arrow-clockwise"></i> rebuild
</button>
</li>
<li> <li>
<button id="package-remove-btn" class="btn dropdown-item" onclick="removePackages()" disabled hidden> <button id="package-remove-btn" class="btn dropdown-item" onclick="removePackages()" disabled hidden>
<i class="bi bi-trash"></i> remove <i class="bi bi-trash"></i> remove
@ -126,6 +131,7 @@
{% include "build-status/success-modal.jinja2" %} {% include "build-status/success-modal.jinja2" %}
{% include "build-status/package-add-modal.jinja2" %} {% include "build-status/package-add-modal.jinja2" %}
{% include "build-status/package-rebuild-modal.jinja2" %}
{% include "build-status/key-import-modal.jinja2" %} {% include "build-status/key-import-modal.jinja2" %}
{% include "build-status/package-info-modal.jinja2" %} {% include "build-status/package-info-modal.jinja2" %}

View File

@ -0,0 +1,39 @@
<div id="package-rebuild-modal" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog" role="document">
<div class="modal-content">
<form id="package-rebuild-form" onsubmit="return false">
<div class="modal-header">
<h4 class="modal-title">Rebuild depending packages</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div>
<div class="modal-body">
<div class="form-group row">
<label for="dependency-input" class="col-sm-4 col-form-label">dependency</label>
<div class="col-sm-8">
<input id="dependency-input" type="text" class="form-control" placeholder="packages dependency" name="package" required>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary" onclick="packagesRebuild()"><i class="bi bi-play"></i> rebuild</button>
</div>
</form>
</div>
</div>
</div>
<script>
const packageRebuildModal = $("#package-rebuild-modal");
const packageRebuildForm = $("#package-rebuild-form");
packageRebuildModal.on("hidden.bs.modal", () => { packageRebuildForm.trigger("reset"); });
const dependencyInput = $("#dependency-input");
function packagesRebuild() {
const packages = dependencyInput.val();
if (packages) {
packageRebuildModal.modal("hide");
doPackageAction("/api/v1/service/rebuild", [packages], "Repository rebuild ran for the following dependencies:", "Repository rebuild failed:");
}
}
</script>

View File

@ -1,6 +1,7 @@
<script> <script>
const keyImportButton = $("#key-import-btn"); const keyImportButton = $("#key-import-btn");
const packageAddButton = $("#package-add-btn"); const packageAddButton = $("#package-add-btn");
const packageRebuildButton = $("#package-rebuild-btn");
const packageRemoveButton = $("#package-remove-btn"); const packageRemoveButton = $("#package-remove-btn");
const packageUpdateButton = $("#package-update-btn"); const packageUpdateButton = $("#package-update-btn");
@ -57,6 +58,7 @@
function hideControls(hidden) { function hideControls(hidden) {
keyImportButton.attr("hidden", hidden); keyImportButton.attr("hidden", hidden);
packageAddButton.attr("hidden", hidden); packageAddButton.attr("hidden", hidden);
packageRebuildButton.attr("hidden", hidden);
packageRemoveButton.attr("hidden", hidden); packageRemoveButton.attr("hidden", hidden);
packageUpdateButton.attr("hidden", hidden); packageUpdateButton.attr("hidden", hidden);
} }

View File

@ -77,6 +77,7 @@ setup(
"package/share/ahriman/templates/build-status/login-modal.jinja2", "package/share/ahriman/templates/build-status/login-modal.jinja2",
"package/share/ahriman/templates/build-status/package-add-modal.jinja2", "package/share/ahriman/templates/build-status/package-add-modal.jinja2",
"package/share/ahriman/templates/build-status/package-info-modal.jinja2", "package/share/ahriman/templates/build-status/package-info-modal.jinja2",
"package/share/ahriman/templates/build-status/package-rebuild-modal.jinja2",
"package/share/ahriman/templates/build-status/success-modal.jinja2", "package/share/ahriman/templates/build-status/success-modal.jinja2",
"package/share/ahriman/templates/build-status/table.jinja2", "package/share/ahriman/templates/build-status/table.jinja2",
]), ]),

View File

@ -76,5 +76,5 @@ class Rebuild(Handler):
List[Package]: list of packages which were stored in database List[Package]: list of packages which were stored in database
""" """
if from_database: if from_database:
return application.repository.packages()
return [package for (package, _) in application.database.packages_get()] return [package for (package, _) in application.database.packages_get()]
return application.repository.packages()

View File

@ -102,6 +102,15 @@ class Spawn(Thread, LazyLogging):
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:
"""
rebuild packages which depend on the specified package
Args:
depends_on(str): packages dependency
"""
self.spawn_process("repo-rebuild", **{"depends-on": depends_on})
def packages_remove(self, packages: Iterable[str]) -> None: def packages_remove(self, packages: Iterable[str]) -> None:
""" """
remove packages remove packages

View File

@ -23,6 +23,7 @@ from pathlib import Path
from ahriman.web.views.index import IndexView from ahriman.web.views.index import IndexView
from ahriman.web.views.service.add import AddView from ahriman.web.views.service.add import AddView
from ahriman.web.views.service.pgp import PGPView from ahriman.web.views.service.pgp import PGPView
from ahriman.web.views.service.rebuild import RebuildView
from ahriman.web.views.service.remove import RemoveView from ahriman.web.views.service.remove import RemoveView
from ahriman.web.views.service.request import RequestView from ahriman.web.views.service.request import RequestView
from ahriman.web.views.service.search import SearchView from ahriman.web.views.service.search import SearchView
@ -52,6 +53,8 @@ def setup_routes(application: Application, static_path: Path) -> None:
* ``GET /api/v1/service/pgp`` fetch PGP key from the keyserver * ``GET /api/v1/service/pgp`` fetch PGP key from the keyserver
* ``POST /api/v1/service/pgp`` import PGP key from the keyserver * ``POST /api/v1/service/pgp`` import PGP key from the keyserver
* ``POST /api/v1/service/rebuild`` rebuild packages based on their dependency list
* ``POST /api/v1/service/remove`` remove existing package from repository * ``POST /api/v1/service/remove`` remove existing package from repository
* ``POST /api/v1/service/request`` request to add new packages to repository * ``POST /api/v1/service/request`` request to add new packages to repository
@ -92,6 +95,8 @@ def setup_routes(application: Application, static_path: Path) -> None:
application.router.add_get("/api/v1/service/pgp", PGPView, allow_head=True) application.router.add_get("/api/v1/service/pgp", PGPView, allow_head=True)
application.router.add_post("/api/v1/service/pgp", PGPView) application.router.add_post("/api/v1/service/pgp", PGPView)
application.router.add_post("/api/v1/service/rebuild", RebuildView)
application.router.add_post("/api/v1/service/remove", RemoveView) application.router.add_post("/api/v1/service/remove", RemoveView)
application.router.add_post("/api/v1/service/request", RequestView) application.router.add_post("/api/v1/service/request", RequestView)

View File

@ -0,0 +1,75 @@
#
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from aiohttp.web import HTTPBadRequest, HTTPNoContent
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
class RebuildView(BaseView):
"""
rebuild packages web view
Attributes:
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
POST_PERMISSION = UserAccess.Full
async def post(self) -> None:
"""
rebuild packages based on their dependency
JSON body must be supplied, the following model is used::
{
"packages": ["ahriman"] # either list of packages or package name of dependency
}
Raises:
HTTPBadRequest: if bad data is supplied
HTTPNoContent: in case of success response
Examples:
Example of command by using curl::
$ curl -v -H 'Content-Type: application/json' 'http://example.com/api/v1/service/rebuild' -d '{"packages": ["python"]}'
> POST /api/v1/service/rebuild HTTP/1.1
> Host: example.com
> User-Agent: curl/7.86.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 24
>
< HTTP/1.1 204 No Content
< Date: Sun, 27 Nov 2022 00:22:26 GMT
< Server: Python/3.10 aiohttp/3.8.3
<
"""
try:
data = await self.extract_data(["packages"])
packages = self.get_non_empty(lambda key: [package for package in data[key] if package], "packages")
depends_on = next(package for package in packages)
except Exception as e:
raise HTTPBadRequest(reason=str(e))
self.spawner.packages_rebuild(depends_on)
raise HTTPNoContent()

View File

@ -75,7 +75,7 @@ def test_run_dry_run(args: argparse.Namespace, configuration: Configuration,
args = _default_args(args) args = _default_args(args)
args.dry_run = True args.dry_run = True
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
mocker.patch("ahriman.core.repository.repository.Repository.packages_depend_on", return_value=[package_ahriman]) mocker.patch("ahriman.application.handlers.Rebuild.extract_packages", return_value=[package_ahriman])
application_mock = mocker.patch("ahriman.application.application.Application.update") application_mock = mocker.patch("ahriman.application.application.Application.update")
check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty") check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty")
@ -92,6 +92,7 @@ def test_run_filter(args: argparse.Namespace, configuration: Configuration, mock
args.depends_on = ["python-aur"] args.depends_on = ["python-aur"]
mocker.patch("ahriman.application.application.Application.update") mocker.patch("ahriman.application.application.Application.update")
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
mocker.patch("ahriman.application.handlers.Rebuild.extract_packages", return_value=[])
application_packages_mock = mocker.patch("ahriman.core.repository.repository.Repository.packages_depend_on") application_packages_mock = mocker.patch("ahriman.core.repository.repository.Repository.packages_depend_on")
Rebuild.run(args, "x86_64", configuration, report=False, unsafe=False) Rebuild.run(args, "x86_64", configuration, report=False, unsafe=False)
@ -105,6 +106,7 @@ def test_run_without_filter(args: argparse.Namespace, configuration: Configurati
args = _default_args(args) args = _default_args(args)
mocker.patch("ahriman.application.application.Application.update") mocker.patch("ahriman.application.application.Application.update")
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
mocker.patch("ahriman.application.handlers.Rebuild.extract_packages", return_value=[])
application_packages_mock = mocker.patch("ahriman.core.repository.repository.Repository.packages_depend_on") application_packages_mock = mocker.patch("ahriman.core.repository.repository.Repository.packages_depend_on")
Rebuild.run(args, "x86_64", configuration, report=False, unsafe=False) Rebuild.run(args, "x86_64", configuration, report=False, unsafe=False)
@ -120,6 +122,7 @@ def test_run_update_empty_exception(args: argparse.Namespace, configuration: Con
args.exit_code = True args.exit_code = True
args.dry_run = True args.dry_run = True
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
mocker.patch("ahriman.application.handlers.Rebuild.extract_packages")
mocker.patch("ahriman.core.repository.repository.Repository.packages_depend_on", return_value=[]) mocker.patch("ahriman.core.repository.repository.Repository.packages_depend_on", return_value=[])
check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty") check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty")
@ -135,6 +138,7 @@ def test_run_build_empty_exception(args: argparse.Namespace, configuration: Conf
args = _default_args(args) args = _default_args(args)
args.exit_code = True args.exit_code = True
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
mocker.patch("ahriman.application.handlers.Rebuild.extract_packages")
mocker.patch("ahriman.core.repository.repository.Repository.packages_depend_on", return_value=[package_ahriman]) mocker.patch("ahriman.core.repository.repository.Repository.packages_depend_on", return_value=[package_ahriman])
mocker.patch("ahriman.application.application.Application.update", return_value=Result()) mocker.patch("ahriman.application.application.Application.update", return_value=Result())
check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty") check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty")
@ -147,7 +151,7 @@ def test_extract_packages(application: Application, mocker: MockerFixture) -> No
""" """
must extract packages from database must extract packages from database
""" """
packages_mock = mocker.patch("ahriman.core.database.SQLite.packages_get") packages_mock = mocker.patch("ahriman.core.repository.repository.Repository.packages")
Rebuild.extract_packages(application, from_database=False) Rebuild.extract_packages(application, from_database=False)
packages_mock.assert_called_once_with() packages_mock.assert_called_once_with()
@ -156,6 +160,6 @@ def test_extract_packages_from_database(application: Application, mocker: Mocker
""" """
must extract packages from database must extract packages from database
""" """
packages_mock = mocker.patch("ahriman.core.repository.repository.Repository.packages") packages_mock = mocker.patch("ahriman.core.database.SQLite.packages_get")
Rebuild.extract_packages(application, from_database=True) Rebuild.extract_packages(application, from_database=True)
packages_mock.assert_called_once_with() packages_mock.assert_called_once_with()

View File

@ -72,6 +72,15 @@ def test_packages_add_with_build(spawner: Spawn, mocker: MockerFixture) -> None:
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", now="")
def test_packages_rebuild(spawner: Spawn, mocker: MockerFixture) -> None:
"""
must call package rebuild
"""
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn.spawn_process")
spawner.packages_rebuild("python")
spawn_mock.assert_called_once_with("repo-rebuild", **{"depends-on": "python"})
def test_packages_remove(spawner: Spawn, mocker: MockerFixture) -> None: def test_packages_remove(spawner: Spawn, mocker: MockerFixture) -> None:
""" """
must call package removal must call package removal

View File

@ -329,6 +329,7 @@ def test_walk(resource_path_root: Path) -> None:
resource_path_root / "web" / "templates" / "build-status" / "login-modal.jinja2", resource_path_root / "web" / "templates" / "build-status" / "login-modal.jinja2",
resource_path_root / "web" / "templates" / "build-status" / "package-add-modal.jinja2", resource_path_root / "web" / "templates" / "build-status" / "package-add-modal.jinja2",
resource_path_root / "web" / "templates" / "build-status" / "package-info-modal.jinja2", resource_path_root / "web" / "templates" / "build-status" / "package-info-modal.jinja2",
resource_path_root / "web" / "templates" / "build-status" / "package-rebuild-modal.jinja2",
resource_path_root / "web" / "templates" / "build-status" / "success-modal.jinja2", resource_path_root / "web" / "templates" / "build-status" / "success-modal.jinja2",
resource_path_root / "web" / "templates" / "build-status" / "table.jinja2", resource_path_root / "web" / "templates" / "build-status" / "table.jinja2",
resource_path_root / "web" / "templates" / "static" / "favicon.ico", resource_path_root / "web" / "templates" / "static" / "favicon.ico",

View File

@ -0,0 +1,38 @@
import pytest
from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from ahriman.models.user_access import UserAccess
from ahriman.web.views.service.rebuild import RebuildView
async def test_get_permission() -> None:
"""
must return correct permission for the request
"""
for method in ("POST",):
request = pytest.helpers.request("", "", method)
assert await RebuildView.get_permission(request) == UserAccess.Full
async def test_post(client: TestClient, mocker: MockerFixture) -> None:
"""
must call post request correctly
"""
rebuild_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_rebuild")
response = await client.post("/api/v1/service/rebuild", json={"packages": ["python", "ahriman"]})
assert response.ok
rebuild_mock.assert_called_once_with("python")
async def test_post_exception(client: TestClient, mocker: MockerFixture) -> None:
"""
must raise exception on missing packages payload
"""
rebuild_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_rebuild")
response = await client.post("/api/v1/service/rebuild")
assert response.status == 400
rebuild_mock.assert_not_called()