(feature) Add staff updater page

Add a Django updater view for checking configured Gitea releases, inspecting
the installed git checkout, fetching tags, pulling the current branch, and
running the configured native systemd update command.

Document the updater environment settings and keep the page staff-only so
readonly status users cannot trigger deployment actions.
This commit is contained in:
2026-05-28 22:10:45 +02:00
parent b4833560b5
commit 0450f8bdb0
11 changed files with 516 additions and 0 deletions

View File

@@ -62,6 +62,7 @@ class ViewTests(TestCase):
self.assertContains(response, reverse("ssh_credentials"))
self.assertContains(response, reverse("notification_targets"))
self.assertContains(response, reverse("logs"))
self.assertContains(response, reverse("updater"))
self.assertContains(response, reverse("purged_snapshots"))
self.assertContains(response, reverse("self_check"))
self.assertContains(response, reverse("changelog"))
@@ -82,6 +83,7 @@ class ViewTests(TestCase):
self.assertNotContains(response, reverse("ssh_credentials"))
self.assertNotContains(response, reverse("notification_targets"))
self.assertNotContains(response, reverse("logs"))
self.assertNotContains(response, reverse("updater"))
self.assertNotContains(response, reverse("self_check"))
self.assertNotContains(response, reverse("admin:index"))
@@ -135,6 +137,7 @@ class ViewTests(TestCase):
blocked_urls = [
reverse("ssh_credentials"),
reverse("logs"),
reverse("updater"),
reverse("self_check"),
reverse("edit_global_config"),
reverse("create_host_config"),
@@ -147,6 +150,90 @@ class ViewTests(TestCase):
response = self.client.get(url)
self.assertEqual(response.status_code, 403)
def test_updater_view_renders_status(self) -> None:
self.client.force_login(self.staff_user)
with patch("pobsync_backend.views.collect_update_status") as collect_update_status:
collect_update_status.return_value = {
"app_dir": "/opt/pobsync/app",
"installed_version": "1.1.0",
"release_check_configured": True,
"update_command": "sudo -n scripts/update-systemd",
"git_remote": "origin",
"git": {"branch": "master", "commit": "abc1234", "describe": "v1.1.0"},
"latest_release": None,
"release_error": "",
"update_available": None,
}
response = self.client.get(reverse("updater"))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Updater")
self.assertContains(response, "1.1.0")
self.assertContains(response, "master")
collect_update_status.assert_called_once_with(check_release=False)
def test_updater_check_release_requests_release_status(self) -> None:
self.client.force_login(self.staff_user)
with patch("pobsync_backend.views.collect_update_status") as collect_update_status:
collect_update_status.return_value = {
"app_dir": "/opt/pobsync/app",
"installed_version": "1.1.0",
"release_check_configured": True,
"update_command": "sudo -n scripts/update-systemd",
"git_remote": "origin",
"git": {"branch": "master", "commit": "abc1234", "describe": "v1.1.0"},
"latest_release": {"tag_name": "v1.2.0", "html_url": "https://code.example.test/releases/v1.2.0"},
"release_error": "",
"update_available": True,
}
response = self.client.post(reverse("updater"), {"action": "check_release"})
self.assertEqual(response.status_code, 200)
self.assertContains(response, "update available")
self.assertContains(response, "v1.2.0")
collect_update_status.assert_called_once_with(check_release=True)
def test_updater_git_fetch_runs_action_and_renders_output(self) -> None:
self.client.force_login(self.staff_user)
result = type(
"Result",
(),
{
"ok": True,
"exit_code": 0,
"command": ["git", "fetch", "--tags", "--prune", "origin"],
"stdout": "fetched",
"stderr": "",
},
)()
with patch("pobsync_backend.views.run_git_fetch", return_value=result) as run_git_fetch, patch(
"pobsync_backend.views.collect_update_status"
) as collect_update_status:
collect_update_status.return_value = {
"app_dir": "/opt/pobsync/app",
"installed_version": "1.1.0",
"release_check_configured": False,
"update_command": "sudo -n scripts/update-systemd",
"git_remote": "origin",
"git": {"branch": "master", "commit": "abc1234", "describe": "v1.1.0"},
"latest_release": None,
"release_error": "",
"update_available": None,
}
response = self.client.post(reverse("updater"), {"action": "git_fetch"})
self.assertEqual(response.status_code, 200)
self.assertContains(response, "fetched")
self.assertContains(response, "Updater action completed successfully.")
run_git_fetch.assert_called_once_with()
collect_update_status.assert_called_once_with(check_release=True)
def test_readonly_host_detail_hides_backup_controls_and_sensitive_config(self) -> None:
self.client.force_login(self.readonly_user)
GlobalConfig.objects.create(name="default", backup_root="/backups")