#!/usr/bin/env python3 from __future__ import annotations import argparse import json import shlex import sys from pathlib import Path from typing import Any sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from deploy.tencent_tat.hyapp_tat_deploy import build_tat_client, load_env_file, load_inventory, run_tat_shell DEFAULT_REPO_URL = "git@gitea.haiyihy.com:hy/hyapp-server.git" DEFAULT_SOURCE_DIR = "/opt/deploy/sources/hyapp-server" DEFAULT_BRANCH = "main" DEFAULT_ADMIN_HOST = "134.175.160.86" DEFAULT_ADMIN_BIND_ADDR = "172.16.0.6" DEFAULT_ADMIN_PORT = 13100 def shell_join(items: list[str]) -> str: return " ".join(shlex.quote(item) for item in items) def admin_endpoint(host: str, user: str) -> str: return f"{user}@{host}" if user else host def build_remote_script(args: argparse.Namespace) -> str: endpoint = admin_endpoint(args.admin_host, args.admin_user) dry_run = "1" if args.dry_run else "0" ssh_opts = [ "ssh", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=accept-new", "-o", f"ConnectTimeout={args.ssh_connect_timeout}", ] rsync_ssh = shell_join(ssh_opts) admin_ssh = shell_join([*ssh_opts, endpoint]) return f"""#!/usr/bin/env bash set -euo pipefail REPO_URL={shlex.quote(args.repo_url)} SOURCE_DIR={shlex.quote(args.source_dir)} BRANCH={shlex.quote(args.branch)} DRY_RUN={dry_run} ADMIN_ENDPOINT={shlex.quote(endpoint)} ADMIN_BIND_ADDR={shlex.quote(args.admin_bind_addr)} ADMIN_PORT={shlex.quote(str(args.admin_port))} SSH_CMD={shlex.quote(rsync_ssh)} ADMIN_SSH={shlex.quote(admin_ssh)} export GIT_SSH_COMMAND="ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=15" echo "repo=$REPO_URL" echo "source_dir=$SOURCE_DIR" echo "branch=$BRANCH" echo "admin_endpoint=$ADMIN_ENDPOINT" if [ "$DRY_RUN" = "1" ]; then target_commit="$(git ls-remote "$REPO_URL" "refs/heads/$BRANCH" | awk '{{print $1}}')" if [ -z "$target_commit" ]; then echo "cannot resolve origin/$BRANCH" >&2 exit 12 fi echo "target_commit=$target_commit" $ADMIN_SSH "ADMIN_BIND_ADDR='$ADMIN_BIND_ADDR' ADMIN_PORT='$ADMIN_PORT' bash -s" <<'REMOTE' set -e echo "admin_active=$(systemctl is-active hyapp-admin-server || true)" ss -lntp | grep "$ADMIN_PORT" || true curl -fsS -m 3 "http://$ADMIN_BIND_ADDR:$ADMIN_PORT/healthz" || true echo REMOTE exit 0 fi if [ -d "$SOURCE_DIR/.git" ]; then cd "$SOURCE_DIR" git remote set-url origin "$REPO_URL" else mkdir -p "$(dirname "$SOURCE_DIR")" git clone "$REPO_URL" "$SOURCE_DIR" cd "$SOURCE_DIR" fi git fetch --prune origin "$BRANCH" git checkout -B "$BRANCH" "origin/$BRANCH" git reset --hard "origin/$BRANCH" git clean -fd commit="$(git rev-parse HEAD)" short_commit="$(git rev-parse --short HEAD)" build_id="$(date +%Y%m%d%H%M%S)-$short_commit" build_image="hyapp/admin-server-build:$build_id" local_binary="/tmp/admin-server-$build_id" remote_binary="/tmp/admin-server-$build_id" echo "commit=$commit" echo "build_id=$build_id" docker build -t "$build_image" -f server/admin/Dockerfile . container_id="$(docker create "$build_image")" trap 'docker rm -f "$container_id" >/dev/null 2>&1 || true' EXIT docker cp "$container_id:/app/server" "$local_binary" docker rm "$container_id" >/dev/null trap - EXIT chmod 0755 "$local_binary" binary_sha="$(sha256sum "$local_binary" | awk '{{print $1}}')" binary_size="$(stat -c %s "$local_binary")" echo "binary_sha=$binary_sha" echo "binary_size=$binary_size" rsync -az --partial -e "$SSH_CMD" "$local_binary" "$ADMIN_ENDPOINT:$remote_binary" $ADMIN_SSH "REMOTE_BINARY='$remote_binary' EXPECTED_SHA='$binary_sha' ADMIN_BIND_ADDR='$ADMIN_BIND_ADDR' ADMIN_PORT='$ADMIN_PORT' COMMIT='$commit' bash -s" <<'REMOTE' set -euo pipefail BIN=/opt/hyapp/admin-server/admin-server BACKUP="$BIN.bak.$(date +%Y%m%d%H%M%S)" actual_sha="$(sha256sum "$REMOTE_BINARY" | awk '{{print $1}}')" if [ "$actual_sha" != "$EXPECTED_SHA" ]; then echo "uploaded binary sha mismatch: $actual_sha != $EXPECTED_SHA" >&2 exit 21 fi cp "$BIN" "$BACKUP" install -o root -g root -m 0755 "$REMOTE_BINARY" "$BIN.new" mv "$BIN.new" "$BIN" systemctl restart hyapp-admin-server ready=0 for _ in $(seq 1 40); do active="$(systemctl is-active hyapp-admin-server || true)" if [ "$active" = "active" ] && ss -lntp | grep -q "$ADMIN_BIND_ADDR:$ADMIN_PORT"; then if curl -fsS -m 3 "http://$ADMIN_BIND_ADDR:$ADMIN_PORT/healthz" >/tmp/hyapp-admin-healthz.out && curl -fsS -m 3 "http://$ADMIN_BIND_ADDR:$ADMIN_PORT/readyz" >/tmp/hyapp-admin-readyz.out; then ready=1 break fi fi sleep 2 done if [ "$ready" != "1" ]; then echo "admin-server verification failed; rollback to $BACKUP" >&2 cp "$BACKUP" "$BIN" systemctl restart hyapp-admin-server || true exit 22 fi echo "backup=$BACKUP" echo "active=$(systemctl is-active hyapp-admin-server || true)" echo "installed_sha=$(sha256sum "$BIN" | awk '{{print $1}}')" echo "listener=$(ss -lntp | grep "$ADMIN_BIND_ADDR:$ADMIN_PORT" || true)" echo "healthz=$(cat /tmp/hyapp-admin-healthz.out)" echo "readyz=$(cat /tmp/hyapp-admin-readyz.out)" REMOTE """ def run_deploy(args: argparse.Namespace) -> dict[str, Any]: inventory = load_inventory(args.inventory) client = build_tat_client(inventory) script = build_remote_script(args) result = run_tat_shell( client, instance_id=inventory["deploy_server"]["instance_id"], script=script, command_name="hyapp-admin-server-deploy", timeout_seconds=args.timeout_seconds, poll_seconds=int(inventory.get("tat_poll_seconds") or 2), ) output = str(result.get("output") or "") return { "adminHost": args.admin_host, "dryRun": args.dry_run, "ok": result.get("status") == "SUCCESS", "output": output[-12000:], "sourceDir": args.source_dir, "status": result.get("status"), "target": inventory.get("deploy_server") or {}, } def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="Deploy production admin-server from Gitea origin/main through the deploy host.") parser.add_argument("action", choices=["deploy"]) parser.add_argument("--env-file", default="") parser.add_argument("--inventory", default="deploy/tencent_tat/hyapp-services.inventory.prod.example.json") parser.add_argument("--repo-url", default=DEFAULT_REPO_URL) parser.add_argument("--source-dir", default=DEFAULT_SOURCE_DIR) parser.add_argument("--branch", default=DEFAULT_BRANCH) parser.add_argument("--admin-host", default=DEFAULT_ADMIN_HOST) parser.add_argument("--admin-user", default="") parser.add_argument("--admin-bind-addr", default=DEFAULT_ADMIN_BIND_ADDR) parser.add_argument("--admin-port", type=int, default=DEFAULT_ADMIN_PORT) parser.add_argument("--ssh-connect-timeout", type=int, default=15) parser.add_argument("--timeout-seconds", type=int, default=1800) parser.add_argument("--dry-run", action="store_true") parser.add_argument("--yes", action="store_true") return parser def main() -> int: parser = build_parser() args = parser.parse_args() try: load_env_file(args.env_file) if args.branch != DEFAULT_BRANCH: raise RuntimeError("admin-server production deploy only supports origin/main") if not args.dry_run and not args.yes: raise RuntimeError("real execution requires --yes") payload = run_deploy(args) print(json.dumps(payload, ensure_ascii=False, indent=2)) return 0 if payload.get("ok") else 1 except Exception as exc: # noqa: BLE001 print(json.dumps({"error": str(exc), "ok": False}, ensure_ascii=False, indent=2)) return 1 if __name__ == "__main__": raise SystemExit(main())