open Manage straight to the list and simplify registry rows

This commit is contained in:
MacRimi
2026-10-17 21:49:00 +02:00
parent 750421a12f
commit d8ae19c76e
6 changed files with 65 additions and 9 deletions
@@ -42,13 +42,13 @@ class SelectionSetupWording(TestCase):
shutil=SimpleNamespace(which=lambda _: '/fake/pct'), saved_inventory=lambda _: [], shutil=SimpleNamespace(which=lambda _: '/fake/pct'), saved_inventory=lambda _: [],
_clean_orphans=lambda project: None, _clean_orphans=lambda project: None,
recover_automatically=lambda project: None, offer_recovery=lambda project, ui: None, recover_automatically=lambda project: None, offer_recovery=lambda project, ui: None,
carry_records=lambda project, **options: {}, carry_records=lambda project, **options: {}, _records_unread=lambda project: False,
translate=lambda s: s) translate=lambda s: s)
fn(Path('/fixture'), ui) fn(Path('/fixture'), ui)
ui.message.assert_called_once_with(EMPTY, 'OCI management') ui.message.assert_called_once_with(EMPTY, 'OCI management')
ui.choose.assert_not_called() ui.choose.assert_not_called()
def _menu(self, inventories, selections): def _menu(self, inventories, selections, unread=True):
rows = iter(inventories) rows = iter(inventories)
ui = SimpleNamespace(message=Mock(), choose=Mock(side_effect=selections)) ui = SimpleNamespace(message=Mock(), choose=Mock(side_effect=selections))
refresh = Mock(return_value={}) refresh = Mock(return_value={})
@@ -57,7 +57,8 @@ class SelectionSetupWording(TestCase):
shutil=SimpleNamespace(which=lambda _: '/fake/pct'), saved_inventory=lambda _: next(rows), shutil=SimpleNamespace(which=lambda _: '/fake/pct'), saved_inventory=lambda _: next(rows),
_clean_orphans=lambda project: None, _clean_orphans=lambda project: None,
recover_automatically=lambda project: None, offer_recovery=lambda project, ui: None, recover_automatically=lambda project: None, offer_recovery=lambda project, ui: None,
carry_records=refresh, manage_instance=manage, menu_size=lambda: 10, carry_records=refresh, _records_unread=lambda project: unread,
manage_instance=manage, menu_size=lambda: 10,
STATUS_LABELS={}, translate=lambda s: s) STATUS_LABELS={}, translate=lambda s: s)
fn(Path('/fixture'), ui) fn(Path('/fixture'), ui)
return ui, refresh, manage return ui, refresh, manage
@@ -67,7 +68,14 @@ class SelectionSetupWording(TestCase):
ui, refresh, manage = self._menu([[row], [row]], ['101', None]) ui, refresh, manage = self._menu([[row], [row]], ['101', None])
self.assertEqual(ui.choose.call_count, 2) self.assertEqual(ui.choose.call_count, 2)
manage.assert_called_once() manage.assert_called_once()
refresh.assert_called_once_with(Path('/fixture'), mount_stopped=False, shown=True) refresh.assert_called_once_with(Path('/fixture'), mount_stopped=False, shown=True,
heading='Manage installed OCI applications')
def test_the_list_opens_without_the_sync_notice_once_every_configuration_is_cached(self):
row = {'vmid': 101, 'title': 'Jellyfin', 'hostname': 'jellyfin', 'status': 'installed', 'image': 'jellyfin/jellyfin'}
_, refresh, _ = self._menu([[row]], [None], unread=False)
refresh.assert_called_once_with(Path('/fixture'), mount_stopped=False, shown=False,
heading='Manage installed OCI applications')
def test_removing_the_last_application_returns_without_a_message(self): def test_removing_the_last_application_returns_without_a_message(self):
row = {'vmid': 101, 'title': 'Jellyfin', 'hostname': 'jellyfin', 'status': 'installed', 'image': 'jellyfin/jellyfin'} row = {'vmid': 101, 'title': 'Jellyfin', 'hostname': 'jellyfin', 'status': 'installed', 'image': 'jellyfin/jellyfin'}
@@ -50,6 +50,7 @@ jobs:
PYTHONDONTWRITEBYTECODE: '1' PYTHONDONTWRITEBYTECODE: '1'
run: | run: |
python3 -m unittest discover -s oci/tests -p test_modify_menu.py -v python3 -m unittest discover -s oci/tests -p test_modify_menu.py -v
python3 -m unittest discover -s oci/tests -p test_manage_sync_notice.py -v
python3 -m unittest discover -s oci/tests -p test_recreation.py -v python3 -m unittest discover -s oci/tests -p test_recreation.py -v
python3 -m unittest discover -s oci/tests -p test_shared_directories.py -v python3 -m unittest discover -s oci/tests -p test_shared_directories.py -v
python3 -m unittest discover -s oci/tests -p 'test_wizard*.py' -v python3 -m unittest discover -s oci/tests -p 'test_wizard*.py' -v
-2
View File
@@ -578,8 +578,6 @@ def interactive(catalog: Catalog) -> int:
f"{translate('All applications')} ({len(applications)})") f"{translate('All applications')} ({len(applications)})")
elif selection == "manage": elif selection == "manage":
from .management import interactive_management from .management import interactive_management
console.show_logo()
console.msg_title(translate("Manage installed OCI applications"))
interactive_management(PROJECT_ROOT, ui) interactive_management(PROJECT_ROOT, ui)
elif selection == "registries": elif selection == "registries":
from .registry_accounts import open_registry_accounts from .registry_accounts import open_registry_accounts
+17 -2
View File
@@ -132,7 +132,7 @@ def _run_lifecycle(command, title):
return completed.returncode == 0 return completed.returncode == 0
def carry_records(project, mount_stopped=True, vmids=None, verify=False, shown=False): def carry_records(project, mount_stopped=True, vmids=None, verify=False, shown=False, heading=None):
"""Leave inside each container the copy of its record that a restore on """Leave inside each container the copy of its record that a restore on
another host needs. It is refreshed after every operation. Returns what another host needs. It is refreshed after every operation. Returns what
was found for each container. Every installed application is looked at, was found for each container. Every installed application is looked at,
@@ -140,6 +140,9 @@ def carry_records(project, mount_stopped=True, vmids=None, verify=False, shown=F
from . import console from . import console
sys.path.insert(0, str(project / 'remote')) sys.path.insert(0, str(project / 'remote'))
if shown: if shown:
if heading:
console.show_logo()
console.msg_title(heading)
console.msg_info(translate('Updating the copy of the record...')) console.msg_info(translate('Updating the copy of the record...'))
try: try:
import oci_carried_record import oci_carried_record
@@ -234,6 +237,17 @@ def interactive_management(project, ui):
ui.message(translate('OCI management could not be completed. Check the backend status; no additional cleanup has been authorized.'), translate('OCI management')) ui.message(translate('OCI management could not be completed. Check the backend status; no additional cleanup has been authorized.'), translate('OCI management'))
def _records_unread(project):
"""Whether an installed application has no cached configuration yet; only
then does the first sync read every container and take a while."""
sys.path.insert(0, str(project / 'remote'))
import oci_carried_record as carried
return any(not (carried.instances.location(carried.instances.ROOT, row['vmid']).parent
/ carried.CONFIG_CACHE).exists()
for row in saved_inventory(project)
if row.get('status') == 'installed' and not row.get('pending'))
def _clean_orphans(project): def _clean_orphans(project):
"""Remove, silently, what is left of containers that exist on no node of the cluster.""" """Remove, silently, what is left of containers that exist on no node of the cluster."""
sys.path.insert(0, str(project / 'remote')) sys.path.insert(0, str(project / 'remote'))
@@ -253,7 +267,8 @@ def _interactive_management(project, ui):
_clean_orphans(project) _clean_orphans(project)
recover_automatically(project) recover_automatically(project)
offer_recovery(project, ui) offer_recovery(project, ui)
carry_records(project, mount_stopped=False, shown=True) carry_records(project, mount_stopped=False, shown=_records_unread(project),
heading=translate('Manage installed OCI applications'))
# After each operation the list is shown again. Every operation leaves # After each operation the list is shown again. Every operation leaves
# the copies of the records up to date, so they are not all refreshed again. # the copies of the records up to date, so they are not all refreshed again.
first = True first = True
+1 -1
View File
@@ -166,7 +166,7 @@ def registry_accounts_menu(ui, engine=ENGINE_ROOT, auth=None):
return return
try: try:
options = [('add', translate('Sign in to a registry'))] options = [('add', translate('Sign in to a registry'))]
options += [(f'account-{index}', f"{row['registry']:<28} {row['account'] or row['method']} [{translate('saved')}]") options += [(f'account-{index}', f"{row['registry']:<28} [{translate('saved')}]")
for index, row in enumerate(rows)] for index, row in enumerate(rows)]
options += [('storage', translate('Credential storage and access'))] options += [('storage', translate('Credential storage and access'))]
count = translate('{count} saved account.') if len(rows) == 1 else translate('{count} saved accounts.') count = translate('{count} saved account.') if len(rows) == 1 else translate('{count} saved accounts.')
+34
View File
@@ -0,0 +1,34 @@
"""Opening Manage shows the sync notice only while some configuration is still unread."""
from pathlib import Path
import sys
import tempfile
import unittest
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / 'src'))
sys.path.insert(0, str(ROOT / 'remote'))
from proxmenux_oci import management
import oci_carried_record as carried
class ManageSyncNoticeTests(unittest.TestCase):
def test_notice_only_while_an_installed_application_has_no_cache(self):
rows = [{'vmid': 101, 'status': 'installed', 'pending': False},
{'vmid': 102, 'status': 'installed', 'pending': True},
{'vmid': 103, 'status': 'failed', 'pending': False}]
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
with patch.object(carried.instances, 'ROOT', root), \
patch.object(management, 'saved_inventory', return_value=rows):
self.assertTrue(management._records_unread(ROOT))
cache = carried.instances.location(root, 101).parent / carried.CONFIG_CACHE
cache.parent.mkdir(parents=True)
cache.write_text('{}')
# The pending and failed applications are never cached by the sync.
self.assertFalse(management._records_unread(ROOT))
if __name__ == '__main__':
unittest.main()