From 1f370c854204d718d4724c7cb1c63c443df5c609 Mon Sep 17 00:00:00 2001 From: zhx Date: Wed, 8 Jul 2026 19:02:55 +0800 Subject: [PATCH] chore: adapt test deploy to compose ops panel --- .deploy/test-deploy/deploy-likei-services.sh | 363 +++++++----------- .../test-deploy/ops/aslan-test-ops.service | 21 +- .deploy/test-deploy/ops/aslan_ops.py | 274 +++++++------ .deploy/test-deploy/sync-and-deploy.sh | 61 +-- 4 files changed, 296 insertions(+), 423 deletions(-) mode change 100644 => 100755 .deploy/test-deploy/ops/aslan_ops.py diff --git a/.deploy/test-deploy/deploy-likei-services.sh b/.deploy/test-deploy/deploy-likei-services.sh index f69abc0c..ac8631b3 100755 --- a/.deploy/test-deploy/deploy-likei-services.sh +++ b/.deploy/test-deploy/deploy-likei-services.sh @@ -1,40 +1,38 @@ #!/usr/bin/env bash set -euo pipefail -BASE_DIR="${BASE_DIR:-/opt/aslan-test-deploy}" -SRC_DIR="${SRC_DIR:-$BASE_DIR/source/likei-services}" -KUBECONFIG="${KUBECONFIG:-$BASE_DIR/kubeconfig}" +BASE_DIR="${BASE_DIR:-/opt/aslan-test}" +SRC_DIR="${SRC_DIR:-$BASE_DIR/source/aslan-server}" +COMPOSE_FILE="${COMPOSE_FILE:-$BASE_DIR/docker-compose.yml}" +ENV_FILE="${ENV_FILE:-$BASE_DIR/config/.env}" LOCK_FILE="${LOCK_FILE:-$BASE_DIR/deploy.lock}" -NAMESPACE="${NAMESPACE:-test}" -MODE="${MODE:-preload}" -SKIP_BUILD="${SKIP_BUILD:-0}" -USE_GIT_SOURCE="${USE_GIT_SOURCE:-0}" GIT_REMOTE_URL="${GIT_REMOTE_URL:-git@gitea.haiyihy.com:hy/aslan-server.git}" GIT_REF="${GIT_REF:-aslan_test}" +ALLOWED_GIT_REFS="${ALLOWED_GIT_REFS:-aslan_test}" MAVEN_GOALS="${MAVEN_GOALS:-clean package}" -MAVEN_PROFILE="${MAVEN_PROFILE:-test}" +MAVEN_PROFILE="${MAVEN_PROFILE:-prod}" MAVEN_EXTRA_ARGS="${MAVEN_EXTRA_ARGS:-}" -BUILD_TS="${BUILD_TS:-$(date +%Y%m%dv%H%M%S)}" -IMAGE_REGISTRY="${IMAGE_REGISTRY:-tuokemi-con-registry.ap-southeast-1.cr.aliyuncs.com}" -IMAGE_PROJECT="${IMAGE_PROJECT:-atyou-test}" -BASE_IMAGE="tuokemi-con-registry.ap-southeast-1.cr.aliyuncs.com/public_mirror_images/eclipse-temurin:17-jdk-jammy" -FALLBACK_BASE_IMAGE="${FALLBACK_BASE_IMAGE:-eclipse-temurin:17-jdk-jammy}" +SKIP_BUILD="${SKIP_BUILD:-0}" +MODE="${MODE:-compose}" +BUILD_TS="${BUILD_TS:-$(date +%Y%m%d%H%M%S)}" + +ALL_SERVICES=(auth gateway external wallet order live other console) usage() { cat <<'EOF' Usage: - deploy-likei-services.sh [--mode preload|push|build-only|status] [--skip-build] [other|external|console ...] + deploy-likei-services.sh [--mode compose|build-only|status] [--skip-build] [--fast] [auth|gateway|external|wallet|order|live|other|console ...] Examples: - /opt/aslan-test-deploy/deploy-likei-services.sh status - /opt/aslan-test-deploy/deploy-likei-services.sh --mode preload other external console - USE_GIT_SOURCE=1 GIT_REF=aslan_test /opt/aslan-test-deploy/deploy-likei-services.sh --mode preload other - MODE=push ALIYUN_USER=xxx ALIYUN_PASS=xxx /opt/aslan-test-deploy/deploy-likei-services.sh other + /opt/aslan-test/deploy-likei-services.sh status + /opt/aslan-test/deploy-likei-services.sh other + /opt/aslan-test/deploy-likei-services.sh --fast other external console + GIT_REF=aslan_test /opt/aslan-test/deploy-likei-services.sh auth gateway external wallet order live other console Notes: - - preload mode builds the image on this server, imports it into each TKE node's containerd, then updates the deployment. - - push mode follows the original Jenkins flow and requires registry credentials in ALIYUN_USER/ALIYUN_PASS. - - USE_GIT_SOURCE=1 clones or resets source from GIT_REMOTE_URL/GIT_REF before building. + - This is the single-machine test deployment flow. + - It pulls aslan_test, builds local aslan-test/* images, then replaces docker compose app containers. + - MySQL/Mongo/Redis/Nacos/RocketMQ are not rebuilt or restarted by this script. EOF } @@ -51,8 +49,9 @@ need_cmd() { acquire_deploy_lock() { need_cmd flock + mkdir -p "$(dirname "$LOCK_FILE")" exec 9>"$LOCK_FILE" - # Only one deploy should mutate source, build images, preload nodes, and roll deployments at a time. + # The visual ops panel and manual SSH deploys share this lock so builds cannot interleave. flock -n 9 || { echo "another deploy is already running: $LOCK_FILE" >&2 exit 5 @@ -64,46 +63,23 @@ validate_git_ref() { echo "invalid GIT_REF: $GIT_REF" >&2 exit 2 fi -} - -ensure_base_image() { - if docker image inspect "$BASE_IMAGE" >/dev/null 2>&1; then - return - fi - if docker pull "$BASE_IMAGE"; then - return - fi - log "base image not available from primary registry, pulling $FALLBACK_BASE_IMAGE" - docker pull "$FALLBACK_BASE_IMAGE" - docker tag "$FALLBACK_BASE_IMAGE" "$BASE_IMAGE" -} - -update_source_from_git() { - if [[ "$USE_GIT_SOURCE" != "1" ]]; then - return - fi - - need_cmd git - validate_git_ref - mkdir -p "$(dirname "$SRC_DIR")" - if [[ ! -d "$SRC_DIR/.git" ]]; then - rm -rf "$SRC_DIR" - log "git clone $GIT_REMOTE_URL ($GIT_REF) -> $SRC_DIR" - git clone --branch "$GIT_REF" "$GIT_REMOTE_URL" "$SRC_DIR" - return - fi - - log "git reset source to origin/$GIT_REF from $GIT_REMOTE_URL" - git -C "$SRC_DIR" remote set-url origin "$GIT_REMOTE_URL" - git -C "$SRC_DIR" fetch origin "$GIT_REF" - git -C "$SRC_DIR" checkout -B "$GIT_REF" "origin/$GIT_REF" - git -C "$SRC_DIR" reset --hard "origin/$GIT_REF" + local allowed + for allowed in $ALLOWED_GIT_REFS; do + [[ "$GIT_REF" == "$allowed" ]] && return 0 + done + echo "GIT_REF is not allowed for test deploy: $GIT_REF (allowed: $ALLOWED_GIT_REFS)" >&2 + exit 2 } service_module() { case "$1" in - other) echo "rc-service/rc-service-other/other-start" ;; + auth) echo "rc-auth" ;; + gateway) echo "rc-gateway" ;; external) echo "rc-service/rc-service-external/external-start" ;; + wallet) echo "rc-service/rc-service-wallet/wallet-start" ;; + order) echo "rc-service/rc-service-order/order-start" ;; + live) echo "rc-service/rc-service-live/live-start" ;; + other) echo "rc-service/rc-service-other/other-start" ;; console) echo "rc-service/rc-service-console/console-start" ;; *) echo "unsupported service: $1" >&2; exit 2 ;; esac @@ -111,190 +87,99 @@ service_module() { service_dir() { case "$1" in - other) echo "rc-service/rc-service-other" ;; + auth) echo "rc-auth" ;; + gateway) echo "rc-gateway" ;; external) echo "rc-service/rc-service-external" ;; + wallet) echo "rc-service/rc-service-wallet" ;; + order) echo "rc-service/rc-service-order" ;; + live) echo "rc-service/rc-service-live" ;; + other) echo "rc-service/rc-service-other" ;; console) echo "rc-service/rc-service-console" ;; *) echo "unsupported service: $1" >&2; exit 2 ;; esac } -image_repo() { +service_image() { case "$1" in - other) echo "$IMAGE_REGISTRY/$IMAGE_PROJECT/other" ;; - external) echo "$IMAGE_REGISTRY/$IMAGE_PROJECT/external" ;; - console) echo "$IMAGE_REGISTRY/$IMAGE_PROJECT/console" ;; + auth|gateway|external|wallet|order|live|other|console) echo "aslan-test/$1:latest" ;; *) echo "unsupported service: $1" >&2; exit 2 ;; esac } -build_service() { - local svc="$1" - local module - local dir - local repo - local tag - local image - module="$(service_module "$svc")" - dir="$(service_dir "$svc")" - repo="$(image_repo "$svc")" - tag="${svc}-${BUILD_TS}" - image="${repo}:${tag}" +contains_service() { + local needle="$1" + local item + for item in "${ALL_SERVICES[@]}"; do + [[ "$item" == "$needle" ]] && return 0 + done + return 1 +} - cd "$SRC_DIR" - if [[ "$SKIP_BUILD" != "1" ]]; then - log "maven package $svc ($module)" - # shellcheck disable=SC2086 - # MAVEN_GOALS=package keeps the existing target cache for code-only fast builds; use the default clean package for safer full builds. - mvn $MAVEN_GOALS -pl "$module" -am -P "$MAVEN_PROFILE" -Dmaven.test.skip=true $MAVEN_EXTRA_ARGS +update_source_from_git() { + need_cmd git + validate_git_ref + [[ -d "$SRC_DIR/.git" ]] || { echo "missing git source: $SRC_DIR" >&2; exit 2; } + + log "reset source to origin/$GIT_REF from $GIT_REMOTE_URL" + git -C "$SRC_DIR" remote set-url origin "$GIT_REMOTE_URL" + git -C "$SRC_DIR" fetch origin "$GIT_REF" + git -C "$SRC_DIR" checkout -B "$GIT_REF" "origin/$GIT_REF" + git -C "$SRC_DIR" reset --hard "origin/$GIT_REF" +} + +package_services() { + local services=("$@") + local modules=() + local svc + for svc in "${services[@]}"; do + modules+=("$(service_module "$svc")") + done + local module_csv + module_csv="$(IFS=,; echo "${modules[*]}")" + + if [[ "$SKIP_BUILD" == "1" ]]; then + log "skip maven build" + return fi - ensure_base_image + cd "$SRC_DIR" + log "maven $MAVEN_GOALS -pl $module_csv -P $MAVEN_PROFILE" + # The test compose stack still reads the prod-profile Nacos configs; use -P prod unless the caller overrides it. + # shellcheck disable=SC2086 + mvn $MAVEN_GOALS -pl "$module_csv" -am -P "$MAVEN_PROFILE" -Dmaven.test.skip=true $MAVEN_EXTRA_ARGS +} + +build_image() { + local svc="$1" + local dir image + dir="$(service_dir "$svc")" + image="$(service_image "$svc")" + cd "$SRC_DIR" log "docker build $image" docker build \ - --build-arg "SERVICE_VERSION=${IMAGE_PROJECT}:${tag}" \ + --build-arg "SERVICE_VERSION=aslan-test:${svc}-${BUILD_TS}" \ -f "$dir/Dockerfile" \ -t "$image" \ "$dir" - docker tag "$image" "${repo}:latest" - - case "$MODE" in - build-only) - log "build-only completed for $svc: $image" - ;; - push) - push_image "$svc" "$image" "$repo" - rollout "$svc" "$image" - ;; - preload) - ensure_preload_pull_policy "$svc" - preload_image_to_nodes "$svc" "$image" - rollout "$svc" "$image" - ;; - *) - echo "unsupported MODE: $MODE" >&2 - exit 2 - ;; - esac } -push_image() { - local svc="$1" - local image="$2" - local repo="$3" - if [[ -n "${ALIYUN_USER:-}" && -n "${ALIYUN_PASS:-}" ]]; then - log "docker login registry for $svc" - printf '%s' "$ALIYUN_PASS" | docker login --username "$ALIYUN_USER" --password-stdin tuokemi-con-registry.ap-southeast-1.cr.aliyuncs.com - fi - log "docker push $image" - docker push "$image" - docker push "${repo}:latest" -} - -preload_image_to_nodes() { - local svc="$1" - local image="$2" - local safe_tag="${image//[^A-Za-z0-9_.-]/-}" - local tar_path="$BASE_DIR/artifacts/${safe_tag}.tar" - mkdir -p "$BASE_DIR/artifacts" - log "docker save $image -> $tar_path" - docker save "$image" -o "$tar_path" - - mapfile -t nodes < <(kubectl --kubeconfig "$KUBECONFIG" get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}') - if [[ "${#nodes[@]}" -eq 0 ]]; then - echo "no kubernetes nodes found" >&2 - exit 3 - fi - - for node in "${nodes[@]}"; do - preload_image_to_node "$svc" "$image" "$tar_path" "$node" - done -} - -ensure_preload_pull_policy() { - local svc="$1" - local policy - policy="$(kubectl --kubeconfig "$KUBECONFIG" -n "$NAMESPACE" get "deployment/$svc" \ - -o "jsonpath={.spec.template.spec.containers[?(@.name=='$svc')].imagePullPolicy}")" - # Preload mode imports images into node containerd; Always-pull pods would bypass that cache and fail. - if [[ "$policy" == "Always" ]]; then - echo "deployment/$svc uses imagePullPolicy=Always; preload mode requires IfNotPresent or unset" >&2 - exit 4 - fi -} - -preload_image_to_node() { - local svc="$1" - local image="$2" - local tar_path="$3" - local node="$4" - local pod="image-preload-${svc}-${node//[^a-zA-Z0-9-]/-}-$(date +%s)" - local manifest - manifest="$(mktemp)" - cat >"$manifest" </dev/null - rm -f "$manifest" - if ! kubectl --kubeconfig "$KUBECONFIG" -n "$NAMESPACE" wait --for=condition=Ready "pod/$pod" --timeout=180s >/dev/null; then - kubectl --kubeconfig "$KUBECONFIG" -n "$NAMESPACE" describe "pod/$pod" || true - kubectl --kubeconfig "$KUBECONFIG" -n "$NAMESPACE" delete pod "$pod" --ignore-not-found=true >/dev/null - return 1 - fi - log "copy image tar to $pod" - if ! kubectl --kubeconfig "$KUBECONFIG" -n "$NAMESPACE" cp "$tar_path" "$pod:/tmp/image.tar" -c preload >/dev/null; then - kubectl --kubeconfig "$KUBECONFIG" -n "$NAMESPACE" delete pod "$pod" --ignore-not-found=true >/dev/null - return 1 - fi - log "import $image into node $node" - if ! kubectl --kubeconfig "$KUBECONFIG" -n "$NAMESPACE" exec "$pod" -c preload -- \ - ctr --address /run/containerd/containerd.sock -n k8s.io images import /tmp/image.tar >/dev/null; then - kubectl --kubeconfig "$KUBECONFIG" -n "$NAMESPACE" delete pod "$pod" --ignore-not-found=true >/dev/null - return 1 - fi - kubectl --kubeconfig "$KUBECONFIG" -n "$NAMESPACE" delete pod "$pod" --ignore-not-found=true >/dev/null -} - -rollout() { - local svc="$1" - local image="$2" - log "set image deployment/$svc $svc=$image" - kubectl --kubeconfig "$KUBECONFIG" -n "$NAMESPACE" set image "deployment/$svc" "$svc=$image" - kubectl --kubeconfig "$KUBECONFIG" -n "$NAMESPACE" rollout status "deployment/$svc" --timeout=600s - kubectl --kubeconfig "$KUBECONFIG" -n "$NAMESPACE" get deploy "$svc" -o wide +compose_up() { + local services=("$@") + cd "$BASE_DIR" + log "docker compose recreate: ${services[*]}" + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" up -d --no-deps --force-recreate "${services[@]}" } status() { - kubectl --kubeconfig "$KUBECONFIG" -n "$NAMESPACE" get deploy other external console -o wide - kubectl --kubeconfig "$KUBECONFIG" get nodes -o wide + log "source" + git -C "$SRC_DIR" status -sb || true + git -C "$SRC_DIR" log -1 --oneline --decorate || true + log "compose" + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" ps --format table } main() { + local services=() while [[ "$#" -gt 0 ]]; do case "$1" in --mode) @@ -306,40 +191,66 @@ main() { SKIP_BUILD=1 shift ;; + --fast) + MAVEN_GOALS=package + shift + ;; -h|--help) usage exit 0 ;; + status) + MODE=status + shift + ;; *) - break + services+=("$1") + shift ;; esac done - if [[ "${1:-}" == "status" || "$MODE" == "status" ]]; then - need_cmd kubectl - [[ -f "$KUBECONFIG" ]] || { echo "missing kubeconfig: $KUBECONFIG" >&2; exit 2; } + need_cmd git + need_cmd docker + [[ -f "$COMPOSE_FILE" ]] || { echo "missing compose file: $COMPOSE_FILE" >&2; exit 2; } + [[ -f "$ENV_FILE" ]] || { echo "missing env file: $ENV_FILE" >&2; exit 2; } + + if [[ "$MODE" == "status" ]]; then status exit 0 fi need_cmd java need_cmd mvn - need_cmd docker - need_cmd kubectl - [[ -f "$KUBECONFIG" ]] || { echo "missing kubeconfig: $KUBECONFIG" >&2; exit 2; } - acquire_deploy_lock update_source_from_git - local services=("$@") if [[ "${#services[@]}" -eq 0 ]]; then - services=(other external console) + services=("${ALL_SERVICES[@]}") fi - + local svc for svc in "${services[@]}"; do - build_service "$svc" + contains_service "$svc" || { echo "unsupported service: $svc" >&2; exit 2; } done + + package_services "${services[@]}" + for svc in "${services[@]}"; do + build_image "$svc" + done + + case "$MODE" in + compose) + compose_up "${services[@]}" + ;; + build-only) + log "build-only completed: ${services[*]}" + ;; + *) + echo "unsupported MODE: $MODE" >&2 + exit 2 + ;; + esac + status } main "$@" diff --git a/.deploy/test-deploy/ops/aslan-test-ops.service b/.deploy/test-deploy/ops/aslan-test-ops.service index 77ae9ad6..9954effa 100644 --- a/.deploy/test-deploy/ops/aslan-test-ops.service +++ b/.deploy/test-deploy/ops/aslan-test-ops.service @@ -1,21 +1,28 @@ [Unit] -Description=Aslan test deployment operations panel +Description=Aslan test docker compose deployment panel After=network-online.target docker.service Wants=network-online.target [Service] Type=simple -User=ubuntu -Group=ubuntu -WorkingDirectory=/opt/aslan-test-deploy +User=root +Group=root +WorkingDirectory=/opt/aslan-test Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin -Environment=HOME=/home/ubuntu +Environment=HOME=/root +Environment=ASLAN_DEPLOY_BASE=/opt/aslan-test +Environment=ASLAN_SOURCE_DIR=/opt/aslan-test/source/aslan-server +Environment=ASLAN_DEPLOY_SCRIPT=/opt/aslan-test/deploy-likei-services.sh +Environment=ASLAN_COMPOSE_FILE=/opt/aslan-test/docker-compose.yml +Environment=ASLAN_ENV_FILE=/opt/aslan-test/config/.env Environment=ASLAN_ALLOWED_BRANCHES=aslan_test +Environment=ASLAN_OPS_HOST=127.0.0.1 +Environment=ASLAN_OPS_PORT=18081 +Environment=ASLAN_OPS_COOKIE_PATH=/deploy EnvironmentFile=/etc/aslan-test-ops.env -ExecStart=/usr/bin/python3 /opt/aslan-test-deploy/ops/aslan_ops.py +ExecStart=/usr/bin/python3 /opt/aslan-test/ops/aslan_ops.py Restart=always RestartSec=5 -NoNewPrivileges=true [Install] WantedBy=multi-user.target diff --git a/.deploy/test-deploy/ops/aslan_ops.py b/.deploy/test-deploy/ops/aslan_ops.py old mode 100644 new mode 100755 index a07ef7df..6fe043a0 --- a/.deploy/test-deploy/ops/aslan_ops.py +++ b/.deploy/test-deploy/ops/aslan_ops.py @@ -15,19 +15,20 @@ from pathlib import Path from urllib.parse import parse_qs, urlparse -BASE_DIR = Path(os.environ.get("ASLAN_DEPLOY_BASE", "/opt/aslan-test-deploy")) +BASE_DIR = Path(os.environ.get("ASLAN_DEPLOY_BASE", "/opt/aslan-test")) RUN_DIR = Path(os.environ.get("ASLAN_OPS_RUN_DIR", str(BASE_DIR / "ops" / "runs"))) -KUBECONFIG = Path(os.environ.get("KUBECONFIG", str(BASE_DIR / "kubeconfig"))) DEPLOY_SCRIPT = Path(os.environ.get("ASLAN_DEPLOY_SCRIPT", str(BASE_DIR / "deploy-likei-services.sh"))) -SOURCE_DIR = Path(os.environ.get("ASLAN_SOURCE_DIR", str(BASE_DIR / "source" / "likei-services"))) -NAMESPACE = os.environ.get("ASLAN_NAMESPACE", "test") +SOURCE_DIR = Path(os.environ.get("ASLAN_SOURCE_DIR", str(BASE_DIR / "source" / "aslan-server"))) +COMPOSE_FILE = Path(os.environ.get("ASLAN_COMPOSE_FILE", str(BASE_DIR / "docker-compose.yml"))) +ENV_FILE = Path(os.environ.get("ASLAN_ENV_FILE", str(BASE_DIR / "config" / ".env"))) GIT_REF = os.environ.get("ASLAN_GIT_REF", "aslan_test") REMOTE_URL = os.environ.get("ASLAN_GIT_REMOTE_URL", "git@gitea.haiyihy.com:hy/aslan-server.git") -HOST = os.environ.get("ASLAN_OPS_HOST", "0.0.0.0") +HOST = os.environ.get("ASLAN_OPS_HOST", "127.0.0.1") PORT = int(os.environ.get("ASLAN_OPS_PORT", "18081")) USERNAME = os.environ.get("ASLAN_OPS_USER", "admin") PASSWORD = os.environ.get("ASLAN_OPS_PASSWORD", "") SESSION_SECRET = os.environ.get("ASLAN_OPS_SESSION_SECRET", PASSWORD or secrets.token_hex(32)) +COOKIE_PATH = os.environ.get("ASLAN_OPS_COOKIE_PATH", "/ops") ALLOWED_BRANCHES = { branch.strip() for branch in os.environ.get("ASLAN_ALLOWED_BRANCHES", GIT_REF).split(",") @@ -35,24 +36,18 @@ ALLOWED_BRANCHES = { } SERVICES = { - "other": { - "label": "Other", - "port": "5800", - "health": "/actuator/health", - }, - "external": { - "label": "External", - "port": "5200", - "health": "/actuator/health", - }, - "console": { - "label": "Console", - "port": "5300", - "health": "/console/actuator/health", - }, + "auth": {"label": "Auth", "port": "", "health": ""}, + "gateway": {"label": "Gateway", "port": "9000", "health": "/"}, + "external": {"label": "External", "port": "", "health": ""}, + "wallet": {"label": "Wallet", "port": "", "health": ""}, + "order": {"label": "Order", "port": "", "health": ""}, + "live": {"label": "Live", "port": "", "health": ""}, + "other": {"label": "Other", "port": "", "health": ""}, + "console": {"label": "Console", "port": "2700", "health": "/console/"}, } BRANCH_RE = re.compile(r"^[A-Za-z0-9._/-]{1,80}$") +JOB_RE = re.compile(r"^[A-Za-z0-9-]+$") job_lock = threading.Lock() active_job = None @@ -136,101 +131,74 @@ def record_login_failure(ip): login_failures[ip] = window[-10:] -def run_json(cmd, timeout=20): +def run_text(cmd, timeout=20): result = subprocess.run(cmd, text=True, capture_output=True, timeout=timeout) if result.returncode != 0: - raise RuntimeError(result.stderr.strip() or result.stdout.strip() or f"command failed: {cmd[0]}") - return json.loads(result.stdout) - - -def run_text(cmd, timeout=10): - result = subprocess.run(cmd, text=True, capture_output=True, timeout=timeout) - if result.returncode != 0: - return "" + return (result.stderr.strip() or result.stdout.strip()) return result.stdout.strip() -def deployment_status(): - names = list(SERVICES) - payload = run_json([ - "kubectl", - "--kubeconfig", - str(KUBECONFIG), - "-n", - NAMESPACE, - "get", - "deploy", - *names, - "-o", - "json", - ]) +def run_checked(cmd, timeout=30): + result = subprocess.run(cmd, text=True, capture_output=True, timeout=timeout) + if result.returncode != 0: + raise RuntimeError(result.stderr.strip() or result.stdout.strip() or f"command failed: {cmd[0]}") + return result.stdout + + +def compose_cmd(*args): + return ["docker", "compose", "-f", str(COMPOSE_FILE), "--env-file", str(ENV_FILE), *args] + + +def parse_compose_json_lines(text): + rows = [] + for line in text.splitlines(): + line = line.strip() + if not line: + continue + try: + rows.append(json.loads(line)) + except json.JSONDecodeError: + pass + return rows + + +def compose_status(): + output = run_checked(compose_cmd("ps", "--all", "--format", "json"), timeout=20) + rows = parse_compose_json_lines(output) + by_service = {row.get("Service"): row for row in rows} items = [] - for item in payload.get("items", []): - name = item["metadata"]["name"] - spec = item.get("spec", {}) - status = item.get("status", {}) - containers = item.get("spec", {}).get("template", {}).get("spec", {}).get("containers", []) - image = next((c.get("image", "") for c in containers if c.get("name") == name), containers[0].get("image", "") if containers else "") - conditions = {c.get("type"): c.get("status") for c in status.get("conditions", [])} + for name, meta in SERVICES.items(): + row = by_service.get(name, {}) + state = row.get("State") or "missing" + health = row.get("Health") or "" + status = row.get("Status") or "" + image = row.get("Image") or f"aslan-test/{name}:latest" items.append({ "name": name, - "label": SERVICES.get(name, {}).get("label", name), - "ready": f"{status.get('readyReplicas', 0)}/{spec.get('replicas', 0)}", - "updated": status.get("updatedReplicas", 0), - "available": status.get("availableReplicas", 0), + "label": meta["label"], + "state": state, + "status": status, "image": image, - "port": SERVICES.get(name, {}).get("port", ""), - "health": SERVICES.get(name, {}).get("health", ""), - "healthy": conditions.get("Available") == "True", + "ports": row.get("Ports", ""), + "healthy": state == "running" and health not in {"unhealthy", "starting"}, + "port": meta["port"], + "health": meta["health"], }) - order = {name: idx for idx, name in enumerate(names)} - return sorted(items, key=lambda x: order.get(x["name"], 99)) - - -def pod_status(): - selector = "app in (other,external,console)" - payload = run_json([ - "kubectl", - "--kubeconfig", - str(KUBECONFIG), - "-n", - NAMESPACE, - "get", - "pods", - "-l", - selector, - "-o", - "json", - ]) - pods = [] - for item in payload.get("items", []): - status = item.get("status", {}) - containers = status.get("containerStatuses", []) - ready_count = sum(1 for c in containers if c.get("ready")) - pods.append({ - "name": item["metadata"]["name"], - "app": item["metadata"].get("labels", {}).get("app", ""), - "ready": f"{ready_count}/{len(containers)}", - "phase": status.get("phase", ""), - "restarts": sum(c.get("restartCount", 0) for c in containers), - "node": item.get("spec", {}).get("nodeName", ""), - "ip": status.get("podIP", ""), - "age": item["metadata"].get("creationTimestamp", ""), - }) - return pods + return items def git_status(): head = run_text(["git", "-C", str(SOURCE_DIR), "rev-parse", "HEAD"]) branch = run_text(["git", "-C", str(SOURCE_DIR), "branch", "--show-current"]) short = run_text(["git", "-C", str(SOURCE_DIR), "status", "--short", "--branch"]) - return {"head": head, "branch": branch, "status": short} + last = run_text(["git", "-C", str(SOURCE_DIR), "log", "-1", "--oneline", "--decorate"]) + return {"head": head, "branch": branch, "status": short, "last": last} def list_jobs(): RUN_DIR.mkdir(parents=True, exist_ok=True) jobs = [] - for meta_path in sorted(RUN_DIR.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True)[:20]: + for meta_path in sorted(RUN_DIR.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True)[:30]: try: jobs.append(json.loads(meta_path.read_text())) except json.JSONDecodeError: @@ -315,24 +283,31 @@ def start_deploy(services, branch, fast): env = os.environ.copy() env.update({ - "USE_GIT_SOURCE": "1", + "BASE_DIR": str(BASE_DIR), + "SRC_DIR": str(SOURCE_DIR), + "COMPOSE_FILE": str(COMPOSE_FILE), + "ENV_FILE": str(ENV_FILE), "GIT_REF": branch, + "ALLOWED_GIT_REFS": " ".join(sorted(ALLOWED_BRANCHES)), "GIT_REMOTE_URL": REMOTE_URL, - "MODE": "preload", - "KUBECONFIG": str(KUBECONFIG), - "NAMESPACE": NAMESPACE, - "ASLAN_NAMESPACE": NAMESPACE, + "MODE": "compose", "HOME": str(Path.home()), }) if fast: - # Fast mode avoids Maven clean so unchanged modules and already downloaded dependencies can reuse local target output. + # Fast mode keeps Maven target caches and is enough for small Java-only updates. env["MAVEN_GOALS"] = "package" - cmd = [str(DEPLOY_SCRIPT), "--mode", "preload", *services] + cmd = [str(DEPLOY_SCRIPT), *services] thread = threading.Thread(target=stream_process, args=(job, cmd, env), daemon=True) thread.start() return job +def service_log(svc): + if svc not in SERVICES: + raise ValueError(f"unsupported service: {svc}") + return run_checked(compose_cmd("logs", "--tail=300", svc), timeout=20) + + def login_page(error=""): message = f"
{html.escape(error)}
" if error else "" return f""" @@ -340,9 +315,9 @@ def login_page(error=""): - Aslan Ops + Aslan Test Deploy -

Aslan Ops

{message}
+

Aslan 测试部署

{message}
""" @@ -363,7 +338,7 @@ INDEX_HTML = """ - Aslan Ops + Aslan Test Deploy
-

Aslan Ops

- +

Aslan 测试部署

+
-

微服务

+

服务状态

-

部署

固定 preload 到 TKE 节点,不走 registry push
+

更新部署

拉取 aslan_test,构建本地镜像,docker compose 替换容器
@@ -422,26 +397,20 @@ INDEX_HTML = """
-
-
-

部署日志

无运行任务
-

-      
-
-

Pod

-
名称状态节点
-
-
+
+

部署日志

无运行任务
+

+    

最近任务

ID服务分支状态时间