safety-eval-service/docs/build-tools2/build_push.py

244 lines
9.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"""
GBS 通用镜像打包脚本
====================
从本地 Windows 执行: SFTP 上传 JAR -> 远程 Docker build -> ACR push
支持两种打包模式:
1. 项目 Dockerfile 模式 (--project-dockerfile): 使用项目自带的 Dockerfile
2. 模板 Dockerfile 模式 (默认): 使用 CentOS 7 + JDK 模板
用法:
python build_push.py --jar <JAR路径> --app <应用名> --version <版本号>
python build_push.py --jar <JAR路径> --app <应用名> --version <版本号> --project-dockerfile <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()