"""Update only the IKAR API container, with backup and rollback.""" from pathlib import Path import json import os import shutil import subprocess import tarfile import time import urllib.request root = Path('/mnt/data/ikar_server') release = root / 'releases/20261006-1.0.1' current = root / 'current' previous = current.resolve(strict=True) if not os.path.ismount('/mnt/data') or not previous.is_relative_to(root / 'releases') or release.exists(): raise RuntimeError('Unexpected release or storage state') def snapshot(): containers = subprocess.check_output(['docker', 'ps', '-q'], text=True).split() return {item['Name']: {'id': item['Id'], 'startedAt': item['State']['StartedAt']} for item in json.loads(subprocess.check_output(['docker', 'inspect', *containers], text=True)) if item['Name'] != '/ikar_server'} protected_before = snapshot() (root / 'deployment-1.0.1-before.json').write_text(json.dumps(protected_before, indent=2)) backup = subprocess.check_output(['python3', str(previous / 'backup.py')], text=True).strip() shutil.copytree(previous, release, ignore=shutil.ignore_patterns('publish', 'release.tar.gz')) with tarfile.open(root / 'messaging-fix.tar.gz') as archive: archive.extractall(release, filter='data') compose = release / 'compose.yaml' text = compose.read_text() if 'image: ikar_server:20261006' not in text: raise RuntimeError('Unexpected API image in compose') compose.write_text(text.replace('image: ikar_server:20261006', 'image: ikar_server:20261006-1.0.1', 1)) with (root / 'build-1.0.1.log').open('w') as log: subprocess.run(['docker', 'build', '-f', 'Dockerfile.release', '-t', 'ikar_server:20261006-1.0.1', '.'], cwd=release, stdout=log, stderr=subprocess.STDOUT, check=True) def update(path): subprocess.run(['docker', 'compose', '--project-directory', str(path), '--env-file', str(root / 'shared/turn.env'), 'up', '-d', '--no-deps', 'ikar_server'], check=True) try: update(release) health = None for attempt in range(30): try: with urllib.request.urlopen('http://127.0.0.1:5099/health', timeout=3) as response: health = json.load(response) if health.get('status') == 'ok': break except Exception: pass time.sleep(1) if health is None or health.get('status') != 'ok': raise RuntimeError('New API failed health verification') pending = root / 'current.next' pending.symlink_to(release, target_is_directory=True) os.replace(pending, current) except Exception: update(previous) raise report = { 'release': str(release), 'previousRelease': str(previous), 'backup': backup, 'health': health, 'protectedServicesUnchanged': snapshot() == protected_before, } (root / 'deployment-1.0.1.json').write_text(json.dumps(report, indent=2)) print(json.dumps(report))