#!/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}" LOCK_FILE="${LOCK_FILE:-$BASE_DIR/deploy.lock}" 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:-aslan_prod}" MAVEN_GOALS="${MAVEN_GOALS:-clean package}" 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-prod}" 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] [--fast] [wallet|other|external|console ...] Examples: /opt/aslan-deploy/deploy-likei-services.sh status /opt/aslan-deploy/deploy-likei-services.sh --mode preload wallet /opt/aslan-deploy/deploy-likei-services.sh --mode preload other external console /opt/aslan-deploy/deploy-likei-services.sh --fast wallet USE_GIT_SOURCE=1 GIT_REF=aslan_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 } } acquire_deploy_lock() { need_cmd flock exec 9>"$LOCK_FILE" # Only one deploy should mutate source, build images, preload nodes, and roll deployments at a time. flock -n 9 || { echo "another deploy is already running: $LOCK_FILE" >&2 exit 5 } } validate_git_ref() { if [[ ! "$GIT_REF" =~ ^[A-Za-z0-9._/-]{1,80}$ || "$GIT_REF" == -* || "$GIT_REF" == *..* ]]; then 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" } service_module() { case "$1" in wallet) echo "rc-service/rc-service-wallet/wallet-start" ;; 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 wallet) echo "rc-service/rc-service-wallet" ;; 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 wallet) echo "$IMAGE_REGISTRY/$IMAGE_PROJECT/wallet" ;; other) echo "$IMAGE_REGISTRY/$IMAGE_PROJECT/other" ;; external) echo "$IMAGE_REGISTRY/$IMAGE_PROJECT/external" ;; console) echo "$IMAGE_REGISTRY/$IMAGE_PROJECT/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 # 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 fi ensure_base_image log "docker build $image" docker build \ --build-arg "SERVICE_VERSION=${IMAGE_PROJECT}:${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) 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 } status() { kubectl --kubeconfig "$KUBECONFIG" -n "$NAMESPACE" get deploy wallet other external console -o wide kubectl --kubeconfig "$KUBECONFIG" get nodes -o wide } main() { while [[ "$#" -gt 0 ]]; do case "$1" in --mode) [[ -n "${2:-}" ]] || { echo "--mode requires an argument" >&2; exit 2; } MODE="$2" shift 2 ;; --skip-build) SKIP_BUILD=1 shift ;; --fast) # Code-only wallet changes can keep Maven's target cache instead of forcing clean package. MAVEN_GOALS=package shift ;; -h|--help) usage exit 0 ;; *) break ;; esac done if [[ "${1:-}" == "status" || "$MODE" == "status" ]]; then need_cmd kubectl [[ -f "$KUBECONFIG" ]] || { echo "missing kubeconfig: $KUBECONFIG" >&2; exit 2; } 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) fi for svc in "${services[@]}"; do build_service "$svc" done } main "$@"