mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-04-28 01:07:18 +00:00
feat: add patch controls to web, review web, enrich info tab (#115)
* add ability to specify one-time patch on package addition * support vars in interface
This commit is contained in:
parent
54bd016c17
commit
8f047f9a96
@ -200,6 +200,14 @@ Alternatively you can create full-diff patches, which are calculated by using ``
|
|||||||
|
|
||||||
The last command will calculate diff from current tree to the ``HEAD`` and will store it locally. Patches will be applied on any package actions (e.g. it can be used for dependency management).
|
The last command will calculate diff from current tree to the ``HEAD`` and will store it locally. Patches will be applied on any package actions (e.g. it can be used for dependency management).
|
||||||
|
|
||||||
|
It is also possible to create simple patch during package addition, e.g.:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
sudo -u ahriman ahriman package-add ahriman --variable PKGEXT=.pkg.tar.xz
|
||||||
|
|
||||||
|
The ``--variable`` argument accepts variables in shell like format: quotation and lists are supported as usual, but functions are not. This feature is useful in particular in order to override specific makepkg variables during build.
|
||||||
|
|
||||||
How to build package from official repository
|
How to build package from official repository
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<button id="package-update-button" class="btn dropdown-item" onclick="updatePackages()" hidden>
|
<button id="package-update-button" class="btn dropdown-item" onclick="packagesUpdate()" hidden>
|
||||||
<i class="bi bi-play"></i> update
|
<i class="bi bi-play"></i> update
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
@ -61,7 +61,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<button id="package-remove-button" class="btn dropdown-item" onclick="removePackages()" disabled hidden>
|
<button id="package-remove-button" class="btn dropdown-item" onclick="packagesRemove()" disabled hidden>
|
||||||
<i class="bi bi-trash"></i> remove
|
<i class="bi bi-trash"></i> remove
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
@ -77,7 +77,8 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table id="packages" class="table table-striped table-hover"
|
<table id="packages"
|
||||||
|
data-classes="table table-hover"
|
||||||
data-export-options='{"fileName": "packages"}'
|
data-export-options='{"fileName": "packages"}'
|
||||||
data-filter-control="true"
|
data-filter-control="true"
|
||||||
data-filter-control-visible="false"
|
data-filter-control-visible="false"
|
||||||
@ -102,13 +103,13 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th data-checkbox="true"></th>
|
<th data-checkbox="true"></th>
|
||||||
<th data-sortable="true" data-switchable="false" data-field="base" data-filter-control="input" data-filter-control-placeholder="(any base)">package base</th>
|
<th data-sortable="true" data-switchable="false" data-field="base" data-filter-control="input" data-filter-control-placeholder="(any base)">package base</th>
|
||||||
<th data-sortable="true" data-field="version" data-filter-control="input" data-filter-control-placeholder="(any version)">version</th>
|
<th data-sortable="true" data-align="right" data-field="version" data-filter-control="input" data-filter-control-placeholder="(any version)">version</th>
|
||||||
<th data-sortable="true" data-field="packages" data-filter-control="input" data-filter-control-placeholder="(any package)">packages</th>
|
<th data-sortable="true" data-field="packages" data-filter-control="input" data-filter-control-placeholder="(any package)">packages</th>
|
||||||
<th data-sortable="true" data-visible="false" data-field="groups" data-filter-control="select" data-filter-data="func:filterListGroups" data-filter-custom-search="filterList" data-filter-control-placeholder="(any group)">groups</th>
|
<th data-sortable="true" data-visible="false" data-field="groups" data-filter-control="select" data-filter-data="func:filterListGroups" data-filter-custom-search="filterList" data-filter-control-placeholder="(any group)">groups</th>
|
||||||
<th data-sortable="true" data-visible="false" data-field="licenses" data-filter-control="select" data-filter-data="func:filterListLicenses" data-filter-custom-search="filterList" data-filter-control-placeholder="(any license)">licenses</th>
|
<th data-sortable="true" data-visible="false" data-field="licenses" data-filter-control="select" data-filter-data="func:filterListLicenses" data-filter-custom-search="filterList" data-filter-control-placeholder="(any license)">licenses</th>
|
||||||
<th data-sortable="true" data-visible="false" data-field="packager" data-filter-control="select" data-filter-custom-search="filterContains" data-filter-control-placeholder="(any packager)">packager</th>
|
<th data-sortable="true" data-visible="false" data-field="packager" data-filter-control="select" data-filter-custom-search="filterContains" data-filter-control-placeholder="(any packager)">packager</th>
|
||||||
<th data-sortable="true" data-field="timestamp" data-filter-control="input" data-filter-custom-search="filterDateRange" data-filter-control-placeholder="(any date)">last update</th>
|
<th data-sortable="true" data-align="right" data-field="timestamp" data-filter-control="input" data-filter-custom-search="filterDateRange" data-filter-control-placeholder="(any date)">last update</th>
|
||||||
<th data-sortable="true" data-cell-style="statusFormat" data-field="status" data-filter-control="select" data-filter-control-placeholder="(any status)">status</th>
|
<th data-sortable="true" data-align="center" data-cell-style="statusFormat" data-field="status" data-filter-control="select" data-filter-control-placeholder="(any status)">status</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
</table>
|
</table>
|
||||||
|
@ -8,20 +8,20 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label for="key-import-fingerprint-input" class="col-sm-2 col-form-label">fingerprint</label>
|
<label for="key-import-fingerprint-input" class="col-2 col-form-label">fingerprint</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-10">
|
||||||
<input id="key-import-fingerprint-input" type="text" class="form-control" placeholder="PGP key fingerprint" name="key" required>
|
<input id="key-import-fingerprint-input" type="text" class="form-control" placeholder="PGP key fingerprint" name="key" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label for="key-import-server-input" class="col-sm-2 col-form-label">key server</label>
|
<label for="key-import-server-input" class="col-2 col-form-label">key server</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-10">
|
||||||
<input id="key-import-server-input" type="text" class="form-control" placeholder="PGP key server" name="server" value="keyserver.ubuntu.com" required>
|
<input id="key-import-server-input" type="text" class="form-control" placeholder="PGP key server" name="server" value="keyserver.ubuntu.com" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col-sm-2"></div>
|
<div class="col-2"></div>
|
||||||
<div class="col-sm-10">
|
<div class="col-10">
|
||||||
<pre class="language-less"><samp id="key-import-body-input" class="pre-scrollable language-less"></samp><button id="key-import-copy-button" type="button" class="btn language-less" onclick="copyPgpKey()"><i class="bi bi-clipboard"></i> copy</button></pre>
|
<pre class="language-less"><samp id="key-import-body-input" class="pre-scrollable language-less"></samp><button id="key-import-copy-button" type="button" class="btn language-less" onclick="copyPgpKey()"><i class="bi bi-clipboard"></i> copy</button></pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,14 +8,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label for="login-username" class="col-sm-4 col-form-label">username</label>
|
<label for="login-username" class="col-4 col-form-label">username</label>
|
||||||
<div class="col-sm-8">
|
<div class="col-8">
|
||||||
<input id="login-username" type="text" class="form-control" placeholder="enter username" name="username" required>
|
<input id="login-username" type="text" class="form-control" placeholder="enter username" name="username" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label for="login-password" class="col-sm-4 col-form-label">password</label>
|
<label for="login-password" class="col-4 col-form-label">password</label>
|
||||||
<div class="col-sm-8">
|
<div class="col-8">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input id="login-password" type="password" class="form-control" placeholder="enter password" name="password" required>
|
<input id="login-password" type="password" class="form-control" placeholder="enter password" name="password" required>
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<div id="package-add-modal" 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 modal-lg" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<form id="package-add-form" onsubmit="return false">
|
<form id="package-add-form" onsubmit="return false">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@ -8,9 +8,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label for="package-add-repository-input" class="col-sm-4 col-form-label">repository</label>
|
<label for="package-add-repository-input" class="col-3 col-form-label">repository</label>
|
||||||
<div class="col-sm-8">
|
<div class="col-9">
|
||||||
<select id="package-add-repository-input" class="form-control" name="repository" required>
|
<select id="package-add-repository-input" class="form-control" required>
|
||||||
{% for repository in repositories %}
|
{% for repository in repositories %}
|
||||||
<option value="{{ repository.id }}" data-repository="{{ repository.repository }}" data-architecture="{{ repository.architecture }}">{{ repository.repository }} ({{ repository.architecture }})</option>
|
<option value="{{ repository.id }}" data-repository="{{ repository.repository }}" data-architecture="{{ repository.architecture }}">{{ repository.repository }} ({{ repository.architecture }})</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -18,12 +18,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label for="package-add-input" class="col-sm-4 col-form-label">package</label>
|
<label for="package-add-input" class="col-3 col-form-label">package</label>
|
||||||
<div class="col-sm-8">
|
<div class="col-9">
|
||||||
<input id="package-add-input" type="text" list="known-packages-dlist" autocomplete="off" class="form-control" placeholder="AUR package" name="package" required>
|
<input id="package-add-input" type="text" list="package-add-known-packages-dlist" autocomplete="off" class="form-control" placeholder="AUR package" required>
|
||||||
<datalist id="package-add-known-packages-dlist"></datalist>
|
<datalist id="package-add-known-packages-dlist"></datalist>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col-12">
|
||||||
|
<button id="package-add-variable-button" type="button" class="form-control btn btn-light rounded" onclick="packageAddVariableInputCreate()"><i class="bi bi-plus"></i> add environment variable </button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="package-add-variables-div" class="form-group row"></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="submit" class="btn btn-primary" onclick="packagesAdd()"><i class="bi bi-play"></i> add</button>
|
||||||
@ -39,9 +45,11 @@
|
|||||||
const packageAddForm = $("#package-add-form");
|
const packageAddForm = $("#package-add-form");
|
||||||
packageAddModal.on("shown.bs.modal", () => {
|
packageAddModal.on("shown.bs.modal", () => {
|
||||||
$(`#package-add-repository-input option[value="${repository.architecture}-${repository.repository}"]`).prop("selected", true);
|
$(`#package-add-repository-input option[value="${repository.architecture}-${repository.repository}"]`).prop("selected", true);
|
||||||
|
|
||||||
});
|
});
|
||||||
packageAddModal.on("hidden.bs.modal", () => { packageAddForm.trigger("reset"); });
|
packageAddModal.on("hidden.bs.modal", () => {
|
||||||
|
packageAddVariablesDiv.empty();
|
||||||
|
packageAddForm.trigger("reset");
|
||||||
|
});
|
||||||
|
|
||||||
const packageAddInput = $("#package-add-input");
|
const packageAddInput = $("#package-add-input");
|
||||||
const packageAddRepositoryInput = $("#package-add-repository-input");
|
const packageAddRepositoryInput = $("#package-add-repository-input");
|
||||||
@ -71,25 +79,81 @@
|
|||||||
}, this), 500));
|
}, this), 500));
|
||||||
});
|
});
|
||||||
|
|
||||||
function packagesAdd() {
|
const packageAddVariablesDiv = $("#package-add-variables-div");
|
||||||
const packages = packageAddInput.val();
|
|
||||||
|
function packageAddVariableInputCreate() {
|
||||||
|
const variableInput = document.createElement("div");
|
||||||
|
variableInput.classList.add("input-group");
|
||||||
|
variableInput.classList.add("package-add-variable");
|
||||||
|
|
||||||
|
const variableNameInput = document.createElement("input");
|
||||||
|
variableNameInput.type = "text";
|
||||||
|
variableNameInput.classList.add("form-control");
|
||||||
|
variableNameInput.classList.add("package-add-variable-name");
|
||||||
|
variableNameInput.placeholder = "name";
|
||||||
|
variableNameInput.ariaLabel = "variable name";
|
||||||
|
|
||||||
|
const variableSeparator = document.createElement("span");
|
||||||
|
variableSeparator.classList.add("input-group-text")
|
||||||
|
variableSeparator.textContent = "=";
|
||||||
|
|
||||||
|
const variableValueInput = document.createElement("input");
|
||||||
|
variableValueInput.type = "text";
|
||||||
|
variableValueInput.classList.add("form-control");
|
||||||
|
variableValueInput.classList.add("package-add-variable-value");
|
||||||
|
variableValueInput.placeholder = "value";
|
||||||
|
variableValueInput.ariaLabel = "variable value";
|
||||||
|
|
||||||
|
const variableButtonRemove = document.createElement("button");
|
||||||
|
variableButtonRemove.type = "button";
|
||||||
|
variableButtonRemove.classList.add("btn");
|
||||||
|
variableButtonRemove.classList.add("btn-outline-danger");
|
||||||
|
variableButtonRemove.innerHTML = "<i class=\"bi bi-trash\"></i>";
|
||||||
|
variableButtonRemove.onclick = _ => { return variableInput.remove(); };
|
||||||
|
|
||||||
|
// bring them together
|
||||||
|
variableInput.appendChild(variableNameInput);
|
||||||
|
variableInput.appendChild(variableSeparator);
|
||||||
|
variableInput.appendChild(variableValueInput);
|
||||||
|
variableInput.appendChild(variableButtonRemove);
|
||||||
|
|
||||||
|
packageAddVariablesDiv.append(variableInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchesParse() {
|
||||||
|
const patches = packageAddVariablesDiv.find(".package-add-variable").map((_, element) => {
|
||||||
|
const richElement = $(element);
|
||||||
|
return {
|
||||||
|
key: richElement.find(".package-add-variable-name").val(),
|
||||||
|
value: richElement.find(".package-add-variable-value").val(),
|
||||||
|
};
|
||||||
|
}).filter((_, patch) => patch.key).get();
|
||||||
|
return {patches: patches};
|
||||||
|
}
|
||||||
|
|
||||||
|
function packagesAdd(packages, patches) {
|
||||||
|
packages = packages ?? packageAddInput.val();
|
||||||
|
patches = patches ?? patchesParse();
|
||||||
const repository = getRepositorySelector(packageAddRepositoryInput);
|
const repository = getRepositorySelector(packageAddRepositoryInput);
|
||||||
|
|
||||||
if (packages) {
|
if (packages) {
|
||||||
packageAddModal.modal("hide");
|
packageAddModal.modal("hide");
|
||||||
const onSuccess = update => `Packages ${update} have been added`;
|
const onSuccess = update => `Packages ${update} have been added`;
|
||||||
const onFailure = error => `Package addition failed: ${error}`;
|
const onFailure = error => `Package addition failed: ${error}`;
|
||||||
doPackageAction("/api/v1/service/add", [packages], repository, onSuccess, onFailure);
|
doPackageAction("/api/v1/service/add", [packages], repository, onSuccess, onFailure, patches);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function packagesRequest() {
|
function packagesRequest(packages, patches) {
|
||||||
const packages = packageAddInput.val();
|
packages = packages ?? packageAddInput.val();
|
||||||
|
patches = patches ?? patchesParse();
|
||||||
const repository = getRepositorySelector(packageAddRepositoryInput);
|
const repository = getRepositorySelector(packageAddRepositoryInput);
|
||||||
|
|
||||||
if (packages) {
|
if (packages) {
|
||||||
packageAddModal.modal("hide");
|
packageAddModal.modal("hide");
|
||||||
const onSuccess = update => `Packages ${update} have been requested`;
|
const onSuccess = update => `Packages ${update} have been requested`;
|
||||||
const onFailure = error => `Package request failed: ${error}`;
|
const onFailure = error => `Package request failed: ${error}`;
|
||||||
doPackageAction("/api/v1/service/request", [packages], repository, onSuccess, onFailure);
|
doPackageAction("/api/v1/service/request", [packages], repository, onSuccess, onFailure, patches);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -6,10 +6,47 @@
|
|||||||
<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">
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="input-group">
|
||||||
|
<table class="table table-borderless">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="width: 10%; text-align: right">version</td>
|
||||||
|
<td id="package-info-version" style="width: 40%"></td>
|
||||||
|
<td style="width: 10%; text-align: right">packager</td>
|
||||||
|
<td id="package-info-packager" style="width: 40%"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="width: 10%; text-align: right">groups</td>
|
||||||
|
<td id="package-info-groups" style="width: 40%"></td>
|
||||||
|
<td style="width: 10%; text-align: right">licenses</td>
|
||||||
|
<td id="package-info-licenses" style="width: 40%"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="width: 10%; text-align: right">packages</td>
|
||||||
|
<td id="package-info-packages" style="width: 40%"></td>
|
||||||
|
<td style="width: 10%; text-align: right">depends</td>
|
||||||
|
<td id="package-info-depends" style="width: 40%"></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="col-12">
|
||||||
|
|
||||||
|
<h3>Environment variables</h3>
|
||||||
|
<div id="package-info-variables-div" class="form-group row"></div>
|
||||||
|
|
||||||
|
<hr class="col-12">
|
||||||
|
|
||||||
|
<h3>Build logs</h3>
|
||||||
<pre class="language-logs"><samp id="package-info-logs-input" class="pre-scrollable language-logs"></samp><button id="package-info-logs-copy-button" type="button" class="btn language-logs" onclick="copyLogs()"><i class="bi bi-clipboard"></i> copy</button></pre>
|
<pre class="language-logs"><samp id="package-info-logs-input" class="pre-scrollable language-logs"></samp><button id="package-info-logs-copy-button" 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="submit" class="btn btn-success" onclick="packageInfoUpdate()" data-bs-dismiss="modal"><i class="bi bi-play"></i> update</button>
|
||||||
|
<button type="submit" class="btn btn-danger" onclick="packageInfoRemove()" data-bs-dismiss="modal"><i class="bi bi-trash"></i> remove</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="showPackageInfo()"><i class="bi bi-arrow-clockwise"></i> reload</button>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -20,30 +57,78 @@
|
|||||||
const packageInfoModal = $("#package-info-modal");
|
const packageInfoModal = $("#package-info-modal");
|
||||||
const packageInfoModalHeader = $("#package-info-modal-header");
|
const packageInfoModalHeader = $("#package-info-modal-header");
|
||||||
const packageInfo = $("#package-info");
|
const packageInfo = $("#package-info");
|
||||||
|
packageInfoModal.on("hidden.bs.modal", () => {
|
||||||
|
packageInfoDepends.empty();
|
||||||
|
packageInfoGroups.empty();
|
||||||
|
packageInfoLicenses.empty();
|
||||||
|
packageInfoPackager.empty();
|
||||||
|
packageInfoPackages.empty();
|
||||||
|
packageInfoVersion.empty();
|
||||||
|
|
||||||
|
packageInfoVariablesDiv.empty();
|
||||||
|
|
||||||
|
packageInfoModal.trigger("reset");
|
||||||
|
});
|
||||||
|
|
||||||
const packageInfoLogsInput = $("#package-info-logs-input");
|
const packageInfoLogsInput = $("#package-info-logs-input");
|
||||||
const packageInfoLogsCopyButton = $("#package-info-logs-copy-button");
|
const packageInfoLogsCopyButton = $("#package-info-logs-copy-button");
|
||||||
|
|
||||||
|
const packageInfoDepends = $("#package-info-depends");
|
||||||
|
const packageInfoGroups = $("#package-info-groups");
|
||||||
|
const packageInfoLicenses = $("#package-info-licenses");
|
||||||
|
const packageInfoPackager = $("#package-info-packager");
|
||||||
|
const packageInfoPackages = $("#package-info-packages");
|
||||||
|
const packageInfoVersion = $("#package-info-version");
|
||||||
|
|
||||||
|
const packageInfoVariablesDiv = $("#package-info-variables-div");
|
||||||
|
|
||||||
async function copyLogs() {
|
async function copyLogs() {
|
||||||
const logs = packageInfoLogsInput.text();
|
const logs = packageInfoLogsInput.text();
|
||||||
await copyToClipboard(logs, packageInfoLogsCopyButton);
|
await copyToClipboard(logs, packageInfoLogsCopyButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showLogs(packageBase) {
|
function insertVariable(packageBase, variable) {
|
||||||
const isPackageBaseSet = packageBase !== undefined;
|
const variableInput = document.createElement("div");
|
||||||
if (isPackageBaseSet)
|
variableInput.classList.add("input-group");
|
||||||
packageInfoModal.data("package", packageBase); // set package base as currently used
|
|
||||||
else
|
|
||||||
packageBase = packageInfoModal.data("package"); // read package base from the current window attribute
|
|
||||||
|
|
||||||
const headerClass = status => {
|
const variableNameInput = document.createElement("input");
|
||||||
if (status === "pending") return ["bg-warning"];
|
variableNameInput.classList.add("form-control");
|
||||||
if (status === "building") return ["bg-warning"];
|
variableNameInput.readOnly = true;
|
||||||
if (status === "failed") return ["bg-danger", "text-white"];
|
variableNameInput.value = variable.key;
|
||||||
if (status === "success") return ["bg-success", "text-white"];
|
|
||||||
return ["bg-secondary", "text-white"];
|
const variableSeparator = document.createElement("span");
|
||||||
|
variableSeparator.classList.add("input-group-text")
|
||||||
|
variableSeparator.textContent = "=";
|
||||||
|
|
||||||
|
const variableValueInput = document.createElement("input");
|
||||||
|
variableValueInput.classList.add("form-control");
|
||||||
|
variableValueInput.readOnly = true;
|
||||||
|
variableValueInput.value = variable.value;
|
||||||
|
|
||||||
|
const variableButtonRemove = document.createElement("button");
|
||||||
|
variableButtonRemove.type = "button";
|
||||||
|
variableButtonRemove.classList.add("btn");
|
||||||
|
variableButtonRemove.classList.add("btn-outline-danger");
|
||||||
|
variableButtonRemove.innerHTML = "<i class=\"bi bi-trash\"></i>";
|
||||||
|
variableButtonRemove.onclick = _ => {
|
||||||
|
$.ajax({
|
||||||
|
url: `/api/v1/packages/${packageBase}/patches/${variable.key}`,
|
||||||
|
type: "DELETE",
|
||||||
|
dataType: "json",
|
||||||
|
success: _ => variableInput.remove(),
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// bring them together
|
||||||
|
variableInput.appendChild(variableNameInput);
|
||||||
|
variableInput.appendChild(variableSeparator);
|
||||||
|
variableInput.appendChild(variableValueInput);
|
||||||
|
variableInput.appendChild(variableButtonRemove);
|
||||||
|
|
||||||
|
packageInfoVariablesDiv.append(variableInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadLogs(packageBase, onFailure) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: `/api/v2/packages/${packageBase}/logs`,
|
url: `/api/v2/packages/${packageBase}/logs`,
|
||||||
data: {
|
data: {
|
||||||
@ -53,26 +138,101 @@
|
|||||||
type: "GET",
|
type: "GET",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
success: response => {
|
success: response => {
|
||||||
packageInfo.text(`${response.package_base} ${response.status.status} at ${new Date(1000 * response.status.timestamp).toISOString()}`);
|
const logs = response.map(log_record => {
|
||||||
const logs = response.logs.map(log_record => {
|
return `[${new Date(1000 * log_record.created).toISOString()}] ${log_record.message}`;
|
||||||
const [timestamp, record] = log_record;
|
|
||||||
return `[${new Date(1000 * timestamp).toISOString()}] ${record}`;
|
|
||||||
});
|
});
|
||||||
packageInfoLogsInput.text(logs.join("\n"));
|
packageInfoLogsInput.text(logs.join("\n"));
|
||||||
|
},
|
||||||
|
error: onFailure,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadPackage(packageBase, onFailure) {
|
||||||
|
const headerClass = status => {
|
||||||
|
if (status === "pending") return ["bg-warning"];
|
||||||
|
if (status === "building") return ["bg-warning"];
|
||||||
|
if (status === "failed") return ["bg-danger", "text-white"];
|
||||||
|
if (status === "success") return ["bg-success", "text-white"];
|
||||||
|
return ["bg-secondary", "text-white"];
|
||||||
|
};
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: `/api/v1/packages/${packageBase}`,
|
||||||
|
data: {
|
||||||
|
architecture: repository.architecture,
|
||||||
|
repository: repository.repository,
|
||||||
|
},
|
||||||
|
type: "GET",
|
||||||
|
dataType: "json",
|
||||||
|
success: response => {
|
||||||
|
const description = response.find(Boolean);
|
||||||
|
const packages = Object.keys(description.package.packages);
|
||||||
|
|
||||||
|
packageInfo.text(`${description.package.base} ${description.status.status} at ${new Date(1000 * description.status.timestamp).toISOStringShort()}`);
|
||||||
|
|
||||||
packageInfoModalHeader.removeClass();
|
packageInfoModalHeader.removeClass();
|
||||||
packageInfoModalHeader.addClass("modal-header");
|
packageInfoModalHeader.addClass("modal-header");
|
||||||
headerClass(response.status.status).forEach((clz) => packageInfoModalHeader.addClass(clz));
|
headerClass(description.status.status).forEach(clz => packageInfoModalHeader.addClass(clz));
|
||||||
|
|
||||||
if (isPackageBaseSet) packageInfoModal.modal("show"); // we don't need to show window again
|
packageInfoDepends.html(listToTable(
|
||||||
},
|
Object.values(description.package.packages)
|
||||||
error: (jqXHR, _, errorThrown) => {
|
.reduce((accumulator, currentValue) => {
|
||||||
// show failed modal in case if first time loading
|
return accumulator.concat(currentValue.depends.filter(v => packages.indexOf(v) === -1))
|
||||||
if (isPackageBaseSet) {
|
.concat(currentValue.make_depends.filter(v => packages.indexOf(v) === -1).map(v => `${v} (make)`))
|
||||||
const message = error => `Could not load package ${packageBase} logs: ${error}`;
|
.concat(currentValue.opt_depends.filter(v => packages.indexOf(v) === -1).map(v => `${v} (optional)`));
|
||||||
showFailure("Load failure", message, jqXHR, errorThrown);
|
}, [])
|
||||||
}
|
));
|
||||||
|
packageInfoGroups.html(listToTable(extractListProperties(description.package, "groups")));
|
||||||
|
packageInfoLicenses.html(listToTable(extractListProperties(description.package, "licenses")));
|
||||||
|
packageInfoPackager.text(description.package.packager);
|
||||||
|
packageInfoPackages.html(listToTable(packages));
|
||||||
|
packageInfoVersion.text(description.package.version);
|
||||||
},
|
},
|
||||||
|
error: onFailure,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadPatches(packageBase, onFailure) {
|
||||||
|
$.ajax({
|
||||||
|
url: `/api/v1/packages/${packageBase}/patches`,
|
||||||
|
type: "GET",
|
||||||
|
dataType: "json",
|
||||||
|
success: response => {
|
||||||
|
packageInfoVariablesDiv.empty();
|
||||||
|
response.map(patch => insertVariable(packageBase, patch));
|
||||||
|
},
|
||||||
|
error: onFailure,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function packageInfoRemove() {
|
||||||
|
const packageBase = packageInfoModal.data("package");
|
||||||
|
if (packageBase) return packagesRemove([packageBase]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function packageInfoUpdate() {
|
||||||
|
const packageBase = packageInfoModal.data("package");
|
||||||
|
if (packageBase) return packagesAdd(packageBase, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPackageInfo(packageBase) {
|
||||||
|
const isPackageBaseSet = packageBase !== undefined;
|
||||||
|
if (isPackageBaseSet)
|
||||||
|
packageInfoModal.data("package", packageBase); // set package base as currently used
|
||||||
|
else
|
||||||
|
packageBase = packageInfoModal.data("package"); // read package base from the current window attribute
|
||||||
|
|
||||||
|
const onFailure = (jqXHR, _, errorThrown) => {
|
||||||
|
if (isPackageBaseSet) {
|
||||||
|
const message = error => `Could not load package ${packageBase} info: ${error}`;
|
||||||
|
showFailure("Load failure", message, jqXHR, errorThrown);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadPackage(packageBase, onFailure);
|
||||||
|
loadPatches(packageBase, onFailure);
|
||||||
|
loadLogs(packageBase, onFailure);
|
||||||
|
|
||||||
|
if (isPackageBaseSet) packageInfoModal.modal("show");
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<div id="package-rebuild-modal" tabindex="-1" role="dialog" class="modal fade">
|
<div id="package-rebuild-modal" tabindex="-1" role="dialog" class="modal fade">
|
||||||
<div class="modal-dialog" role="document">
|
<div class="modal-dialog modal-lg" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<form id="package-rebuild-form" onsubmit="return false">
|
<form id="package-rebuild-form" onsubmit="return false">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@ -8,8 +8,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label for="package-rebuild-repository-input" class="col-sm-4 col-form-label">repository</label>
|
<label for="package-rebuild-repository-input" class="col-3 col-form-label">repository</label>
|
||||||
<div class="col-sm-8">
|
<div class="col-9">
|
||||||
<select id="package-rebuild-repository-input" class="form-control" name="repository" required>
|
<select id="package-rebuild-repository-input" class="form-control" name="repository" required>
|
||||||
{% for repository in repositories %}
|
{% for repository in repositories %}
|
||||||
<option value="{{ repository.id }}" data-repository="{{ repository.repository }}" data-architecture="{{ repository.architecture }}">{{ repository.repository }} ({{ repository.architecture }})</option>
|
<option value="{{ repository.id }}" data-repository="{{ repository.repository }}" data-architecture="{{ repository.architecture }}">{{ repository.repository }} ({{ repository.architecture }})</option>
|
||||||
@ -18,8 +18,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label for="package-rebuild-dependency-input" class="col-sm-4 col-form-label">dependency</label>
|
<label for="package-rebuild-dependency-input" class="col-3 col-form-label">dependency</label>
|
||||||
<div class="col-sm-8">
|
<div class="col-9">
|
||||||
<input id="package-rebuild-dependency-input" type="text" class="form-control" placeholder="packages dependency" name="package" required>
|
<input id="package-rebuild-dependency-input" type="text" class="form-control" placeholder="packages dependency" name="package" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
if (0 === cell || "base" === cell) {
|
if (0 === cell || "base" === cell) {
|
||||||
const method = data[0] === true ? "uncheckBy" : "checkBy"; // fck javascript
|
const method = data[0] === true ? "uncheckBy" : "checkBy"; // fck javascript
|
||||||
table.bootstrapTable(method, {field: "id", values: [data.id]});
|
table.bootstrapTable(method, {field: "id", values: [data.id]});
|
||||||
} else showLogs(data.id);
|
} else showPackageInfo(data.id);
|
||||||
});
|
});
|
||||||
table.on("created-controls.bs.table", () => {
|
table.on("created-controls.bs.table", () => {
|
||||||
const pickerInput = $(".bootstrap-table-filter-control-timestamp");
|
const pickerInput = $(".bootstrap-table-filter-control-timestamp");
|
||||||
@ -50,7 +50,7 @@
|
|||||||
const statusBadge = $("#badge-status");
|
const statusBadge = $("#badge-status");
|
||||||
const versionBadge = $("#badge-version");
|
const versionBadge = $("#badge-version");
|
||||||
|
|
||||||
function doPackageAction(uri, packages, repository, successText, failureText) {
|
function doPackageAction(uri, packages, repository, successText, failureText, data) {
|
||||||
const queryParams = $.param({
|
const queryParams = $.param({
|
||||||
architecture: repository.architecture,
|
architecture: repository.architecture,
|
||||||
repository: repository.repository,
|
repository: repository.repository,
|
||||||
@ -58,7 +58,7 @@
|
|||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: `${uri}?${queryParams}`,
|
url: `${uri}?${queryParams}`,
|
||||||
data: JSON.stringify({packages: packages}),
|
data: JSON.stringify(Object.assign({}, {packages: packages}, data || {})),
|
||||||
type: "POST",
|
type: "POST",
|
||||||
contentType: "application/json",
|
contentType: "application/json",
|
||||||
success: _ => {
|
success: _ => {
|
||||||
@ -71,31 +71,30 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function filterListGroups() {
|
||||||
|
return extractDataList(table.bootstrapTable("getData"), "groups");
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterListLicenses() {
|
||||||
|
return extractDataList(table.bootstrapTable("getData"), "licenses");
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterListPackagers() {
|
||||||
|
return extractDataList(table.bootstrapTable("getData"), "packager");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRepositorySelector(selector) {
|
||||||
|
const selected = selector.find(":selected");
|
||||||
|
return {
|
||||||
|
architecture: selected.data("architecture"),
|
||||||
|
repository: selected.data("repository"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function getSelection() {
|
function getSelection() {
|
||||||
return table.bootstrapTable("getSelections").map(row => row.id);
|
return table.bootstrapTable("getSelections").map(row => row.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function removePackages() {
|
|
||||||
const onSuccess = update => `Packages ${update} have been removed`;
|
|
||||||
const onFailure = error => `Could not remove packages: ${error}`;
|
|
||||||
doPackageAction("/api/v1/service/remove", getSelection(), repository, onSuccess, onFailure);
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectRepository() {
|
|
||||||
const fragment = window.location.hash.replace("#", "") || "{{ repositories[0].id }}";
|
|
||||||
const element = $(`#${fragment}-lnk`);
|
|
||||||
element.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updatePackages() {
|
|
||||||
const currentSelection = getSelection();
|
|
||||||
const [url, onSuccess] = currentSelection.length === 0
|
|
||||||
? ["/api/v1/service/update", _ => `Repository update has been run`]
|
|
||||||
: ["/api/v1/service/add", update => `Run update for packages ${update}`];
|
|
||||||
const onFailure = error => `Packages update failed: ${error}`;
|
|
||||||
doPackageAction(url, currentSelection, repository, onSuccess, onFailure);
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideControls(hidden) {
|
function hideControls(hidden) {
|
||||||
keyImportButton.attr("hidden", hidden);
|
keyImportButton.attr("hidden", hidden);
|
||||||
packageAddButton.attr("hidden", hidden);
|
packageAddButton.attr("hidden", hidden);
|
||||||
@ -104,6 +103,24 @@
|
|||||||
packageUpdateButton.attr("hidden", hidden);
|
packageUpdateButton.attr("hidden", hidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function packagesRemove(packages) {
|
||||||
|
packages = packages ?? getSelection();
|
||||||
|
const onSuccess = update => `Packages ${update} have been removed`;
|
||||||
|
const onFailure = error => `Could not remove packages: ${error}`;
|
||||||
|
|
||||||
|
doPackageAction("/api/v1/service/remove", packages, repository, onSuccess, onFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
function packagesUpdate() {
|
||||||
|
const currentSelection = getSelection();
|
||||||
|
const [url, onSuccess] = currentSelection.length === 0
|
||||||
|
? ["/api/v1/service/update", _ => `Repository update has been run`]
|
||||||
|
: ["/api/v1/service/add", update => `Run update for packages ${update}`];
|
||||||
|
const onFailure = error => `Packages update failed: ${error}`;
|
||||||
|
|
||||||
|
doPackageAction(url, currentSelection, repository, onSuccess, onFailure);
|
||||||
|
}
|
||||||
|
|
||||||
function reload() {
|
function reload() {
|
||||||
table.bootstrapTable("showLoading");
|
table.bootstrapTable("showLoading");
|
||||||
|
|
||||||
@ -124,18 +141,6 @@
|
|||||||
type: "GET",
|
type: "GET",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
success: response => {
|
success: response => {
|
||||||
const extractListProperties = (description, property) => {
|
|
||||||
return Object.values(description.packages)
|
|
||||||
.map(pkg => pkg[property])
|
|
||||||
.reduce((left, right) => left.concat(right), []);
|
|
||||||
};
|
|
||||||
const listToTable = data => {
|
|
||||||
return Array.from(new Set(data))
|
|
||||||
.sort()
|
|
||||||
.map(entry => safe(entry))
|
|
||||||
.join("<br>");
|
|
||||||
};
|
|
||||||
|
|
||||||
const payload = response.map(description => {
|
const payload = response.map(description => {
|
||||||
const package_base = description.package.base;
|
const package_base = description.package.base;
|
||||||
const web_url = description.package.remote.web_url;
|
const web_url = description.package.remote.web_url;
|
||||||
@ -147,7 +152,7 @@
|
|||||||
packages: listToTable(Object.keys(description.package.packages)),
|
packages: listToTable(Object.keys(description.package.packages)),
|
||||||
groups: listToTable(extractListProperties(description.package, "groups")),
|
groups: listToTable(extractListProperties(description.package, "groups")),
|
||||||
licenses: listToTable(extractListProperties(description.package, "licenses")),
|
licenses: listToTable(extractListProperties(description.package, "licenses")),
|
||||||
timestamp: new Date(1000 * description.status.timestamp).toISOString(),
|
timestamp: new Date(1000 * description.status.timestamp).toISOStringShort(),
|
||||||
status: description.status.status,
|
status: description.status.status,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -186,7 +191,7 @@
|
|||||||
|
|
||||||
statusBadge
|
statusBadge
|
||||||
.popover("dispose")
|
.popover("dispose")
|
||||||
.attr("data-bs-content", `${response.status.status} at ${new Date(1000 * response.status.timestamp).toISOString()}`)
|
.attr("data-bs-content", `${response.status.status} at ${new Date(1000 * response.status.timestamp).toISOStringShort()}`)
|
||||||
.popover();
|
.popover();
|
||||||
statusBadge.removeClass();
|
statusBadge.removeClass();
|
||||||
statusBadge.addClass("btn");
|
statusBadge.addClass("btn");
|
||||||
@ -195,6 +200,12 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectRepository() {
|
||||||
|
const fragment = window.location.hash.replace("#", "") || "{{ repositories[0].id }}";
|
||||||
|
const element = $(`#${fragment}-lnk`);
|
||||||
|
element.click();
|
||||||
|
}
|
||||||
|
|
||||||
function statusFormat(value) {
|
function statusFormat(value) {
|
||||||
const cellClass = status => {
|
const cellClass = status => {
|
||||||
if (status === "pending") return "table-warning";
|
if (status === "pending") return "table-warning";
|
||||||
@ -206,26 +217,6 @@
|
|||||||
return {classes: cellClass(value)};
|
return {classes: cellClass(value)};
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterListGroups() {
|
|
||||||
return extractDataList(table.bootstrapTable("getData"), "groups");
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterListLicenses() {
|
|
||||||
return extractDataList(table.bootstrapTable("getData"), "licenses");
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterListPackagers() {
|
|
||||||
return extractDataList(table.bootstrapTable("getData"), "packager");
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRepositorySelector(selector) {
|
|
||||||
const selected = selector.find(":selected");
|
|
||||||
return {
|
|
||||||
architecture: selected.data("architecture"),
|
|
||||||
repository: selected.data("repository"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
$(() => {
|
$(() => {
|
||||||
table.bootstrapTable({});
|
table.bootstrapTable({});
|
||||||
statusBadge.popover();
|
statusBadge.popover();
|
||||||
|
@ -30,7 +30,8 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<table id="packages" class="table table-striped table-hover"
|
<table id="packages"
|
||||||
|
data-classes="table table-hover"
|
||||||
data-export-options='{"fileName": "packages"}'
|
data-export-options='{"fileName": "packages"}'
|
||||||
data-filter-control="true"
|
data-filter-control="true"
|
||||||
data-filter-control-visible="false"
|
data-filter-control-visible="false"
|
||||||
@ -53,16 +54,16 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
|
|||||||
<thead class="table-primary">
|
<thead class="table-primary">
|
||||||
<tr>
|
<tr>
|
||||||
<th data-sortable="true" data-switchable="false" data-field="name" data-filter-control="input" data-filter-control-placeholder="(any package)">package</th>
|
<th data-sortable="true" data-switchable="false" data-field="name" data-filter-control="input" data-filter-control-placeholder="(any package)">package</th>
|
||||||
<th data-sortable="true" data-field="version" data-filter-control="input" data-filter-control-placeholder="(any version)">version</th>
|
<th data-sortable="true" data-align="right" data-field="version" data-filter-control="input" data-filter-control-placeholder="(any version)">version</th>
|
||||||
<th data-sortable="true" data-visible="false" data-field="architecture" data-filter-control="select" data-filter-control-placeholder="(any arch)">architecture</th>
|
<th data-sortable="true" data-visible="false" data-field="architecture" data-filter-control="select" data-filter-control-placeholder="(any arch)">architecture</th>
|
||||||
<th data-sortable="true" data-visible="false" data-field="description" data-filter-control="input" data-filter-control-placeholder="(any description)">description</th>
|
<th data-sortable="true" data-visible="false" data-field="description" data-filter-control="input" data-filter-control-placeholder="(any description)">description</th>
|
||||||
<th data-sortable="true" data-visible="false" data-field="url">upstream url</th>
|
<th data-sortable="true" data-visible="false" data-field="url">upstream url</th>
|
||||||
<th data-sortable="true" data-visible="false" data-field="licenses" data-filter-control="select" data-filter-data="func:filterListLicenses" data-filter-custom-search="filterList" data-filter-control-placeholder="(any license)">licenses</th>
|
<th data-sortable="true" data-visible="false" data-field="licenses" data-filter-control="select" data-filter-data="func:filterListLicenses" data-filter-custom-search="filterList" data-filter-control-placeholder="(any license)">licenses</th>
|
||||||
<th data-sortable="true" data-visible="false" data-field="groups" data-filter-control="select" data-filter-data="func:filterListGroups" data-filter-custom-search="filterList" data-filter-control-placeholder="(any group)">groups</th>
|
<th data-sortable="true" data-visible="false" data-field="groups" data-filter-control="select" data-filter-data="func:filterListGroups" data-filter-custom-search="filterList" data-filter-control-placeholder="(any group)">groups</th>
|
||||||
<th data-sortable="true" data-visible="false" data-field="depends" data-filter-control="select" data-filter-data="func:filterListDepends" data-filter-custom-search="filterList" data-filter-control-placeholder="(any depends)">depends</th>
|
<th data-sortable="true" data-visible="false" data-field="depends" data-filter-control="select" data-filter-data="func:filterListDepends" data-filter-custom-search="filterList" data-filter-control-placeholder="(any depends)">depends</th>
|
||||||
<th data-sortable="true" data-field="archive_size">archive size</th>
|
<th data-sortable="true" data-align="right" data-field="archive_size">archive size</th>
|
||||||
<th data-sortable="true" data-field="installed_size">installed size</th>
|
<th data-sortable="true" data-align="right" data-field="installed_size">installed size</th>
|
||||||
<th data-sortable="true" data-field="timestamp" data-filter-control="input" data-filter-custom-search="filterDateRange" data-filter-control-placeholder="(any date)">build date</th>
|
<th data-sortable="true" data-align="right" data-field="timestamp" data-filter-control="input" data-filter-custom-search="filterDateRange" data-filter-control-placeholder="(any date)">build date</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
|
@ -36,19 +36,17 @@
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function safe(string) {
|
|
||||||
return String(string)
|
|
||||||
.replace(/&/g, "&")
|
|
||||||
.replace(/</g, "<")
|
|
||||||
.replace(/>/g, ">")
|
|
||||||
.replace(/"/g, """);
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractDataList(data, column) {
|
function extractDataList(data, column) {
|
||||||
const elements = data.flatMap(row => row[column].split("<br>")).filter(v => v); // remove empty elements from array
|
const elements = data.flatMap(row => row[column].split("<br>")).filter(v => v); // remove empty elements from array
|
||||||
return Array.from(new Set(elements)).sort();
|
return Array.from(new Set(elements)).sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractListProperties(description, property) {
|
||||||
|
return Object.values(description.packages)
|
||||||
|
.map(pkg => pkg[property])
|
||||||
|
.reduce((left, right) => left.concat(right), []);
|
||||||
|
}
|
||||||
|
|
||||||
function filterContains(text, value) {
|
function filterContains(text, value) {
|
||||||
return value.includes(text.toLowerCase().trim());
|
return value.includes(text.toLowerCase().trim());
|
||||||
}
|
}
|
||||||
@ -67,4 +65,24 @@
|
|||||||
// the library removes all symbols from string, so it is just string
|
// the library removes all symbols from string, so it is just string
|
||||||
return value.includes(dataList[index].toLowerCase());
|
return value.includes(dataList[index].toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function listToTable(data) {
|
||||||
|
return Array.from(new Set(data))
|
||||||
|
.sort()
|
||||||
|
.map(entry => safe(entry))
|
||||||
|
.join("<br>");
|
||||||
|
}
|
||||||
|
|
||||||
|
function safe(string) {
|
||||||
|
return String(string)
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """);
|
||||||
|
}
|
||||||
|
|
||||||
|
Date.prototype.toISOStringShort = function() {
|
||||||
|
const pad = number => String(number).padStart(2, "0");
|
||||||
|
return `${this.getFullYear()}-${pad(this.getMonth())}-${pad(this.getDate())} ${pad(this.getHours())}:${pad(this.getMinutes())}:${pad(this.getSeconds())}`;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -276,6 +276,7 @@ def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
|||||||
parser.add_argument("-s", "--source", help="explicitly specify the package source for this command",
|
parser.add_argument("-s", "--source", help="explicitly specify the package source for this command",
|
||||||
type=PackageSource, choices=enum_values(PackageSource), default=PackageSource.Auto)
|
type=PackageSource, choices=enum_values(PackageSource), default=PackageSource.Auto)
|
||||||
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
|
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
|
||||||
|
parser.add_argument("-v", "--variable", help="apply specified makepkg variables to the next build", action="append")
|
||||||
parser.set_defaults(handler=handlers.Add)
|
parser.set_defaults(handler=handlers.Add)
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ from ahriman.application.application import Application
|
|||||||
from ahriman.application.handlers import Handler
|
from ahriman.application.handlers import Handler
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.models.packagers import Packagers
|
from ahriman.models.packagers import Packagers
|
||||||
|
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||||
from ahriman.models.repository_id import RepositoryId
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
@ -45,7 +46,12 @@ class Add(Handler):
|
|||||||
"""
|
"""
|
||||||
application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh)
|
application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh)
|
||||||
application.on_start()
|
application.on_start()
|
||||||
|
|
||||||
application.add(args.package, args.source, args.username)
|
application.add(args.package, args.source, args.username)
|
||||||
|
patches = [PkgbuildPatch.from_env(patch) for patch in args.variable] if args.variable is not None else []
|
||||||
|
for package in args.package: # for each requested package insert patch
|
||||||
|
application.database.patches_insert(package, patches)
|
||||||
|
|
||||||
if not args.now:
|
if not args.now:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -162,7 +162,7 @@ class Handler:
|
|||||||
if args.repository_id is not None:
|
if args.repository_id is not None:
|
||||||
separator = "/" if "/" in args.repository_id else "-" # systemd and non-systemd identifiers
|
separator = "/" if "/" in args.repository_id else "-" # systemd and non-systemd identifiers
|
||||||
# repository parts is optional for backward compatibility
|
# repository parts is optional for backward compatibility
|
||||||
architecture, *repository_parts = args.repository_id.split(separator)
|
architecture, *repository_parts = args.repository_id.split(separator) # maxsplit isn't used intentionally
|
||||||
args.architecture = architecture
|
args.architecture = architecture
|
||||||
if repository_parts:
|
if repository_parts:
|
||||||
args.repository = "-".join(repository_parts) # replace slash with dash
|
args.repository = "-".join(repository_parts) # replace slash with dash
|
||||||
|
@ -115,7 +115,7 @@ class Patch(Handler):
|
|||||||
package_base(str): package base
|
package_base(str): package base
|
||||||
patch(PkgbuildPatch): patch descriptor
|
patch(PkgbuildPatch): patch descriptor
|
||||||
"""
|
"""
|
||||||
application.database.patches_insert(package_base, patch)
|
application.database.patches_insert(package_base, [patch])
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def patch_set_list(application: Application, package_base: str | None, variables: list[str] | None,
|
def patch_set_list(application: Application, package_base: str | None, variables: list[str] | None,
|
||||||
|
@ -33,8 +33,8 @@ class PackageOperations(Operations):
|
|||||||
package operations
|
package operations
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _package_remove_package_base(self, connection: Connection, package_base: str,
|
@staticmethod
|
||||||
repository_id: RepositoryId) -> None:
|
def _package_remove_package_base(connection: Connection, package_base: str, repository_id: RepositoryId) -> None:
|
||||||
"""
|
"""
|
||||||
remove package base information
|
remove package base information
|
||||||
|
|
||||||
@ -50,8 +50,9 @@ class PackageOperations(Operations):
|
|||||||
"""delete from package_bases where package_base = :package_base and repository = :repository""",
|
"""delete from package_bases where package_base = :package_base and repository = :repository""",
|
||||||
{"package_base": package_base, "repository": repository_id.id})
|
{"package_base": package_base, "repository": repository_id.id})
|
||||||
|
|
||||||
def _package_remove_packages(self, connection: Connection, package_base: str,
|
@staticmethod
|
||||||
current_packages: Iterable[str], repository_id: RepositoryId) -> None:
|
def _package_remove_packages(connection: Connection, package_base: str, current_packages: Iterable[str],
|
||||||
|
repository_id: RepositoryId) -> None:
|
||||||
"""
|
"""
|
||||||
remove packages belong to the package base
|
remove packages belong to the package base
|
||||||
|
|
||||||
@ -74,8 +75,8 @@ class PackageOperations(Operations):
|
|||||||
"""delete from packages where package = :package and repository = :repository""",
|
"""delete from packages where package = :package and repository = :repository""",
|
||||||
packages)
|
packages)
|
||||||
|
|
||||||
def _package_update_insert_base(self, connection: Connection, package: Package,
|
@staticmethod
|
||||||
repository_id: RepositoryId) -> None:
|
def _package_update_insert_base(connection: Connection, package: Package, repository_id: RepositoryId) -> None:
|
||||||
"""
|
"""
|
||||||
insert base package into table
|
insert base package into table
|
||||||
|
|
||||||
@ -107,8 +108,8 @@ class PackageOperations(Operations):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
def _package_update_insert_packages(self, connection: Connection, package: Package,
|
@staticmethod
|
||||||
repository_id: RepositoryId) -> None:
|
def _package_update_insert_packages(connection: Connection, package: Package, repository_id: RepositoryId) -> None:
|
||||||
"""
|
"""
|
||||||
insert packages into table
|
insert packages into table
|
||||||
|
|
||||||
@ -149,7 +150,8 @@ class PackageOperations(Operations):
|
|||||||
""",
|
""",
|
||||||
package_list)
|
package_list)
|
||||||
|
|
||||||
def _package_update_insert_status(self, connection: Connection, package_base: str, status: BuildStatus,
|
@staticmethod
|
||||||
|
def _package_update_insert_status(connection: Connection, package_base: str, status: BuildStatus,
|
||||||
repository_id: RepositoryId) -> None:
|
repository_id: RepositoryId) -> None:
|
||||||
"""
|
"""
|
||||||
insert base package status into table
|
insert base package status into table
|
||||||
@ -176,8 +178,8 @@ class PackageOperations(Operations):
|
|||||||
"repository": repository_id.id,
|
"repository": repository_id.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
def _packages_get_select_package_bases(self, connection: Connection,
|
@staticmethod
|
||||||
repository_id: RepositoryId) -> dict[str, Package]:
|
def _packages_get_select_package_bases(connection: Connection, repository_id: RepositoryId) -> dict[str, Package]:
|
||||||
"""
|
"""
|
||||||
select package bases from the table
|
select package bases from the table
|
||||||
|
|
||||||
@ -201,7 +203,8 @@ class PackageOperations(Operations):
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
def _packages_get_select_packages(self, connection: Connection, packages: dict[str, Package],
|
@staticmethod
|
||||||
|
def _packages_get_select_packages(connection: Connection, packages: dict[str, Package],
|
||||||
repository_id: RepositoryId) -> dict[str, Package]:
|
repository_id: RepositoryId) -> dict[str, Package]:
|
||||||
"""
|
"""
|
||||||
select packages from the table
|
select packages from the table
|
||||||
@ -223,8 +226,8 @@ class PackageOperations(Operations):
|
|||||||
packages[row["package_base"]].packages[row["package"]] = PackageDescription.from_json(row)
|
packages[row["package_base"]].packages[row["package"]] = PackageDescription.from_json(row)
|
||||||
return packages
|
return packages
|
||||||
|
|
||||||
def _packages_get_select_statuses(self, connection: Connection,
|
@staticmethod
|
||||||
repository_id: RepositoryId) -> dict[str, BuildStatus]:
|
def _packages_get_select_statuses(connection: Connection, repository_id: RepositoryId) -> dict[str, BuildStatus]:
|
||||||
"""
|
"""
|
||||||
select package build statuses from the table
|
select package build statuses from the table
|
||||||
|
|
||||||
|
@ -18,7 +18,6 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from sqlite3 import Connection
|
from sqlite3 import Connection
|
||||||
|
|
||||||
from ahriman.core.database.operations import Operations
|
from ahriman.core.database.operations import Operations
|
||||||
@ -42,16 +41,16 @@ class PatchOperations(Operations):
|
|||||||
"""
|
"""
|
||||||
return self.patches_list(package_base, None).get(package_base, [])
|
return self.patches_list(package_base, None).get(package_base, [])
|
||||||
|
|
||||||
def patches_insert(self, package_base: str, patch: PkgbuildPatch) -> None:
|
def patches_insert(self, package_base: str, patches: list[PkgbuildPatch]) -> None:
|
||||||
"""
|
"""
|
||||||
insert or update patch in database
|
insert or update patch in database
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
package_base(str): package base to insert
|
package_base(str): package base to insert
|
||||||
patch(PkgbuildPatch): patch content
|
patches(list[PkgbuildPatch]): patch content
|
||||||
"""
|
"""
|
||||||
def run(connection: Connection) -> None:
|
def run(connection: Connection) -> None:
|
||||||
connection.execute(
|
connection.executemany(
|
||||||
"""
|
"""
|
||||||
insert into patches
|
insert into patches
|
||||||
(package_base, variable, patch)
|
(package_base, variable, patch)
|
||||||
@ -60,7 +59,14 @@ class PatchOperations(Operations):
|
|||||||
on conflict (package_base, coalesce(variable, '')) do update set
|
on conflict (package_base, coalesce(variable, '')) do update set
|
||||||
patch = :patch
|
patch = :patch
|
||||||
""",
|
""",
|
||||||
{"package_base": package_base, "variable": patch.key, "patch": patch.value})
|
[
|
||||||
|
{
|
||||||
|
"package_base": package_base,
|
||||||
|
"variable": patch.key,
|
||||||
|
"patch": patch.value,
|
||||||
|
} for patch in patches
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
return self.with_connection(run, commit=True)
|
return self.with_connection(run, commit=True)
|
||||||
|
|
||||||
@ -89,7 +95,7 @@ class PatchOperations(Operations):
|
|||||||
if variables is not None and patch.key not in variables:
|
if variables is not None and patch.key not in variables:
|
||||||
continue
|
continue
|
||||||
patches[package].append(patch)
|
patches[package].append(patch)
|
||||||
return dict(patches)
|
return patches
|
||||||
|
|
||||||
def patches_remove(self, package_base: str, variables: list[str] | None) -> None:
|
def patches_remove(self, package_base: str, variables: list[str] | None) -> None:
|
||||||
"""
|
"""
|
||||||
@ -102,12 +108,21 @@ class PatchOperations(Operations):
|
|||||||
def run_many(connection: Connection) -> None:
|
def run_many(connection: Connection) -> None:
|
||||||
patches = variables or [] # suppress mypy warning
|
patches = variables or [] # suppress mypy warning
|
||||||
connection.executemany(
|
connection.executemany(
|
||||||
"""delete from patches where package_base = :package_base and variable = :variable""",
|
"""
|
||||||
[{"package_base": package_base, "variable": variable} for variable in patches])
|
delete from patches where package_base = :package_base and variable = :variable
|
||||||
|
""",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"package_base": package_base,
|
||||||
|
"variable": variable,
|
||||||
|
} for variable in patches
|
||||||
|
])
|
||||||
|
|
||||||
def run(connection: Connection) -> None:
|
def run(connection: Connection) -> None:
|
||||||
connection.execute(
|
connection.execute(
|
||||||
"""delete from patches where package_base = :package_base""",
|
"""
|
||||||
|
delete from patches where package_base = :package_base
|
||||||
|
""",
|
||||||
{"package_base": package_base})
|
{"package_base": package_base})
|
||||||
|
|
||||||
if variables is not None:
|
if variables is not None:
|
||||||
|
@ -28,6 +28,7 @@ from multiprocessing import Process, Queue
|
|||||||
from threading import Lock, Thread
|
from threading import Lock, Thread
|
||||||
|
|
||||||
from ahriman.core.log import LazyLogging
|
from ahriman.core.log import LazyLogging
|
||||||
|
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||||
from ahriman.models.process_status import ProcessStatus
|
from ahriman.models.process_status import ProcessStatus
|
||||||
from ahriman.models.repository_id import RepositoryId
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
@ -96,7 +97,8 @@ class Spawn(Thread, LazyLogging):
|
|||||||
|
|
||||||
queue.put(ProcessStatus(process_id, result, consumed_time))
|
queue.put(ProcessStatus(process_id, result, consumed_time))
|
||||||
|
|
||||||
def _spawn_process(self, repository_id: RepositoryId, command: str, *args: str, **kwargs: str | None) -> str:
|
def _spawn_process(self, repository_id: RepositoryId, command: str, *args: str,
|
||||||
|
**kwargs: str | list[str] | None) -> str:
|
||||||
"""
|
"""
|
||||||
spawn external ahriman process with supplied arguments
|
spawn external ahriman process with supplied arguments
|
||||||
|
|
||||||
@ -104,7 +106,7 @@ class Spawn(Thread, LazyLogging):
|
|||||||
repository_id(RepositoryId): repository unique identifier
|
repository_id(RepositoryId): repository unique identifier
|
||||||
command(str): subcommand to run
|
command(str): subcommand to run
|
||||||
*args(str): positional command arguments
|
*args(str): positional command arguments
|
||||||
**kwargs(str): named command arguments
|
**kwargs(str | list[str] | None): named command arguments
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: spawned process identifier
|
str: spawned process identifier
|
||||||
@ -118,9 +120,13 @@ class Spawn(Thread, LazyLogging):
|
|||||||
for argument, value in kwargs.items():
|
for argument, value in kwargs.items():
|
||||||
if value is None:
|
if value is None:
|
||||||
continue # skip null values
|
continue # skip null values
|
||||||
arguments.append(f"--{argument}")
|
flag = f"--{argument}"
|
||||||
if value:
|
if isinstance(value, list):
|
||||||
arguments.append(value)
|
arguments.extend(list(sum(((flag, v) for v in value), ())))
|
||||||
|
elif value:
|
||||||
|
arguments.extend([flag, value])
|
||||||
|
else:
|
||||||
|
arguments.append(flag) # boolean argument
|
||||||
|
|
||||||
process_id = str(uuid.uuid4())
|
process_id = str(uuid.uuid4())
|
||||||
self.logger.info("full command line arguments of %s are %s using repository %s",
|
self.logger.info("full command line arguments of %s are %s using repository %s",
|
||||||
@ -167,7 +173,7 @@ class Spawn(Thread, LazyLogging):
|
|||||||
return self._spawn_process(repository_id, "service-key-import", key, **kwargs)
|
return self._spawn_process(repository_id, "service-key-import", key, **kwargs)
|
||||||
|
|
||||||
def packages_add(self, repository_id: RepositoryId, packages: Iterable[str], username: str | None, *,
|
def packages_add(self, repository_id: RepositoryId, packages: Iterable[str], username: str | None, *,
|
||||||
now: bool) -> str:
|
patches: list[PkgbuildPatch], now: bool) -> str:
|
||||||
"""
|
"""
|
||||||
add packages
|
add packages
|
||||||
|
|
||||||
@ -175,14 +181,18 @@ class Spawn(Thread, LazyLogging):
|
|||||||
repository_id(RepositoryId): repository unique identifier
|
repository_id(RepositoryId): repository unique identifier
|
||||||
packages(Iterable[str]): packages list to add
|
packages(Iterable[str]): packages list to add
|
||||||
username(str | None): optional override of username for build process
|
username(str | None): optional override of username for build process
|
||||||
|
patches(list[PkgbuildPatch]): list of patches to be passed
|
||||||
now(bool): build packages now
|
now(bool): build packages now
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: spawned process identifier
|
str: spawned process identifier
|
||||||
"""
|
"""
|
||||||
kwargs = {"username": username}
|
kwargs: dict[str, str | list[str] | None] = {"username": username}
|
||||||
if now:
|
if now:
|
||||||
kwargs["now"] = ""
|
kwargs["now"] = ""
|
||||||
|
if patches:
|
||||||
|
kwargs["variable"] = [patch.serialize() for patch in patches]
|
||||||
|
|
||||||
return self._spawn_process(repository_id, "package-add", *packages, **kwargs)
|
return self._spawn_process(repository_id, "package-add", *packages, **kwargs)
|
||||||
|
|
||||||
def packages_rebuild(self, repository_id: RepositoryId, depends_on: str, username: str | None) -> str:
|
def packages_rebuild(self, repository_id: RepositoryId, depends_on: str, username: str | None) -> str:
|
||||||
|
@ -23,6 +23,7 @@ from ahriman.core.log import LazyLogging
|
|||||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||||
from ahriman.models.log_record_id import LogRecordId
|
from ahriman.models.log_record_id import LogRecordId
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
|
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||||
from ahriman.models.repository_id import RepositoryId
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
@ -162,6 +163,40 @@ class Watcher(LazyLogging):
|
|||||||
self.known[package_base] = (package, full_status)
|
self.known[package_base] = (package, full_status)
|
||||||
self.database.package_update(package, full_status, self.repository_id)
|
self.database.package_update(package, full_status, self.repository_id)
|
||||||
|
|
||||||
|
def patches_get(self, package_base: str, variable: str | None) -> list[PkgbuildPatch]:
|
||||||
|
"""
|
||||||
|
get patches for the package
|
||||||
|
|
||||||
|
Args:
|
||||||
|
package_base(str): package base
|
||||||
|
variable(str | None): patch variable name if any
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[PkgbuildPatch]: list of patches which are stored for the package
|
||||||
|
"""
|
||||||
|
variables = [variable] if variable is not None else None
|
||||||
|
return self.database.patches_list(package_base, variables).get(package_base, [])
|
||||||
|
|
||||||
|
def patches_remove(self, package_base: str, variable: str) -> None:
|
||||||
|
"""
|
||||||
|
remove package patch
|
||||||
|
|
||||||
|
Args:
|
||||||
|
package_base(str): package base
|
||||||
|
variable(str): patch variable name
|
||||||
|
"""
|
||||||
|
self.database.patches_remove(package_base, [variable])
|
||||||
|
|
||||||
|
def patches_update(self, package_base: str, patch: PkgbuildPatch) -> None:
|
||||||
|
"""
|
||||||
|
update package patch
|
||||||
|
|
||||||
|
Args:
|
||||||
|
package_base(str): package base
|
||||||
|
patch(PkgbuildPatch): package patch
|
||||||
|
"""
|
||||||
|
self.database.patches_insert(package_base, [patch])
|
||||||
|
|
||||||
def status_update(self, status: BuildStatusEnum) -> None:
|
def status_update(self, status: BuildStatusEnum) -> None:
|
||||||
"""
|
"""
|
||||||
update service status
|
update service status
|
||||||
|
@ -56,6 +56,7 @@ __all__ = [
|
|||||||
"srcinfo_property",
|
"srcinfo_property",
|
||||||
"srcinfo_property_list",
|
"srcinfo_property_list",
|
||||||
"trim_package",
|
"trim_package",
|
||||||
|
"unquote",
|
||||||
"utcnow",
|
"utcnow",
|
||||||
"walk",
|
"walk",
|
||||||
]
|
]
|
||||||
@ -465,6 +466,38 @@ def trim_package(package_name: str) -> str:
|
|||||||
return package_name
|
return package_name
|
||||||
|
|
||||||
|
|
||||||
|
def unquote(source: str) -> str:
|
||||||
|
"""
|
||||||
|
like ``shlex.quote``, but opposite
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source(str): source string to remove quotes
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: string with quotes removed
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: if no closing quotation
|
||||||
|
"""
|
||||||
|
def generator() -> Generator[str, None, None]:
|
||||||
|
token = None
|
||||||
|
for char in source:
|
||||||
|
if token is not None:
|
||||||
|
if char == token:
|
||||||
|
token = None # closed quote
|
||||||
|
else:
|
||||||
|
yield char # character inside quotes
|
||||||
|
elif char in ("'", "\""):
|
||||||
|
token = char # first quote found
|
||||||
|
else:
|
||||||
|
yield char # normal character
|
||||||
|
|
||||||
|
if token is not None:
|
||||||
|
raise ValueError("No closing quotation")
|
||||||
|
|
||||||
|
return "".join(generator())
|
||||||
|
|
||||||
|
|
||||||
def utcnow() -> datetime.datetime:
|
def utcnow() -> datetime.datetime:
|
||||||
"""
|
"""
|
||||||
get current time
|
get current time
|
||||||
|
@ -19,8 +19,11 @@
|
|||||||
#
|
#
|
||||||
import shlex
|
import shlex
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any, Self
|
||||||
|
|
||||||
|
from ahriman.core.util import dataclass_view, unquote
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@ -33,12 +36,12 @@ class PkgbuildPatch:
|
|||||||
considered as full PKGBUILD diffs
|
considered as full PKGBUILD diffs
|
||||||
value(str | list[str]): value of the stored PKGBUILD property. It must be either string or list of string
|
value(str | list[str]): value of the stored PKGBUILD property. It must be either string or list of string
|
||||||
values
|
values
|
||||||
unsafe(bool): if set, value will be not quoted, might break PKGBUILD
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key: str | None
|
key: str | None
|
||||||
value: str | list[str]
|
value: str | list[str]
|
||||||
unsafe: bool = field(default=False, kw_only=True)
|
|
||||||
|
quote = shlex.quote
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -66,17 +69,26 @@ class PkgbuildPatch:
|
|||||||
"""
|
"""
|
||||||
return self.key is None
|
return self.key is None
|
||||||
|
|
||||||
def quote(self, value: str) -> str:
|
@classmethod
|
||||||
|
def from_env(cls, variable: str) -> Self:
|
||||||
"""
|
"""
|
||||||
quote value according to the unsafe flag
|
construct patch from environment variable. Functions are not supported
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
value(str): value to be quoted
|
variable(str): variable in bash form, i.e. KEY=VALUE
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: quoted string in case if unsafe is False and as is otherwise
|
Self: package properties
|
||||||
"""
|
"""
|
||||||
return value if self.unsafe else shlex.quote(value)
|
key, *value_parts = variable.split("=", maxsplit=1)
|
||||||
|
|
||||||
|
raw_value = next(iter(value_parts), "") # extract raw value
|
||||||
|
if raw_value.startswith("(") and raw_value.endswith(")"):
|
||||||
|
value: str | list[str] = shlex.split(raw_value[1:-1]) # arrays for poor
|
||||||
|
else:
|
||||||
|
value = unquote(raw_value)
|
||||||
|
|
||||||
|
return cls(key, value)
|
||||||
|
|
||||||
def serialize(self) -> str:
|
def serialize(self) -> str:
|
||||||
"""
|
"""
|
||||||
@ -88,14 +100,23 @@ class PkgbuildPatch:
|
|||||||
str: serialized key-value pair, print-friendly
|
str: serialized key-value pair, print-friendly
|
||||||
"""
|
"""
|
||||||
if isinstance(self.value, list): # list like
|
if isinstance(self.value, list): # list like
|
||||||
value = " ".join(map(self.quote, self.value))
|
value = " ".join(map(PkgbuildPatch.quote, self.value))
|
||||||
return f"""{self.key}=({value})"""
|
return f"""{self.key}=({value})"""
|
||||||
if self.is_plain_diff: # no additional logic for plain diffs
|
if self.is_plain_diff: # no additional logic for plain diffs
|
||||||
return self.value
|
return self.value
|
||||||
# we suppose that function values are only supported in string-like values
|
# we suppose that function values are only supported in string-like values
|
||||||
if self.is_function:
|
if self.is_function:
|
||||||
return f"{self.key} {self.value}" # no quoting enabled here
|
return f"{self.key} {self.value}" # no quoting enabled here
|
||||||
return f"""{self.key}={self.quote(self.value)}"""
|
return f"""{self.key}={PkgbuildPatch.quote(self.value)}"""
|
||||||
|
|
||||||
|
def view(self) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
generate json patch view
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict[str, Any]: json-friendly dictionary
|
||||||
|
"""
|
||||||
|
return dataclass_view(self)
|
||||||
|
|
||||||
def write(self, pkgbuild_path: Path) -> None:
|
def write(self, pkgbuild_path: Path) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -52,6 +52,8 @@ class RepositoryId:
|
|||||||
Returns:
|
Returns:
|
||||||
str: unique id for this repository
|
str: unique id for this repository
|
||||||
"""
|
"""
|
||||||
|
if self.is_empty:
|
||||||
|
return ""
|
||||||
return f"{self.architecture}-{self.name}" # basically the same as used for command line
|
return f"{self.architecture}-{self.name}" # basically the same as used for command line
|
||||||
|
|
||||||
def query(self) -> list[tuple[str, str]]:
|
def query(self) -> list[tuple[str, str]]:
|
||||||
|
@ -25,14 +25,17 @@ from ahriman.web.schemas.file_schema import FileSchema
|
|||||||
from ahriman.web.schemas.internal_status_schema import InternalStatusSchema
|
from ahriman.web.schemas.internal_status_schema import InternalStatusSchema
|
||||||
from ahriman.web.schemas.log_schema import LogSchema
|
from ahriman.web.schemas.log_schema import LogSchema
|
||||||
from ahriman.web.schemas.login_schema import LoginSchema
|
from ahriman.web.schemas.login_schema import LoginSchema
|
||||||
from ahriman.web.schemas.logs_schema import LogsSchema, LogsSchemaV2
|
from ahriman.web.schemas.logs_schema import LogsSchema
|
||||||
from ahriman.web.schemas.oauth2_schema import OAuth2Schema
|
from ahriman.web.schemas.oauth2_schema import OAuth2Schema
|
||||||
from ahriman.web.schemas.package_name_schema import PackageNameSchema
|
from ahriman.web.schemas.package_name_schema import PackageNameSchema
|
||||||
from ahriman.web.schemas.package_names_schema import PackageNamesSchema
|
from ahriman.web.schemas.package_names_schema import PackageNamesSchema
|
||||||
|
from ahriman.web.schemas.package_patch_schema import PackagePatchSchema
|
||||||
from ahriman.web.schemas.package_properties_schema import PackagePropertiesSchema
|
from ahriman.web.schemas.package_properties_schema import PackagePropertiesSchema
|
||||||
from ahriman.web.schemas.package_schema import PackageSchema
|
from ahriman.web.schemas.package_schema import PackageSchema
|
||||||
from ahriman.web.schemas.package_status_schema import PackageStatusSimplifiedSchema, PackageStatusSchema
|
from ahriman.web.schemas.package_status_schema import PackageStatusSimplifiedSchema, PackageStatusSchema
|
||||||
from ahriman.web.schemas.pagination_schema import PaginationSchema
|
from ahriman.web.schemas.pagination_schema import PaginationSchema
|
||||||
|
from ahriman.web.schemas.patch_name_schema import PatchNameSchema
|
||||||
|
from ahriman.web.schemas.patch_schema import PatchSchema
|
||||||
from ahriman.web.schemas.pgp_key_id_schema import PGPKeyIdSchema
|
from ahriman.web.schemas.pgp_key_id_schema import PGPKeyIdSchema
|
||||||
from ahriman.web.schemas.pgp_key_schema import PGPKeySchema
|
from ahriman.web.schemas.pgp_key_schema import PGPKeySchema
|
||||||
from ahriman.web.schemas.process_id_schema import ProcessIdSchema
|
from ahriman.web.schemas.process_id_schema import ProcessIdSchema
|
||||||
@ -41,4 +44,5 @@ from ahriman.web.schemas.remote_schema import RemoteSchema
|
|||||||
from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema
|
from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema
|
||||||
from ahriman.web.schemas.search_schema import SearchSchema
|
from ahriman.web.schemas.search_schema import SearchSchema
|
||||||
from ahriman.web.schemas.status_schema import StatusSchema
|
from ahriman.web.schemas.status_schema import StatusSchema
|
||||||
|
from ahriman.web.schemas.versioned_log_schema import VersionedLogSchema
|
||||||
from ahriman.web.schemas.update_flags_schema import UpdateFlagsSchema
|
from ahriman.web.schemas.update_flags_schema import UpdateFlagsSchema
|
||||||
|
@ -17,13 +17,10 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
from marshmallow import fields
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
from ahriman import __version__
|
|
||||||
from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema
|
|
||||||
|
|
||||||
|
|
||||||
class LogSchema(RepositoryIdSchema):
|
class LogSchema(Schema):
|
||||||
"""
|
"""
|
||||||
request package log schema
|
request package log schema
|
||||||
"""
|
"""
|
||||||
@ -32,10 +29,6 @@ class LogSchema(RepositoryIdSchema):
|
|||||||
"description": "Log record timestamp",
|
"description": "Log record timestamp",
|
||||||
"example": 1680537091.233495,
|
"example": 1680537091.233495,
|
||||||
})
|
})
|
||||||
version = fields.Integer(required=True, metadata={
|
|
||||||
"description": "Package version to tag",
|
|
||||||
"example": __version__,
|
|
||||||
})
|
|
||||||
message = fields.String(required=True, metadata={
|
message = fields.String(required=True, metadata={
|
||||||
"description": "Log message",
|
"description": "Log message",
|
||||||
})
|
})
|
||||||
|
@ -27,23 +27,9 @@ class LogsSchema(Schema):
|
|||||||
response package logs schema
|
response package logs schema
|
||||||
"""
|
"""
|
||||||
|
|
||||||
package_base = fields.String(required=True, metadata={
|
|
||||||
"description": "Package base name",
|
|
||||||
"example": "ahriman",
|
|
||||||
})
|
|
||||||
status = fields.Nested(StatusSchema(), required=True, metadata={
|
|
||||||
"description": "Last package status",
|
|
||||||
})
|
|
||||||
logs = fields.String(required=True, metadata={
|
logs = fields.String(required=True, metadata={
|
||||||
"description": "Full package log from the last build",
|
"description": "Full package log from the last build",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class LogsSchemaV2(Schema):
|
|
||||||
"""
|
|
||||||
response package logs api v2 schema
|
|
||||||
"""
|
|
||||||
|
|
||||||
package_base = fields.String(required=True, metadata={
|
package_base = fields.String(required=True, metadata={
|
||||||
"description": "Package base name",
|
"description": "Package base name",
|
||||||
"example": "ahriman",
|
"example": "ahriman",
|
||||||
@ -51,7 +37,3 @@ class LogsSchemaV2(Schema):
|
|||||||
status = fields.Nested(StatusSchema(), required=True, metadata={
|
status = fields.Nested(StatusSchema(), required=True, metadata={
|
||||||
"description": "Last package status",
|
"description": "Last package status",
|
||||||
})
|
})
|
||||||
logs = fields.List(fields.Tuple([fields.Float(), fields.String()]), required=True, metadata={ # type: ignore[no-untyped-call]
|
|
||||||
"description": "Package log records timestamp and message",
|
|
||||||
"example": [(1680537091.233495, "log record")]
|
|
||||||
})
|
|
||||||
|
33
src/ahriman/web/schemas/package_patch_schema.py
Normal file
33
src/ahriman/web/schemas/package_patch_schema.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from marshmallow import fields
|
||||||
|
|
||||||
|
from ahriman.web.schemas.package_names_schema import PackageNamesSchema
|
||||||
|
from ahriman.web.schemas.patch_schema import PatchSchema
|
||||||
|
|
||||||
|
|
||||||
|
class PackagePatchSchema(PackageNamesSchema):
|
||||||
|
"""
|
||||||
|
response schema with packages and patches
|
||||||
|
"""
|
||||||
|
|
||||||
|
patches = fields.Nested(PatchSchema(many=True), metadata={
|
||||||
|
"description": "optional environment variables to be applied as patches"
|
||||||
|
})
|
33
src/ahriman/web/schemas/patch_name_schema.py
Normal file
33
src/ahriman/web/schemas/patch_name_schema.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from marshmallow import fields
|
||||||
|
|
||||||
|
from ahriman.web.schemas.package_name_schema import PackageNameSchema
|
||||||
|
|
||||||
|
|
||||||
|
class PatchNameSchema(PackageNameSchema):
|
||||||
|
"""
|
||||||
|
request package patch schema
|
||||||
|
"""
|
||||||
|
|
||||||
|
patch = fields.String(required=True, metadata={
|
||||||
|
"description": "Variable name",
|
||||||
|
"example": "PKGEXT",
|
||||||
|
})
|
33
src/ahriman/web/schemas/patch_schema.py
Normal file
33
src/ahriman/web/schemas/patch_schema.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
|
||||||
|
class PatchSchema(Schema):
|
||||||
|
"""
|
||||||
|
request and response patch schema
|
||||||
|
"""
|
||||||
|
|
||||||
|
key = fields.String(required=True, metadata={
|
||||||
|
"description": "environment variable name",
|
||||||
|
})
|
||||||
|
value = fields.String(metadata={
|
||||||
|
"description": "environment variable value",
|
||||||
|
})
|
35
src/ahriman/web/schemas/versioned_log_schema.py
Normal file
35
src/ahriman/web/schemas/versioned_log_schema.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from marshmallow import fields
|
||||||
|
|
||||||
|
from ahriman import __version__
|
||||||
|
from ahriman.web.schemas.log_schema import LogSchema
|
||||||
|
from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema
|
||||||
|
|
||||||
|
|
||||||
|
class VersionedLogSchema(LogSchema, RepositoryIdSchema):
|
||||||
|
"""
|
||||||
|
request package log schema
|
||||||
|
"""
|
||||||
|
|
||||||
|
version = fields.Integer(required=True, metadata={
|
||||||
|
"description": "Package version to tag",
|
||||||
|
"example": __version__,
|
||||||
|
})
|
@ -21,8 +21,9 @@ import aiohttp_apispec # type: ignore[import-untyped]
|
|||||||
|
|
||||||
from aiohttp.web import HTTPBadRequest, Response, json_response
|
from aiohttp.web import HTTPBadRequest, Response, json_response
|
||||||
|
|
||||||
|
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema, RepositoryIdSchema
|
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackagePatchSchema, ProcessIdSchema, RepositoryIdSchema
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
|
|
||||||
@ -53,7 +54,7 @@ class AddView(BaseView):
|
|||||||
)
|
)
|
||||||
@aiohttp_apispec.cookies_schema(AuthSchema)
|
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||||
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
|
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
|
||||||
@aiohttp_apispec.json_schema(PackageNamesSchema)
|
@aiohttp_apispec.json_schema(PackagePatchSchema)
|
||||||
async def post(self) -> Response:
|
async def post(self) -> Response:
|
||||||
"""
|
"""
|
||||||
add new package
|
add new package
|
||||||
@ -65,13 +66,14 @@ class AddView(BaseView):
|
|||||||
HTTPBadRequest: if bad data is supplied
|
HTTPBadRequest: if bad data is supplied
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
data = await self.extract_data(["packages"])
|
data = await self.extract_data(["packages", "patches"])
|
||||||
packages = self.get_non_empty(lambda key: [package for package in data[key] if package], "packages")
|
packages = self.get_non_empty(lambda key: [package for package in data[key] if package], "packages")
|
||||||
|
patches = [PkgbuildPatch(patch["key"], patch.get("value", "")) for patch in data.get("patches", [])]
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
raise HTTPBadRequest(reason=str(ex))
|
raise HTTPBadRequest(reason=str(ex))
|
||||||
|
|
||||||
repository_id = self.repository_id()
|
repository_id = self.repository_id()
|
||||||
username = await self.username()
|
username = await self.username()
|
||||||
process_id = self.spawner.packages_add(repository_id, packages, username, now=True)
|
process_id = self.spawner.packages_add(repository_id, packages, username, patches=patches, now=True)
|
||||||
|
|
||||||
return json_response({"process_id": process_id})
|
return json_response({"process_id": process_id})
|
||||||
|
@ -21,8 +21,9 @@ import aiohttp_apispec # type: ignore[import-untyped]
|
|||||||
|
|
||||||
from aiohttp.web import HTTPBadRequest, Response, json_response
|
from aiohttp.web import HTTPBadRequest, Response, json_response
|
||||||
|
|
||||||
|
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema, RepositoryIdSchema
|
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackagePatchSchema, ProcessIdSchema, RepositoryIdSchema
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
|
|
||||||
@ -53,7 +54,7 @@ class RequestView(BaseView):
|
|||||||
)
|
)
|
||||||
@aiohttp_apispec.cookies_schema(AuthSchema)
|
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||||
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
|
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
|
||||||
@aiohttp_apispec.json_schema(PackageNamesSchema)
|
@aiohttp_apispec.json_schema(PackagePatchSchema)
|
||||||
async def post(self) -> Response:
|
async def post(self) -> Response:
|
||||||
"""
|
"""
|
||||||
request to add new package
|
request to add new package
|
||||||
@ -65,13 +66,14 @@ class RequestView(BaseView):
|
|||||||
HTTPBadRequest: if bad data is supplied
|
HTTPBadRequest: if bad data is supplied
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
data = await self.extract_data(["packages"])
|
data = await self.extract_data(["packages", "patches"])
|
||||||
packages = self.get_non_empty(lambda key: [package for package in data[key] if package], "packages")
|
packages = self.get_non_empty(lambda key: [package for package in data[key] if package], "packages")
|
||||||
|
patches = [PkgbuildPatch(patch["key"], patch.get("value", "")) for patch in data.get("patches", [])]
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
raise HTTPBadRequest(reason=str(ex))
|
raise HTTPBadRequest(reason=str(ex))
|
||||||
|
|
||||||
username = await self.username()
|
username = await self.username()
|
||||||
repository_id = self.repository_id()
|
repository_id = self.repository_id()
|
||||||
process_id = self.spawner.packages_add(repository_id, packages, username, now=False)
|
process_id = self.spawner.packages_add(repository_id, packages, username, patches=patches, now=False)
|
||||||
|
|
||||||
return json_response({"process_id": process_id})
|
return json_response({"process_id": process_id})
|
||||||
|
@ -25,7 +25,8 @@ from ahriman.core.exceptions import UnknownPackageError
|
|||||||
from ahriman.core.util import pretty_datetime
|
from ahriman.core.util import pretty_datetime
|
||||||
from ahriman.models.log_record_id import LogRecordId
|
from ahriman.models.log_record_id import LogRecordId
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, LogSchema, LogsSchema, PackageNameSchema, RepositoryIdSchema
|
from ahriman.web.schemas import AuthSchema, ErrorSchema, LogsSchema, PackageNameSchema, RepositoryIdSchema, \
|
||||||
|
VersionedLogSchema
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
|
|
||||||
@ -128,7 +129,7 @@ class LogsView(BaseView):
|
|||||||
)
|
)
|
||||||
@aiohttp_apispec.cookies_schema(AuthSchema)
|
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||||
@aiohttp_apispec.match_info_schema(PackageNameSchema)
|
@aiohttp_apispec.match_info_schema(PackageNameSchema)
|
||||||
@aiohttp_apispec.json_schema(LogSchema)
|
@aiohttp_apispec.json_schema(VersionedLogSchema)
|
||||||
async def post(self) -> None:
|
async def post(self) -> None:
|
||||||
"""
|
"""
|
||||||
create new package log record
|
create new package log record
|
||||||
|
103
src/ahriman/web/views/v1/status/patch.py
Normal file
103
src/ahriman/web/views/v1/status/patch.py
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import aiohttp_apispec # type: ignore[import-untyped]
|
||||||
|
|
||||||
|
from aiohttp.web import HTTPNoContent, HTTPNotFound, Response, json_response
|
||||||
|
|
||||||
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.schemas import AuthSchema, ErrorSchema, PatchNameSchema, PatchSchema
|
||||||
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
|
|
||||||
|
class PatchView(BaseView):
|
||||||
|
"""
|
||||||
|
package patch web view
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
DELETE_PERMISSION(UserAccess): (class attribute) delete permissions of self
|
||||||
|
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
||||||
|
"""
|
||||||
|
|
||||||
|
DELETE_PERMISSION = UserAccess.Full
|
||||||
|
GET_PERMISSION = UserAccess.Reporter
|
||||||
|
ROUTES = ["/api/v1/packages/{package}/patches/{patch}"]
|
||||||
|
|
||||||
|
@aiohttp_apispec.docs(
|
||||||
|
tags=["Packages"],
|
||||||
|
summary="Delete package patch",
|
||||||
|
description="Delete package patch by variable",
|
||||||
|
responses={
|
||||||
|
204: {"description": "Success response"},
|
||||||
|
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||||
|
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||||
|
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||||
|
},
|
||||||
|
security=[{"token": [DELETE_PERMISSION]}],
|
||||||
|
)
|
||||||
|
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||||
|
@aiohttp_apispec.match_info_schema(PatchNameSchema)
|
||||||
|
async def delete(self) -> None:
|
||||||
|
"""
|
||||||
|
delete package patch
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPNoContent: on success response
|
||||||
|
"""
|
||||||
|
package_base = self.request.match_info["package"]
|
||||||
|
variable = self.request.match_info["patch"]
|
||||||
|
self.service().patches_remove(package_base, variable)
|
||||||
|
|
||||||
|
raise HTTPNoContent
|
||||||
|
|
||||||
|
@aiohttp_apispec.docs(
|
||||||
|
tags=["Packages"],
|
||||||
|
summary="Get package patch",
|
||||||
|
description="Retrieve package patch by variable",
|
||||||
|
responses={
|
||||||
|
200: {"description": "Success response", "schema": PatchSchema},
|
||||||
|
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||||
|
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||||
|
404: {"description": "Patch name is unknown", "schema": ErrorSchema},
|
||||||
|
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||||
|
},
|
||||||
|
security=[{"token": [GET_PERMISSION]}],
|
||||||
|
)
|
||||||
|
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||||
|
@aiohttp_apispec.match_info_schema(PatchNameSchema)
|
||||||
|
async def get(self) -> Response:
|
||||||
|
"""
|
||||||
|
get package patch
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: 200 with package patch on success
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPNotFound: if package base is unknown
|
||||||
|
"""
|
||||||
|
package_base = self.request.match_info["package"]
|
||||||
|
variable = self.request.match_info["patch"]
|
||||||
|
|
||||||
|
patches = self.service().patches_get(package_base, variable)
|
||||||
|
selected = next((patch for patch in patches if patch.key == variable), None)
|
||||||
|
|
||||||
|
if selected is None:
|
||||||
|
raise HTTPNotFound
|
||||||
|
|
||||||
|
return json_response(selected.view())
|
108
src/ahriman/web/views/v1/status/patches.py
Normal file
108
src/ahriman/web/views/v1/status/patches.py
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import aiohttp_apispec # type: ignore[import-untyped]
|
||||||
|
|
||||||
|
from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
|
||||||
|
|
||||||
|
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||||
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNameSchema, PatchSchema
|
||||||
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
|
|
||||||
|
class PatchesView(BaseView):
|
||||||
|
"""
|
||||||
|
package patches web view
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
||||||
|
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||||
|
"""
|
||||||
|
|
||||||
|
GET_PERMISSION = UserAccess.Reporter
|
||||||
|
POST_PERMISSION = UserAccess.Full
|
||||||
|
ROUTES = ["/api/v1/packages/{package}/patches"]
|
||||||
|
|
||||||
|
@aiohttp_apispec.docs(
|
||||||
|
tags=["Packages"],
|
||||||
|
summary="Get package patches",
|
||||||
|
description="Retrieve all package patches",
|
||||||
|
responses={
|
||||||
|
200: {"description": "Success response", "schema": PatchSchema(many=True)},
|
||||||
|
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||||
|
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||||
|
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||||
|
},
|
||||||
|
security=[{"token": [GET_PERMISSION]}],
|
||||||
|
)
|
||||||
|
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||||
|
@aiohttp_apispec.match_info_schema(PackageNameSchema)
|
||||||
|
async def get(self) -> Response:
|
||||||
|
"""
|
||||||
|
get package patches
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: 200 with package patches on success
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPNotFound: if package base is unknown
|
||||||
|
"""
|
||||||
|
package_base = self.request.match_info["package"]
|
||||||
|
patches = self.service().patches_get(package_base, None)
|
||||||
|
|
||||||
|
response = [patch.view() for patch in patches]
|
||||||
|
return json_response(response)
|
||||||
|
|
||||||
|
@aiohttp_apispec.docs(
|
||||||
|
tags=["Packages"],
|
||||||
|
summary="Update package patch",
|
||||||
|
description="Update or create package patch",
|
||||||
|
responses={
|
||||||
|
204: {"description": "Success response"},
|
||||||
|
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
|
||||||
|
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||||
|
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||||
|
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||||
|
},
|
||||||
|
security=[{"token": [GET_PERMISSION]}],
|
||||||
|
)
|
||||||
|
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||||
|
@aiohttp_apispec.match_info_schema(PackageNameSchema)
|
||||||
|
@aiohttp_apispec.json_schema(PatchSchema)
|
||||||
|
async def post(self) -> None:
|
||||||
|
"""
|
||||||
|
update or create package patch
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPBadRequest: if bad data is supplied
|
||||||
|
HTTPNoContent: on success response
|
||||||
|
"""
|
||||||
|
package_base = self.request.match_info["package"]
|
||||||
|
data = await self.extract_data()
|
||||||
|
|
||||||
|
try:
|
||||||
|
key = data["key"]
|
||||||
|
value = data["value"]
|
||||||
|
except Exception as ex:
|
||||||
|
raise HTTPBadRequest(reason=str(ex))
|
||||||
|
|
||||||
|
self.service().patches_update(package_base, PkgbuildPatch(key, value))
|
||||||
|
|
||||||
|
raise HTTPNoContent
|
@ -19,11 +19,10 @@
|
|||||||
#
|
#
|
||||||
import aiohttp_apispec # type: ignore[import-untyped]
|
import aiohttp_apispec # type: ignore[import-untyped]
|
||||||
|
|
||||||
from aiohttp.web import HTTPNotFound, Response, json_response
|
from aiohttp.web import Response, json_response
|
||||||
|
|
||||||
from ahriman.core.exceptions import UnknownPackageError
|
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, LogsSchemaV2, PackageNameSchema, PaginationSchema
|
from ahriman.web.schemas import AuthSchema, ErrorSchema, LogSchema, PackageNameSchema, PaginationSchema
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
|
|
||||||
@ -43,7 +42,7 @@ class LogsView(BaseView):
|
|||||||
summary="Get paginated package logs",
|
summary="Get paginated package logs",
|
||||||
description="Retrieve package logs and the last package status",
|
description="Retrieve package logs and the last package status",
|
||||||
responses={
|
responses={
|
||||||
200: {"description": "Success response", "schema": LogsSchemaV2},
|
200: {"description": "Success response", "schema": LogSchema(many=True)},
|
||||||
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
|
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
|
||||||
401: {"description": "Authorization required", "schema": ErrorSchema},
|
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||||
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||||
@ -67,16 +66,12 @@ class LogsView(BaseView):
|
|||||||
"""
|
"""
|
||||||
package_base = self.request.match_info["package"]
|
package_base = self.request.match_info["package"]
|
||||||
limit, offset = self.page()
|
limit, offset = self.page()
|
||||||
|
|
||||||
try:
|
|
||||||
_, status = self.service().package_get(package_base)
|
|
||||||
except UnknownPackageError:
|
|
||||||
raise HTTPNotFound(reason=f"Package {package_base} is unknown")
|
|
||||||
logs = self.service().logs_get(package_base, limit, offset)
|
logs = self.service().logs_get(package_base, limit, offset)
|
||||||
|
|
||||||
response = {
|
response = [
|
||||||
"package_base": package_base,
|
{
|
||||||
"status": status.view(),
|
"created": created,
|
||||||
"logs": logs,
|
"message": message,
|
||||||
}
|
} for created, message in logs
|
||||||
|
]
|
||||||
return json_response(response)
|
return json_response(response)
|
||||||
|
@ -9,6 +9,7 @@ from ahriman.core.repository import Repository
|
|||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.package_source import PackageSource
|
from ahriman.models.package_source import PackageSource
|
||||||
from ahriman.models.packagers import Packagers
|
from ahriman.models.packagers import Packagers
|
||||||
|
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||||
from ahriman.models.result import Result
|
from ahriman.models.result import Result
|
||||||
|
|
||||||
|
|
||||||
@ -22,7 +23,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
|||||||
Returns:
|
Returns:
|
||||||
argparse.Namespace: generated arguments for these test cases
|
argparse.Namespace: generated arguments for these test cases
|
||||||
"""
|
"""
|
||||||
args.package = []
|
args.package = ["ahriman"]
|
||||||
args.exit_code = False
|
args.exit_code = False
|
||||||
args.increment = True
|
args.increment = True
|
||||||
args.now = False
|
args.now = False
|
||||||
@ -30,6 +31,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
|||||||
args.source = PackageSource.Auto
|
args.source = PackageSource.Auto
|
||||||
args.dependencies = True
|
args.dependencies = True
|
||||||
args.username = "username"
|
args.username = "username"
|
||||||
|
args.variable = None
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
|
||||||
@ -51,6 +53,22 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository:
|
|||||||
on_start_mock.assert_called_once_with()
|
on_start_mock.assert_called_once_with()
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_with_patches(args: argparse.Namespace, configuration: Configuration, repository: Repository,
|
||||||
|
mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must run command and insert temporary patches
|
||||||
|
"""
|
||||||
|
args = _default_args(args)
|
||||||
|
args.variable = ["KEY=VALUE"]
|
||||||
|
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
||||||
|
mocker.patch("ahriman.application.application.Application.add")
|
||||||
|
application_mock = mocker.patch("ahriman.core.database.SQLite.patches_insert")
|
||||||
|
|
||||||
|
_, repository_id = configuration.check_loaded()
|
||||||
|
Add.run(args, repository_id, configuration, report=False)
|
||||||
|
application_mock.assert_called_once_with(args.package[0], [PkgbuildPatch("KEY", "VALUE")])
|
||||||
|
|
||||||
|
|
||||||
def test_run_with_updates(args: argparse.Namespace, configuration: Configuration, repository: Repository,
|
def test_run_with_updates(args: argparse.Namespace, configuration: Configuration, repository: Repository,
|
||||||
package_ahriman: Package, mocker: MockerFixture) -> None:
|
package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -180,7 +180,7 @@ def test_patch_set_create(application: Application, package_ahriman: Package, mo
|
|||||||
"""
|
"""
|
||||||
create_mock = mocker.patch("ahriman.core.database.SQLite.patches_insert")
|
create_mock = mocker.patch("ahriman.core.database.SQLite.patches_insert")
|
||||||
Patch.patch_set_create(application, package_ahriman.base, PkgbuildPatch("version", package_ahriman.version))
|
Patch.patch_set_create(application, package_ahriman.base, PkgbuildPatch("version", package_ahriman.version))
|
||||||
create_mock.assert_called_once_with(package_ahriman.base, PkgbuildPatch("version", package_ahriman.version))
|
create_mock.assert_called_once_with(package_ahriman.base, [PkgbuildPatch("version", package_ahriman.version)])
|
||||||
|
|
||||||
|
|
||||||
def test_patch_set_remove(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
|
def test_patch_set_remove(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||||
|
@ -254,6 +254,22 @@ def test_subparsers_package_add_option_refresh(parser: argparse.ArgumentParser)
|
|||||||
assert args.refresh == 2
|
assert args.refresh == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_subparsers_package_add_option_variable_empty(parser: argparse.ArgumentParser) -> None:
|
||||||
|
"""
|
||||||
|
package-add command must accept empty variable list as None
|
||||||
|
"""
|
||||||
|
args = parser.parse_args(["package-add", "ahriman"])
|
||||||
|
assert args.variable is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_subparsers_package_add_option_variable_multiple(parser: argparse.ArgumentParser) -> None:
|
||||||
|
"""
|
||||||
|
repo-rebuild command must accept multiple depends-on
|
||||||
|
"""
|
||||||
|
args = parser.parse_args(["package-add", "ahriman", "-v", "var1", "-v", "var2"])
|
||||||
|
assert args.variable == ["var1", "var2"]
|
||||||
|
|
||||||
|
|
||||||
def test_subparsers_package_remove_option_architecture(parser: argparse.ArgumentParser) -> None:
|
def test_subparsers_package_remove_option_architecture(parser: argparse.ArgumentParser) -> None:
|
||||||
"""
|
"""
|
||||||
package-remove command must correctly parse architecture list
|
package-remove command must correctly parse architecture list
|
||||||
|
@ -20,6 +20,7 @@ def test_init(task_ahriman: Task, database: SQLite, mocker: MockerFixture) -> No
|
|||||||
"""
|
"""
|
||||||
mocker.patch("ahriman.models.package.Package.from_build", return_value=task_ahriman.package)
|
mocker.patch("ahriman.models.package.Package.from_build", return_value=task_ahriman.package)
|
||||||
load_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.load")
|
load_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.load")
|
||||||
|
|
||||||
task_ahriman.init(Path("ahriman"), database, None)
|
task_ahriman.init(Path("ahriman"), database, None)
|
||||||
load_mock.assert_called_once_with(Path("ahriman"), task_ahriman.package, [], task_ahriman.paths)
|
load_mock.assert_called_once_with(Path("ahriman"), task_ahriman.package, [], task_ahriman.paths)
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ from ahriman.core.configuration import Configuration
|
|||||||
from ahriman.core.database.migrations.m011_repository_name import migrate_data, migrate_package_repository, steps
|
from ahriman.core.database.migrations.m011_repository_name import migrate_data, migrate_package_repository, steps
|
||||||
|
|
||||||
|
|
||||||
def test_migration_check_depends() -> None:
|
def test_migration_repository_name() -> None:
|
||||||
"""
|
"""
|
||||||
migration must not be empty
|
migration must not be empty
|
||||||
"""
|
"""
|
||||||
|
@ -7,9 +7,9 @@ def test_patches_get_insert(database: SQLite, package_ahriman: Package, package_
|
|||||||
"""
|
"""
|
||||||
must insert patch to database
|
must insert patch to database
|
||||||
"""
|
"""
|
||||||
database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch_1"))
|
database.patches_insert(package_ahriman.base, [PkgbuildPatch(None, "patch_1")])
|
||||||
database.patches_insert(package_ahriman.base, PkgbuildPatch("key", "patch_3"))
|
database.patches_insert(package_ahriman.base, [PkgbuildPatch("key", "patch_3")])
|
||||||
database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch_2"))
|
database.patches_insert(package_python_schedule.base, [PkgbuildPatch(None, "patch_2")])
|
||||||
assert database.patches_get(package_ahriman.base) == [
|
assert database.patches_get(package_ahriman.base) == [
|
||||||
PkgbuildPatch(None, "patch_1"), PkgbuildPatch("key", "patch_3")
|
PkgbuildPatch(None, "patch_1"), PkgbuildPatch("key", "patch_3")
|
||||||
]
|
]
|
||||||
@ -19,9 +19,9 @@ def test_patches_list(database: SQLite, package_ahriman: Package, package_python
|
|||||||
"""
|
"""
|
||||||
must list all patches
|
must list all patches
|
||||||
"""
|
"""
|
||||||
database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1"))
|
database.patches_insert(package_ahriman.base, [PkgbuildPatch(None, "patch1")])
|
||||||
database.patches_insert(package_ahriman.base, PkgbuildPatch("key", "patch3"))
|
database.patches_insert(package_ahriman.base, [PkgbuildPatch("key", "patch3")])
|
||||||
database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch2"))
|
database.patches_insert(package_python_schedule.base, [PkgbuildPatch(None, "patch2")])
|
||||||
assert database.patches_list(None, None) == {
|
assert database.patches_list(None, None) == {
|
||||||
package_ahriman.base: [PkgbuildPatch(None, "patch1"), PkgbuildPatch("key", "patch3")],
|
package_ahriman.base: [PkgbuildPatch(None, "patch1"), PkgbuildPatch("key", "patch3")],
|
||||||
package_python_schedule.base: [PkgbuildPatch(None, "patch2")],
|
package_python_schedule.base: [PkgbuildPatch(None, "patch2")],
|
||||||
@ -32,8 +32,8 @@ def test_patches_list_filter(database: SQLite, package_ahriman: Package, package
|
|||||||
"""
|
"""
|
||||||
must list all patches filtered by package name (same as get)
|
must list all patches filtered by package name (same as get)
|
||||||
"""
|
"""
|
||||||
database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1"))
|
database.patches_insert(package_ahriman.base, [PkgbuildPatch(None, "patch1")])
|
||||||
database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch2"))
|
database.patches_insert(package_python_schedule.base, [PkgbuildPatch(None, "patch2")])
|
||||||
|
|
||||||
assert database.patches_list(package_ahriman.base, None) == {package_ahriman.base: [PkgbuildPatch(None, "patch1")]}
|
assert database.patches_list(package_ahriman.base, None) == {package_ahriman.base: [PkgbuildPatch(None, "patch1")]}
|
||||||
assert database.patches_list(package_python_schedule.base, None) == {
|
assert database.patches_list(package_python_schedule.base, None) == {
|
||||||
@ -46,9 +46,9 @@ def test_patches_list_filter_by_variable(database: SQLite, package_ahriman: Pack
|
|||||||
"""
|
"""
|
||||||
must list all patches filtered by package name (same as get)
|
must list all patches filtered by package name (same as get)
|
||||||
"""
|
"""
|
||||||
database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1"))
|
database.patches_insert(package_ahriman.base, [PkgbuildPatch(None, "patch1")])
|
||||||
database.patches_insert(package_ahriman.base, PkgbuildPatch("key", "patch2"))
|
database.patches_insert(package_ahriman.base, [PkgbuildPatch("key", "patch2")])
|
||||||
database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch3"))
|
database.patches_insert(package_python_schedule.base, [PkgbuildPatch(None, "patch3")])
|
||||||
|
|
||||||
assert database.patches_list(None, None) == {
|
assert database.patches_list(None, None) == {
|
||||||
package_ahriman.base: [PkgbuildPatch(None, "patch1"), PkgbuildPatch("key", "patch2")],
|
package_ahriman.base: [PkgbuildPatch(None, "patch1"), PkgbuildPatch("key", "patch2")],
|
||||||
@ -63,8 +63,8 @@ def test_patches_insert_remove(database: SQLite, package_ahriman: Package, packa
|
|||||||
"""
|
"""
|
||||||
must remove patch from database
|
must remove patch from database
|
||||||
"""
|
"""
|
||||||
database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1"))
|
database.patches_insert(package_ahriman.base, [PkgbuildPatch(None, "patch1")])
|
||||||
database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch2"))
|
database.patches_insert(package_python_schedule.base, [PkgbuildPatch(None, "patch2")])
|
||||||
database.patches_remove(package_ahriman.base, None)
|
database.patches_remove(package_ahriman.base, None)
|
||||||
|
|
||||||
assert database.patches_get(package_ahriman.base) == []
|
assert database.patches_get(package_ahriman.base) == []
|
||||||
@ -76,9 +76,9 @@ def test_patches_insert_remove_by_variable(database: SQLite, package_ahriman: Pa
|
|||||||
"""
|
"""
|
||||||
must remove patch from database by variable
|
must remove patch from database by variable
|
||||||
"""
|
"""
|
||||||
database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1"))
|
database.patches_insert(package_ahriman.base, [PkgbuildPatch(None, "patch1")])
|
||||||
database.patches_insert(package_ahriman.base, PkgbuildPatch("key", "patch3"))
|
database.patches_insert(package_ahriman.base, [PkgbuildPatch("key", "patch3")])
|
||||||
database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch2"))
|
database.patches_insert(package_python_schedule.base, [PkgbuildPatch(None, "patch2")])
|
||||||
database.patches_remove(package_ahriman.base, ["key"])
|
database.patches_remove(package_ahriman.base, ["key"])
|
||||||
|
|
||||||
assert database.patches_get(package_ahriman.base) == [PkgbuildPatch(None, "patch1")]
|
assert database.patches_get(package_ahriman.base) == [PkgbuildPatch(None, "patch1")]
|
||||||
@ -89,8 +89,14 @@ def test_patches_insert_insert(database: SQLite, package_ahriman: Package) -> No
|
|||||||
"""
|
"""
|
||||||
must update patch in database
|
must update patch in database
|
||||||
"""
|
"""
|
||||||
database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1"))
|
database.patches_insert(package_ahriman.base, [PkgbuildPatch(None, "patch1")])
|
||||||
assert database.patches_get(package_ahriman.base) == [PkgbuildPatch(None, "patch1")]
|
assert database.patches_get(package_ahriman.base) == [PkgbuildPatch(None, "patch1")]
|
||||||
|
|
||||||
database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch2"))
|
database.patches_insert(package_ahriman.base, [PkgbuildPatch(None, "patch2")])
|
||||||
assert database.patches_get(package_ahriman.base) == [PkgbuildPatch(None, "patch2")]
|
assert database.patches_get(package_ahriman.base) == [PkgbuildPatch(None, "patch2")]
|
||||||
|
|
||||||
|
database.patches_insert(package_ahriman.base, [PkgbuildPatch(None, "patch3"), PkgbuildPatch("key", "patch4")])
|
||||||
|
assert database.patches_get(package_ahriman.base) == [
|
||||||
|
PkgbuildPatch(None, "patch3"),
|
||||||
|
PkgbuildPatch("key", "patch4"),
|
||||||
|
]
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
|
from unittest.mock import call as MockCall
|
||||||
|
|
||||||
from ahriman.core.exceptions import UnknownPackageError
|
from ahriman.core.exceptions import UnknownPackageError
|
||||||
from ahriman.core.status.watcher import Watcher
|
from ahriman.core.status.watcher import Watcher
|
||||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||||
from ahriman.models.log_record_id import LogRecordId
|
from ahriman.models.log_record_id import LogRecordId
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
|
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||||
|
|
||||||
|
|
||||||
def test_load(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
|
def test_load(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||||
@ -163,6 +165,42 @@ def test_package_update_unknown(watcher: Watcher, package_ahriman: Package) -> N
|
|||||||
watcher.package_update(package_ahriman.base, BuildStatusEnum.Unknown, None)
|
watcher.package_update(package_ahriman.base, BuildStatusEnum.Unknown, None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_patches_get(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must return patches for the package
|
||||||
|
"""
|
||||||
|
patches_mock = mocker.patch("ahriman.core.database.SQLite.patches_list")
|
||||||
|
|
||||||
|
watcher.patches_get(package_ahriman.base, None)
|
||||||
|
watcher.patches_get(package_ahriman.base, "var")
|
||||||
|
patches_mock.assert_has_calls([
|
||||||
|
MockCall(package_ahriman.base, None),
|
||||||
|
MockCall().get(package_ahriman.base, []),
|
||||||
|
MockCall(package_ahriman.base, ["var"]),
|
||||||
|
MockCall().get(package_ahriman.base, []),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def test_patches_remove(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must remove patches for the package
|
||||||
|
"""
|
||||||
|
patches_mock = mocker.patch("ahriman.core.database.SQLite.patches_remove")
|
||||||
|
watcher.patches_remove(package_ahriman.base, "var")
|
||||||
|
patches_mock.assert_called_once_with(package_ahriman.base, ["var"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_patches_update(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must update patches for the package
|
||||||
|
"""
|
||||||
|
patch = PkgbuildPatch("key", "value")
|
||||||
|
patches_mock = mocker.patch("ahriman.core.database.SQLite.patches_insert")
|
||||||
|
|
||||||
|
watcher.patches_update(package_ahriman.base, patch)
|
||||||
|
patches_mock.assert_called_once_with(package_ahriman.base, [patch])
|
||||||
|
|
||||||
|
|
||||||
def test_status_update(watcher: Watcher) -> None:
|
def test_status_update(watcher: Watcher) -> None:
|
||||||
"""
|
"""
|
||||||
must update service status
|
must update service status
|
||||||
|
@ -4,6 +4,7 @@ from pytest_mock import MockerFixture
|
|||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from ahriman.core.spawn import Spawn
|
from ahriman.core.spawn import Spawn
|
||||||
|
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||||
from ahriman.models.process_status import ProcessStatus
|
from ahriman.models.process_status import ProcessStatus
|
||||||
from ahriman.models.repository_id import RepositoryId
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
@ -56,11 +57,12 @@ def test_spawn_process(spawner: Spawn, repository_id: RepositoryId, mocker: Mock
|
|||||||
"""
|
"""
|
||||||
start_mock = mocker.patch("multiprocessing.Process.start")
|
start_mock = mocker.patch("multiprocessing.Process.start")
|
||||||
|
|
||||||
assert spawner._spawn_process(repository_id, "add", "ahriman", now="", maybe="?", none=None)
|
assert spawner._spawn_process(repository_id, "command", "argument",
|
||||||
|
empty="", string="v", list=["a", "b"], empty_list=[], none=None)
|
||||||
start_mock.assert_called_once_with()
|
start_mock.assert_called_once_with()
|
||||||
spawner.args_parser.parse_args.assert_called_once_with(
|
spawner.args_parser.parse_args.assert_called_once_with(
|
||||||
spawner.command_arguments + [
|
spawner.command_arguments + [
|
||||||
"add", "ahriman", "--now", "--maybe", "?"
|
"command", "argument", "--empty", "--string", "v", "--list", "a", "--list", "b",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -99,7 +101,7 @@ def test_packages_add(spawner: Spawn, repository_id: RepositoryId, mocker: Mocke
|
|||||||
must call package addition
|
must call package addition
|
||||||
"""
|
"""
|
||||||
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process")
|
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process")
|
||||||
assert spawner.packages_add(repository_id, ["ahriman", "linux"], None, now=False)
|
assert spawner.packages_add(repository_id, ["ahriman", "linux"], None, patches=[], now=False)
|
||||||
spawn_mock.assert_called_once_with(repository_id, "package-add", "ahriman", "linux", username=None)
|
spawn_mock.assert_called_once_with(repository_id, "package-add", "ahriman", "linux", username=None)
|
||||||
|
|
||||||
|
|
||||||
@ -108,7 +110,7 @@ def test_packages_add_with_build(spawner: Spawn, repository_id: RepositoryId, mo
|
|||||||
must call package addition with update
|
must call package addition with update
|
||||||
"""
|
"""
|
||||||
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process")
|
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process")
|
||||||
assert spawner.packages_add(repository_id, ["ahriman", "linux"], None, now=True)
|
assert spawner.packages_add(repository_id, ["ahriman", "linux"], None, patches=[], now=True)
|
||||||
spawn_mock.assert_called_once_with(repository_id, "package-add", "ahriman", "linux", username=None, now="")
|
spawn_mock.assert_called_once_with(repository_id, "package-add", "ahriman", "linux", username=None, now="")
|
||||||
|
|
||||||
|
|
||||||
@ -117,10 +119,21 @@ def test_packages_add_with_username(spawner: Spawn, repository_id: RepositoryId,
|
|||||||
must call package addition with username
|
must call package addition with username
|
||||||
"""
|
"""
|
||||||
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process")
|
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process")
|
||||||
assert spawner.packages_add(repository_id, ["ahriman", "linux"], "username", now=False)
|
assert spawner.packages_add(repository_id, ["ahriman", "linux"], "username", patches=[], now=False)
|
||||||
spawn_mock.assert_called_once_with(repository_id, "package-add", "ahriman", "linux", username="username")
|
spawn_mock.assert_called_once_with(repository_id, "package-add", "ahriman", "linux", username="username")
|
||||||
|
|
||||||
|
|
||||||
|
def test_packages_add_with_patches(spawner: Spawn, repository_id: RepositoryId, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must call package addition with patches
|
||||||
|
"""
|
||||||
|
patches = [PkgbuildPatch("key", "value"), PkgbuildPatch("key", "value")]
|
||||||
|
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process")
|
||||||
|
assert spawner.packages_add(repository_id, ["ahriman", "linux"], None, patches=patches, now=False)
|
||||||
|
spawn_mock.assert_called_once_with(repository_id, "package-add", "ahriman", "linux", username=None,
|
||||||
|
variable=[patch.serialize() for patch in patches])
|
||||||
|
|
||||||
|
|
||||||
def test_packages_rebuild(spawner: Spawn, repository_id: RepositoryId, mocker: MockerFixture) -> None:
|
def test_packages_rebuild(spawner: Spawn, repository_id: RepositoryId, mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must call package rebuild
|
must call package rebuild
|
||||||
|
@ -2,6 +2,7 @@ import datetime
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import pytest
|
import pytest
|
||||||
|
import shlex
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
@ -11,7 +12,7 @@ from unittest.mock import call as MockCall
|
|||||||
from ahriman.core.exceptions import BuildError, CalledProcessError, OptionError, UnsafeRunError
|
from ahriman.core.exceptions import BuildError, CalledProcessError, OptionError, UnsafeRunError
|
||||||
from ahriman.core.util import check_output, check_user, dataclass_view, enum_values, extract_user, filter_json, \
|
from ahriman.core.util import check_output, check_user, dataclass_view, enum_values, extract_user, filter_json, \
|
||||||
full_version, minmax, package_like, parse_version, partition, pretty_datetime, pretty_size, safe_filename, \
|
full_version, minmax, package_like, parse_version, partition, pretty_datetime, pretty_size, safe_filename, \
|
||||||
srcinfo_property, srcinfo_property_list, trim_package, utcnow, walk
|
srcinfo_property, srcinfo_property_list, trim_package, unquote, utcnow, walk
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.package_source import PackageSource
|
from ahriman.models.package_source import PackageSource
|
||||||
from ahriman.models.repository_paths import RepositoryPaths
|
from ahriman.models.repository_paths import RepositoryPaths
|
||||||
@ -443,6 +444,26 @@ def test_trim_package() -> None:
|
|||||||
assert trim_package("package: a description") == "package"
|
assert trim_package("package: a description") == "package"
|
||||||
|
|
||||||
|
|
||||||
|
def test_unquote() -> None:
|
||||||
|
"""
|
||||||
|
must remove quotation marks
|
||||||
|
"""
|
||||||
|
for source in (
|
||||||
|
"abc",
|
||||||
|
"ab'c",
|
||||||
|
"ab\"c",
|
||||||
|
):
|
||||||
|
assert unquote(shlex.quote(source)) == source
|
||||||
|
|
||||||
|
|
||||||
|
def test_unquote_error() -> None:
|
||||||
|
"""
|
||||||
|
must raise value error on invalid quotation
|
||||||
|
"""
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
unquote("ab'c")
|
||||||
|
|
||||||
|
|
||||||
def test_utcnow() -> None:
|
def test_utcnow() -> None:
|
||||||
"""
|
"""
|
||||||
must generate correct timestamp
|
must generate correct timestamp
|
||||||
|
@ -34,9 +34,18 @@ def test_quote() -> None:
|
|||||||
"""
|
"""
|
||||||
must quote strings if unsafe flag is not set
|
must quote strings if unsafe flag is not set
|
||||||
"""
|
"""
|
||||||
assert PkgbuildPatch("key", "value").quote("value") == """value"""
|
assert PkgbuildPatch.quote("value") == """value"""
|
||||||
assert PkgbuildPatch("key", "va'lue").quote("va'lue") == """'va'"'"'lue'"""
|
assert PkgbuildPatch.quote("va'lue") == """'va'"'"'lue'"""
|
||||||
assert PkgbuildPatch("key", "va'lue", unsafe=True).quote("va'lue") == """va'lue"""
|
|
||||||
|
|
||||||
|
def test_from_env() -> None:
|
||||||
|
"""
|
||||||
|
must construct patch from environment variable
|
||||||
|
"""
|
||||||
|
assert PkgbuildPatch.from_env("KEY=VALUE") == PkgbuildPatch("KEY", "VALUE")
|
||||||
|
assert PkgbuildPatch.from_env("KEY=VA=LUE") == PkgbuildPatch("KEY", "VA=LUE")
|
||||||
|
assert PkgbuildPatch.from_env("KEY=") == PkgbuildPatch("KEY", "")
|
||||||
|
assert PkgbuildPatch.from_env("KEY") == PkgbuildPatch("KEY", "")
|
||||||
|
|
||||||
|
|
||||||
def test_serialize() -> None:
|
def test_serialize() -> None:
|
||||||
@ -46,7 +55,19 @@ def test_serialize() -> None:
|
|||||||
assert PkgbuildPatch("key", "value").serialize() == "key=value"
|
assert PkgbuildPatch("key", "value").serialize() == "key=value"
|
||||||
assert PkgbuildPatch("key", "42").serialize() == "key=42"
|
assert PkgbuildPatch("key", "42").serialize() == "key=42"
|
||||||
assert PkgbuildPatch("key", "4'2").serialize() == """key='4'"'"'2'"""
|
assert PkgbuildPatch("key", "4'2").serialize() == """key='4'"'"'2'"""
|
||||||
assert PkgbuildPatch("key", "4'2", unsafe=True).serialize() == "key=4'2"
|
|
||||||
|
|
||||||
|
def test_from_env_serialize() -> None:
|
||||||
|
"""
|
||||||
|
must serialize and parse back
|
||||||
|
"""
|
||||||
|
for patch in (
|
||||||
|
PkgbuildPatch("key", "value"),
|
||||||
|
PkgbuildPatch("key", "4'2"),
|
||||||
|
PkgbuildPatch("arch", ["i686", "x86_64"]),
|
||||||
|
PkgbuildPatch("key", ["val'ue", "val\"ue2"]),
|
||||||
|
):
|
||||||
|
assert PkgbuildPatch.from_env(patch.serialize()) == patch
|
||||||
|
|
||||||
|
|
||||||
def test_serialize_plain_diff() -> None:
|
def test_serialize_plain_diff() -> None:
|
||||||
@ -60,7 +81,7 @@ def test_serialize_function() -> None:
|
|||||||
"""
|
"""
|
||||||
must correctly serialize function values
|
must correctly serialize function values
|
||||||
"""
|
"""
|
||||||
assert PkgbuildPatch("key()", "{ value }", unsafe=True).serialize() == "key() { value }"
|
assert PkgbuildPatch("key()", "{ value }").serialize() == "key() { value }"
|
||||||
|
|
||||||
|
|
||||||
def test_serialize_list() -> None:
|
def test_serialize_list() -> None:
|
||||||
@ -69,7 +90,13 @@ def test_serialize_list() -> None:
|
|||||||
"""
|
"""
|
||||||
assert PkgbuildPatch("arch", ["i686", "x86_64"]).serialize() == """arch=(i686 x86_64)"""
|
assert PkgbuildPatch("arch", ["i686", "x86_64"]).serialize() == """arch=(i686 x86_64)"""
|
||||||
assert PkgbuildPatch("key", ["val'ue", "val\"ue2"]).serialize() == """key=('val'"'"'ue' 'val"ue2')"""
|
assert PkgbuildPatch("key", ["val'ue", "val\"ue2"]).serialize() == """key=('val'"'"'ue' 'val"ue2')"""
|
||||||
assert PkgbuildPatch("key", ["val'ue", "val\"ue2"], unsafe=True).serialize() == """key=(val'ue val"ue2)"""
|
|
||||||
|
|
||||||
|
def test_view() -> None:
|
||||||
|
"""
|
||||||
|
must correctly serialize to json
|
||||||
|
"""
|
||||||
|
assert PkgbuildPatch("key", "value").view() == {"key": "key", "value": "value"}
|
||||||
|
|
||||||
|
|
||||||
def test_write(mocker: MockerFixture) -> None:
|
def test_write(mocker: MockerFixture) -> None:
|
||||||
|
@ -17,7 +17,7 @@ def test_id() -> None:
|
|||||||
"""
|
"""
|
||||||
must correctly generate id
|
must correctly generate id
|
||||||
"""
|
"""
|
||||||
assert RepositoryId("", "").id == "-"
|
assert RepositoryId("", "").id == ""
|
||||||
assert RepositoryId("arch", "repo").id == "arch-repo"
|
assert RepositoryId("arch", "repo").id == "arch-repo"
|
||||||
|
|
||||||
|
|
||||||
|
1
tests/ahriman/web/schemas/test_package_patch_schema.py
Normal file
1
tests/ahriman/web/schemas/test_package_patch_schema.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# schema testing goes in view class tests
|
1
tests/ahriman/web/schemas/test_patch_name_schema.py
Normal file
1
tests/ahriman/web/schemas/test_patch_name_schema.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# schema testing goes in view class tests
|
1
tests/ahriman/web/schemas/test_patch_schema.py
Normal file
1
tests/ahriman/web/schemas/test_patch_schema.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# schema testing goes in view class tests
|
1
tests/ahriman/web/schemas/test_versioned_log_schema.py
Normal file
1
tests/ahriman/web/schemas/test_versioned_log_schema.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# schema testing goes in view class tests
|
@ -4,6 +4,7 @@ from aiohttp.test_utils import TestClient
|
|||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
from unittest.mock import AsyncMock
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||||
from ahriman.models.repository_id import RepositoryId
|
from ahriman.models.repository_id import RepositoryId
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
from ahriman.web.views.v1.service.add import AddView
|
from ahriman.web.views.v1.service.add import AddView
|
||||||
@ -40,13 +41,42 @@ async def test_post(client: TestClient, repository_id: RepositoryId, mocker: Moc
|
|||||||
assert not request_schema.validate(payload)
|
assert not request_schema.validate(payload)
|
||||||
response = await client.post("/api/v1/service/add", json=payload)
|
response = await client.post("/api/v1/service/add", json=payload)
|
||||||
assert response.ok
|
assert response.ok
|
||||||
add_mock.assert_called_once_with(repository_id, ["ahriman"], "username", now=True)
|
add_mock.assert_called_once_with(repository_id, ["ahriman"], "username", patches=[], now=True)
|
||||||
|
|
||||||
json = await response.json()
|
json = await response.json()
|
||||||
assert json["process_id"] == "abc"
|
assert json["process_id"] == "abc"
|
||||||
assert not response_schema.validate(json)
|
assert not response_schema.validate(json)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_post_patches(client: TestClient, repository_id: RepositoryId, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must call post request with patches correctly
|
||||||
|
"""
|
||||||
|
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add", return_value="abc")
|
||||||
|
user_mock = AsyncMock()
|
||||||
|
user_mock.return_value = "username"
|
||||||
|
mocker.patch("ahriman.web.views.base.BaseView.username", side_effect=user_mock)
|
||||||
|
request_schema = pytest.helpers.schema_request(AddView.post)
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"packages": ["ahriman"],
|
||||||
|
"patches": [
|
||||||
|
{
|
||||||
|
"key": "k",
|
||||||
|
"value": "v",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "k2",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
assert not request_schema.validate(payload)
|
||||||
|
response = await client.post("/api/v1/service/add", json=payload)
|
||||||
|
assert response.ok
|
||||||
|
add_mock.assert_called_once_with(repository_id, ["ahriman"], "username",
|
||||||
|
patches=[PkgbuildPatch("k", "v"), PkgbuildPatch("k2", "")], now=True)
|
||||||
|
|
||||||
|
|
||||||
async def test_post_empty(client: TestClient, mocker: MockerFixture) -> None:
|
async def test_post_empty(client: TestClient, mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must call raise 400 on empty request
|
must call raise 400 on empty request
|
||||||
|
@ -4,6 +4,7 @@ from aiohttp.test_utils import TestClient
|
|||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
from unittest.mock import AsyncMock
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||||
from ahriman.models.repository_id import RepositoryId
|
from ahriman.models.repository_id import RepositoryId
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
from ahriman.web.views.v1.service.request import RequestView
|
from ahriman.web.views.v1.service.request import RequestView
|
||||||
@ -40,13 +41,42 @@ async def test_post(client: TestClient, repository_id: RepositoryId, mocker: Moc
|
|||||||
assert not request_schema.validate(payload)
|
assert not request_schema.validate(payload)
|
||||||
response = await client.post("/api/v1/service/request", json=payload)
|
response = await client.post("/api/v1/service/request", json=payload)
|
||||||
assert response.ok
|
assert response.ok
|
||||||
add_mock.assert_called_once_with(repository_id, ["ahriman"], "username", now=False)
|
add_mock.assert_called_once_with(repository_id, ["ahriman"], "username", patches=[], now=False)
|
||||||
|
|
||||||
json = await response.json()
|
json = await response.json()
|
||||||
assert json["process_id"] == "abc"
|
assert json["process_id"] == "abc"
|
||||||
assert not response_schema.validate(json)
|
assert not response_schema.validate(json)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_post_patches(client: TestClient, repository_id: RepositoryId, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must call post request with patches correctly
|
||||||
|
"""
|
||||||
|
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add", return_value="abc")
|
||||||
|
user_mock = AsyncMock()
|
||||||
|
user_mock.return_value = "username"
|
||||||
|
mocker.patch("ahriman.web.views.base.BaseView.username", side_effect=user_mock)
|
||||||
|
request_schema = pytest.helpers.schema_request(RequestView.post)
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"packages": ["ahriman"],
|
||||||
|
"patches": [
|
||||||
|
{
|
||||||
|
"key": "k",
|
||||||
|
"value": "v",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "k2",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
assert not request_schema.validate(payload)
|
||||||
|
response = await client.post("/api/v1/service/request", json=payload)
|
||||||
|
assert response.ok
|
||||||
|
add_mock.assert_called_once_with(repository_id, ["ahriman"], "username",
|
||||||
|
patches=[PkgbuildPatch("k", "v"), PkgbuildPatch("k2", "")], now=False)
|
||||||
|
|
||||||
|
|
||||||
async def test_post_exception(client: TestClient, mocker: MockerFixture) -> None:
|
async def test_post_exception(client: TestClient, mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must raise exception on missing packages payload
|
must raise exception on missing packages payload
|
||||||
|
@ -0,0 +1,81 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from aiohttp.test_utils import TestClient
|
||||||
|
|
||||||
|
from ahriman.models.build_status import BuildStatusEnum
|
||||||
|
from ahriman.models.package import Package
|
||||||
|
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||||
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.views.v1.status.patch import PatchView
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_permission() -> None:
|
||||||
|
"""
|
||||||
|
must return correct permission for the request
|
||||||
|
"""
|
||||||
|
for method in ("GET",):
|
||||||
|
request = pytest.helpers.request("", "", method)
|
||||||
|
assert await PatchView.get_permission(request) == UserAccess.Reporter
|
||||||
|
for method in ("DELETE",):
|
||||||
|
request = pytest.helpers.request("", "", method)
|
||||||
|
assert await PatchView.get_permission(request) == UserAccess.Full
|
||||||
|
|
||||||
|
|
||||||
|
def test_routes() -> None:
|
||||||
|
"""
|
||||||
|
must return correct routes
|
||||||
|
"""
|
||||||
|
assert PatchView.ROUTES == ["/api/v1/packages/{package}/patches/{patch}"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_delete(client: TestClient, package_ahriman: Package, package_python_schedule: Package) -> None:
|
||||||
|
"""
|
||||||
|
must delete patch for package
|
||||||
|
"""
|
||||||
|
await client.post(f"/api/v1/packages/{package_ahriman.base}",
|
||||||
|
json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
|
||||||
|
await client.post(f"/api/v1/packages/{package_python_schedule.base}",
|
||||||
|
json={"status": BuildStatusEnum.Success.value, "package": package_python_schedule.view()})
|
||||||
|
|
||||||
|
patch_key = "k"
|
||||||
|
await client.post(f"/api/v1/packages/{package_ahriman.base}/patches", json={"key": patch_key, "value": "v"})
|
||||||
|
await client.post(f"/api/v1/packages/{package_python_schedule.base}/patches",
|
||||||
|
json={"key": patch_key, "value": "v2"})
|
||||||
|
|
||||||
|
response = await client.delete(f"/api/v1/packages/{package_ahriman.base}/patches/{patch_key}")
|
||||||
|
assert response.status == 204
|
||||||
|
|
||||||
|
response = await client.get(f"/api/v1/packages/{package_ahriman.base}/patches")
|
||||||
|
assert not await response.json()
|
||||||
|
|
||||||
|
response = await client.get(f"/api/v1/packages/{package_python_schedule.base}/patches")
|
||||||
|
assert await response.json()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get(client: TestClient, package_ahriman: Package) -> None:
|
||||||
|
"""
|
||||||
|
must get patch for package
|
||||||
|
"""
|
||||||
|
patch = PkgbuildPatch("k", "v")
|
||||||
|
await client.post(f"/api/v1/packages/{package_ahriman.base}",
|
||||||
|
json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
|
||||||
|
await client.post(f"/api/v1/packages/{package_ahriman.base}/patches", json=patch.view())
|
||||||
|
response_schema = pytest.helpers.schema_response(PatchView.get)
|
||||||
|
|
||||||
|
response = await client.get(f"/api/v1/packages/{package_ahriman.base}/patches/{patch.key}")
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
patches = await response.json()
|
||||||
|
assert not response_schema.validate(patches)
|
||||||
|
assert patches == patch.view()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_not_found(client: TestClient, package_ahriman: Package) -> None:
|
||||||
|
"""
|
||||||
|
must return not found for missing package
|
||||||
|
"""
|
||||||
|
response_schema = pytest.helpers.schema_response(PatchView.get, code=404)
|
||||||
|
|
||||||
|
response = await client.get(f"/api/v1/packages/{package_ahriman.base}/patches/random")
|
||||||
|
assert response.status == 404
|
||||||
|
assert not response_schema.validate(await response.json())
|
@ -0,0 +1,75 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from aiohttp.test_utils import TestClient
|
||||||
|
|
||||||
|
from ahriman.models.build_status import BuildStatusEnum
|
||||||
|
from ahriman.models.package import Package
|
||||||
|
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||||
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.views.v1.status.patches import PatchesView
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_permission() -> None:
|
||||||
|
"""
|
||||||
|
must return correct permission for the request
|
||||||
|
"""
|
||||||
|
for method in ("GET",):
|
||||||
|
request = pytest.helpers.request("", "", method)
|
||||||
|
assert await PatchesView.get_permission(request) == UserAccess.Reporter
|
||||||
|
for method in ("POST",):
|
||||||
|
request = pytest.helpers.request("", "", method)
|
||||||
|
assert await PatchesView.get_permission(request) == UserAccess.Full
|
||||||
|
|
||||||
|
|
||||||
|
def test_routes() -> None:
|
||||||
|
"""
|
||||||
|
must return correct routes
|
||||||
|
"""
|
||||||
|
assert PatchesView.ROUTES == ["/api/v1/packages/{package}/patches"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get(client: TestClient, package_ahriman: Package) -> None:
|
||||||
|
"""
|
||||||
|
must get patch for package
|
||||||
|
"""
|
||||||
|
patch = PkgbuildPatch("k", "v")
|
||||||
|
await client.post(f"/api/v1/packages/{package_ahriman.base}",
|
||||||
|
json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
|
||||||
|
await client.post(f"/api/v1/packages/{package_ahriman.base}/patches", json=patch.view())
|
||||||
|
response_schema = pytest.helpers.schema_response(PatchesView.get)
|
||||||
|
|
||||||
|
response = await client.get(f"/api/v1/packages/{package_ahriman.base}/patches")
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
patches = await response.json()
|
||||||
|
assert not response_schema.validate(patches)
|
||||||
|
assert patches == [patch.view()]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_post(client: TestClient, package_ahriman: Package) -> None:
|
||||||
|
"""
|
||||||
|
must create patch
|
||||||
|
"""
|
||||||
|
await client.post(f"/api/v1/packages/{package_ahriman.base}",
|
||||||
|
json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
|
||||||
|
request_schema = pytest.helpers.schema_request(PatchesView.post)
|
||||||
|
|
||||||
|
payload = {"key": "k", "value": "v"}
|
||||||
|
assert not request_schema.validate(payload)
|
||||||
|
response = await client.post(f"/api/v1/packages/{package_ahriman.base}/patches", json=payload)
|
||||||
|
assert response.status == 204
|
||||||
|
|
||||||
|
response = await client.get(f"/api/v1/packages/{package_ahriman.base}/patches")
|
||||||
|
patches = await response.json()
|
||||||
|
assert patches == [payload]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_post_exception(client: TestClient, package_ahriman: Package) -> None:
|
||||||
|
"""
|
||||||
|
must raise exception on invalid payload
|
||||||
|
"""
|
||||||
|
response_schema = pytest.helpers.schema_response(PatchesView.post, code=400)
|
||||||
|
|
||||||
|
response = await client.post(f"/api/v1/packages/{package_ahriman.base}/patches", json={})
|
||||||
|
assert response.status == 400
|
||||||
|
assert not response_schema.validate(await response.json())
|
@ -44,7 +44,16 @@ async def test_get(client: TestClient, package_ahriman: Package) -> None:
|
|||||||
|
|
||||||
logs = await response.json()
|
logs = await response.json()
|
||||||
assert not response_schema.validate(logs)
|
assert not response_schema.validate(logs)
|
||||||
assert logs["logs"] == [[42.0, "message 1"], [43.0, "message 2"]]
|
assert logs == [
|
||||||
|
{
|
||||||
|
"created": 42.0,
|
||||||
|
"message": "message 1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"created": 43.0,
|
||||||
|
"message": "message 2",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
async def test_get_with_pagination(client: TestClient, package_ahriman: Package) -> None:
|
async def test_get_with_pagination(client: TestClient, package_ahriman: Package) -> None:
|
||||||
@ -67,18 +76,7 @@ async def test_get_with_pagination(client: TestClient, package_ahriman: Package)
|
|||||||
|
|
||||||
logs = await response.json()
|
logs = await response.json()
|
||||||
assert not response_schema.validate(logs)
|
assert not response_schema.validate(logs)
|
||||||
assert logs["logs"] == [[43.0, "message 2"]]
|
assert logs == [{"created": 43.0, "message": "message 2"}]
|
||||||
|
|
||||||
|
|
||||||
async def test_get_not_found(client: TestClient, package_ahriman: Package) -> None:
|
|
||||||
"""
|
|
||||||
must return not found for missing package
|
|
||||||
"""
|
|
||||||
response_schema = pytest.helpers.schema_response(LogsView.get, code=404)
|
|
||||||
|
|
||||||
response = await client.get(f"/api/v2/packages/{package_ahriman.base}/logs")
|
|
||||||
assert response.status == 404
|
|
||||||
assert not response_schema.validate(await response.json())
|
|
||||||
|
|
||||||
|
|
||||||
async def test_get_bad_request(client: TestClient, package_ahriman: Package) -> None:
|
async def test_get_bad_request(client: TestClient, package_ahriman: Package) -> None:
|
||||||
|
Loading…
Reference in New Issue
Block a user