From 2ab83c0b881e607e43f86cfc00dea7f9cf322ca2 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Wed, 28 Oct 2026 22:57:15 +0100 Subject: [PATCH] fix(oci): clarify the summary of imported Compose stacks Each registry line names its image, the variables of a stack are listed without announcing questions that are not asked, and the name given to an imported stack names its containers. --- oci/src/proxmenux_oci/custom.py | 12 +++++++-- oci/src/proxmenux_oci/stack.py | 3 ++- oci/tests/test_custom_compose_stacks.py | 34 +++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/oci/src/proxmenux_oci/custom.py b/oci/src/proxmenux_oci/custom.py index 39347d5b..49ac90df 100644 --- a/oci/src/proxmenux_oci/custom.py +++ b/oci/src/proxmenux_oci/custom.py @@ -809,7 +809,9 @@ def describe(template: dict[str, Any]) -> str: for volume in volumes] environment = contract.get('environment') or [] if environment: - lines += ['', translate('Variables the installation asks for:')] + # A stack takes its values from the Compose file; none is asked here. + lines += ['', translate('Variables the installation asks for:') if len(services) <= 1 else + f"{translate('Variables')} ({template['compose_stack']['main_service']}):"] lines += [f" {item['name']}" + (f" = {item['example']}" if item.get('example') and not item['sensitive'] and len(services) <= 1 else '') + ('' if item.get('required', True) else f" ({translate('optional')})") @@ -852,13 +854,17 @@ def _ask_secrets(template: dict[str, Any]) -> list[str]: def _name(ui, template: dict[str, Any]) -> str: - default = normalize_app_id(source_text(template['catalog_ui'].get('title')) + stack = template.get('compose_stack') if len((template.get('compose_stack') or {}).get('services') or []) > 1 else None + default = normalize_app_id((stack or {}).get('project_name') or source_text(template['catalog_ui'].get('title')) or template['container_contract'].get('container_name') or 'oci-app') name = normalize_app_id(ui.ask(translate('Name for this application'), default)) if not name: raise UserCancelled(translate('No name was given')) template['catalog_ui']['title'] = {'en_US': name} template['container_contract']['container_name'] = name + if stack: + # The containers of the stack are named after it. + stack['project_name'] = name return name @@ -897,6 +903,8 @@ def _explore(ui) -> None: if offer_sign_in(ui, reference, title): accessible, report = registry_report(reference) available = available and accessible + if accessible and len(references) > 1: + report = f'{reference}: {report}' reports.append(report) summary += '\n\n' + report if notes: diff --git a/oci/src/proxmenux_oci/stack.py b/oci/src/proxmenux_oci/stack.py index e465564b..b4d52ea2 100644 --- a/oci/src/proxmenux_oci/stack.py +++ b/oci/src/proxmenux_oci/stack.py @@ -552,7 +552,8 @@ def build_stack(template, ui, mode='advanced'): # A default installation takes the name, the VMID and the settings from the # recipe; it still asks the storage, the address and how the stack starts. quiet = DefaultsUI() if mode == DEFAULT_MODE else ui - name = _hostname_default(quiet.ask(translate('Stack name'), template['compose_stack']['project_name'])) + # An imported Compose file was already named before its installation. + name = _hostname_default((DefaultsUI() if custom else quiet).ask(translate('Stack name'), template['compose_stack']['project_name'])) vmid = quiet.ask(translate('Base VMID (empty = next free block)'), '', required=False) from . import host from . import network as access diff --git a/oci/tests/test_custom_compose_stacks.py b/oci/tests/test_custom_compose_stacks.py index 142ffea4..99c5cf61 100644 --- a/oci/tests/test_custom_compose_stacks.py +++ b/oci/tests/test_custom_compose_stacks.py @@ -109,6 +109,40 @@ class CustomComposeStacks(unittest.TestCase): install.assert_not_called() self.assertIn('app missing', ui.text) + def test_each_registry_line_names_its_image(self): + class UI: + def message(self, text, title): + self.text = text + ui = UI() + with patch.object(custom, 'read_definition', return_value=(COMPOSE, 'Compose')), \ + patch.object(custom, 'registry_report', side_effect=[(True, 'in registry'), (False, 'app missing')]), \ + patch.object(custom, '_registry_refused', return_value=False), \ + patch.object(custom, 'install_template'): + custom.explore(ui) + self.assertIn('postgres:16: in registry', ui.text) + self.assertNotIn('adminer:4: app missing', ui.text) + + @patch('proxmenux_oci.i18n.language', return_value='en') + def test_stack_summary_does_not_announce_questions_about_its_variables(self, _): + template = custom.template_from_compose(COMPOSE.replace( + " ports:", " environment:\n ADMINER_DEFAULT_SERVER: db\n ports:")) + summary = custom.describe(template) + self.assertNotIn('Variables the installation asks for', summary) + self.assertIn('Variables (app):\n ADMINER_DEFAULT_SERVER', summary) + single = custom.template_from_compose('services: {app: {image: nginx:1.27, environment: {MODE: x}}}') + self.assertIn('Variables the installation asks for', custom.describe(single)) + + def test_the_name_given_names_the_stack(self): + template = custom.template_from_compose('name: adminer-postgres\n' + COMPOSE) + class UI: + def ask(self, text, default=None, required=True): + self.default = default + return 'tools' + ui = UI() + self.assertEqual(custom._name(ui, template), 'tools') + self.assertEqual(ui.default, 'adminer-postgres') + self.assertEqual(template['compose_stack']['project_name'], 'tools') + def test_single_image_still_uses_single_container_flow(self): template = custom.template_from_compose('services: {app: {image: nginx:1.27}}') self.assertNotEqual(template['proxmox'].get('installer_profile', {}).get('stack_driver'), 'generic-multi-lxc-stack')