""" GBS 通用镜像打包脚本 ==================== 从本地 Windows 执行: SFTP 上传 JAR -> 远程 Docker build -> ACR push 支持两种打包模式: 1. 项目 Dockerfile 模式 (--project-dockerfile): 使用项目自带的 Dockerfile 2. 模板 Dockerfile 模式 (默认): 使用 CentOS 7 + JDK 模板 用法: python build_push.py --jar --app <应用名> --version <版本号> python build_push.py --jar --app <应用名> --version <版本号> --project-dockerfile 示例: # 使用项目自带 Dockerfile 打包 safety-eval-service python build_push.py \ --jar "E:\\works\\projects\\safety-eval-service\\safety-eval-start\\target\\safety-eval-start-1.0-SNAPSHOT.jar" \ --app jjb-saas-safety-eval \ --version 1 \ --project-dockerfile "E:\\works\\projects\\safety-eval-service\\Dockerfile" # 使用模板 Dockerfile 打包 certificate python build_push.py \ --jar "E:\\works\\projects\\zcloud_gbs_certificate\\start\\target\\start.jar" \ --app zcloud-gbs-certificate \ --version 1 """ import paramiko import sys import os import time import argparse from datetime import datetime # ---- 读取配置 (从同目录 credentials.conf) ---- def load_credentials(): """从 credentials.conf 读取配置""" cred_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "credentials.conf") creds = {} if os.path.exists(cred_path): with open(cred_path, 'r', encoding='utf-8') as f: for line in f: line = line.strip() if line and not line.startswith('#') and '=' in line: key, _, value = line.partition('=') creds[key.strip()] = value.strip() return creds def ssh_exec(client, cmd, timeout=600): """执行 SSH 命令并打印结果""" print(f" > {cmd[:150]}{'...' if len(cmd) > 150 else ''}") stdin, stdout, stderr = client.exec_command(cmd, timeout=timeout) out = stdout.read().decode('utf-8', errors='replace') err = stderr.read().decode('utf-8', errors='replace') exit_code = stdout.channel.recv_exit_status() if out.strip(): for line in out.strip().split('\n')[-15:]: print(f" {line}") if err.strip(): important = [l for l in err.strip().split('\n') if not any(x in l for x in ['Downloading', 'Downloaded', 'Extracting', 'Progress'])] if important: print(f" [stderr] {'; '.join(important[:5])}") return out.strip(), err.strip(), exit_code def main(): parser = argparse.ArgumentParser(description="GBS Docker Build & Push") parser.add_argument('--jar', required=True, help='JAR 文件本地路径') parser.add_argument('--app', required=True, help='应用名称 (用于镜像 tag)') parser.add_argument('--version', default='1', help='版本后缀 (默认: 1)') parser.add_argument('--prefix', default='ota', help='标签前缀 (默认: ota)') parser.add_argument('--project-dockerfile', default=None, help='项目自带 Dockerfile 路径 (不指定则使用模板模式)') parser.add_argument('--dry-run', action='store_true', help='仅打印信息,不执行') args = parser.parse_args() # ---- 加载凭据 ---- creds = load_credentials() if not creds: print("ERROR: credentials.conf not found or empty!") sys.exit(1) MASTER = creds['MASTER_HOST'] SSH_USER = creds['SSH_USER'] SSH_PASSWD = creds['SSH_PASSWD'] ZJK_REGISTRY = creds['ZJK_REGISTRY'] ZJK_REPO = creds['ZJK_REPO'] ZJK_USER = creds['ZJK_USER'] ZJK_PASS = creds['ZJK_PASS'] HZ_REGISTRY = creds['HZ_REGISTRY'] HZ_USER = creds['HZ_USER'] HZ_PASS = creds['HZ_PASS'] # ---- 构建参数 ---- jar_local = args.jar app_name = args.app version_suffix = args.version env_prefix = args.prefix use_project_dockerfile = args.project_dockerfile is not None dockerfile_local = args.project_dockerfile if use_project_dockerfile else None script_dir = os.path.dirname(os.path.abspath(__file__)) template_dockerfile = os.path.join(script_dir, "Dockerfile.template") date_tag = datetime.now().strftime("%Y%m%d") image_tag = f"{env_prefix}-{date_tag}-{version_suffix}" full_image = f"{ZJK_REGISTRY}/{ZJK_REPO}:{app_name}-{image_tag}" # ---- 校验 ---- if not os.path.exists(jar_local): print(f"ERROR: JAR not found: {jar_local}") sys.exit(1) if use_project_dockerfile and not os.path.exists(dockerfile_local): print(f"ERROR: Dockerfile not found: {dockerfile_local}") sys.exit(1) jar_size_mb = os.path.getsize(jar_local) / 1024 / 1024 build_dir = f"/tmp/docker-build-{app_name}" mode = "项目 Dockerfile" if use_project_dockerfile else "模板 Dockerfile (CentOS 7 + JDK)" print("=" * 65) print(" GBS Docker Build & Push") print("=" * 65) print(f" 模式: {mode}") print(f" JAR: {jar_local} ({jar_size_mb:.1f} MB)") if use_project_dockerfile: print(f" Dockerfile: {dockerfile_local}") print(f" App: {app_name}") print(f" Tag: {image_tag}") print(f" Image: {full_image}") print(f" Registry: {ZJK_REGISTRY}/{ZJK_REPO}") print(f" Target: {MASTER}") print("=" * 65) if args.dry_run: print("\n[DRY RUN] 以上为打包配置,未实际执行。") return # ---- SSH 连接 ---- client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect(MASTER, username=SSH_USER, password=SSH_PASSWD, timeout=30) print("\n[1/6] SSH 连接成功") # ---- 准备远程目录 ---- if use_project_dockerfile: # 项目 Dockerfile 模式: JAR 路径为 ./start/target/start.jar ssh_exec(client, f"rm -rf {build_dir} && mkdir -p {build_dir}/start/target") else: # 模板模式: JAR 路径为 ./target/*.jar,需复制 JDK ssh_exec(client, f"rm -rf {build_dir} && mkdir -p {build_dir}/target") # ---- 上传 JAR ---- print(f"\n[2/6] 上传 JAR ({jar_size_mb:.1f} MB)...") sftp = client.open_sftp() start = time.time() last_pct = -1 def progress(transferred, total): nonlocal last_pct pct = int(transferred * 100 / total) if pct >= last_pct + 10: last_pct = pct mb = transferred / 1024 / 1024 elapsed = time.time() - start speed = mb / elapsed if elapsed > 0 else 0 print(f" {pct}% ({mb:.1f} MB) - {speed:.1f} MB/s") if use_project_dockerfile: remote_jar = f"{build_dir}/start/target/start.jar" else: remote_jar = f"{build_dir}/target/{os.path.basename(jar_local)}" sftp.put(jar_local, remote_jar, callback=progress) elapsed = time.time() - start print(f" Done in {elapsed:.1f}s ({jar_size_mb/elapsed:.1f} MB/s)") # ---- 上传 Dockerfile ---- print(f"\n 上传 Dockerfile...") if use_project_dockerfile: sftp.put(dockerfile_local, f"{build_dir}/Dockerfile") else: with open(template_dockerfile, 'r', encoding='utf-8') as f: dockerfile_content = f.read() stdin, stdout, stderr = client.exec_command(f"cat > {build_dir}/Dockerfile") stdin.write(dockerfile_content) stdin.channel.shutdown_write() stdout.read() sftp.close() # ---- 模板模式: 复制 JDK ---- if not use_project_dockerfile: print("\n[3/6] 准备构建上下文 (复制 JDK)...") ssh_exec(client, f"cp -r /opt/jdk1.8.0_202 {build_dir}/", timeout=120) else: print("\n[3/6] 构建上下文已就绪") ssh_exec(client, f"du -sh {build_dir}/") # ---- ACR 登录 ---- print("\n[4/6] 登录 ACR...") if use_project_dockerfile: # 项目 Dockerfile 可能引用杭州 ACR 的基础镜像 ssh_exec(client, f"echo '{HZ_PASS}' | docker login --username={HZ_USER} --password-stdin {HZ_REGISTRY}") ssh_exec(client, f"echo '{ZJK_PASS}' | docker login --username={ZJK_USER} --password-stdin {ZJK_REGISTRY}") # ---- Docker build ---- print("\n[5/6] 构建 Docker 镜像...") out, err, code = ssh_exec(client, f"docker build -t '{full_image}' {build_dir}", timeout=300) if code != 0: print("ERROR: Docker build failed!") client.close() sys.exit(1) # ---- Push to ACR ---- print("\n[6/6] 推送到张家口 ACR...") out, err, code = ssh_exec(client, f"docker push '{full_image}'", timeout=600) if code != 0: print("ERROR: Docker push failed!") client.close() sys.exit(1) # ---- 验证 & 清理 ---- ssh_exec(client, f"docker images | grep '{app_name}'") ssh_exec(client, f"rm -rf {build_dir}") client.close() # ---- 输出结果 ---- print("\n" + "=" * 65) print(" BUILD COMPLETE") print("=" * 65) print(f"\n 镜像地址:\n {full_image}\n") print(f" K8s 部署:\n" f" kubectl set image deployment/{app_name} \\\n" f" {app_name}={full_image} -n jjb-dragon\n") print("=" * 65) if __name__ == "__main__": main()