244 lines
9.1 KiB
Python
244 lines
9.1 KiB
Python
|
|
"""
|
|||
|
|
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()
|