mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-04-24 07:17:17 +00:00
add key-import button to interface
This commit is contained in:
parent
577bd9e5f8
commit
9fa1fa108f
@ -22,6 +22,11 @@ fi
|
|||||||
[ -d "$AHRIMAN_REPOSITORY_ROOT" ] || mkdir "$AHRIMAN_REPOSITORY_ROOT"
|
[ -d "$AHRIMAN_REPOSITORY_ROOT" ] || mkdir "$AHRIMAN_REPOSITORY_ROOT"
|
||||||
chown "$AHRIMAN_USER":"$AHRIMAN_USER" "$AHRIMAN_REPOSITORY_ROOT"
|
chown "$AHRIMAN_USER":"$AHRIMAN_USER" "$AHRIMAN_REPOSITORY_ROOT"
|
||||||
|
|
||||||
|
# create .gnupg directory which is required for keys
|
||||||
|
AHRIMAN_GNUPG_HOME="$(getent passwd "$AHRIMAN_USER" | cut -d : -f 6)/.gnupg"
|
||||||
|
[ -d "$AHRIMAN_GNUPG_HOME" ] || mkdir -m700 "$AHRIMAN_GNUPG_HOME"
|
||||||
|
chown "$AHRIMAN_USER":"$AHRIMAN_USER" "$AHRIMAN_GNUPG_HOME"
|
||||||
|
|
||||||
# run built-in setup command
|
# run built-in setup command
|
||||||
AHRIMAN_SETUP_ARGS=("--build-as-user" "$AHRIMAN_USER")
|
AHRIMAN_SETUP_ARGS=("--build-as-user" "$AHRIMAN_USER")
|
||||||
AHRIMAN_SETUP_ARGS+=("--packager" "$AHRIMAN_PACKAGER")
|
AHRIMAN_SETUP_ARGS+=("--packager" "$AHRIMAN_PACKAGER")
|
||||||
|
@ -12,6 +12,14 @@ ahriman.web.views.service.add module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.web.views.service.pgp module
|
||||||
|
------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.web.views.service.pgp
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.web.views.service.remove module
|
ahriman.web.views.service.remove module
|
||||||
---------------------------------------
|
---------------------------------------
|
||||||
|
|
||||||
@ -36,6 +44,14 @@ ahriman.web.views.service.search module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.web.views.service.update module
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.web.views.service.update
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
Module contents
|
Module contents
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
|
56
docs/faq.rst
56
docs/faq.rst
@ -219,6 +219,62 @@ Also, there is command ``repo-remove-unknown`` which checks packages in AUR and
|
|||||||
|
|
||||||
Remove commands also remove any package files (patches, caches etc).
|
Remove commands also remove any package files (patches, caches etc).
|
||||||
|
|
||||||
|
How to sign repository
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Repository sign feature is available in several configurations. The recommended way is just to sign repository database file by single key instead of trying to sign each package. However, the steps are pretty same, just configuration is a bit differ. For more details about options kindly refer to :doc:`configuration reference <configuration>`.
|
||||||
|
|
||||||
|
#.
|
||||||
|
First you would need to create the key on your local machine:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
gpg --full-generate-key
|
||||||
|
|
||||||
|
This command will prompt you for several questions. Most of them may be left default, but you will need to fill real name and email address with some data. Because at the moment the service doesn't support passphrases, it must be left blank.
|
||||||
|
|
||||||
|
#.
|
||||||
|
The command above will generate key and print its hash, something like ``8BE91E5A773FB48AC05CC1EDBED105AED6246B39``. Copy it.
|
||||||
|
|
||||||
|
#.
|
||||||
|
Export your private key by using the hash above:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
gpg --export-secret-keys -a 8BE91E5A773FB48AC05CC1EDBED105AED6246B39 > repository-key.gpg
|
||||||
|
|
||||||
|
#.
|
||||||
|
|
||||||
|
Copy the specified key to the build machine (i.e. where the service is running).
|
||||||
|
|
||||||
|
#.
|
||||||
|
Import the specified key to the service user:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
sudo -u ahriman gpg --import repository-key.gpg
|
||||||
|
|
||||||
|
Don't forget to remove the key from filesystem after import.
|
||||||
|
|
||||||
|
#.
|
||||||
|
Change trust level to ``ultimate``:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
sudo -u ahriman gpg --edit-key 8BE91E5A773FB48AC05CC1EDBED105AED6246B39
|
||||||
|
|
||||||
|
The command above will drop you into gpg shell, in which you will need to type ``trust``, choose ``5 = I trust ultimately``, confirm and exit ``quit``.
|
||||||
|
|
||||||
|
#.
|
||||||
|
Proceed with service configuration according to the :doc:`configuration <configuration>`:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[sign]
|
||||||
|
target = repository
|
||||||
|
key = 8BE91E5A773FB48AC05CC1EDBED105AED6246B39
|
||||||
|
|
||||||
|
|
||||||
How to rebuild packages after library update
|
How to rebuild packages after library update
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
@ -26,15 +26,18 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div id="toolbar">
|
<div id="toolbar">
|
||||||
{% if not auth.enabled or auth.username is not none %}
|
{% if not auth.enabled or auth.username is not none %}
|
||||||
<button id="add-btn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-form" hidden>
|
<button id="package-add-btn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#package-add-modal" hidden>
|
||||||
<i class="bi bi-plus"></i> add
|
<i class="bi bi-plus"></i> add
|
||||||
</button>
|
</button>
|
||||||
<button id="update-btn" class="btn btn-secondary" onclick="updatePackages()" hidden>
|
<button id="package-update-btn" class="btn btn-secondary" onclick="updatePackages()" hidden>
|
||||||
<i class="bi bi-play"></i> update
|
<i class="bi bi-play"></i> update
|
||||||
</button>
|
</button>
|
||||||
<button id="remove-btn" class="btn btn-danger" onclick="removePackages()" disabled hidden>
|
<button id="package-remove-btn" class="btn btn-danger" onclick="removePackages()" disabled hidden>
|
||||||
<i class="bi bi-trash"></i> remove
|
<i class="bi bi-trash"></i> remove
|
||||||
</button>
|
</button>
|
||||||
|
<button id="key-import-btn" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#key-import-modal" hidden>
|
||||||
|
<i class="bi bi-key"></i> import key
|
||||||
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<button class="btn btn-secondary" onclick="reload()">
|
<button class="btn btn-secondary" onclick="reload()">
|
||||||
<i class="bi bi-arrow-clockwise"></i> reload
|
<i class="bi bi-arrow-clockwise"></i> reload
|
||||||
@ -112,6 +115,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/key-import-modal.jinja2" %}
|
||||||
|
|
||||||
{% include "build-status/package-info-modal.jinja2" %}
|
{% include "build-status/package-info-modal.jinja2" %}
|
||||||
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
<div id="failed-form" tabindex="-1" role="dialog" class="modal fade">
|
<div id="failed-modal" tabindex="-1" role="dialog" class="modal fade">
|
||||||
<div class="modal-dialog" role="document">
|
<div class="modal-dialog" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header bg-danger text-white">
|
<div class="modal-header bg-danger text-white">
|
||||||
<h4 id="error-title" class="modal-title"></h4>
|
<h4 id="failed-title" class="modal-title"></h4>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p id="error-description"></p>
|
<p id="failed-description"></p>
|
||||||
<p id="error-details"></p>
|
<p id="failed-details"></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal"><i class="bi bi-x"></i> close</button>
|
<button type="button" class="btn btn-primary" data-bs-dismiss="modal"><i class="bi bi-x"></i> close</button>
|
||||||
@ -17,16 +17,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const failedForm = $("#failed-form");
|
const failedModal = $("#failed-modal");
|
||||||
const errorDescription = $("#error-description");
|
failedModal.on("hidden.bs.modal", () => { reload(); });
|
||||||
const errorDetails = $("#error-details");
|
|
||||||
const errorTitle = $("#error-title");
|
const failedDescription = $("#failed-description");
|
||||||
failedForm.on("hidden.bs.modal", () => { reload(); });
|
const failedDetails = $("#failed-details");
|
||||||
|
const failedTitle = $("#failed-title");
|
||||||
|
|
||||||
function showFailure(title, description, details) {
|
function showFailure(title, description, details) {
|
||||||
errorTitle.text(title);
|
failedTitle.text(title);
|
||||||
errorDescription.text(description);
|
failedDescription.text(description);
|
||||||
errorDetails.text(details);
|
failedDetails.text(details);
|
||||||
failedForm.modal("show");
|
|
||||||
|
failedModal.modal("show");
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -0,0 +1,92 @@
|
|||||||
|
<div id="key-import-modal" tabindex="-1" role="dialog" class="modal fade">
|
||||||
|
<div class="modal-dialog modal-xl" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form id="key-import-form" onsubmit="return false">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title">Import key from PGP server</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="key-fingerprint-input" class="col-sm-2 col-form-label">fingerprint</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input id="key-fingerprint-input" type="text" class="form-control" placeholder="PGP key fingerprint" name="key" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="key-server-input" class="col-sm-2 col-form-label">key server</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input id="key-server-input" type="text" class="form-control" placeholder="PGP key server" name="server" value="keyserver.ubuntu.com" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col-sm-2"></div>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<pre class="language-less"><code id="key-body-input" class="pre-scrollable language-less"></code><button id="key-copy-btn" type="button" class="btn language-less" onclick="copyPgpKey()"><i class="bi bi-clipboard"></i> copy</button></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="submit" class="btn btn-primary" onclick="importPgpKey()"><i class="bi bi-play"></i> import</button>
|
||||||
|
<button type="submit" class="btn btn-success" onclick="fetchPgpKey()"><i class="bi bi-arrow-clockwise"></i> fetch</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const keyImportModal = $("#key-import-modal");
|
||||||
|
const keyImportForm = $("#key-import-form");
|
||||||
|
keyImportModal.on("hidden.bs.modal", () => {
|
||||||
|
keyBodyInput.text("");
|
||||||
|
keyImportForm.trigger("reset");
|
||||||
|
});
|
||||||
|
|
||||||
|
const keyBodyInput = $("#key-body-input");
|
||||||
|
const keyCopyButton = $("#key-copy-btn");
|
||||||
|
|
||||||
|
const keyFingerprintInput = $("#key-fingerprint-input");
|
||||||
|
const keyServerInput = $("#key-server-input");
|
||||||
|
|
||||||
|
async function copyPgpKey() {
|
||||||
|
const logs = keyBodyInput.text();
|
||||||
|
await copyToClipboard(logs, keyCopyButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchPgpKey() {
|
||||||
|
const key = keyFingerprintInput.val();
|
||||||
|
const server = keyServerInput.val();
|
||||||
|
|
||||||
|
if (key && server) {
|
||||||
|
$.ajax({
|
||||||
|
url: "/api/v1/service/pgp",
|
||||||
|
data: {"key": key, "server": server},
|
||||||
|
type: "GET",
|
||||||
|
dataType: "json",
|
||||||
|
success: response => { keyBodyInput.text(response.key); },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function importPgpKey() {
|
||||||
|
const key = keyFingerprintInput.val();
|
||||||
|
const server = keyServerInput.val();
|
||||||
|
|
||||||
|
if (key && server) {
|
||||||
|
$.ajax({
|
||||||
|
url: "/api/v1/service/pgp",
|
||||||
|
data: JSON.stringify({key: key, server: server}),
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json",
|
||||||
|
success: _ => {
|
||||||
|
keyImportModal.modal("hide");
|
||||||
|
showSuccess("Success", `Key ${key} has been imported`, "");
|
||||||
|
},
|
||||||
|
error: (jqXHR, _, errorThrown) => {
|
||||||
|
showFailure("Action failed", `Could not import key ${key} from ${server}`, errorThrown);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -1,4 +1,4 @@
|
|||||||
<div id="loginForm" tabindex="-1" role="dialog" class="modal fade">
|
<div id="login-modal" tabindex="-1" role="dialog" class="modal fade">
|
||||||
<div class="modal-dialog" role="document">
|
<div class="modal-dialog" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<form action="/api/v1/login" method="post">
|
<form action="/api/v1/login" method="post">
|
||||||
@ -19,7 +19,7 @@
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input id="password" type="password" class="form-control" placeholder="enter password" name="password" required>
|
<input id="password" type="password" class="form-control" placeholder="enter password" name="password" required>
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<button class="btn btn-outline-secondary" type="button" onclick="showPassword()"><i id="show-hide-password" class="bi bi-eye"></i></button>
|
<button class="btn btn-outline-secondary" type="button" onclick="showPassword()"><i id="show-hide-password-btn" class="bi bi-eye"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -35,7 +35,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
const passwordInput = $("#password");
|
const passwordInput = $("#password");
|
||||||
const showHidePasswordButton = $("#show-hide-password");
|
const showHidePasswordButton = $("#show-hide-password-btn");
|
||||||
|
|
||||||
function showPassword() {
|
function showPassword() {
|
||||||
if (passwordInput.attr("type") === "password") {
|
if (passwordInput.attr("type") === "password") {
|
||||||
|
@ -1,61 +1,74 @@
|
|||||||
<div id="add-form" tabindex="-1" role="dialog" class="modal fade">
|
<div id="package-add-modal" tabindex="-1" role="dialog" class="modal fade">
|
||||||
<div class="modal-dialog" role="document">
|
<div class="modal-dialog" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<form id="package-add-form" onsubmit="return false">
|
||||||
<h4 class="modal-title">Add new packages</h4>
|
<div class="modal-header">
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
<h4 class="modal-title">Add new packages</h4>
|
||||||
</div>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
||||||
<div class="modal-body">
|
</div>
|
||||||
<div class="form-group row">
|
<div class="modal-body">
|
||||||
<label for="package" class="col-sm-2 col-form-label">package</label>
|
<div class="form-group row">
|
||||||
<div class="col-sm-10">
|
<label for="package-input" class="col-sm-2 col-form-label">package</label>
|
||||||
<input id="package-form" type="text" list="known-packages-dlist" autocomplete="off" class="form-control" placeholder="AUR package" name="package" required>
|
<div class="col-sm-10">
|
||||||
<datalist id="known-packages-dlist"></datalist>
|
<input id="package-input" type="text" list="known-packages-dlist" autocomplete="off" class="form-control" placeholder="AUR package" name="package" required>
|
||||||
|
<datalist id="known-packages-dlist"></datalist>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="modal-footer">
|
||||||
<div class="modal-footer">
|
<button type="submit" class="btn btn-primary" onclick="packagesAdd()"><i class="bi bi-play"></i> add</button>
|
||||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="addPackages()"><i class="bi bi-play"></i> add</button>
|
<button type="submit" class="btn btn-success" onclick="packagesRequest()"><i class="bi bi-plus"></i> request</button>
|
||||||
<button type="button" class="btn btn-success" data-bs-dismiss="modal" onclick="requestPackages()"><i class="bi bi-plus"></i> request</button>
|
</div>
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><i class="bi bi-x"></i> close</button>
|
</form>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const packageInput = $("#package-form");
|
const packageAddModal = $("#package-add-modal");
|
||||||
const knownPackages = $("#known-packages-dlist");
|
const packageAddForm = $("#package-add-form");
|
||||||
|
packageAddModal.on("hidden.bs.modal", () => { packageAddForm.trigger("reset"); });
|
||||||
|
|
||||||
|
const packageInput = $("#package-input");
|
||||||
|
const knownPackagesList = $("#known-packages-dlist");
|
||||||
packageInput.keyup(() => {
|
packageInput.keyup(() => {
|
||||||
clearTimeout(packageInput.data("timeout"));
|
clearTimeout(packageInput.data("timeout"));
|
||||||
packageInput.data("timeout", setTimeout($.proxy(() => {
|
packageInput.data("timeout", setTimeout($.proxy(() => {
|
||||||
const value = packageInput.val();
|
const value = packageInput.val();
|
||||||
|
|
||||||
$.ajax({
|
if (value.length >= 3) {
|
||||||
url: "/api/v1/service/search",
|
$.ajax({
|
||||||
data: {"for": value},
|
url: "/api/v1/service/search",
|
||||||
type: "GET",
|
data: {"for": value},
|
||||||
dataType: "json",
|
type: "GET",
|
||||||
success: response => {
|
dataType: "json",
|
||||||
const options = response.map(pkg => {
|
success: response => {
|
||||||
const option = document.createElement("option");
|
const options = response.map(pkg => {
|
||||||
option.value = pkg.package;
|
const option = document.createElement("option");
|
||||||
option.innerText = `${pkg.package} (${pkg.description})`;
|
option.value = pkg.package;
|
||||||
return option;
|
option.innerText = `${pkg.package} (${pkg.description})`;
|
||||||
});
|
return option;
|
||||||
knownPackages.empty().append(options);
|
});
|
||||||
},
|
knownPackagesList.empty().append(options);
|
||||||
})
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}, this), 500));
|
}, this), 500));
|
||||||
});
|
});
|
||||||
|
|
||||||
function addPackages() {
|
function packagesAdd() {
|
||||||
const packages = [packageInput.val()];
|
const packages = packageInput.val();
|
||||||
doPackageAction("/api/v1/service/add", packages, "The following package has been added:", "Package addition failed:");
|
if (packages) {
|
||||||
|
packageAddModal.modal("hide");
|
||||||
|
doPackageAction("/api/v1/service/add", [packages], "The following package has been added:", "Package addition failed:");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function requestPackages() {
|
function packagesRequest() {
|
||||||
const packages = [packageInput.val()];
|
const packages = packageInput.val();
|
||||||
doPackageAction("/api/v1/service/request", packages, "The following package has been requested:", "Package request failed:");
|
if (packages) {
|
||||||
|
packageAddModal.modal("hide");
|
||||||
|
doPackageAction("/api/v1/service/request", [packages], "The following package has been requested:", "Package request failed:");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div id="package-info-form" tabindex="-1" role="dialog" class="modal fade">
|
<div id="package-info-modal" tabindex="-1" role="dialog" class="modal fade">
|
||||||
<div class="modal-dialog modal-xl" role="document">
|
<div class="modal-dialog modal-xl" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div id="package-info-modal-header" class="modal-header">
|
<div id="package-info-modal-header" class="modal-header">
|
||||||
@ -6,7 +6,7 @@
|
|||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<pre class="language-logs"><code id="package-info-logs" class="pre-scrollable language-logs"></code><button id="copy-btn" type="button" class="btn language-logs" onclick="copyLogs()"><i class="bi bi-clipboard"></i> copy</button></pre>
|
<pre class="language-logs"><code id="package-info-logs-input" class="pre-scrollable language-logs"></code><button id="logs-copy-btn" type="button" class="btn language-logs" onclick="copyLogs()"><i class="bi bi-clipboard"></i> copy</button></pre>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" onclick="showLogs()"><i class="bi bi-arrow-clockwise"></i> reload</button>
|
<button type="button" class="btn btn-secondary" onclick="showLogs()"><i class="bi bi-arrow-clockwise"></i> reload</button>
|
||||||
@ -17,28 +17,24 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
const packageInfoModal = $("#package-info-modal");
|
||||||
|
const packageInfoModalHeader = $("#package-info-modal-header");
|
||||||
const packageInfo = $("#package-info");
|
const packageInfo = $("#package-info");
|
||||||
const packageInfoForm = $("#package-info-form");
|
|
||||||
const packageInfoHeader = $("#package-info-modal-header");
|
const packageInfoLogsInput = $("#package-info-logs-input");
|
||||||
const packageInfoLogs = $("#package-info-logs");
|
const packageInfoLogsCopyButton = $("#logs-copy-btn");
|
||||||
const packageInfoLogsCopyButton = $("#copy-btn");
|
|
||||||
|
|
||||||
async function copyLogs() {
|
async function copyLogs() {
|
||||||
const logs = packageInfoLogs.text();
|
const logs = packageInfoLogsInput.text();
|
||||||
await copyToClipboard(logs);
|
await copyToClipboard(logs, packageInfoLogsCopyButton);
|
||||||
|
|
||||||
packageInfoLogsCopyButton.html("<i class=\"bi bi-clipboard-check\"></i> copied");
|
|
||||||
setTimeout(()=> {
|
|
||||||
packageInfoLogsCopyButton.html("<i class=\"bi bi-clipboard\"></i> copy");
|
|
||||||
}, 2000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showLogs(package) {
|
function showLogs(package) {
|
||||||
const isPackageBaseSet = package !== undefined;
|
const isPackageBaseSet = package !== undefined;
|
||||||
if (isPackageBaseSet)
|
if (isPackageBaseSet)
|
||||||
packageInfoForm.data("package", package); // set package base as currently used
|
packageInfoModal.data("package", package); // set package base as currently used
|
||||||
else
|
else
|
||||||
package = packageInfoForm.data("package"); // read package base from the current window attribute
|
package = packageInfoModal.data("package"); // read package base from the current window attribute
|
||||||
|
|
||||||
const headerClass = status => {
|
const headerClass = status => {
|
||||||
if (status === "pending") return ["bg-warning"];
|
if (status === "pending") return ["bg-warning"];
|
||||||
@ -54,13 +50,13 @@
|
|||||||
dataType: "json",
|
dataType: "json",
|
||||||
success: response => {
|
success: response => {
|
||||||
packageInfo.text(`${response.package_base} ${response.status.status} at ${new Date(1000 * response.status.timestamp).toISOString()}`);
|
packageInfo.text(`${response.package_base} ${response.status.status} at ${new Date(1000 * response.status.timestamp).toISOString()}`);
|
||||||
packageInfoLogs.text(response.logs);
|
packageInfoLogsInput.text(response.logs);
|
||||||
|
|
||||||
packageInfoHeader.removeClass();
|
packageInfoModalHeader.removeClass();
|
||||||
packageInfoHeader.addClass("modal-header");
|
packageInfoModalHeader.addClass("modal-header");
|
||||||
headerClass(response.status.status).forEach((clz) => packageInfoHeader.addClass(clz));
|
headerClass(response.status.status).forEach((clz) => packageInfoModalHeader.addClass(clz));
|
||||||
|
|
||||||
if (isPackageBaseSet) packageInfoForm.modal("show"); // we don't need to show window again
|
if (isPackageBaseSet) packageInfoModal.modal("show"); // we don't need to show window again
|
||||||
},
|
},
|
||||||
error: (jqXHR, _, errorThrown) => {
|
error: (jqXHR, _, errorThrown) => {
|
||||||
// show failed modal in case if first time loading
|
// show failed modal in case if first time loading
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div id="success-form" tabindex="-1" role="dialog" class="modal fade">
|
<div id="success-modal" tabindex="-1" role="dialog" class="modal fade">
|
||||||
<div class="modal-dialog" role="document">
|
<div class="modal-dialog" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header bg-success text-white">
|
<div class="modal-header bg-success text-white">
|
||||||
@ -17,16 +17,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const successForm = $("#success-form");
|
const successModal = $("#success-modal");
|
||||||
|
successModal.on("hidden.bs.modal", () => { reload(); });
|
||||||
|
|
||||||
const successDescription = $("#success-description");
|
const successDescription = $("#success-description");
|
||||||
const successDetails = $("#success-details");
|
const successDetails = $("#success-details");
|
||||||
const successTitle = $("#success-title");
|
const successTitle = $("#success-title");
|
||||||
successForm.on("hidden.bs.modal", () => { reload(); });
|
|
||||||
|
|
||||||
function showSuccess(title, description, details) {
|
function showSuccess(title, description, details) {
|
||||||
successTitle.text(title);
|
successTitle.text(title);
|
||||||
successDescription.text(description);
|
successDescription.text(description);
|
||||||
successDetails.empty().append(details);
|
successDetails.empty().append(details);
|
||||||
successForm.modal("show");
|
|
||||||
|
successModal.modal("show");
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
<script>
|
<script>
|
||||||
const addButton = $("#add-btn");
|
const keyImportButton = $("#key-import-btn");
|
||||||
const removeButton = $("#remove-btn");
|
const packageAddButton = $("#package-add-btn");
|
||||||
const updateButton = $("#update-btn");
|
const packageRemoveButton = $("#package-remove-btn");
|
||||||
|
const packageUpdateButton = $("#package-update-btn");
|
||||||
|
|
||||||
const table = $("#packages");
|
const table = $("#packages");
|
||||||
table.on("check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table",
|
table.on("check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table", () => {
|
||||||
() => {
|
packageRemoveButton.prop("disabled", !table.bootstrapTable("getSelections").length);
|
||||||
removeButton.prop("disabled", !table.bootstrapTable("getSelections").length);
|
});
|
||||||
});
|
table.on("click-row.bs.table", (self, data, row, cell) => {
|
||||||
table.on("click-row.bs.table", (_, row) => { showLogs(row.id); });
|
if (0 === cell || "base" === cell) {
|
||||||
|
const method = data[0] === true ? "uncheckBy" : "checkBy"; // fck javascript
|
||||||
|
table.bootstrapTable(method, {field: "id", values: [data.id]});
|
||||||
|
} else showLogs(data.id);
|
||||||
|
});
|
||||||
|
|
||||||
const architectureBadge = $("#badge-architecture");
|
const architectureBadge = $("#badge-architecture");
|
||||||
const repositoryBadge = $("#badge-repository");
|
const repositoryBadge = $("#badge-repository");
|
||||||
@ -50,9 +55,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function hideControls(hidden) {
|
function hideControls(hidden) {
|
||||||
addButton.attr("hidden", hidden);
|
keyImportButton.attr("hidden", hidden);
|
||||||
removeButton.attr("hidden", hidden);
|
packageAddButton.attr("hidden", hidden);
|
||||||
updateButton.attr("hidden", hidden);
|
packageRemoveButton.attr("hidden", hidden);
|
||||||
|
packageUpdateButton.attr("hidden", hidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
function reload() {
|
function reload() {
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{% if pgp_key is not none %}
|
{% if pgp_key is not none %}
|
||||||
<p>This repository is signed with <a href="https://pgp.mit.edu/pks/lookup?search=0x{{ pgp_key }}&fingerprint=on&op=index" title="key search">{{ pgp_key }}</a> by default.</p>
|
<p>This repository is signed with <a href="https://keyserver.ubuntu.com/pks/lookup?search=0x{{ pgp_key }}&fingerprint=on&op=index" title="key search">{{ pgp_key }}</a> by default.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<p>In order to use this repository edit your <code>/etc/pacman.conf</code> as following:</p>
|
<p>In order to use this repository edit your <code>/etc/pacman.conf</code> as following:</p>
|
||||||
@ -101,12 +101,7 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
|
|||||||
|
|
||||||
async function copyPacmanConf() {
|
async function copyPacmanConf() {
|
||||||
const conf = pacmanConf.text();
|
const conf = pacmanConf.text();
|
||||||
await copyToClipboard(conf);
|
await copyToClipboard(conf, pacmanConfCopyButton);
|
||||||
|
|
||||||
pacmanConfCopyButton.html("<i class=\"bi bi-clipboard-check\"></i> copied");
|
|
||||||
setTimeout(() => {
|
|
||||||
pacmanConfCopyButton.html("<i class=\"bi bi-clipboard\"></i> copy");
|
|
||||||
}, 2000);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -13,16 +13,21 @@
|
|||||||
<script src="https://unpkg.com/bootstrap-table@1.21.1/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
|
<script src="https://unpkg.com/bootstrap-table@1.21.1/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
async function copyToClipboard(text) {
|
async function copyToClipboard(text, button) {
|
||||||
if (navigator.clipboard === undefined) {
|
if (navigator.clipboard === undefined) {
|
||||||
const input = document.createElement("textarea");
|
const input = document.createElement("textarea");
|
||||||
input.innerHTML = text;
|
input.innerHTML = text;
|
||||||
document.body.appendChild(input);
|
document.body.appendChild(input);
|
||||||
input.select();
|
input.select();
|
||||||
document.execCommand('copy');
|
document.execCommand("copy");
|
||||||
document.body.removeChild(input);
|
document.body.removeChild(input);
|
||||||
} else {
|
} else {
|
||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.html("<i class=\"bi bi-clipboard-check\"></i> copied");
|
||||||
|
setTimeout(()=> {
|
||||||
|
button.html("<i class=\"bi bi-clipboard\"></i> copy");
|
||||||
|
}, 2000);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
1
setup.py
1
setup.py
@ -73,6 +73,7 @@ setup(
|
|||||||
]),
|
]),
|
||||||
("share/ahriman/templates/build-status", [
|
("share/ahriman/templates/build-status", [
|
||||||
"package/share/ahriman/templates/build-status/failed-modal.jinja2",
|
"package/share/ahriman/templates/build-status/failed-modal.jinja2",
|
||||||
|
"package/share/ahriman/templates/build-status/key-import-modal.jinja2",
|
||||||
"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",
|
||||||
|
@ -229,7 +229,7 @@ def _set_key_import_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
|||||||
"fail in case if key is not known for build user. This subcommand can be used "
|
"fail in case if key is not known for build user. This subcommand can be used "
|
||||||
"in order to import the PGP key to user keychain.",
|
"in order to import the PGP key to user keychain.",
|
||||||
formatter_class=_formatter)
|
formatter_class=_formatter)
|
||||||
parser.add_argument("--key-server", help="key server for key import", default="pgp.mit.edu")
|
parser.add_argument("--key-server", help="key server for key import", default="keyserver.ubuntu.com")
|
||||||
parser.add_argument("key", help="PGP key to import from public server")
|
parser.add_argument("key", help="PGP key to import from public server")
|
||||||
parser.set_defaults(handler=handlers.KeyImport, architecture=[""], lock=None, report=False)
|
parser.set_defaults(handler=handlers.KeyImport, architecture=[""], lock=None, report=False)
|
||||||
return parser
|
return parser
|
||||||
|
@ -62,7 +62,7 @@ class Auth(LazyLogging):
|
|||||||
Returns:
|
Returns:
|
||||||
str: login control as html code to insert
|
str: login control as html code to insert
|
||||||
"""
|
"""
|
||||||
return """<button type="button" class="btn btn-link" data-bs-toggle="modal" data-bs-target="#loginForm" style="text-decoration: none"><i class="bi bi-box-arrow-in-right"></i> login</button>"""
|
return """<button type="button" class="btn btn-link" data-bs-toggle="modal" data-bs-target="#login-modal" style="text-decoration: none"><i class="bi bi-box-arrow-in-right"></i> login</button>"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls: Type[Auth], configuration: Configuration, database: SQLite) -> Auth:
|
def load(cls: Type[Auth], configuration: Configuration, database: SQLite) -> Auth:
|
||||||
|
@ -118,7 +118,7 @@ class GPG(LazyLogging):
|
|||||||
"""
|
"""
|
||||||
key = key if key.startswith("0x") else f"0x{key}"
|
key = key if key.startswith("0x") else f"0x{key}"
|
||||||
try:
|
try:
|
||||||
response = requests.get(f"http://{server}/pks/lookup", params={
|
response = requests.get(f"https://{server}/pks/lookup", params={
|
||||||
"op": "get",
|
"op": "get",
|
||||||
"options": "mr",
|
"options": "mr",
|
||||||
"search": key
|
"search": key
|
||||||
|
@ -24,7 +24,7 @@ import uuid
|
|||||||
|
|
||||||
from multiprocessing import Process, Queue
|
from multiprocessing import Process, Queue
|
||||||
from threading import Lock, Thread
|
from threading import Lock, Thread
|
||||||
from typing import Callable, Dict, Iterable, Tuple
|
from typing import Callable, Dict, Iterable, Optional, Tuple
|
||||||
|
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.log import LazyLogging
|
from ahriman.core.log import LazyLogging
|
||||||
@ -78,6 +78,17 @@ class Spawn(Thread, LazyLogging):
|
|||||||
result = callback(args, architecture)
|
result = callback(args, architecture)
|
||||||
queue.put((process_id, result))
|
queue.put((process_id, result))
|
||||||
|
|
||||||
|
def key_import(self, key: str, server: Optional[str]) -> None:
|
||||||
|
"""
|
||||||
|
import key to service cache
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key(str): key to import
|
||||||
|
server(str): PGP key server
|
||||||
|
"""
|
||||||
|
kwargs = {} if server is None else {"key-server": server}
|
||||||
|
self.spawn_process("key-import", key, **kwargs)
|
||||||
|
|
||||||
def packages_add(self, packages: Iterable[str], *, now: bool) -> None:
|
def packages_add(self, packages: Iterable[str], *, now: bool) -> None:
|
||||||
"""
|
"""
|
||||||
add packages
|
add packages
|
||||||
@ -86,12 +97,10 @@ class Spawn(Thread, LazyLogging):
|
|||||||
packages(Iterable[str]): packages list to add
|
packages(Iterable[str]): packages list to add
|
||||||
now(bool): build packages now
|
now(bool): build packages now
|
||||||
"""
|
"""
|
||||||
if not packages:
|
|
||||||
return self.spawn_process("repo-update")
|
|
||||||
kwargs = {"source": PackageSource.AUR.value} # avoid abusing by building non-aur packages
|
kwargs = {"source": PackageSource.AUR.value} # avoid abusing by building non-aur packages
|
||||||
if now:
|
if now:
|
||||||
kwargs["now"] = ""
|
kwargs["now"] = ""
|
||||||
return self.spawn_process("package-add", *packages, **kwargs)
|
self.spawn_process("package-add", *packages, **kwargs)
|
||||||
|
|
||||||
def packages_remove(self, packages: Iterable[str]) -> None:
|
def packages_remove(self, packages: Iterable[str]) -> None:
|
||||||
"""
|
"""
|
||||||
@ -102,6 +111,12 @@ class Spawn(Thread, LazyLogging):
|
|||||||
"""
|
"""
|
||||||
self.spawn_process("package-remove", *packages)
|
self.spawn_process("package-remove", *packages)
|
||||||
|
|
||||||
|
def packages_update(self, ) -> None:
|
||||||
|
"""
|
||||||
|
run full repository update
|
||||||
|
"""
|
||||||
|
self.spawn_process("repo-update")
|
||||||
|
|
||||||
def spawn_process(self, command: str, *args: str, **kwargs: str) -> None:
|
def spawn_process(self, command: str, *args: str, **kwargs: str) -> None:
|
||||||
"""
|
"""
|
||||||
spawn external ahriman process with supplied arguments
|
spawn external ahriman process with supplied arguments
|
||||||
|
@ -20,9 +20,8 @@
|
|||||||
import aiohttp_jinja2
|
import aiohttp_jinja2
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aiohttp.web import middleware, Request
|
from aiohttp.web import HTTPClientError, HTTPException, HTTPServerError, HTTPUnauthorized, Request, StreamResponse, \
|
||||||
from aiohttp.web_exceptions import HTTPClientError, HTTPException, HTTPServerError, HTTPUnauthorized
|
json_response, middleware
|
||||||
from aiohttp.web_response import json_response, StreamResponse
|
|
||||||
|
|
||||||
from ahriman.web.middlewares import HandlerType, MiddlewareType
|
from ahriman.web.middlewares import HandlerType, MiddlewareType
|
||||||
|
|
||||||
|
@ -22,9 +22,11 @@ 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.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
|
||||||
|
from ahriman.web.views.service.update import UpdateView
|
||||||
from ahriman.web.views.status.logs import LogsView
|
from ahriman.web.views.status.logs import LogsView
|
||||||
from ahriman.web.views.status.package import PackageView
|
from ahriman.web.views.status.package import PackageView
|
||||||
from ahriman.web.views.status.packages import PackagesView
|
from ahriman.web.views.status.packages import PackagesView
|
||||||
@ -47,13 +49,16 @@ def setup_routes(application: Application, static_path: Path) -> None:
|
|||||||
|
|
||||||
* ``POST /api/v1/service/add`` add new packages to repository
|
* ``POST /api/v1/service/add`` add new packages to repository
|
||||||
|
|
||||||
|
* ``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/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
|
||||||
|
|
||||||
* ``GET /api/v1/service/search`` search for substring in AUR
|
* ``GET /api/v1/service/search`` search for substring in AUR
|
||||||
|
|
||||||
* ``POST /api/v1/service/update`` update packages in repository, actually it is just alias for add
|
* ``POST /api/v1/service/update`` update all packages in repository
|
||||||
|
|
||||||
* ``GET /api/v1/packages`` get all known packages
|
* ``GET /api/v1/packages`` get all known packages
|
||||||
* ``POST /api/v1/packages`` force update every package from repository
|
* ``POST /api/v1/packages`` force update every package from repository
|
||||||
@ -84,13 +89,16 @@ def setup_routes(application: Application, static_path: Path) -> None:
|
|||||||
|
|
||||||
application.router.add_post("/api/v1/service/add", AddView)
|
application.router.add_post("/api/v1/service/add", AddView)
|
||||||
|
|
||||||
|
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/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)
|
||||||
|
|
||||||
application.router.add_get("/api/v1/service/search", SearchView, allow_head=False)
|
application.router.add_get("/api/v1/service/search", SearchView, allow_head=False)
|
||||||
|
|
||||||
application.router.add_post("/api/v1/service/update", AddView)
|
application.router.add_post("/api/v1/service/update", UpdateView)
|
||||||
|
|
||||||
application.router.add_get("/api/v1/packages", PackagesView, allow_head=True)
|
application.router.add_get("/api/v1/packages", PackagesView, allow_head=True)
|
||||||
application.router.add_post("/api/v1/packages", PackagesView)
|
application.router.add_post("/api/v1/packages", PackagesView)
|
||||||
|
@ -20,16 +20,18 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from aiohttp.web import Request, View
|
from aiohttp.web import Request, View
|
||||||
from typing import Any, Dict, List, Optional, Type
|
from typing import Any, Callable, Dict, List, Optional, Type, TypeVar
|
||||||
|
|
||||||
from ahriman.core.auth import Auth
|
from ahriman.core.auth import Auth
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.database import SQLite
|
|
||||||
from ahriman.core.spawn import Spawn
|
from ahriman.core.spawn import Spawn
|
||||||
from ahriman.core.status.watcher import Watcher
|
from ahriman.core.status.watcher import Watcher
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar("T", str, List[str])
|
||||||
|
|
||||||
|
|
||||||
class BaseView(View):
|
class BaseView(View):
|
||||||
"""
|
"""
|
||||||
base web view to make things typed
|
base web view to make things typed
|
||||||
@ -46,17 +48,6 @@ class BaseView(View):
|
|||||||
configuration: Configuration = self.request.app["configuration"]
|
configuration: Configuration = self.request.app["configuration"]
|
||||||
return configuration
|
return configuration
|
||||||
|
|
||||||
@property
|
|
||||||
def database(self) -> SQLite:
|
|
||||||
"""
|
|
||||||
get database instance
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
SQLite: database instance
|
|
||||||
"""
|
|
||||||
database: SQLite = self.request.app["database"]
|
|
||||||
return database
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def service(self) -> Watcher:
|
def service(self) -> Watcher:
|
||||||
"""
|
"""
|
||||||
@ -104,6 +95,29 @@ class BaseView(View):
|
|||||||
permission: UserAccess = getattr(cls, f"{request.method.upper()}_PERMISSION", UserAccess.Full)
|
permission: UserAccess = getattr(cls, f"{request.method.upper()}_PERMISSION", UserAccess.Full)
|
||||||
return permission
|
return permission
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_non_empty(extractor: Callable[[str], Optional[T]], key: str) -> T:
|
||||||
|
"""
|
||||||
|
get non-empty value from request parameters
|
||||||
|
|
||||||
|
Args:
|
||||||
|
extractor(Callable[[str], T]): function to get value by key
|
||||||
|
key(str): key to extract value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
T: extracted values if it is presented and not empty
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
KeyError: in case if key was not found or value is empty
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
value = extractor(key)
|
||||||
|
if not value:
|
||||||
|
raise KeyError(key)
|
||||||
|
except Exception:
|
||||||
|
raise KeyError(f"Key {key} is missing or empty")
|
||||||
|
return value
|
||||||
|
|
||||||
async def extract_data(self, list_keys: Optional[List[str]] = None) -> Dict[str, Any]:
|
async def extract_data(self, list_keys: Optional[List[str]] = None) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
extract json data from either json or form data
|
extract json data from either json or form data
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
# 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 aiohttp.web import HTTPNoContent
|
from aiohttp.web import HTTPBadRequest, HTTPNoContent
|
||||||
|
|
||||||
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
|
||||||
@ -62,8 +62,11 @@ class AddView(BaseView):
|
|||||||
< Server: Python/3.10 aiohttp/3.8.3
|
< Server: Python/3.10 aiohttp/3.8.3
|
||||||
<
|
<
|
||||||
"""
|
"""
|
||||||
data = await self.extract_data(["packages"])
|
try:
|
||||||
packages = data.get("packages", [])
|
data = await self.extract_data(["packages"])
|
||||||
|
packages = self.get_non_empty(lambda key: [package for package in data[key] if package], "packages")
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPBadRequest(reason=str(e))
|
||||||
|
|
||||||
self.spawner.packages_add(packages, now=True)
|
self.spawner.packages_add(packages, now=True)
|
||||||
|
|
||||||
|
121
src/ahriman/web/views/service/pgp.py
Normal file
121
src/ahriman/web/views/service/pgp.py
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
#
|
||||||
|
# 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, HTTPNotFound, Response, json_response
|
||||||
|
|
||||||
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
|
|
||||||
|
class PGPView(BaseView):
|
||||||
|
"""
|
||||||
|
pgp key management web view
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
||||||
|
HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self
|
||||||
|
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||||
|
"""
|
||||||
|
|
||||||
|
POST_PERMISSION = UserAccess.Full
|
||||||
|
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Reporter
|
||||||
|
|
||||||
|
async def get(self) -> Response:
|
||||||
|
"""
|
||||||
|
retrieve key from the key server. It supports two query parameters: ``key`` - pgp key fingerprint and
|
||||||
|
``server`` which points to valid PGP key server
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: 200 with key body on success
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPBadRequest: if bad data is supplied
|
||||||
|
HTTPNotFound: if key wasn't found or service was unable to fetch it
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
Example of command by using curl::
|
||||||
|
|
||||||
|
$ curl -v -H 'Accept: application/json' 'http://example.com/api/v1/service/pgp?key=0xE989490C&server=keyserver.ubuntu.com'
|
||||||
|
> GET /api/v1/service/pgp?key=0xE989490C&server=keyserver.ubuntu.com HTTP/1.1
|
||||||
|
> Host: example.com
|
||||||
|
> User-Agent: curl/7.86.0
|
||||||
|
> Accept: application/json
|
||||||
|
>
|
||||||
|
< HTTP/1.1 200 OK
|
||||||
|
< Content-Type: application/json; charset=utf-8
|
||||||
|
< Content-Length: 3275
|
||||||
|
< Date: Fri, 25 Nov 2022 22:54:02 GMT
|
||||||
|
< Server: Python/3.10 aiohttp/3.8.3
|
||||||
|
<
|
||||||
|
{"key": "key"}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
key = self.get_non_empty(self.request.query.getone, "key")
|
||||||
|
server = self.get_non_empty(self.request.query.getone, "server")
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPBadRequest(reason=str(e))
|
||||||
|
|
||||||
|
try:
|
||||||
|
key = self.service.repository.sign.key_download(server, key)
|
||||||
|
except Exception:
|
||||||
|
raise HTTPNotFound()
|
||||||
|
|
||||||
|
return json_response({"key": key})
|
||||||
|
|
||||||
|
async def post(self) -> None:
|
||||||
|
"""
|
||||||
|
store key to the local service environment
|
||||||
|
|
||||||
|
JSON body must be supplied, the following model is used::
|
||||||
|
|
||||||
|
{
|
||||||
|
"key": "0x8BE91E5A773FB48AC05CC1EDBED105AED6246B39", # key fingerprint to import
|
||||||
|
"server": "keyserver.ubuntu.com" # optional pgp server address
|
||||||
|
}
|
||||||
|
|
||||||
|
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/pgp' -d '{"key": "0xE989490C"}'
|
||||||
|
> POST /api/v1/service/pgp HTTP/1.1
|
||||||
|
> Host: example.com
|
||||||
|
> User-Agent: curl/7.86.0
|
||||||
|
> Accept: */*
|
||||||
|
> Content-Type: application/json
|
||||||
|
> Content-Length: 21
|
||||||
|
>
|
||||||
|
< HTTP/1.1 204 No Content
|
||||||
|
< Date: Fri, 25 Nov 2022 22:55:56 GMT
|
||||||
|
< Server: Python/3.10 aiohttp/3.8.3
|
||||||
|
<
|
||||||
|
"""
|
||||||
|
data = await self.extract_data()
|
||||||
|
|
||||||
|
try:
|
||||||
|
key = self.get_non_empty(data.get, "key")
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPBadRequest(reason=str(e))
|
||||||
|
|
||||||
|
self.spawner.key_import(key, data.get("server"))
|
||||||
|
|
||||||
|
raise HTTPNoContent()
|
@ -65,7 +65,7 @@ class RemoveView(BaseView):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
data = await self.extract_data(["packages"])
|
data = await self.extract_data(["packages"])
|
||||||
packages = data["packages"]
|
packages = self.get_non_empty(lambda key: [package for package in data[key] if package], "packages")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPBadRequest(reason=str(e))
|
raise HTTPBadRequest(reason=str(e))
|
||||||
|
|
||||||
|
@ -65,7 +65,7 @@ class RequestView(BaseView):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
data = await self.extract_data(["packages"])
|
data = await self.extract_data(["packages"])
|
||||||
packages = data["packages"]
|
packages = self.get_non_empty(lambda key: [package for package in data[key] if package], "packages")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPBadRequest(reason=str(e))
|
raise HTTPBadRequest(reason=str(e))
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
# 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 aiohttp.web import HTTPNotFound, Response, json_response
|
from aiohttp.web import HTTPBadRequest, HTTPNotFound, Response, json_response
|
||||||
from typing import Callable, List
|
from typing import Callable, List
|
||||||
|
|
||||||
from ahriman.core.alpm.remote import AUR
|
from ahriman.core.alpm.remote import AUR
|
||||||
@ -45,6 +45,7 @@ class SearchView(BaseView):
|
|||||||
Response: 200 with found package bases and descriptions sorted by base
|
Response: 200 with found package bases and descriptions sorted by base
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
|
HTTPBadRequest: in case if bad data is supplied
|
||||||
HTTPNotFound: if no packages found
|
HTTPNotFound: if no packages found
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
@ -64,8 +65,12 @@ class SearchView(BaseView):
|
|||||||
<
|
<
|
||||||
[{"package": "ahriman", "description": "ArcH linux ReposItory MANager"}, {"package": "ahriman-git", "description": "ArcH Linux ReposItory MANager"}]
|
[{"package": "ahriman", "description": "ArcH linux ReposItory MANager"}, {"package": "ahriman-git", "description": "ArcH Linux ReposItory MANager"}]
|
||||||
"""
|
"""
|
||||||
search: List[str] = self.request.query.getall("for", default=[])
|
try:
|
||||||
packages = AUR.multisearch(*search, pacman=self.service.repository.pacman)
|
search: List[str] = self.get_non_empty(lambda key: self.request.query.getall(key, default=[]), "for")
|
||||||
|
packages = AUR.multisearch(*search, pacman=self.service.repository.pacman)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPBadRequest(reason=str(e))
|
||||||
|
|
||||||
if not packages:
|
if not packages:
|
||||||
raise HTTPNotFound(reason=f"No packages found for terms: {search}")
|
raise HTTPNotFound(reason=f"No packages found for terms: {search}")
|
||||||
|
|
||||||
|
59
src/ahriman/web/views/service/update.py
Normal file
59
src/ahriman/web/views/service/update.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
#
|
||||||
|
# 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 HTTPNoContent
|
||||||
|
|
||||||
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateView(BaseView):
|
||||||
|
"""
|
||||||
|
update repository web view
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||||
|
"""
|
||||||
|
|
||||||
|
POST_PERMISSION = UserAccess.Full
|
||||||
|
|
||||||
|
async def post(self) -> None:
|
||||||
|
"""
|
||||||
|
run repository update. No parameters supported here
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPNoContent: in case of success response
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
Example of command by using curl::
|
||||||
|
|
||||||
|
$ curl -v -XPOST 'http://example.com/api/v1/service/update'
|
||||||
|
> POST /api/v1/service/update HTTP/1.1
|
||||||
|
> Host: example.com
|
||||||
|
> User-Agent: curl/7.86.0
|
||||||
|
> Accept: */*
|
||||||
|
>
|
||||||
|
< HTTP/1.1 204 No Content
|
||||||
|
< Date: Fri, 25 Nov 2022 22:57:56 GMT
|
||||||
|
< Server: Python/3.10 aiohttp/3.8.3
|
||||||
|
<
|
||||||
|
"""
|
||||||
|
self.spawner.packages_update()
|
||||||
|
|
||||||
|
raise HTTPNoContent()
|
@ -17,8 +17,7 @@
|
|||||||
# 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 aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
|
from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response
|
||||||
from aiohttp.web_exceptions import HTTPNotFound
|
|
||||||
|
|
||||||
from ahriman.core.exceptions import UnknownPackageError
|
from ahriman.core.exceptions import UnknownPackageError
|
||||||
from ahriman.models.log_record_id import LogRecordId
|
from ahriman.models.log_record_id import LogRecordId
|
||||||
|
@ -17,8 +17,7 @@
|
|||||||
# 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 aiohttp.web import HTTPFound
|
from aiohttp.web import HTTPFound, HTTPUnauthorized
|
||||||
from aiohttp.web_exceptions import HTTPUnauthorized
|
|
||||||
|
|
||||||
from ahriman.core.auth.helpers import check_authorized, forget
|
from ahriman.core.auth.helpers import check_authorized, forget
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
|
@ -17,7 +17,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.key = "0xE989490C"
|
args.key = "0xE989490C"
|
||||||
args.key_server = "pgp.mit.edu"
|
args.key_server = "keyserver.ubuntu.com"
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
@ -81,9 +81,9 @@ def test_key_download(gpg: GPG, mocker: MockerFixture) -> None:
|
|||||||
must download the key from public server
|
must download the key from public server
|
||||||
"""
|
"""
|
||||||
requests_mock = mocker.patch("requests.get")
|
requests_mock = mocker.patch("requests.get")
|
||||||
gpg.key_download("pgp.mit.edu", "0xE989490C")
|
gpg.key_download("keyserver.ubuntu.com", "0xE989490C")
|
||||||
requests_mock.assert_called_once_with(
|
requests_mock.assert_called_once_with(
|
||||||
"http://pgp.mit.edu/pks/lookup",
|
"https://keyserver.ubuntu.com/pks/lookup",
|
||||||
params={"op": "get", "options": "mr", "search": "0xE989490C"},
|
params={"op": "get", "options": "mr", "search": "0xE989490C"},
|
||||||
timeout=gpg.DEFAULT_TIMEOUT)
|
timeout=gpg.DEFAULT_TIMEOUT)
|
||||||
|
|
||||||
@ -94,7 +94,7 @@ def test_key_download_failure(gpg: GPG, mocker: MockerFixture) -> None:
|
|||||||
"""
|
"""
|
||||||
mocker.patch("requests.get", side_effect=requests.exceptions.HTTPError())
|
mocker.patch("requests.get", side_effect=requests.exceptions.HTTPError())
|
||||||
with pytest.raises(requests.exceptions.HTTPError):
|
with pytest.raises(requests.exceptions.HTTPError):
|
||||||
gpg.key_download("pgp.mit.edu", "0xE989490C")
|
gpg.key_download("keyserver.ubuntu.com", "0xE989490C")
|
||||||
|
|
||||||
|
|
||||||
def test_key_import(gpg: GPG, mocker: MockerFixture) -> None:
|
def test_key_import(gpg: GPG, mocker: MockerFixture) -> None:
|
||||||
@ -104,7 +104,7 @@ def test_key_import(gpg: GPG, mocker: MockerFixture) -> None:
|
|||||||
mocker.patch("ahriman.core.sign.gpg.GPG.key_download", return_value="key")
|
mocker.patch("ahriman.core.sign.gpg.GPG.key_download", return_value="key")
|
||||||
check_output_mock = mocker.patch("ahriman.core.sign.gpg.GPG._check_output")
|
check_output_mock = mocker.patch("ahriman.core.sign.gpg.GPG._check_output")
|
||||||
|
|
||||||
gpg.key_import("pgp.mit.edu", "0xE989490C")
|
gpg.key_import("keyserver.ubuntu.com", "0xE989490C")
|
||||||
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))
|
||||||
|
|
||||||
|
|
||||||
|
@ -36,6 +36,24 @@ def test_process_error(spawner: Spawn) -> None:
|
|||||||
assert spawner.queue.empty()
|
assert spawner.queue.empty()
|
||||||
|
|
||||||
|
|
||||||
|
def test_key_import(spawner: Spawn, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must call key import
|
||||||
|
"""
|
||||||
|
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn.spawn_process")
|
||||||
|
spawner.key_import("0xdeadbeaf", None)
|
||||||
|
spawn_mock.assert_called_once_with("key-import", "0xdeadbeaf")
|
||||||
|
|
||||||
|
|
||||||
|
def test_key_import_with_server(spawner: Spawn, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must call key import with server specified
|
||||||
|
"""
|
||||||
|
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn.spawn_process")
|
||||||
|
spawner.key_import("0xdeadbeaf", "keyserver.ubuntu.com")
|
||||||
|
spawn_mock.assert_called_once_with("key-import", "0xdeadbeaf", **{"key-server": "keyserver.ubuntu.com"})
|
||||||
|
|
||||||
|
|
||||||
def test_packages_add(spawner: Spawn, mocker: MockerFixture) -> None:
|
def test_packages_add(spawner: Spawn, mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must call package addition
|
must call package addition
|
||||||
@ -54,15 +72,6 @@ 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_add_update(spawner: Spawn, mocker: MockerFixture) -> None:
|
|
||||||
"""
|
|
||||||
must call repo update
|
|
||||||
"""
|
|
||||||
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn.spawn_process")
|
|
||||||
spawner.packages_add([], now=False)
|
|
||||||
spawn_mock.assert_called_once_with("repo-update")
|
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
@ -72,6 +81,15 @@ def test_packages_remove(spawner: Spawn, mocker: MockerFixture) -> None:
|
|||||||
spawn_mock.assert_called_once_with("package-remove", "ahriman", "linux")
|
spawn_mock.assert_called_once_with("package-remove", "ahriman", "linux")
|
||||||
|
|
||||||
|
|
||||||
|
def test_packages_update(spawner: Spawn, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must call repo update
|
||||||
|
"""
|
||||||
|
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn.spawn_process")
|
||||||
|
spawner.packages_update()
|
||||||
|
spawn_mock.assert_called_once_with("repo-update")
|
||||||
|
|
||||||
|
|
||||||
def test_spawn_process(spawner: Spawn, mocker: MockerFixture) -> None:
|
def test_spawn_process(spawner: Spawn, mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must correctly spawn child process
|
must correctly spawn child process
|
||||||
|
@ -325,6 +325,7 @@ def test_walk(resource_path_root: Path) -> None:
|
|||||||
resource_path_root / "models" / "package_tpacpi-bat-git_srcinfo",
|
resource_path_root / "models" / "package_tpacpi-bat-git_srcinfo",
|
||||||
resource_path_root / "models" / "package_yay_srcinfo",
|
resource_path_root / "models" / "package_yay_srcinfo",
|
||||||
resource_path_root / "web" / "templates" / "build-status" / "failed-modal.jinja2",
|
resource_path_root / "web" / "templates" / "build-status" / "failed-modal.jinja2",
|
||||||
|
resource_path_root / "web" / "templates" / "build-status" / "key-import-modal.jinja2",
|
||||||
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",
|
||||||
|
@ -2,7 +2,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from aiohttp.web_exceptions import HTTPBadRequest, HTTPInternalServerError, HTTPNoContent, HTTPUnauthorized
|
from aiohttp.web import HTTPBadRequest, HTTPInternalServerError, HTTPNoContent, HTTPUnauthorized
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import AsyncMock, MagicMock
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
@ -21,18 +21,26 @@ 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")
|
||||||
response = await client.post("/api/v1/service/add", json={"packages": ["ahriman"]})
|
|
||||||
|
|
||||||
|
response = await client.post("/api/v1/service/add", json={"packages": ["ahriman"]})
|
||||||
assert response.ok
|
assert response.ok
|
||||||
add_mock.assert_called_once_with(["ahriman"], now=True)
|
add_mock.assert_called_once_with(["ahriman"], now=True)
|
||||||
|
|
||||||
|
|
||||||
async def test_post_update(client: TestClient, mocker: MockerFixture) -> None:
|
async def test_post_empty(client: TestClient, mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must call post request correctly for alias
|
must call raise 400 on empty request
|
||||||
"""
|
"""
|
||||||
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add")
|
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add")
|
||||||
response = await client.post("/api/v1/service/update", json={"packages": ["ahriman"]})
|
|
||||||
|
|
||||||
assert response.ok
|
response = await client.post("/api/v1/service/add", json={"packages": [""]})
|
||||||
add_mock.assert_called_once_with(["ahriman"], now=True)
|
assert response.status == 400
|
||||||
|
add_mock.assert_not_called()
|
||||||
|
|
||||||
|
response = await client.post("/api/v1/service/add", json={"packages": []})
|
||||||
|
assert response.status == 400
|
||||||
|
add_mock.assert_not_called()
|
||||||
|
|
||||||
|
response = await client.post("/api/v1/service/add", json={})
|
||||||
|
assert response.status == 400
|
||||||
|
add_mock.assert_not_called()
|
||||||
|
75
tests/ahriman/web/views/service/test_views_service_pgp.py
Normal file
75
tests/ahriman/web/views/service/test_views_service_pgp.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
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.pgp import PGPView
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_permission() -> None:
|
||||||
|
"""
|
||||||
|
must return correct permission for the request
|
||||||
|
"""
|
||||||
|
for method in ("GET", "HEAD"):
|
||||||
|
request = pytest.helpers.request("", "", method)
|
||||||
|
assert await PGPView.get_permission(request) == UserAccess.Reporter
|
||||||
|
for method in ("POST",):
|
||||||
|
request = pytest.helpers.request("", "", method)
|
||||||
|
assert await PGPView.get_permission(request) == UserAccess.Full
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get(client: TestClient, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must retrieve key from the keyserver
|
||||||
|
"""
|
||||||
|
import_mock = mocker.patch("ahriman.core.sign.gpg.GPG.key_download", return_value="imported")
|
||||||
|
|
||||||
|
response = await client.get("/api/v1/service/pgp", params={"key": "0xdeadbeaf", "server": "keyserver.ubuntu.com"})
|
||||||
|
assert response.ok
|
||||||
|
import_mock.assert_called_once_with("keyserver.ubuntu.com", "0xdeadbeaf")
|
||||||
|
assert await response.json() == {"key": "imported"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_empty(client: TestClient, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must raise 400 on missing parameters
|
||||||
|
"""
|
||||||
|
import_mock = mocker.patch("ahriman.core.sign.gpg.GPG.key_download")
|
||||||
|
|
||||||
|
response = await client.get("/api/v1/service/pgp")
|
||||||
|
assert response.status == 400
|
||||||
|
import_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_process_exception(client: TestClient, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must raise 404 on invalid PGP server response
|
||||||
|
"""
|
||||||
|
import_mock = mocker.patch("ahriman.core.sign.gpg.GPG.key_download", side_effect=Exception())
|
||||||
|
|
||||||
|
response = await client.get("/api/v1/service/pgp", params={"key": "0xdeadbeaf", "server": "keyserver.ubuntu.com"})
|
||||||
|
assert response.status == 404
|
||||||
|
import_mock.assert_called_once_with("keyserver.ubuntu.com", "0xdeadbeaf")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_post(client: TestClient, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must call post request correctly
|
||||||
|
"""
|
||||||
|
import_mock = mocker.patch("ahriman.core.spawn.Spawn.key_import")
|
||||||
|
|
||||||
|
response = await client.post("/api/v1/service/pgp", json={"key": "0xdeadbeaf", "server": "keyserver.ubuntu.com"})
|
||||||
|
assert response.ok
|
||||||
|
import_mock.assert_called_once_with("0xdeadbeaf", "keyserver.ubuntu.com")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_post_exception(client: TestClient, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must raise exception on missing key payload
|
||||||
|
"""
|
||||||
|
import_mock = mocker.patch("ahriman.core.spawn.Spawn.key_import")
|
||||||
|
|
||||||
|
response = await client.post("/api/v1/service/pgp")
|
||||||
|
assert response.status == 400
|
||||||
|
import_mock.assert_not_called()
|
@ -21,8 +21,8 @@ async def test_post(client: TestClient, mocker: MockerFixture) -> None:
|
|||||||
must call post request correctly
|
must call post request correctly
|
||||||
"""
|
"""
|
||||||
remove_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_remove")
|
remove_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_remove")
|
||||||
response = await client.post("/api/v1/service/remove", json={"packages": ["ahriman"]})
|
|
||||||
|
|
||||||
|
response = await client.post("/api/v1/service/remove", json={"packages": ["ahriman"]})
|
||||||
assert response.ok
|
assert response.ok
|
||||||
remove_mock.assert_called_once_with(["ahriman"])
|
remove_mock.assert_called_once_with(["ahriman"])
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ async def test_post_exception(client: TestClient, mocker: MockerFixture) -> None
|
|||||||
must raise exception on missing packages payload
|
must raise exception on missing packages payload
|
||||||
"""
|
"""
|
||||||
remove_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_remove")
|
remove_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_remove")
|
||||||
response = await client.post("/api/v1/service/remove")
|
|
||||||
|
|
||||||
|
response = await client.post("/api/v1/service/remove")
|
||||||
assert response.status == 400
|
assert response.status == 400
|
||||||
remove_mock.assert_not_called()
|
remove_mock.assert_not_called()
|
||||||
|
@ -21,8 +21,8 @@ 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")
|
||||||
response = await client.post("/api/v1/service/request", json={"packages": ["ahriman"]})
|
|
||||||
|
|
||||||
|
response = await client.post("/api/v1/service/request", json={"packages": ["ahriman"]})
|
||||||
assert response.ok
|
assert response.ok
|
||||||
add_mock.assert_called_once_with(["ahriman"], now=False)
|
add_mock.assert_called_once_with(["ahriman"], now=False)
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ async def test_post_exception(client: TestClient, mocker: MockerFixture) -> None
|
|||||||
must raise exception on missing packages payload
|
must raise exception on missing packages payload
|
||||||
"""
|
"""
|
||||||
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add")
|
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add")
|
||||||
response = await client.post("/api/v1/service/request")
|
|
||||||
|
|
||||||
|
response = await client.post("/api/v1/service/request")
|
||||||
assert response.status == 400
|
assert response.status == 400
|
||||||
add_mock.assert_not_called()
|
add_mock.assert_not_called()
|
||||||
|
@ -22,8 +22,8 @@ async def test_get(client: TestClient, aur_package_ahriman: AURPackage, mocker:
|
|||||||
must call get request correctly
|
must call get request correctly
|
||||||
"""
|
"""
|
||||||
mocker.patch("ahriman.core.alpm.remote.AUR.multisearch", return_value=[aur_package_ahriman])
|
mocker.patch("ahriman.core.alpm.remote.AUR.multisearch", return_value=[aur_package_ahriman])
|
||||||
response = await client.get("/api/v1/service/search", params={"for": "ahriman"})
|
|
||||||
|
|
||||||
|
response = await client.get("/api/v1/service/search", params={"for": "ahriman"})
|
||||||
assert response.ok
|
assert response.ok
|
||||||
assert await response.json() == [{"package": aur_package_ahriman.package_base,
|
assert await response.json() == [{"package": aur_package_ahriman.package_base,
|
||||||
"description": aur_package_ahriman.description}]
|
"description": aur_package_ahriman.description}]
|
||||||
@ -33,11 +33,20 @@ async def test_get_exception(client: TestClient, mocker: MockerFixture) -> None:
|
|||||||
"""
|
"""
|
||||||
must raise 400 on empty search string
|
must raise 400 on empty search string
|
||||||
"""
|
"""
|
||||||
search_mock = mocker.patch("ahriman.core.alpm.remote.AUR.multisearch", return_value=[])
|
search_mock = mocker.patch("ahriman.core.alpm.remote.AUR.multisearch")
|
||||||
response = await client.get("/api/v1/service/search")
|
|
||||||
|
|
||||||
|
response = await client.get("/api/v1/service/search")
|
||||||
|
assert response.status == 400
|
||||||
|
search_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_empty(client: TestClient, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must raise 404 on empty search result
|
||||||
|
"""
|
||||||
|
mocker.patch("ahriman.core.alpm.remote.AUR.multisearch", return_value=[])
|
||||||
|
response = await client.get("/api/v1/service/search", params={"for": "ahriman"})
|
||||||
assert response.status == 404
|
assert response.status == 404
|
||||||
search_mock.assert_called_once_with(pacman=pytest.helpers.anyvar(int))
|
|
||||||
|
|
||||||
|
|
||||||
async def test_get_join(client: TestClient, mocker: MockerFixture) -> None:
|
async def test_get_join(client: TestClient, mocker: MockerFixture) -> None:
|
||||||
@ -45,7 +54,7 @@ async def test_get_join(client: TestClient, mocker: MockerFixture) -> None:
|
|||||||
must join search args with space
|
must join search args with space
|
||||||
"""
|
"""
|
||||||
search_mock = mocker.patch("ahriman.core.alpm.remote.AUR.multisearch")
|
search_mock = mocker.patch("ahriman.core.alpm.remote.AUR.multisearch")
|
||||||
response = await client.get("/api/v1/service/search", params=[("for", "ahriman"), ("for", "maybe")])
|
|
||||||
|
|
||||||
|
response = await client.get("/api/v1/service/search", params=[("for", "ahriman"), ("for", "maybe")])
|
||||||
assert response.ok
|
assert response.ok
|
||||||
search_mock.assert_called_once_with("ahriman", "maybe", pacman=pytest.helpers.anyvar(int))
|
search_mock.assert_called_once_with("ahriman", "maybe", pacman=pytest.helpers.anyvar(int))
|
||||||
|
13
tests/ahriman/web/views/service/test_views_service_update.py
Normal file
13
tests/ahriman/web/views/service/test_views_service_update.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from aiohttp.test_utils import TestClient
|
||||||
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
|
|
||||||
|
async def test_post_update(client: TestClient, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must call post request correctly for alias
|
||||||
|
"""
|
||||||
|
update_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_update")
|
||||||
|
|
||||||
|
response = await client.post("/api/v1/service/update")
|
||||||
|
assert response.ok
|
||||||
|
update_mock.assert_called_once_with()
|
@ -12,13 +12,6 @@ def test_configuration(base: BaseView) -> None:
|
|||||||
assert base.configuration
|
assert base.configuration
|
||||||
|
|
||||||
|
|
||||||
def test_database(base: BaseView) -> None:
|
|
||||||
"""
|
|
||||||
must return database
|
|
||||||
"""
|
|
||||||
assert base.database
|
|
||||||
|
|
||||||
|
|
||||||
def test_service(base: BaseView) -> None:
|
def test_service(base: BaseView) -> None:
|
||||||
"""
|
"""
|
||||||
must return service
|
must return service
|
||||||
@ -50,6 +43,24 @@ async def test_get_permission(base: BaseView) -> None:
|
|||||||
assert await base.get_permission(request) == "permission"
|
assert await base.get_permission(request) == "permission"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_non_empty() -> None:
|
||||||
|
"""
|
||||||
|
must correctly extract non-empty values
|
||||||
|
"""
|
||||||
|
assert BaseView.get_non_empty(lambda k: k, "key")
|
||||||
|
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
BaseView.get_non_empty(lambda k: None, "key")
|
||||||
|
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
BaseView.get_non_empty(lambda k: "", "key")
|
||||||
|
|
||||||
|
assert BaseView.get_non_empty(lambda k: [k], "key")
|
||||||
|
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
BaseView.get_non_empty(lambda k: [], "key")
|
||||||
|
|
||||||
|
|
||||||
async def test_extract_data_json(base: BaseView) -> None:
|
async def test_extract_data_json(base: BaseView) -> None:
|
||||||
"""
|
"""
|
||||||
must parse and return json
|
must parse and return json
|
||||||
|
Loading…
Reference in New Issue
Block a user