230 lines
7.8 KiB
Python
230 lines
7.8 KiB
Python
#!/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_option_args = [
|
|
"-o",
|
|
"BatchMode=yes",
|
|
"-o",
|
|
"StrictHostKeyChecking=accept-new",
|
|
"-o",
|
|
f"ConnectTimeout={args.ssh_connect_timeout}",
|
|
]
|
|
ssh_opts = ["ssh", *ssh_option_args]
|
|
scp_cmd = shell_join(["timeout", str(args.transfer_timeout_seconds), "scp", *ssh_option_args])
|
|
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))}
|
|
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"
|
|
|
|
{scp_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("--transfer-timeout-seconds", type=int, default=300)
|
|
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())
|