chore: adapt test deploy to compose ops panel

This commit is contained in:
zhx 2026-07-08 19:02:55 +08:00
parent 0edf38041e
commit 1f370c8542
4 changed files with 296 additions and 423 deletions

View File

@ -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" <<EOF
apiVersion: v1
kind: Pod
metadata:
name: ${pod}
namespace: ${NAMESPACE}
spec:
nodeName: "${node}"
restartPolicy: Never
hostPID: true
tolerations:
- operator: Exists
containers:
- name: preload
image: docker:26.1.3-dind
imagePullPolicy: IfNotPresent
command: ["sh", "-c", "sleep 3600"]
securityContext:
privileged: true
volumeMounts:
- name: containerd
mountPath: /run/containerd/containerd.sock
volumes:
- name: containerd
hostPath:
path: /run/containerd/containerd.sock
type: Socket
EOF
log "create preload pod $pod on node $node"
kubectl --kubeconfig "$KUBECONFIG" apply -f "$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 "$@"

View File

@ -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

274
.deploy/test-deploy/ops/aslan_ops.py Normal file → Executable file
View File

@ -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"<div class='error'>{html.escape(error)}</div>" if error else ""
return f"""<!doctype html>
@ -340,9 +315,9 @@ def login_page(error=""):
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Aslan Ops</title>
<title>Aslan Test Deploy</title>
<style>
:root {{ color-scheme: light; --ink:#172026; --muted:#65727f; --line:#d9e1e7; --brand:#0f766e; --bg:#f6f8fa; --danger:#b42318; }}
:root {{ --ink:#172026; --muted:#65727f; --line:#d9e1e7; --brand:#0f766e; --bg:#f6f8fa; --danger:#b42318; }}
* {{ box-sizing:border-box; }}
body {{ margin:0; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Arial,sans-serif; background:var(--bg); color:var(--ink); }}
main {{ min-height:100vh; display:grid; place-items:center; padding:24px; }}
@ -354,7 +329,7 @@ def login_page(error=""):
.error {{ color:var(--danger); font-size:13px; margin-bottom:8px; }}
</style>
</head>
<body><main><form method="post" action="/login"><h1>Aslan Ops</h1>{message}<label>账号</label><input name="username" autocomplete="username" autofocus><label>密码</label><input name="password" type="password" autocomplete="current-password"><button type="submit">登录</button></form></main></body>
<body><main><form method="post" action="login"><h1>Aslan 测试部署</h1>{message}<label>账号</label><input name="username" autocomplete="username" autofocus><label>密码</label><input name="password" type="password" autocomplete="current-password"><button type="submit">登录</button></form></main></body>
</html>"""
@ -363,7 +338,7 @@ INDEX_HTML = """<!doctype html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Aslan Ops</title>
<title>Aslan Test Deploy</title>
<style>
:root { color-scheme: light; --bg:#f4f7f8; --panel:#fff; --ink:#14212b; --muted:#647382; --line:#d8e2e8; --brand:#0f766e; --brand-strong:#0b5f59; --warn:#b54708; --ok:#067647; --bad:#b42318; --chip:#e6f4f1; }
* { box-sizing:border-box; }
@ -380,14 +355,14 @@ INDEX_HTML = """<!doctype html>
.bar { display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom:12px; }
.bar h2 { margin:0; font-size:16px; }
.muted { color:var(--muted); font-size:13px; }
.grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:12px; }
.svc { border:1px solid var(--line); border-radius:8px; padding:14px; display:grid; gap:10px; min-height:172px; }
.grid { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:12px; }
.svc { border:1px solid var(--line); border-radius:8px; padding:14px; display:grid; gap:10px; min-height:154px; }
.svc-head { display:flex; align-items:center; justify-content:space-between; gap:8px; }
.svc h3 { margin:0; font-size:16px; }
.badge { display:inline-flex; align-items:center; height:24px; border-radius:999px; padding:0 9px; font-size:12px; background:var(--chip); color:var(--brand-strong); white-space:nowrap; }
.badge.warn { background:#fff1e7; color:var(--warn); }
.badge.bad { background:#fee4e2; color:var(--bad); }
.meta { display:grid; grid-template-columns:90px 1fr; gap:6px; font-size:13px; }
.meta { display:grid; grid-template-columns:64px 1fr; gap:6px; font-size:13px; }
.meta span:nth-child(odd) { color:var(--muted); }
.image { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:12px; }
.deploy-row { display:flex; flex-wrap:wrap; gap:10px; align-items:center; }
@ -398,23 +373,23 @@ INDEX_HTML = """<!doctype html>
table { width:100%; border-collapse:collapse; font-size:13px; }
th, td { text-align:left; padding:10px; border-top:1px solid var(--line); vertical-align:top; }
th { color:var(--muted); font-weight:600; }
pre { margin:0; height:360px; overflow:auto; background:#101820; color:#e6edf3; padding:14px; border-radius:8px; font-size:12px; line-height:1.45; white-space:pre-wrap; }
.split { display:grid; grid-template-columns:2fr 1fr; gap:16px; }
@media (max-width: 900px) { header { padding:0 14px; } main { padding:14px; } .grid, .split { grid-template-columns:1fr; } .bar { align-items:flex-start; flex-direction:column; } }
pre { margin:0; height:420px; overflow:auto; background:#101820; color:#e6edf3; padding:14px; border-radius:8px; font-size:12px; line-height:1.45; white-space:pre-wrap; }
@media (max-width: 1100px) { .grid { grid-template-columns:repeat(2,minmax(0,1fr)); } }
@media (max-width: 720px) { header { padding:0 14px; } main { padding:14px; } .grid { grid-template-columns:1fr; } .bar { align-items:flex-start; flex-direction:column; } }
</style>
</head>
<body>
<header>
<h1>Aslan Ops</h1>
<nav><span class="muted" id="git"></span><button id="refresh">刷新</button><a class="link" href="/logout">退出</a></nav>
<h1>Aslan 测试部署</h1>
<nav><span class="muted" id="git"></span><button id="refresh">刷新</button><a class="link" href="logout">退出</a></nav>
</header>
<main>
<section>
<div class="bar"><h2>服务</h2><span class="muted" id="updated"></span></div>
<div class="bar"><h2>服务状态</h2><span class="muted" id="updated"></span></div>
<div id="services" class="grid"></div>
</section>
<section>
<div class="bar"><h2>部署</h2><span class="muted">固定 preload TKE 节点不走 registry push</span></div>
<div class="bar"><h2>更新部署</h2><span class="muted">拉取 aslan_test构建本地镜像docker compose 替换容器</span></div>
<div class="deploy-row">
<div class="checks" id="serviceChecks"></div>
<label class="check">快速构建 <input id="fast" type="checkbox" checked></label>
@ -422,26 +397,20 @@ INDEX_HTML = """<!doctype html>
<button class="primary" id="deploy">部署选中服务</button>
</div>
</section>
<div class="split">
<section>
<div class="bar"><h2>部署日志</h2><span class="muted" id="jobState">无运行任务</span></div>
<pre id="log"></pre>
</section>
<section>
<div class="bar"><h2>Pod</h2></div>
<table><thead><tr><th>名称</th><th>状态</th><th>节点</th></tr></thead><tbody id="pods"></tbody></table>
</section>
</div>
<section>
<div class="bar"><h2>部署日志</h2><span class="muted" id="jobState">无运行任务</span></div>
<pre id="log"></pre>
</section>
<section>
<div class="bar"><h2>最近任务</h2></div>
<table><thead><tr><th>ID</th><th>服务</th><th>分支</th><th>状态</th><th>时间</th></tr></thead><tbody id="jobs"></tbody></table>
</section>
</main>
<script>
const serviceNames = ["other", "external", "console"];
const serviceNames = ["auth", "gateway", "external", "wallet", "order", "live", "other", "console"];
let currentJob = "";
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
function badge(ok, text) { return `<span class="badge ${ok ? "" : "warn"}">${esc(text)}</span>`; }
function badge(ok, text) { return `<span class="badge ${ok ? "" : "bad"}">${esc(text)}</span>`; }
async function api(path, opts) {
const res = await fetch(path, Object.assign({headers:{'Content-Type':'application/json','X-Aslan-Ops':'1'}}, opts || {}));
if (!res.ok) throw new Error((await res.text()) || res.statusText);
@ -451,21 +420,21 @@ INDEX_HTML = """<!doctype html>
document.getElementById("serviceChecks").innerHTML = serviceNames.map(s => `<label class="check"><input type="checkbox" value="${s}" checked> ${s}</label>`).join("");
}
async function loadStatus() {
const data = await api("/api/status");
const data = await api("api/status");
document.getElementById("updated").textContent = new Date().toLocaleString();
document.getElementById("git").textContent = `${data.git.branch || "unknown"} ${String(data.git.head || "").slice(0,8)}`;
document.getElementById("services").innerHTML = data.deployments.map(s => `
document.getElementById("services").innerHTML = data.services.map(s => `
<article class="svc">
<div class="svc-head"><h3>${esc(s.label)}</h3>${badge(s.healthy, s.ready + " Ready")}</div>
<div class="svc-head"><h3>${esc(s.label)}</h3>${badge(s.healthy, s.state || "missing")}</div>
<div class="meta">
<span>端口</span><span>${esc(s.port)}</span>
<span>健康检查</span><span>${esc(s.health)}</span>
<span>状态</span><span>${esc(s.status)}</span>
<span>端口</span><span>${esc(s.ports || s.port || "-")}</span>
<span>镜像</span><span class="image" title="${esc(s.image)}">${esc(s.image)}</span>
</div>
<button data-service="${esc(s.name)}">部署 ${esc(s.name)}</button>
<div class="deploy-row"><button data-service="${esc(s.name)}">部署</button><button data-log="${esc(s.name)}">日志</button></div>
</article>`).join("");
document.querySelectorAll("[data-service]").forEach(btn => btn.onclick = () => deploy([btn.dataset.service]));
document.getElementById("pods").innerHTML = data.pods.map(p => `<tr><td>${esc(p.name)}<div class="muted">${esc(p.ip)}</div></td><td>${esc(p.ready)} ${esc(p.phase)}<div class="muted">restart ${esc(p.restarts)}</div></td><td>${esc(p.node)}</td></tr>`).join("");
document.querySelectorAll("[data-log]").forEach(btn => btn.onclick = () => loadServiceLog(btn.dataset.log));
document.getElementById("jobs").innerHTML = data.jobs.map(j => `<tr><td><button data-job="${esc(j.id)}">${esc(j.id)}</button></td><td>${esc((j.services || []).join(","))}</td><td>${esc(j.branch)}</td><td>${esc(j.status)}</td><td>${esc(j.createdAt)}</td></tr>`).join("");
document.querySelectorAll("[data-job]").forEach(btn => btn.onclick = () => { currentJob = btn.dataset.job; loadLog(); });
const running = data.jobs.find(j => j.status === "running" || j.status === "queued");
@ -476,7 +445,7 @@ INDEX_HTML = """<!doctype html>
const selected = services || Array.from(document.querySelectorAll("#serviceChecks input:checked")).map(i => i.value);
document.getElementById("deploy").disabled = true;
try {
const data = await api("/api/deploy", {method:"POST", body:JSON.stringify({services:selected, branch:document.getElementById("branch").value, fast:document.getElementById("fast").checked})});
const data = await api("api/deploy", {method:"POST", body:JSON.stringify({services:selected, branch:document.getElementById("branch").value, fast:document.getElementById("fast").checked})});
currentJob = data.job.id;
await loadStatus();
await loadLog();
@ -488,12 +457,20 @@ INDEX_HTML = """<!doctype html>
}
async function loadLog() {
if (!currentJob) return;
const data = await api(`/api/jobs/${currentJob}/log`);
const data = await api(`api/jobs/${currentJob}/log`);
document.getElementById("jobState").textContent = `${currentJob} ${data.job.status}`;
const log = document.getElementById("log");
log.textContent = data.log || "";
log.scrollTop = log.scrollHeight;
}
async function loadServiceLog(service) {
const data = await api(`api/services/${service}/log`);
currentJob = "";
document.getElementById("jobState").textContent = `${service} 容器日志`;
const log = document.getElementById("log");
log.textContent = data.log || "";
log.scrollTop = log.scrollHeight;
}
renderChecks();
document.getElementById("refresh").onclick = loadStatus;
document.getElementById("deploy").onclick = () => deploy();
@ -505,7 +482,7 @@ INDEX_HTML = """<!doctype html>
class Handler(BaseHTTPRequestHandler):
server_version = "AslanOps/1.0"
server_version = "AslanTestOps/1.0"
def authenticated(self):
cookies = parse_cookies(self.headers.get("Cookie"))
@ -514,7 +491,7 @@ class Handler(BaseHTTPRequestHandler):
def require_auth(self):
if self.authenticated():
return True
redirect_response(self, "/login")
redirect_response(self, "login")
return False
def read_body(self, max_bytes=65536):
@ -528,17 +505,16 @@ class Handler(BaseHTTPRequestHandler):
if parsed.path == "/login":
return text_response(self, 200, login_page(), "text/html; charset=utf-8")
if parsed.path == "/logout":
redirect_response(self, "/login", ["aslan_ops=; Max-Age=0; HttpOnly; SameSite=Lax; Path=/"])
redirect_response(self, "login", [f"aslan_ops=; Max-Age=0; HttpOnly; SameSite=Lax; Path={COOKIE_PATH}"])
return
if not self.require_auth():
return
if parsed.path == "/":
if parsed.path in ("", "/"):
return text_response(self, 200, INDEX_HTML, "text/html; charset=utf-8")
if parsed.path == "/api/status":
try:
return json_response(self, 200, {
"deployments": deployment_status(),
"pods": pod_status(),
"services": compose_status(),
"git": git_status(),
"jobs": list_jobs(),
})
@ -547,13 +523,21 @@ class Handler(BaseHTTPRequestHandler):
match = re.match(r"^/api/jobs/([A-Za-z0-9-]+)/log$", parsed.path)
if match:
job_id = match.group(1)
if not JOB_RE.match(job_id):
return json_response(self, 400, {"error": "invalid job id"})
meta_path = RUN_DIR / f"{job_id}.json"
log_path = RUN_DIR / f"{job_id}.log"
if not meta_path.exists():
return json_response(self, 404, {"error": "job not found"})
job = json.loads(meta_path.read_text())
log = log_path.read_text(errors="replace")[-120000:] if log_path.exists() else ""
log = log_path.read_text(errors="replace")[-160000:] if log_path.exists() else ""
return json_response(self, 200, {"job": job, "log": log})
match = re.match(r"^/api/services/([A-Za-z0-9_-]+)/log$", parsed.path)
if match:
try:
return json_response(self, 200, {"service": match.group(1), "log": service_log(match.group(1))})
except Exception as exc:
return json_response(self, 400, {"error": str(exc)})
return text_response(self, 404, "not found")
def do_POST(self):
@ -570,7 +554,7 @@ class Handler(BaseHTTPRequestHandler):
if hmac.compare_digest(username, USERNAME) and hmac.compare_digest(password, PASSWORD):
login_failures.pop(self.client_address[0], None)
session = sign_session(f"{USERNAME}:{int(time.time())}")
redirect_response(self, "/", [f"aslan_ops={session}; HttpOnly; SameSite=Lax; Path=/"])
redirect_response(self, ".", [f"aslan_ops={session}; HttpOnly; SameSite=Lax; Path={COOKIE_PATH}"])
return
record_login_failure(self.client_address[0])
return text_response(self, 401, login_page("账号或密码错误"), "text/html; charset=utf-8")
@ -599,7 +583,7 @@ def main():
raise SystemExit("ASLAN_OPS_PASSWORD is required")
RUN_DIR.mkdir(parents=True, exist_ok=True)
server = ThreadingHTTPServer((HOST, PORT), Handler)
print(f"aslan ops listening on {HOST}:{PORT}", flush=True)
print(f"aslan test ops listening on {HOST}:{PORT}", flush=True)
server.serve_forever()

View File

@ -1,16 +1,12 @@
#!/usr/bin/env bash
set -euo pipefail
DEPLOY_HOST="${DEPLOY_HOST:-43.160.219.15}"
DEPLOY_USER="${DEPLOY_USER:-ubuntu}"
SSH_KEY="${SSH_KEY:-$HOME/.ssh/aslan-deploy-sg-ed25519}"
REMOTE_BASE="${REMOTE_BASE:-/opt/aslan-test-deploy}"
REMOTE_SRC="$REMOTE_BASE/source/likei-services"
LOCAL_M2_ROOT="${LOCAL_M2_ROOT:-$HOME/.m2/repository}"
REMOTE_M2_ROOT="${REMOTE_M2_ROOT:-/home/$DEPLOY_USER/.m2/repository}"
M2_CACHE_GROUPS="${M2_CACHE_GROUPS:-com/red/circle com/github/sud}"
DEPLOY_HOST="${DEPLOY_HOST:-43.160.220.141}"
DEPLOY_USER="${DEPLOY_USER:-root}"
SSH_KEY="${SSH_KEY:-$HOME/.ssh/aslan-test-singapore-ed25519}"
REMOTE_BASE="${REMOTE_BASE:-/opt/aslan-test}"
REMOTE_SRC="${REMOTE_SRC:-$REMOTE_BASE/source/aslan-server}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
usage() {
cat <<'EOF'
@ -18,18 +14,14 @@ Usage:
.deploy/test-deploy/sync-and-deploy.sh [remote deploy args...]
Examples:
.deploy/test-deploy/sync-and-deploy.sh
.deploy/test-deploy/sync-and-deploy.sh status
.deploy/test-deploy/sync-and-deploy.sh --mode preload other
.deploy/test-deploy/sync-and-deploy.sh --mode preload other external console
.deploy/test-deploy/sync-and-deploy.sh --fast other
Environment:
DEPLOY_HOST default 43.160.219.15
DEPLOY_USER default ubuntu
SSH_KEY default ~/.ssh/aslan-deploy-sg-ed25519
Git source on deploy host:
ssh -i ~/.ssh/aslan-deploy-sg-ed25519 ubuntu@43.160.219.15 \
'USE_GIT_SOURCE=1 GIT_REF=aslan_test /opt/aslan-test-deploy/deploy-likei-services.sh --mode preload other'
DEPLOY_HOST default 43.160.220.141
DEPLOY_USER default root
SSH_KEY default ~/.ssh/aslan-test-singapore-ed25519
EOF
}
@ -41,44 +33,23 @@ fi
ssh_base=(ssh -i "$SSH_KEY" -o StrictHostKeyChecking=accept-new "$DEPLOY_USER@$DEPLOY_HOST")
rsync_ssh="ssh -i $SSH_KEY -o StrictHostKeyChecking=accept-new"
"${ssh_base[@]}" "mkdir -p '$REMOTE_SRC' '$REMOTE_BASE'"
rsync -az --delete \
--exclude '.git/' \
--exclude 'target/' \
--exclude '**/target/' \
--exclude '.idea/' \
--exclude '.DS_Store' \
--exclude 'build.md' \
--exclude 'node_modules/' \
-e "$rsync_ssh" \
"$REPO_ROOT/" "$DEPLOY_USER@$DEPLOY_HOST:$REMOTE_SRC/"
"${ssh_base[@]}" "mkdir -p '$REMOTE_BASE/ops' '$REMOTE_SRC'"
rsync -az \
-e "$rsync_ssh" \
"$SCRIPT_DIR/deploy-likei-services.sh" "$DEPLOY_USER@$DEPLOY_HOST:$REMOTE_BASE/deploy-likei-services.sh"
rsync -az --delete \
--exclude 'runs/' \
-e "$rsync_ssh" \
"$SCRIPT_DIR/ops/" "$DEPLOY_USER@$DEPLOY_HOST:$REMOTE_BASE/ops/"
for cache_group in $M2_CACHE_GROUPS; do
local_cache_path="$LOCAL_M2_ROOT/$cache_group"
remote_cache_parent="$REMOTE_M2_ROOT/$(dirname "$cache_group")"
if [[ ! -d "$local_cache_path" ]]; then
continue
fi
"${ssh_base[@]}" "mkdir -p '$remote_cache_parent'"
rsync -az --delete \
--exclude '*.lastUpdated' \
-e "$rsync_ssh" \
"$local_cache_path" "$DEPLOY_USER@$DEPLOY_HOST:$remote_cache_parent/"
done
"${ssh_base[@]}" "flock -n '$REMOTE_BASE/deploy.lock' sh -c 'chmod +x \"\$0/deploy-likei-services.sh\" \"\$0/ops/aslan_ops.py\" && cp \"\$0/ops/aslan-test-ops.service\" /etc/systemd/system/aslan-test-ops.service && systemctl daemon-reload && systemctl enable aslan-test-ops.service >/dev/null && systemctl restart aslan-test-ops.service' '$REMOTE_BASE'"
printf -v remote_script_q '%q' "$REMOTE_BASE/deploy-likei-services.sh"
remote_cmd="$remote_script_q"
if [[ "$#" -gt 0 ]]; then
printf -v remote_script_q '%q' "$REMOTE_BASE/deploy-likei-services.sh"
remote_cmd="$remote_script_q"
printf -v remote_args_q ' %q' "$@"
remote_cmd+="$remote_args_q"
"${ssh_base[@]}" "$remote_cmd"
fi
"${ssh_base[@]}" "chmod +x $remote_script_q && $remote_cmd"