diff --git a/.deploy/aslan-deploy/deploy-likei-services.sh b/.deploy/aslan-deploy/deploy-likei-services.sh new file mode 100755 index 00000000..89f8d94c --- /dev/null +++ b/.deploy/aslan-deploy/deploy-likei-services.sh @@ -0,0 +1,301 @@ +#!/usr/bin/env bash +set -euo pipefail + +BASE_DIR="${BASE_DIR:-/opt/aslan-deploy}" +SRC_DIR="${SRC_DIR:-$BASE_DIR/source/likei-services}" +KUBECONFIG="${KUBECONFIG:-$BASE_DIR/kubeconfig}" +NAMESPACE="${NAMESPACE:-prod}" +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:-atyou_prod}" +MAVEN_EXTRA_ARGS="${MAVEN_EXTRA_ARGS:-}" +BUILD_TS="${BUILD_TS:-$(date +%Y%m%dv%H%M%S)}" +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}" + +usage() { + cat <<'EOF' +Usage: + deploy-likei-services.sh [--mode preload|push|build-only|status] [--skip-build] [other|external|console ...] + +Examples: + /opt/aslan-deploy/deploy-likei-services.sh status + /opt/aslan-deploy/deploy-likei-services.sh --mode preload other external console + USE_GIT_SOURCE=1 GIT_REF=atyou_prod /opt/aslan-deploy/deploy-likei-services.sh --mode preload other + MODE=push ALIYUN_USER=xxx ALIYUN_PASS=xxx /opt/aslan-deploy/deploy-likei-services.sh other + +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. +EOF +} + +log() { + printf '[%s] %s\n' "$(date '+%F %T')" "$*" +} + +need_cmd() { + command -v "$1" >/dev/null 2>&1 || { + echo "missing command: $1" >&2 + exit 2 + } +} + +ensure_base_image() { + if docker image inspect "$BASE_IMAGE" >/dev/null 2>&1; then + return + fi + log "base image not cached, 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 + 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 "$GIT_REF" + git -C "$SRC_DIR" reset --hard "origin/$GIT_REF" +} + +service_module() { + case "$1" in + other) echo "rc-service/rc-service-other/other-start" ;; + external) echo "rc-service/rc-service-external/external-start" ;; + console) echo "rc-service/rc-service-console/console-start" ;; + *) echo "unsupported service: $1" >&2; exit 2 ;; + esac +} + +service_dir() { + case "$1" in + other) echo "rc-service/rc-service-other" ;; + external) echo "rc-service/rc-service-external" ;; + console) echo "rc-service/rc-service-console" ;; + *) echo "unsupported service: $1" >&2; exit 2 ;; + esac +} + +image_repo() { + case "$1" in + other) echo "tuokemi-con-registry.ap-southeast-1.cr.aliyuncs.com/atyou-prod/other" ;; + external) echo "tuokemi-con-registry.ap-southeast-1.cr.aliyuncs.com/atyou-prod/external" ;; + console) echo "tuokemi-con-registry.ap-southeast-1.cr.aliyuncs.com/atyou-prod/console" ;; + *) 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}" + + cd "$SRC_DIR" + if [[ "$SKIP_BUILD" != "1" ]]; then + log "maven package $svc ($module)" + # shellcheck disable=SC2086 + mvn clean package -pl "$module" -am -P prod -Dmaven.test.skip=true $MAVEN_EXTRA_ARGS + fi + + ensure_base_image + log "docker build $image" + docker build \ + --build-arg "SERVICE_VERSION=atyou-prod:${tag}" \ + -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) + 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 +} + +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 +} + +status() { + kubectl --kubeconfig "$KUBECONFIG" -n "$NAMESPACE" get deploy other external console -o wide + kubectl --kubeconfig "$KUBECONFIG" get nodes -o wide +} + +main() { + while [[ "$#" -gt 0 ]]; do + case "$1" in + --mode) + MODE="$2" + shift 2 + ;; + --skip-build) + SKIP_BUILD=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + break + ;; + esac + done + + need_cmd java + need_cmd mvn + need_cmd docker + need_cmd kubectl + [[ -f "$KUBECONFIG" ]] || { echo "missing kubeconfig: $KUBECONFIG" >&2; exit 2; } + + if [[ "${1:-}" == "status" || "$MODE" == "status" ]]; then + status + exit 0 + fi + + update_source_from_git + + local services=("$@") + if [[ "${#services[@]}" -eq 0 ]]; then + services=(other external console) + fi + + for svc in "${services[@]}"; do + build_service "$svc" + done +} + +main "$@" diff --git a/.deploy/aslan-deploy/sync-and-deploy.sh b/.deploy/aslan-deploy/sync-and-deploy.sh new file mode 100755 index 00000000..c7a4501e --- /dev/null +++ b/.deploy/aslan-deploy/sync-and-deploy.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +set -euo pipefail + +DEPLOY_HOST="${DEPLOY_HOST:-43.160.245.220}" +DEPLOY_USER="${DEPLOY_USER:-ubuntu}" +SSH_KEY="${SSH_KEY:-$HOME/.ssh/aslan-deploy-sg-ed25519}" +REMOTE_BASE="${REMOTE_BASE:-/opt/aslan-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}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +usage() { + cat <<'EOF' +Usage: + .deploy/aslan-deploy/sync-and-deploy.sh [remote deploy args...] + +Examples: + .deploy/aslan-deploy/sync-and-deploy.sh status + .deploy/aslan-deploy/sync-and-deploy.sh --mode preload other + .deploy/aslan-deploy/sync-and-deploy.sh --mode preload other external console + +Environment: + DEPLOY_HOST default 43.160.245.220 + 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.245.220 \ + 'USE_GIT_SOURCE=1 GIT_REF=atyou_prod /opt/aslan-deploy/deploy-likei-services.sh --mode preload other' +EOF +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 +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/" + +rsync -az \ + -e "$rsync_ssh" \ + "$SCRIPT_DIR/deploy-likei-services.sh" "$DEPLOY_USER@$DEPLOY_HOST:$REMOTE_BASE/deploy-likei-services.sh" + +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 + +printf -v remote_script_q '%q' "$REMOTE_BASE/deploy-likei-services.sh" +printf -v remote_args_q '%q ' "$@" +"${ssh_base[@]}" "chmod +x $remote_script_q && $remote_script_q $remote_args_q"