import os import shutil import yaml import subprocess from typing import Dict, Any def run_shell_command(command: str, cwd: str = None) -> bool: """执行shell命令并返回执行结果""" try: print(f"执行命令: {command} (工作目录: {cwd or os.getcwd()})") result = subprocess.run( command, cwd=cwd, shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) print(f"命令输出: {result.stdout}") return True except subprocess.CalledProcessError as e: print(f"命令执行失败: {e.stderr}") return False def cleanup_dangling_images() -> bool: """清除docker虚悬镜像(dangling images)""" print("开始清除虚悬镜像...") return run_shell_command('sudo docker images -f "dangling=true" -q | xargs -r sudo docker rmi') def execute_release_scripts(root_dir: str) -> bool: """遍历所有api_目录并执行release.sh脚本(排除api_template)""" print("开始执行所有api_目录下的release.sh脚本...") # 定义目标镜像目录 images_dir = os.path.join(root_dir, 'deploy', 'images') # 确保目标目录存在并设置权限 os.makedirs(images_dir, exist_ok=True) # 尝试设置目录可写权限(解决目标目录权限问题) try: os.chmod(images_dir, 0o755) except PermissionError: print(f"警告:无法修改目录权限 {images_dir},可能导致文件复制失败") for dir_name in os.listdir(root_dir): # 排除api_template文件夹 if dir_name == "api_template": print(f"跳过模板目录: {dir_name}") continue dir_path = os.path.join(root_dir, dir_name) if os.path.isdir(dir_path) and dir_name.startswith('api_'): release_script = os.path.join(dir_path, 'release.sh') if os.path.exists(release_script): print(f"处理目录: {dir_path}") # 添加执行权限 if not run_shell_command(f"chmod +x {release_script}", dir_path): print(f"为 {release_script} 添加权限失败,跳过执行") continue # 执行release.sh if not run_shell_command(f"./release.sh", dir_path): print(f"{release_script} 执行失败,跳过后续步骤") return False # 复制目录下的.tar文件到根目录的deploy/images下(替换方式) for file in os.listdir(dir_path): if file.endswith('.tar'): src_path = os.path.join(dir_path, file) dest_path = os.path.join(images_dir, file) # 检查源文件是否存在且可读 if not os.path.exists(src_path): print(f"警告:源文件不存在 {src_path},跳过") continue if not os.access(src_path, os.R_OK): print(f"警告:无权限读取 {src_path},尝试修复权限...") try: # 尝试赋予源文件读权限 os.chmod(src_path, 0o644) # 再次检查权限 if not os.access(src_path, os.R_OK): print(f"错误:仍无法读取 {src_path},请手动检查权限") continue except PermissionError: print(f"错误:无法修复 {src_path} 权限,跳过该文件") continue # 处理目标文件 if os.path.exists(dest_path): try: os.remove(dest_path) print(f"已删除旧文件: {dest_path}") except PermissionError: print(f"警告:无权限删除旧文件 {dest_path},尝试用sudo运行脚本") continue # 复制文件(带权限处理) try: shutil.copy2(src_path, dest_path) # 复制后确保目标文件可读写 os.chmod(dest_path, 0o644) print(f"已将 {src_path} 替换复制到 {dest_path}") # 复制成功后删除原文件 try: os.remove(src_path) print(f"已删除原文件: {src_path}") except PermissionError: print(f"警告:无权限删除原文件 {src_path},请手动清理") except Exception as e: print(f"删除原文件 {src_path} 时发生错误: {str(e)}") except PermissionError: print(f"错误:复制 {src_path} 失败(权限不足)") print("建议:使用 sudo 权限重新运行脚本(sudo python3 build.py)") return False except Exception as e: print(f"复制文件时发生错误: {str(e)},跳过该文件") continue else: print(f"{dir_path} 中未找到release.sh,跳过") print("所有release.sh脚本执行完成") return True def merge_yaml_files(root_dir: str) -> Dict[str, Any]: """合并所有 docker-compose 相关 YAML 文件内容(排除api_template)""" merged = { 'services': {}, 'networks': {}, 'volumes': {} } yaml_files = [] # 根目录下的 docker-compose.*.yaml for file in os.listdir(root_dir): if file.startswith('docker-compose.') and file.endswith('.yaml') and file != 'docker-compose.yaml': yaml_files.append(os.path.join(root_dir, file)) # api_ 目录下的 docker-compose.*.yaml(排除api_template) for dir_name in os.listdir(root_dir): if dir_name == "api_template": print(f"跳过模板目录的yaml文件: {dir_name}") continue dir_path = os.path.join(root_dir, dir_name) if os.path.isdir(dir_path) and dir_name.startswith('api_'): for file in os.listdir(dir_path): if file.startswith('docker-compose.') and file.endswith('.yaml'): yaml_files.append(os.path.join(dir_path, file)) # 合并所有 YAML 文件内容 for file_path in yaml_files: print(f"合并文件: {file_path}") try: with open(file_path, 'r', encoding='utf-8') as f: data = yaml.safe_load(f) if not data: continue if 'services' in data: merged['services'].update(data['services']) if 'networks' in data: merged['networks'].update(data['networks']) if 'volumes' in data: merged['volumes'].update(data['volumes']) except yaml.YAMLError as e: print(f"解析 {file_path} 失败: {e}") continue except PermissionError: print(f"错误:无权限读取 {file_path},跳过该文件") continue return merged def main(): # 先执行清除虚悬镜像操作 print("部署流程开始,先执行清除虚悬镜像操作...") if not cleanup_dangling_images(): print("清除虚悬镜像失败(非致命错误,继续部署流程)") else: print("虚悬镜像清除完成") # 获取项目根目录(deploy.py 所在目录) root_dir = os.path.dirname(os.path.abspath(__file__)) print(f"项目根目录: {root_dir}") # 1. 执行所有api_目录下的release.sh脚本 if not execute_release_scripts(root_dir): print("执行release脚本失败,终止部署流程") return # 2. 合并所有YAML文件 merged_data = merge_yaml_files(root_dir) # 输出到根目录的docker-compose.yaml output_path = os.path.join(root_dir, 'docker-compose.yaml') try: with open(output_path, 'w', encoding='utf-8') as f: yaml.dump( merged_data, f, sort_keys=False, allow_unicode=True, default_flow_style=False ) print(f"已生成合并后的 docker-compose.yaml: {output_path}") # 将新的docker-compose.yaml以替换方式复制到根目录的deploy/下 deploy_dir = os.path.join(root_dir, 'deploy') os.makedirs(deploy_dir, exist_ok=True) # 确保deploy目录可写 try: os.chmod(deploy_dir, 0o755) except PermissionError: print(f"警告:无法修改目录权限 {deploy_dir}") dest_compose_path = os.path.join(deploy_dir, 'docker-compose.yaml') # 替换方式复制 if os.path.exists(dest_compose_path): try: os.remove(dest_compose_path) except PermissionError: print(f"警告:无权限删除旧文件 {dest_compose_path}") return False shutil.copy2(output_path, dest_compose_path) # 设置目标文件权限 os.chmod(dest_compose_path, 0o644) print(f"已将 {output_path} 替换复制到 {dest_compose_path}") except PermissionError as e: print(f"错误:无权限操作文件 {output_path} 或 {dest_compose_path}") print(f"详细错误: {str(e)}") print("建议:使用 sudo 权限重新运行脚本(sudo python3 build.py)") return False except Exception as e: print(f"生成或复制docker-compose.yaml时出错: {str(e)}") return False print("\n===== 清理虚悬镜像 =====") if not cleanup_dangling_images(): print("清除虚悬镜像失败(非致命错误,继续部署流程)") else: print("虚悬镜像清除完成") if __name__ == "__main__": # 需要安装 pyyaml 库: pip install pyyaml main()