#!/usr/bin/env python3 """Refuse to adopt another Compose project or replace existing persistent storage.""" import argparse import json import os from pathlib import Path import re import subprocess import sys STATEFUL = {'mongo': {'/data/db'}, 'redis': {'/data'}, 'rabbitmq': {'/var/lib/rabbitmq'}, 'easyai-pgvector': {'/var/lib/postgresql', '/var/lib/postgresql/data'}, 'easyai-server': {'/app/upload'}} def canonical(path): path = path.replace('\\', '/') path = re.sub(r'^/(?:run/desktop/mnt/host|host_mnt)/([A-Za-z])/', r'\1:/', path) if re.match(r'^[A-Za-z]:/', path): return path.rstrip('/').casefold() if path.startswith('/host_mnt/'): path = path[len('/host_mnt'):] return str(Path(path).resolve()).rstrip('/') def validate(configuration, existing, selected=()): for service, settings in configuration['services'].items(): if selected and service not in selected: continue container = existing.get(service) if not container: continue project = (container.get('Config', {}).get('Labels') or {}).get('com.docker.compose.project') if project != configuration['name']: raise ValueError(f'{service} 的现有容器不属于当前 Compose 项目;请核对原安装目录和项目名') actual = {mount['Destination']: mount for mount in container.get('Mounts', [])} for mount in settings.get('volumes', []): if mount['target'] not in STATEFUL.get(service, set()): continue old = actual.get(mount['target'], {}) if mount['type'] == 'bind': expected = canonical(mount['source']) equal = old.get('Type') == 'bind' and canonical(old.get('Source', '')) == expected and Path(expected).is_dir() else: definition = configuration.get('volumes', {}).get(mount.get('source'), {}) name = definition.get('name') equal = bool(name) and old.get('Type') == 'volume' and old.get('Name') == name if not equal: raise ValueError(f'{service} 的 {mount["target"]} 数据位置将改变;请先备份并按数据迁移步骤处理') def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--sudo', action='store_true') parser.add_argument('--services', nargs='*', default=[]) args = parser.parse_args() configuration = json.load(sys.stdin) docker = ['sudo', 'docker'] if args.sudo else ['docker'] if subprocess.run(docker + ['info'], capture_output=True).returncode: raise ValueError('无法连接 Docker,未检查现有容器和数据位置') existing = {} for service, settings in configuration['services'].items(): if args.services and service not in args.services: continue # Explicit global names can collide with another installation. Services # without container_name are already scoped by Compose's project name. if not settings.get('container_name'): continue result = subprocess.run(docker + ['container', 'inspect', settings['container_name']], capture_output=True, text=True) if result.returncode: if result.stderr and not re.search('No such (?:container|object)', result.stderr, re.I): raise ValueError(f'无法核对现有容器 {service},未继续部署') continue existing[service] = json.loads(result.stdout)[0] validate(configuration, existing, args.services) print('✅ 现有容器归属和持久化数据位置检查通过') if __name__ == '__main__': try: main() except (ValueError, KeyError, TypeError, OSError) as error: print('❌ ' + str(error) + ';尚未拉取或重建服务', file=sys.stderr) sys.exit(1)