From cca3516e65d1b8e598509afa1a14aa4079ccba3e Mon Sep 17 00:00:00 2001 From: MacRimi Date: Mon, 26 Oct 2026 22:53:09 +0100 Subject: [PATCH] fix(oci): update and modify apps that mount a host system path (#531) --- CHANGELOG.md | 1 + oci/remote/oci_instance_transaction.py | 26 +++++++++++++++++++++----- oci/remote/oci_native_modify.py | 3 ++- oci/tests/test_privileged_lifecycle.py | 23 +++++++++++++++++++++++ web/data/changelog/es.md | 1 + 5 files changed, 48 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index becc9c69..69337a8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -142,6 +142,7 @@ ProxMenux does not build its own containers for these applications: it reads the - **Variables changed in Proxmox are carried over.** A password changed after the first login, or a variable added from Proxmox, is listed by name, without its value, and saved in oci-compose before Update or Modify continues. - **The downloaded image is offered for deletion only after a container was rebuilt from it**, not after a change applied to the running container. - **The list of installed applications comes back after each operation**, an update, a change or a removal, without reading every application again. +- **An application that mounts a system path of the host, such as `/run/dbus` in Home Assistant or `/run/udev` in Roon Server, is updated and modified like any other** ([#531](https://github.com/MacRimi/ProxMenux/issues/531), reported by [@nortuzar](https://github.com/nortuzar)). ### Watchdog, backups and clusters diff --git a/oci/remote/oci_instance_transaction.py b/oci/remote/oci_instance_transaction.py index 06b1397d..77231bbf 100644 --- a/oci/remote/oci_instance_transaction.py +++ b/oci/remote/oci_instance_transaction.py @@ -354,12 +354,28 @@ def is_file_bind(mount, files=()): return mount['type'] == 'host-bind' and (mount['source'] in files or Path(mount['source']).is_file()) +# The host paths the installer mounts as the recipe gives them, never asking. +SYSTEM_BIND_PREFIXES = ('/dev/', '/proc/', '/sys/', '/run/', '/var/run/') + + def declared_sources(contract): - """The host paths the recipe of a host monitor mounts on its own.""" - if not contract['deployment'].get('host_monitor'): - return set() - profile = contract['template'].get('proxmox', {}).get('installer_profile', {}) - return {item['source'] for item in profile.get('host_monitor_mounts', [])} + """The host paths the recipe mounts on its own: those of a host monitor, + and the system paths of its volumes, such as /run/dbus.""" + template = contract.get('template') or {} + profile = template.get('proxmox', {}).get('installer_profile', {}) + declared = set() + if contract['deployment'].get('host_monitor'): + declared |= {item['source'] for item in profile.get('host_monitor_mounts', [])} + system = {} + for volume in template.get('container_contract', {}).get('volumes', []): + source = str(volume.get('compose_source_example') or volume.get('container_path') or '') + if 'host-bind' in volume.get('installation_choice', []) and ( + source.startswith(SYSTEM_BIND_PREFIXES) + or str(volume.get('container_path', '')).startswith(SYSTEM_BIND_PREFIXES)): + system[volume['container_path']] = source + declared |= {mount['source'] for mount in contract['deployment'].get('mounts', []) + if mount['type'] == 'host-bind' and system.get(mount['container_path']) == mount['source']} + return declared def profile_keys(deployment): diff --git a/oci/remote/oci_native_modify.py b/oci/remote/oci_native_modify.py index 8cefa086..c35d72b6 100644 --- a/oci/remote/oci_native_modify.py +++ b/oci/remote/oci_native_modify.py @@ -365,7 +365,8 @@ def apply(root, record, candidate, config, acknowledge_external_data=False): Path(mount['source']).mkdir(parents=True, exist_ok=True) host_mounts.verify_sources(original_sources) # Capture newly-created directories before installing their mounts. - pinned = {p: host_mounts.validate_source(p) for p in desired_sources} + pinned = {p: host_mounts.validate_source(p, declared=value.get('declared', False)) + for p, value in desired_sources.items()} for path, previous in desired_sources.items(): if previous['exists'] and not host_mounts.same_source(previous, pinned[path]): raise ValueError(translate('A shared source does not match its recorded identity')) diff --git a/oci/tests/test_privileged_lifecycle.py b/oci/tests/test_privileged_lifecycle.py index e2dc2508..9138d681 100644 --- a/oci/tests/test_privileged_lifecycle.py +++ b/oci/tests/test_privileged_lifecycle.py @@ -104,6 +104,29 @@ class DeclaredSourceTests(TestCase): original, desired = transaction.freeze_host_sources(record, record, False) self.assertTrue(original['/usr']['declared'] and desired['/usr']['declared']) + def test_the_system_paths_of_a_recipe_are_declared(self): + for app, path in (('homeassistant-official', '/run/dbus'), ('roonserver', '/run/udev')): + template = json.loads((ROOT / f'catalog/apps/{app}.json').read_text()) + deployment = {'mounts': [bind(path, path)]} + self.assertEqual(transaction.declared_sources({'template': template, 'deployment': deployment}), {path}, app) + moved = {'mounts': [bind('/run/other', path)]} + self.assertEqual(transaction.declared_sources({'template': template, 'deployment': moved}), set(), app) + + def test_a_home_assistant_update_keeps_its_dbus_mount(self): + template = json.loads((ROOT / 'catalog/apps/homeassistant-official.json').read_text()) + record = {'template': template, 'observed': {}, + 'deployment': {'mounts': [bind('/run', '/run/dbus')]}} + with self.assertRaises(ValueError): + transaction.freeze_host_sources(record, record, True) + template = json.loads(json.dumps(template)) + for volume in template['container_contract']['volumes']: + if volume['container_path'] == '/run/dbus': + volume['compose_source_example'] = '/run' + record['template'] = template + with patch.object(transaction, 'log'): + original, desired = transaction.freeze_host_sources(record, record, False) + self.assertTrue(original['/run']['declared'] and desired['/run']['declared']) + class IncludeTests(TestCase): def setUp(self): diff --git a/web/data/changelog/es.md b/web/data/changelog/es.md index c4578f01..4214c6bf 100644 --- a/web/data/changelog/es.md +++ b/web/data/changelog/es.md @@ -141,6 +141,7 @@ ProxMenux no construye sus propios contenedores para estas aplicaciones: lee la - **Las variables cambiadas en Proxmox se conservan.** Una contraseña cambiada tras el primer inicio de sesión, o una variable añadida desde Proxmox, se muestra por su nombre, sin su valor, y se guarda en el oci-compose antes de que Actualizar o Modificar continúen. - **La imagen descargada solo se ofrece para eliminarla después de reconstruir un contenedor a partir de ella**, no después de un cambio aplicado al contenedor en marcha. - **La lista de aplicaciones instaladas vuelve después de cada operación**, ya sea una actualización, un cambio o una eliminación, sin volver a leer todas las aplicaciones. +- **Una aplicación que monta una ruta de sistema del host, como `/run/dbus` en Home Assistant o `/run/udev` en Roon Server, se actualiza y se modifica como cualquier otra** ([#531](https://github.com/MacRimi/ProxMenux/issues/531), reportado por [@nortuzar](https://github.com/nortuzar)). ### Vigilancia, backups y clústeres