fix(oci): keep every declared image data path on a volume so installs can be updated

This commit is contained in:
MacRimi
2026-09-30 22:28:20 +02:00
parent d2ac3b7638
commit c83f84398b
24 changed files with 257 additions and 65 deletions
+2
View File
@@ -211,6 +211,8 @@ def compare(record, current, candidate=None):
if not managed or options.get('backup') != '1':
blockers.append('persistent-mount-needs-backup:' + key)
declared = set(record['image']['defaults'].get('Volumes') or {})
declared -= set(record['template'].get('proxmox', {}).get('installer_profile', {})
.get('non_persistent_image_volumes', []))
declared.update(v['container_path'] for v in record['template'].get('container_contract', {}).get('volumes', []) if v.get('container_path'))
for path in sorted(declared):
if path not in mounts:
+8 -2
View File
@@ -57,6 +57,11 @@ SPINNER_FRAME = re.compile('^ ?[' + ''.join(oci_ui.FRAMES) + ']')
_run = {'log': None, 'pending': [], 'journal': None, 'failed_log': None, 'failed_lines': None}
def non_persistent_image_volumes(template):
"""Image volumes the template states hold no data, which stay in the rootfs."""
return set(template.get('proxmox', {}).get('installer_profile', {}).get('non_persistent_image_volumes', []))
def screen_text(line):
"""What a terminal shows for one output line, without colours."""
parts = [SPINNER_FRAME.sub('', ANSI.sub('', part)).rstrip() for part in line.split('\r')]
@@ -488,7 +493,8 @@ def preflight(record, candidate, config, coordinated=None):
required = {v['container_path'] for v in template['container_contract'].get('volumes', []) if v.get('required', True)}
if not required <= {m['container_path'] for m in plan.get('mounts', [])}:
raise ValueError(translate('Required persistent paths cannot be removed'))
image_paths = record['observed']['image']['defaults'].get('Volumes') or {}
image_paths = set(record['observed']['image']['defaults'].get('Volumes') or {})
image_paths -= non_persistent_image_volumes(record['template'])
if any(not any(p == target or p.startswith(target.rstrip('/') + '/') for target in old) for p in image_paths):
raise ValueError(translate('The image declares data paths that are still stored in the rootfs'))
check = effective_healthcheck(candidate['template'])
@@ -885,7 +891,7 @@ def apply(root, vmid, archive, operation, proposal=None, registry_digest=None, i
return None
required = {m['container_path'] for m in candidate['deployment'].get('mounts', [])}
if any(not any(p == target or p.startswith(target.rstrip('/') + '/') for target in required)
for p in (image['defaults'].get('Volumes') or {})):
for p in set(image['defaults'].get('Volumes') or {}) - non_persistent_image_volumes(candidate['template'])):
raise ValueError(translate('The new image requires additional persistent paths; use Recreate'))
directory = instances.location(root, vmid).parent / 'transactions' / uuid.uuid4().hex
private_directory(directory)
+3 -1
View File
@@ -269,8 +269,10 @@ class NativeAdapter:
if image['architecture'] != old['architecture'] or image['os'] != 'linux':
raise ValueError(translate('Incompatible image platform'))
paths = [m['container_path'] for m in record['deployment'].get('mounts', [])]
transient = set(record.get('template', {}).get('proxmox', {}).get('installer_profile', {})
.get('non_persistent_image_volumes', []))
if any(not any(p == target or p.startswith(target.rstrip('/') + '/') for target in paths)
for p in (image['defaults'].get('Volumes') or {})):
for p in set(image['defaults'].get('Volumes') or {}) - transient):
raise ValueError(translate('The new image requires additional persistent paths'))
profile = record['deployment'].get('replay_profile', {})
if profile.get('adapter') in ('install_nextcloud_stack.sh', 'install_paperless_stack.sh', 'install_tandoor_stack.sh', 'install_immich_stack.sh'):
+3 -1
View File
@@ -114,7 +114,9 @@ def captured_menu_ready(primary, adapter, convert, expected_roles):
return False
for member in converted:
targets = [m['container_path'] for m in member['deployment']['mounts']]
declared = member['observed']['image']['defaults'].get('Volumes') or {}
declared = set(member['observed']['image']['defaults'].get('Volumes') or {})
declared -= set(member.get('template', {}).get('proxmox', {}).get('installer_profile', {})
.get('non_persistent_image_volumes', []))
if any(not any(p == target or p.startswith(target.rstrip('/') + '/') for target in targets)
for p in declared):
return False