Improve artifact availability UX
This commit is contained in:
@@ -232,7 +232,7 @@ Das Plugin unter `netbox_plugin/` ist auf NetBox 4.6.5–4.6.8 begrenzt. Zuerst
|
||||
```text
|
||||
cd netbox_plugin
|
||||
python -m build
|
||||
/opt/netbox/venv/bin/pip install dist/netbox_plugin_store-0.1.1-py3-none-any.whl
|
||||
/opt/netbox/venv/bin/pip install dist/netbox_plugin_store-0.1.2-py3-none-any.whl
|
||||
```
|
||||
|
||||
Das Paket muss außerdem in `/opt/netbox/local_requirements.txt` festgehalten werden. In `configuration.py` wird es zunächst sicher im Dry-run-Modus eingerichtet:
|
||||
|
||||
@@ -22,7 +22,7 @@ The safe default is intentionally non-mutating. Installing the UI alone never gr
|
||||
Install the wheel into NetBox's virtual environment and persist it in `/opt/netbox/local_requirements.txt`:
|
||||
|
||||
```text
|
||||
netbox-plugin-store==0.1.1
|
||||
netbox-plugin-store==0.1.2
|
||||
```
|
||||
|
||||
Add the plugin to `configuration.py`:
|
||||
|
||||
@@ -166,7 +166,7 @@ class CatalogPlugin:
|
||||
)
|
||||
|
||||
def select_release(self, netbox_version: str, requested_version: str = "") -> Release:
|
||||
compatible = [release for release in self.releases if release.supports(netbox_version, self)]
|
||||
compatible = list(self.installable_releases(netbox_version))
|
||||
if requested_version:
|
||||
compatible = [release for release in compatible if release.version == requested_version]
|
||||
elif self.latest_version:
|
||||
@@ -177,6 +177,26 @@ class CatalogPlugin:
|
||||
raise StoreClientError("No compatible release is available for this NetBox version.")
|
||||
return max(compatible, key=lambda release: Version(release.version))
|
||||
|
||||
def installable_releases(
|
||||
self,
|
||||
netbox_version: str,
|
||||
*,
|
||||
require_approval_marker: bool = False,
|
||||
) -> tuple[Release, ...]:
|
||||
return tuple(
|
||||
release
|
||||
for release in self.releases
|
||||
if (
|
||||
release.supports(netbox_version, self)
|
||||
and release.approved
|
||||
and release.immutable
|
||||
and bool(release.download_url)
|
||||
and bool(release.sha256)
|
||||
and release.artifact_kind in {"wheel", "source_archive"}
|
||||
and (not require_approval_marker or bool(release.approved_payload_sha256))
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class StoreClient:
|
||||
API_LIMIT = 5 * 1024 * 1024
|
||||
|
||||
@@ -34,21 +34,20 @@ class LifecycleConfirmForm(forms.Form):
|
||||
self.runtime = runtime
|
||||
self.fields["confirmation"].widget.attrs.update({"autocomplete": "off", "placeholder": plugin.slug})
|
||||
self.fields["dry_run"].initial = runtime.default_dry_run
|
||||
self.has_installable_release = True
|
||||
if action in {"install", "update"}:
|
||||
choices = [
|
||||
(release.version, release.version)
|
||||
for release in plugin.releases
|
||||
if (
|
||||
release.supports(runtime.netbox_version, plugin)
|
||||
and release.approved
|
||||
and release.immutable
|
||||
and bool(release.sha256)
|
||||
and (runtime.execution_mode != "agent" or bool(release.approved_payload_sha256))
|
||||
for release in plugin.installable_releases(
|
||||
runtime.netbox_version,
|
||||
require_approval_marker=runtime.execution_mode == "agent",
|
||||
)
|
||||
]
|
||||
choices.sort(reverse=True)
|
||||
self.fields["version"].choices = choices
|
||||
self.fields["version"].required = True
|
||||
self.has_installable_release = bool(choices)
|
||||
self.fields["version"].required = self.has_installable_release
|
||||
self.fields["version"].disabled = not self.has_installable_release
|
||||
self.fields["version"].initial = plugin.latest_version
|
||||
else:
|
||||
self.fields.pop("version")
|
||||
@@ -64,6 +63,11 @@ class LifecycleConfirmForm(forms.Form):
|
||||
|
||||
def clean(self):
|
||||
cleaned = super().clean()
|
||||
if self.action in {"install", "update"} and not self.has_installable_release:
|
||||
raise forms.ValidationError(
|
||||
"Keine freigegebene, kompatible Plugin-Version ist verfügbar. "
|
||||
"Das Source-Artefakt muss zunächst im Store synchronisiert und freigegeben werden."
|
||||
)
|
||||
if self.runtime.execution_mode == "dry_run" and not cleaned.get("dry_run", False):
|
||||
self.add_error("dry_run", "Reale Aktionen sind in execution_mode=dry_run deaktiviert.")
|
||||
return cleaned
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<p class="text-secondary mb-0">Freigegebene Plugins für diese NetBox-Instanz.</p>
|
||||
</div>
|
||||
{% if runtime %}
|
||||
<span class="badge {% if runtime.execution_mode == 'dry_run' %}bg-yellow text-dark{% else %}bg-green{% endif %}">
|
||||
<span class="badge {% if runtime.execution_mode == 'dry_run' %}text-bg-warning{% else %}text-bg-success{% endif %}">
|
||||
Modus: {{ runtime.execution_mode }}
|
||||
</span>
|
||||
{% endif %}
|
||||
@@ -29,14 +29,14 @@
|
||||
<div class="d-flex justify-content-between">
|
||||
<h3 class="card-title">{{ card.plugin.name }}</h3>
|
||||
{% if card.plugin.approved %}
|
||||
<span class="badge bg-green">Freigegeben</span>
|
||||
<span class="badge text-bg-success">Freigegeben</span>
|
||||
{% else %}
|
||||
<span class="badge bg-red">Nicht freigegeben</span>
|
||||
<span class="badge text-bg-danger">Nicht freigegeben</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p class="text-secondary">{{ card.plugin.summary|default:"Keine Zusammenfassung vorhanden." }}</p>
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-5">Verfügbar</dt><dd class="col-7">{{ card.plugin.latest_version|default:"–" }}</dd>
|
||||
<dt class="col-5">Verfügbar</dt><dd class="col-7">{{ card.plugin.latest_version|default:"Kein Artefakt freigegeben" }}</dd>
|
||||
<dt class="col-5">Installiert</dt><dd class="col-7">{{ card.installed_version|default:"–" }}</dd>
|
||||
<dt class="col-5">Status</dt>
|
||||
<dd class="col-7">
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
{% if runtime.execution_mode == 'dry_run' %}
|
||||
<div class="alert alert-info">Diese Instanz erlaubt ausschließlich Dry-Runs.</div>
|
||||
{% endif %}
|
||||
{% if not form.has_installable_release %}
|
||||
<div class="alert alert-warning">
|
||||
Keine freigegebene, kompatible Version verfügbar. Synchronisiere das Repository im Store-Adminbereich und gib danach das erzeugte Source-Artefakt unter „Release-Artefakte“ frei.
|
||||
</div>
|
||||
{% endif %}
|
||||
<form method="post" action="{% url 'plugins:netbox_plugin_store:lifecycle-action' slug=plugin.slug action=action %}">
|
||||
{% csrf_token %}
|
||||
{{ form.non_field_errors }}
|
||||
@@ -30,7 +35,7 @@
|
||||
{% endfor %}
|
||||
<div class="d-flex justify-content-between">
|
||||
<a class="btn btn-outline-secondary" href="{% url 'plugins:netbox_plugin_store:plugin-detail' slug=plugin.slug %}">Abbrechen</a>
|
||||
<button class="btn btn-danger" type="submit">Bestätigen</button>
|
||||
<button class="btn btn-danger" type="submit" {% if not form.has_installable_release %}disabled{% endif %}>Bestätigen</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,12 @@
|
||||
{% if local_status and local_status.restart_required %}
|
||||
<div class="alert alert-warning">Ein Neustart von NetBox und den Workern ist erforderlich.</div>
|
||||
{% endif %}
|
||||
{% if plugin.approved and not has_installable_release %}
|
||||
<div class="alert alert-warning">
|
||||
Für dieses Plugin ist noch kein freigegebenes, mit NetBox {{ runtime.netbox_version }} kompatibles Artefakt verfügbar.
|
||||
Repositorys ohne eigenen Release werden beim Store-Sync als commitgebundenes Source-Archiv eingelesen; dieses muss anschließend im Store-Adminbereich unter „Release-Artefakte“ freigegeben werden.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
@@ -55,9 +61,9 @@
|
||||
<div class="list-group-item d-flex justify-content-between">
|
||||
<span>{{ item.release.version }}</span>
|
||||
{% if item.compatible and item.release.approved and item.release.immutable and item.release.sha256 %}
|
||||
<span class="badge bg-green">installierbar</span>
|
||||
<span class="badge text-bg-success">installierbar</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">gesperrt</span>
|
||||
<span class="badge text-bg-secondary">gesperrt</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% empty %}
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.1.1"
|
||||
__version__ = "0.1.2"
|
||||
|
||||
@@ -83,12 +83,16 @@ class PluginDetailView(StorePermissionMixin, View):
|
||||
except StoreClientError as exc:
|
||||
raise Http404(redact_text(exc)) from exc
|
||||
state = _plugin_state(plugin, set(django_settings.PLUGINS))
|
||||
installable_releases = plugin.installable_releases(
|
||||
runtime.netbox_version,
|
||||
require_approval_marker=runtime.execution_mode == "agent",
|
||||
)
|
||||
actions: list[str] = []
|
||||
if not state["installed"]:
|
||||
if plugin.approved:
|
||||
if plugin.approved and installable_releases:
|
||||
actions.append("install")
|
||||
else:
|
||||
if plugin.approved:
|
||||
if plugin.approved and installable_releases:
|
||||
actions.append("update")
|
||||
if state["enabled"]:
|
||||
actions.append("disable")
|
||||
@@ -107,7 +111,9 @@ class PluginDetailView(StorePermissionMixin, View):
|
||||
"state": state,
|
||||
"actions": actions,
|
||||
"releases": releases,
|
||||
"has_installable_release": bool(installable_releases),
|
||||
"local_status": local_status,
|
||||
"runtime": runtime,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "netbox-plugin-store"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
description = "A secure NetBox 4.6 plugin lifecycle client for the MrBlake Plugin Store"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
@@ -58,6 +58,27 @@ class URLPolicyTests(unittest.TestCase):
|
||||
with self.assertRaises(StoreClientError):
|
||||
plugin.select_release("4.6.9")
|
||||
|
||||
def test_only_verified_supported_artifacts_are_installable(self):
|
||||
payload, _ = plugin_payload()
|
||||
payload["releases"].append(
|
||||
{
|
||||
"version": "1.3.0",
|
||||
"download_url": "https://store.example/source.tar.gz",
|
||||
"sha256": "b" * 64,
|
||||
"approved": False,
|
||||
"immutable": True,
|
||||
"artifact_kind": "source_archive",
|
||||
"min_netbox_version": "4.6.5",
|
||||
"max_netbox_version": "4.6.8",
|
||||
}
|
||||
)
|
||||
plugin = CatalogPlugin.from_mapping(payload)
|
||||
|
||||
self.assertEqual(
|
||||
tuple(release.version for release in plugin.installable_releases("4.6.8")),
|
||||
("1.2.0",),
|
||||
)
|
||||
|
||||
|
||||
class _Response(io.BytesIO):
|
||||
def __init__(self, body):
|
||||
|
||||
@@ -126,7 +126,7 @@ include dirname(__DIR__) . '/partials/head.php';
|
||||
<table class="admin-table release-admin-table">
|
||||
<thead><tr><th>Plugin / Version</th><th>Artefakt</th><th>Integrität</th><th>Status</th><th>Aktionen</th></tr></thead>
|
||||
<tbody>
|
||||
<?php if (($releases ?? []) === []): ?><tr><td colspan="5" class="empty-row">Keine installierbaren Artefakte gefunden. Prüfe Version und Build-Metadaten des Repositorys.</td></tr><?php endif; ?>
|
||||
<?php if (($releases ?? []) === []): ?><tr><td colspan="5" class="empty-row">Keine installierbaren Artefakte gefunden. Bei Repositorys ohne Release erzeugt eine erneute Synchronisierung automatisch ein commitgebundenes Source-Artefakt, sofern das Python-Paket eine Version definiert.</td></tr><?php endif; ?>
|
||||
<?php foreach (($releases ?? []) as $release): ?>
|
||||
<?php $releasePlugin = $release['plugin'] ?? []; $releaseErrors = Approval::releaseErrors($releasePlugin, $release); ?>
|
||||
<tr>
|
||||
|
||||
Reference in New Issue
Block a user