mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 18:32:13 +00:00
Merge pull request #550 from jcastro/feature/oci-custom-compose-stacks
feat(oci): import multi-service Compose files from Custom image
This commit is contained in:
@@ -29,6 +29,10 @@ TRANSACTION = ROOT / 'oci/remote/oci_stack_transaction.py'
|
||||
|
||||
|
||||
def extracted(path, name, scope):
|
||||
if name == 'remove':
|
||||
# Operational cache cleanup is exercised by the engine tests; keep wording tests inert.
|
||||
scope.setdefault('compose_jobs', SimpleNamespace(forget_completed=lambda *args: None))
|
||||
scope.setdefault('public_routes', SimpleNamespace(remove=lambda *args: None))
|
||||
node = next(n for n in ast.parse(path.read_text()).body
|
||||
if isinstance(n, ast.FunctionDef) and n.name == name)
|
||||
exec(compile(ast.Module(body=[node], type_ignores=[]), str(path), 'exec'), scope)
|
||||
@@ -71,6 +75,9 @@ class InventoryMessages(unittest.TestCase):
|
||||
self.assertNotIn('Host directories that are kept, with their content:', text)
|
||||
|
||||
def test_removal_notice_qualifies_saved_record_source(self):
|
||||
# Extracted operational functions must never inspect the real pmxcfs.
|
||||
host = Path(self.enterContext(tempfile.TemporaryDirectory()))
|
||||
inert_path = lambda value: host / Path(value).name if str(value).startswith('/etc/pve/') else Path(value)
|
||||
events = []
|
||||
scope = {'members_of': lambda root, vmid: (101, {}, [101]),
|
||||
'instances': SimpleNamespace(read=lambda root, vmid: {'installation_id': 'owned'},
|
||||
@@ -81,7 +88,7 @@ class InventoryMessages(unittest.TestCase):
|
||||
'remove_owned_host_firewall': lambda primary: True,
|
||||
'run': lambda *args: events.append(('run', args)),
|
||||
'subprocess': SimpleNamespace(run=lambda *args, **kwargs: None),
|
||||
'Path': Path, 'shutil': SimpleNamespace(rmtree=lambda path: None),
|
||||
'Path': inert_path, 'shutil': SimpleNamespace(rmtree=lambda path: None),
|
||||
'image_cache': SimpleNamespace(prune=lambda root, lock: []),
|
||||
'work_backup': SimpleNamespace(forget=lambda root, vmid: None),
|
||||
'oci_console': SimpleNamespace(remove_log=lambda vmid: events.append(('log', vmid))),
|
||||
|
||||
@@ -35,6 +35,10 @@ KEYS = (PREVIEW, CONFIRM, RESULT, BRIDGE, BRIDGE_FAILURE, SUCCESS, FIREWALL, POR
|
||||
|
||||
|
||||
def extract(path, name, scope):
|
||||
if name == 'remove':
|
||||
# Operational cache cleanup is exercised by the engine tests; keep wording tests inert.
|
||||
scope.setdefault('compose_jobs', SimpleNamespace(forget_completed=lambda *args: None))
|
||||
scope.setdefault('public_routes', SimpleNamespace(remove=lambda *args: None))
|
||||
node = next(n for n in ast.parse(path.read_text()).body if isinstance(n, ast.FunctionDef) and n.name == name)
|
||||
exec(compile(ast.Module(body=[node], type_ignores=[]), str(path), 'exec'), scope)
|
||||
return scope[name]
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
"""The watchdog of an OCI application is reachable from the Monitor and worded in every language."""
|
||||
import ast
|
||||
import copy
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
from unittest import TestCase
|
||||
from unittest.mock import Mock
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
LOCALES = ('en', 'es', 'de', 'fr', 'it', 'pt', 'sk', 'sv')
|
||||
@@ -65,9 +69,45 @@ class OciWatchdogInstaller(TestCase):
|
||||
self.assertLess(install.index('deployment = build_deployment(candidate, wizard, mode)'), asked)
|
||||
self.assertLess(asked, install.index('approved = wizard.review('))
|
||||
self.assertLess(install.index('watchdog = bool(deployment.pop("watchdog", False))'), install.index('run_remote_install('))
|
||||
self.assertIn('set_watchdog(PROJECT_ROOT, sorted(vmids), True)', install)
|
||||
self._assert_jobs_are_excluded_from_installed_watchdog()
|
||||
self.assertIn('row(translate("Watchdog"), _yes_no(plan["watchdog"]))', self.CLI)
|
||||
|
||||
def _assert_jobs_are_excluded_from_installed_watchdog(self):
|
||||
node = next(item for item in ast.parse(self.CLI).body
|
||||
if isinstance(item, ast.FunctionDef) and item.name == 'install_template')
|
||||
class InertImports(ast.NodeTransformer):
|
||||
def visit_ImportFrom(self, item):
|
||||
return None
|
||||
node = InertImports().visit(node)
|
||||
deployment = {'services': [
|
||||
{'name': 'migration', 'deployment': {'execution_mode': 'one-shot'}},
|
||||
{'name': 'database', 'deployment': {}},
|
||||
{'name': 'worker', 'deployment': {}}]}
|
||||
result = {'vmid': 4800, 'stack_vmids': {'web': 4800, 'migration': 4801,
|
||||
'database': 4802, 'worker': 4803}}
|
||||
set_watchdog = Mock(return_value=True)
|
||||
class UI:
|
||||
def confirm(self, *args, **kwargs): return True
|
||||
def review(self, *args, **kwargs): return True
|
||||
def close(self): pass
|
||||
def install(project, template, plan, target):
|
||||
self.assertNotIn('watchdog', plan)
|
||||
return result
|
||||
scope = {'Any': Any, 'copy': copy, 'BacktrackUI': lambda ui: ui,
|
||||
'RestartWizard': RuntimeError, 'InstallError': ValueError,
|
||||
'build_deployment': lambda *args: copy.deepcopy(deployment),
|
||||
'watchdog_default': lambda *args: False,
|
||||
'translate': lambda value: value, '_deployment_summary_text': lambda *args: '',
|
||||
'PROJECT_ROOT': Path('/inert'), 'source_text': lambda value: 'test', 'APP_TITLE': 'test',
|
||||
'console': SimpleNamespace(**{name: lambda *args: None for name in
|
||||
('show_logo', 'msg_title', 'msg_error', 'msg_ok', 'msg_warn', 'wait_for_enter')}),
|
||||
'run_remote_install': install, 'carry_records': lambda *args, **kwargs: None,
|
||||
'images': SimpleNamespace(offer_removal=lambda *args: (False, None)),
|
||||
'_print_installation_summary': lambda *args: None, 'set_watchdog': set_watchdog}
|
||||
exec(compile(ast.fix_missing_locations(ast.Module(body=[node], type_ignores=[])), 'cli.py', 'exec'), scope)
|
||||
scope['install_template'](UI(), {'catalog_ui': {'title': {'en_US': 'test'}}}, 'test', 'advanced')
|
||||
set_watchdog.assert_called_once_with(Path('/inert'), [4800, 4802, 4803], True)
|
||||
|
||||
def test_both_management_menus_offer_it(self):
|
||||
self.assertEqual(self.MENU.count("('watchdog', "), 2)
|
||||
self.assertEqual(self.MENU.count("if action == 'watchdog':"), 2)
|
||||
|
||||
@@ -20,8 +20,7 @@ AFTER = 'The recovery did not complete. Review the log and choose "{choice}" aga
|
||||
REGISTRY_ADVICE = ('Check the image reference and registry access. If the registry is unavailable, '
|
||||
'try again later. For a private image, sign in from Registry accounts in the OCI menu.')
|
||||
NEW_KEYS = (
|
||||
'The Compose file describes several services:',
|
||||
'Only one service at a time can be installed this way.',
|
||||
'Containers of the application:',
|
||||
'No docker run command was given',
|
||||
'Image selection was cancelled',
|
||||
'Could not inspect the image in its registry:',
|
||||
@@ -77,15 +76,19 @@ class OciWordingTests(TestCase):
|
||||
setattr(module, '_cache', {NEW_KEYS[0]: 'Several services (translated):'})
|
||||
self.assertEqual(module.translate(NEW_KEYS[0]), 'Several services (translated):')
|
||||
|
||||
def test_multiple_services_named_not_images(self):
|
||||
def test_multiple_services_keep_their_names_and_enter_stack_conversion(self):
|
||||
# Reusing one image twice still creates two distinct Compose services.
|
||||
document = {'services': {'alpha': {'image': 'same:latest'},
|
||||
'beta': {'image': 'same:latest'}}}
|
||||
converted = object()
|
||||
stack = Mock(return_value=converted)
|
||||
template = function(CUSTOM, 'template_from_compose',
|
||||
_services=lambda doc: doc['services'], _check_mounts=lambda _: None)
|
||||
with self.assertRaises(ConversionError) as caught:
|
||||
template(json.dumps({'services': {'alpha': {'image': 'same:latest'},
|
||||
'beta': {'image': 'same:latest'}}}))
|
||||
self.assertEqual(str(caught.exception),
|
||||
'The Compose file describes several services: alpha, beta. '
|
||||
'Only one service at a time can be installed this way.')
|
||||
_services=lambda doc: doc['services'], _template_from_stack=stack)
|
||||
self.assertIs(template(json.dumps(document)), converted)
|
||||
stack.assert_called_once()
|
||||
self.assertEqual(stack.call_args.args, (document, None))
|
||||
self.assertFalse(stack.call_args.kwargs['interpolated'])
|
||||
self.assertIsNone(stack.call_args.kwargs.get('image_entrypoints'))
|
||||
|
||||
def test_empty_paste_is_specific_and_nonempty_unchanged(self):
|
||||
for title, expected in ((None, 'No Compose file was given'),
|
||||
@@ -108,7 +111,8 @@ class OciWordingTests(TestCase):
|
||||
def test_registry_nonzero_and_success_command_unchanged(self):
|
||||
result = SimpleNamespace(returncode=1, stdout='', stderr='connection timed out')
|
||||
run = Mock(return_value=result)
|
||||
report = function(CUSTOM, 'registry_report', subprocess=SimpleNamespace(run=run))
|
||||
report = function(CUSTOM, 'registry_report', subprocess=SimpleNamespace(run=run),
|
||||
_registry_lookup=lambda reference: reference)
|
||||
self.assertEqual(report('example:latest'),
|
||||
(False, 'Could not inspect the image in its registry: example:latest'))
|
||||
run.assert_called_once_with(['skopeo', 'inspect', '--raw', 'docker://example:latest'],
|
||||
|
||||
Reference in New Issue
Block a user