fix(deploy): 修复组件版本解析和安装升级验收流程
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user