fix(oci): call the editing of an application Modify everywhere

- The option that edits an installed application is Modify for one container and for a multi-container application, in the OCI menu and on the Monitor button. Recreate is only the rebuild of a multi-container application as it is.
- A stack recreation says "Recreating" and "Recreated" in its steps instead of "Updating".
- The notes of the GPU, Coral, disk and share menus no longer name the option.
- Spanish for the new texts; the English copies of the stack texts are removed from de, fr, pt and sv so the bot translates them.
This commit is contained in:
MacRimi
2026-10-06 19:42:30 +02:00
parent 747b32489f
commit 3f75b94ea1
31 changed files with 185 additions and 163 deletions
+6 -6
View File
@@ -354,7 +354,7 @@ def candidate_contract(record, operation, proposal=None):
raise ValueError(translate('Recreating requires a confirmed proposal'))
if (proposal.get('base_config_sha256') is not None
and proposal['base_config_sha256'] != record['observed']['config_sha256']):
raise ValueError(translate('The instance changed while it was being edited; configure Recreate again'))
raise ValueError(translate('The instance changed while it was being edited; open Modify again'))
candidate = proposal['candidate']
if (candidate.get('vmid') != record['vmid']
or candidate.get('installation_id') != record['installation_id']
@@ -872,7 +872,7 @@ def apply(root, vmid, archive, operation, proposal=None, registry_digest=None, i
if coordinated and backup_compression != 'zstd':
raise ValueError(translate('Coordinated backups require zstd'))
if show:
msg_info(translate('Preparing the update...') if update else translate('Preparing the recreation...'))
msg_info(translate('Preparing the update...') if update else translate('Preparing the changes...'))
record = instances.read(root, vmid)
if coordinated and any(coordinated.get(flag) for flag in
('nextcloud_replay', 'paperless_replay', 'tandoor_replay', 'immich_replay')):
@@ -910,7 +910,7 @@ def apply(root, vmid, archive, operation, proposal=None, registry_digest=None, i
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 set(image['defaults'].get('Volumes') or {}) - non_persistent_image_volumes(candidate['template'])):
raise ValueError(translate('The new image requires additional persistent paths; use Recreate'))
raise ValueError(translate('The new image requires additional persistent paths; use Modify'))
directory = instances.location(root, vmid).parent / 'transactions' / uuid.uuid4().hex
private_directory(directory)
open_log(directory)
@@ -956,7 +956,7 @@ def apply(root, vmid, archive, operation, proposal=None, registry_digest=None, i
gpu_devices.verify(original_gpu)
gpu_devices.verify(desired_gpu)
if show:
msg_ok(translate('Update prepared') if update else translate('Recreation prepared'))
msg_ok(translate('Update prepared') if update else translate('Changes prepared'))
msg_info(translate('Stopping the container...'))
stop(vmid)
if show:
@@ -1062,7 +1062,7 @@ def apply(root, vmid, archive, operation, proposal=None, registry_digest=None, i
cleanup_error = exc
log(f'cleanup: {exc}')
msg_ok(translate('Update completed. Data kept.') if update
else translate('Recreation completed. Data kept.'))
else translate('Changes applied. Data kept.'))
if freed:
msg_ok(f"{translate('Unused images removed from the cache:')} {gib(freed)}")
if cleanup_error is not None:
@@ -1275,7 +1275,7 @@ def phase_label(phase):
def show_status(path, state):
operations = {'update': translate('Update'), 'recreate': translate('Recreate')}
operations = {'update': translate('Update'), 'recreate': translate('Modify')}
msg_info2(f"{translate('Interrupted operation:')} {operations.get(state.get('operation'), state.get('operation'))} (CT {state['vmid']})")
msg_info2(f"{translate('Stopped at:')} {phase_label(state.get('phase'))}")
if state.get('stage'):
+2 -2
View File
@@ -254,9 +254,9 @@ def main():
msg_error(translate('Another OCI operation is using the instance registry. Wait for it to finish.'))
return 1
except (OSError, ValueError, KeyError, RuntimeError, StopIteration, subprocess.TimeoutExpired) as error:
msg_error(f"{translate('The application could not be recreated:')} {error}")
msg_error(f"{translate('The application could not be modified:')} {error}")
return 1
msg_ok(translate('The application has been recreated with the new options.'))
msg_ok(translate('The application has been modified with the new options.'))
return 0
+5 -4
View File
@@ -120,6 +120,7 @@ def _apply(path, plan, adapter):
running = {str(vmid): adapter.is_running(vmid) for vmid in plan['start_order']}
if any(type(value) is not bool for value in running.values()):
raise ValueError(translate('Invalid running state'))
recreate = plan['operation'] == 'recreate'
state = {'schema_version': 1, 'id': str(uuid.uuid4()),
'plan': copy.deepcopy(plan), 'running': running,
'prepared': {}, 'backups': {}, 'stop_intent': False,
@@ -152,9 +153,9 @@ def _apply(path, plan, adapter):
state['replacement_intent'] = True
save(path, state, 'replacing')
for vmid in plan['start_order']:
msg_info(f"{translate('Updating')} {member(adapter, vmid)}...")
msg_info(f"{translate('Recreating') if recreate else translate('Updating')} {member(adapter, vmid)}...")
adapter.replace(vmid, state['prepared'][str(vmid)], state['id'])
msg_ok(f"{translate('Updated:')} {member(adapter, vmid)}")
msg_ok(f"{translate('Recreated:') if recreate else translate('Updated:')} {member(adapter, vmid)}")
save(path, state, 'starting')
for vmid in plan['start_order']:
msg_info(f"{translate('Starting')} {member(adapter, vmid)}...")
@@ -162,10 +163,10 @@ def _apply(path, plan, adapter):
adapter.healthcheck(vmid)
msg_ok(f"{translate('Service responding:')} {member(adapter, vmid)}")
save(path, state, 'checking')
msg_info(translate('Checking the updated stack...'))
msg_info(translate('Checking the recreated stack...') if recreate else translate('Checking the updated stack...'))
adapter.validate_candidates(state)
adapter.restore_running_state(running, plan['start_order'])
msg_ok(translate('Updated stack checked'))
msg_ok(translate('Recreated stack checked') if recreate else translate('Updated stack checked'))
save(path, state, 'publishing')
msg_info(translate('Saving the stack records...'))
adapter.publish(state)
+1 -1
View File
@@ -159,7 +159,7 @@ def application_name(record, vmid):
def update(vmid, acknowledge_external_data=False, proposal=None, keep_backup=None):
operation = 'recreate' if proposal is not None else 'update'
msg_info(translate('Checking the container before the update...') if operation == 'update'
else translate('Checking the container before recreating it...'))
else translate('Checking the container before modifying it...'))
with instances.locked(instances.ROOT):
record = instances.read(instances.ROOT, vmid)
if record['status'] != 'installed' or record.get('pending_transaction') or record.get('pending_stack_transaction'):
+8 -7
View File
@@ -285,7 +285,8 @@ def manage_instance(project, ui, row, action=None, lifecycle_args=()):
return False
if row['stack']:
return _manage_stack(project, ui, row, action, lifecycle_args)
if action == 'modify':
# Rebuilding as it is exists only for a multi-container application.
if action == 'recreate':
return False
if not row['pending']:
if row['status'] != 'installed' or row['reason'] != 'matched':
@@ -293,7 +294,7 @@ def manage_instance(project, ui, row, action=None, lifecycle_args=()):
return False
if action is None:
action = ui.choose(translate('Manage OCI'), [('update', translate('Update the image with the saved configuration')),
('recreate', translate('Recreate: edit resources, network, paths and GPU')),
('modify', translate('Modify: edit resources, network, paths and GPU')),
('remove', translate('Remove: delete the application and its containers'))], 'update')
if action is None:
return False
@@ -323,7 +324,7 @@ def manage_instance(project, ui, row, action=None, lifecycle_args=()):
ui.message(str(error), translate('Review external OCI changes'))
return False
proposal = None
if action == 'recreate':
if action == 'modify':
from .recreation import edit_recreation
from .cli import _deployment_summary_text
from .ui import BacktrackUI, RestartWizard
@@ -333,8 +334,8 @@ def manage_instance(project, ui, row, action=None, lifecycle_args=()):
try:
proposal = edit_recreation(record, wizard)
approved = wizard.review(_deployment_summary_text(proposal['candidate']['template'],
proposal['candidate']['deployment']), translate('Recreate OCI'),
question=translate('Recreate with these options?'), default=True)
proposal['candidate']['deployment']), translate('Modify OCI'),
question=translate('Apply these changes?'), default=True)
break
except RestartWizard:
wizard.restart()
@@ -354,7 +355,7 @@ def manage_instance(project, ui, row, action=None, lifecycle_args=()):
# outside the container and its backup.
if '--acknowledge-external-data' not in command:
command.append('--acknowledge-external-data')
title = translate('Recreate OCI') if proposal else translate('Update OCI')
title = translate('Modify OCI') if proposal else translate('Update OCI')
if proposal is None:
completed = _run_lifecycle(command, title)
else:
@@ -598,7 +599,7 @@ def _image_too_recent(project, vmid, min_age_days):
def direct_management(project, vmid, action, lifecycle_args=(), unattended=False, min_image_age_days=0):
"""One operation on one instance, without the list of the menu: the entry
ProxMenux Monitor uses for its Update and Recreate buttons and for
ProxMenux Monitor uses for its Update and Modify buttons and for
scheduled updates."""
from .ui import interactive_ui
ui = UnattendedUI() if unattended else interactive_ui()
+5 -5
View File
@@ -180,12 +180,12 @@ def change_recognition(project, ui, primary, member, run_lifecycle):
choices = recognition_choices(storage)
if [tag for tag, _, _ in choices] == ['cpu'] and current == 'cpu':
ui.message(translate('No usable GPU was found on this host. Recognition stays on the CPU.'),
translate('Recreate OCI'))
translate('Modify OCI stack'))
return False
selected = ui.choose(translate('What runs the recognition of Immich'),
[(tag, label) for tag, label, _ in choices], current)
if selected is None or selected == current:
ui.message(translate('Nothing was changed.'), translate('Recreate OCI'))
ui.message(translate('Nothing was changed.'), translate('Modify OCI stack'))
return False
details = next(extra for tag, _, extra in choices if tag == selected)
if details.get('experimental') and not confirm_experimental_rocm(ui):
@@ -196,7 +196,7 @@ def change_recognition(project, ui, primary, member, run_lifecycle):
'cache, the library and the database are kept. The whole application is stopped and '
'updated, as in an update; if anything fails, the previous containers and the previous '
'choice are restored.'))
if not ui.review(text, translate('Recreate OCI'), question=translate('Change the recognition now?'), default=True):
if not ui.review(text, translate('Modify OCI stack'), question=translate('Change the recognition now?'), default=True):
return False
command = [sys.executable, str(project / 'remote/oci_immich_recognition.py'), str(primary['vmid']),
'--acceleration', selected]
@@ -204,13 +204,13 @@ def change_recognition(project, ui, primary, member, run_lifecycle):
command += ['--render-device', details['render']]
if details.get('override'):
command += ['--gfx-override', details['override']]
return run_lifecycle(command, translate('Recreate OCI'))
return run_lifecycle(command, translate('Modify OCI stack'))
def modify_stack(project, ui, primary, run_lifecycle):
learning = immich_learning(primary)
if learning is not None:
what = ui.choose(translate('What to recreate'),
what = ui.choose(translate('What to modify'),
[('paths', translate('Add or remove extra paths and devices')),
('recognition', translate('Change what runs recognition: CPU or GPU'))], 'paths')
if what is None:
+46
View File
@@ -0,0 +1,46 @@
"""The steps of a coordinated operation name the operation that is running."""
from pathlib import Path
import sys
import unittest
from unittest.mock import MagicMock, patch
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "remote"))
import oci_stack_transaction as transaction
class StackOperationWording(unittest.TestCase):
def steps(self, operation):
plan = {"operation": operation, "primary_vmid": 138, "members": [{"vmid": 138}],
"start_order": [138], "stop_order": [138]}
adapter = MagicMock(spec=["validate", "is_running", "prepare", "stop", "backup", "verify_backups", "replace",
"start", "healthcheck", "validate_candidates", "restore_running_state", "publish"])
adapter.is_running.return_value = False
adapter.prepare.return_value = {}
adapter.backup.return_value = "backup"
said = []
with patch.object(transaction, "save"), patch.object(transaction, "translate", side_effect=lambda text: text), \
patch.object(transaction, "msg_info", side_effect=said.append), \
patch.object(transaction, "msg_ok", side_effect=said.append):
transaction._apply(Path("/nonexistent/journal.json"), plan, adapter)
return said
def test_a_recreation_never_says_it_is_updating(self):
said = self.steps("recreate")
self.assertIn("Recreating CT 138...", said)
self.assertIn("Recreated: CT 138", said)
self.assertIn("Recreated stack checked", said)
self.assertEqual([line for line in said if "pdat" in line], [])
def test_an_update_keeps_its_wording(self):
said = self.steps("update")
self.assertIn("Updating CT 138...", said)
self.assertIn("Updated: CT 138", said)
self.assertIn("Updated stack checked", said)
self.assertEqual([line for line in said if "ecreat" in line], [])
if __name__ == "__main__":
unittest.main()