forked from wangbo/easyai
85 lines
3.8 KiB
Python
85 lines
3.8 KiB
Python
#!/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)
|