#!/usr/bin/env python3 """Exercise reviewed image-pin adoption with real Git and Compose parsing only.""" import json import os from pathlib import Path import subprocess import tempfile import time source = Path(__file__).resolve().parent.parent prepare = Path(os.environ.get('EASYAI_PREPARE_UPDATE_SCRIPT', source / 'scripts/prepare-update-repository.py')) started = time.monotonic() pin = 'repository-web@sha256:' + 'a' * 64 def run(args, root, env=None): return subprocess.run(args, cwd=root, env=env, capture_output=True, text=True, timeout=30) def checked(args, root, env=None): result = run(args, root, env) assert result.returncode == 0, result.stdout + result.stderr return result.stdout for restore in ('', 'easyai-web', 'redis', 'ws-gateway'): with tempfile.TemporaryDirectory(prefix='easyai-image-restore-') as directory: base = Path(directory) root = base / 'easyai' root.mkdir() compose = '''services: easyai-web: image: repository-web:${VERSION} environment: - EXISTING=keep easyai-server: image: repository-server:${VERSION} environment: - LOCAL_SETTING=original redis: image: redis:7 ''' (root / 'docker-compose.yml').write_text(compose) (root / '.gitignore').write_text('.env\ndata/\n') env_original = b'VERSION=latest\nCONFIG_JWT_SECRET=test-private-value\n' (root / '.env').write_bytes(env_original) for args in (['git', 'init', '-q', '-b', 'main'], ['git', 'config', 'user.name', 'Update test'], ['git', 'config', 'user.email', 'test@example.invalid'], ['git', 'add', '.'], ['git', 'commit', '-qm', 'test: 旧部署基准']): checked(args, root) remote = base / 'remote.git' seed = base / 'seed' checked(['git', 'clone', '-q', '--bare', str(root), str(remote)], base) checked(['git', 'remote', 'add', 'origin', str(remote)], root) checked(['git', 'fetch', '-q'], root) checked(['git', 'branch', '--set-upstream-to', 'origin/main'], root) checked(['git', 'clone', '-q', str(remote), str(seed)], base) (seed / 'docker-compose.yml').write_text(compose.replace(' - EXISTING=keep', ' - EXISTING=keep\n - NEW_SETTING=from-repository')) for args in (['git', 'config', 'user.name', 'Update test'], ['git', 'config', 'user.email', 'test@example.invalid'], ['git', 'add', '.'], ['git', 'commit', '-qm', 'test: 新部署配置'], ['git', 'push', '-q']): checked(args, seed) original = compose.replace('repository-web:${VERSION}', pin).replace('LOCAL_SETTING=original', 'LOCAL_SETTING=custom').replace('redis:7', 'redis:6.2') (root / 'docker-compose.yml').write_text(original) env = {**os.environ, 'UPDATE_RESTORE_REPOSITORY_IMAGES': restore, 'UPDATE_SERVICES': 'easyai-server easyai-web'} result = run(['python3', str(prepare), str(root)], root, env) if restore in ('redis', 'ws-gateway'): assert result.returncode != 0, result.stdout assert (root / 'docker-compose.yml').read_text() == original assert (root / '.env').read_bytes() == env_original assert not (root / 'data').exists() continue assert result.returncode == 0, result.stdout + result.stderr checked(['git', 'pull', '--ff-only'], root) config = json.loads(checked(['docker', 'compose', '--env-file', '.env', 'config', '--format', 'json'], root)) services = config['services'] assert services['easyai-web']['image'] == ('repository-web:latest' if restore else pin), services['easyai-web']['image'] assert services['easyai-web']['environment']['NEW_SETTING'] == 'from-repository' assert services['easyai-server']['environment']['LOCAL_SETTING'] == 'custom' assert services['redis']['image'] == 'redis:6.2' assert 'CONFIG_JWT_SECRET=test-private-value' in (root / '.env').read_text() backups = list((root / 'data/deployment-backups').glob('legacy-update-*')) assert len(backups) == 1 assert (backups[0] / 'docker-compose.yml').read_text() == original assert (backups[0] / '.env').read_bytes() == env_original assert (backups[0] / '.env').stat().st_mode & 0o077 == 0 metadata = json.loads((backups[0] / 'migration.json').read_text()) if restore: assert metadata['restoredRepositoryImages'] == ['easyai-web'] # A second adoption preserves the result and creates no extra backup. checked(['python3', str(prepare), str(root)], root, env) assert len(list((root / 'data/deployment-backups').glob('legacy-update-*'))) == 1 # A later main-file customization is adopted without dropping the # existing override or changing an unrelated dependency setting. main_path = root / 'docker-compose.yml' main_path.write_text(main_path.read_text().replace('LOCAL_SETTING=original', 'LOCAL_SETTING=second-custom')) override_file = root / 'data/deployment-config/legacy-compose.json' previous_override = override_file.read_bytes() checked(['python3', str(prepare), str(root)], root, env) config = json.loads(checked(['docker', 'compose', '--env-file', '.env', 'config', '--format', 'json'], root)) assert config['services']['easyai-server']['environment']['LOCAL_SETTING'] == 'second-custom' assert config['services']['redis']['image'] == 'redis:6.2' assert config['services']['easyai-web']['image'] == ('repository-web:latest' if restore else pin) backups = list((root / 'data/deployment-backups').glob('legacy-update-*')) assert len(backups) == 2 newest = max(backups, key=lambda p: p.stat().st_mtime) assert (newest / 'data/deployment-config/legacy-compose.json').read_bytes() == previous_override assert (newest / 'data/deployment-config/legacy-compose.json').stat().st_mode & 0o077 == 0 if not restore: # Main is clean, but an existing override still pins the web image. # Explicit restoration must act on that override and be idempotent. restoration_env = {**env, 'UPDATE_RESTORE_REPOSITORY_IMAGES': 'easyai-web'} checked(['python3', str(prepare), str(root)], root, restoration_env) config = json.loads(checked(['docker', 'compose', '--env-file', '.env', 'config', '--format', 'json'], root)) assert config['services']['easyai-web']['image'] == 'repository-web:latest' assert config['services']['redis']['image'] == 'redis:6.2' assert config['services']['easyai-server']['environment']['LOCAL_SETTING'] == 'second-custom' assert len(list((root / 'data/deployment-backups').glob('legacy-update-*'))) == 3 checked(['python3', str(prepare), str(root)], root, restoration_env) assert len(list((root / 'data/deployment-backups').glob('legacy-update-*'))) == 3 for mode, adopt in (('image', '1'), ('full', '0')): env = {**os.environ, 'UPDATE_MODE': mode, 'UPDATE_ADOPT_LEGACY_CONFIG': adopt, 'UPDATE_RESTORE_REPOSITORY_IMAGES': 'easyai-web'} result = run(['bash', str(source / 'update.sh')], source, env) assert result.returncode != 0 and '尚未修改配置或容器' in result.stderr print(json.dumps({'passed': True, 'cases': 6, 'durationSeconds': round(time.monotonic() - started, 3)}))