aslan-server/.deploy/prod-deploy/deploy-likei-services.sh

356 lines
11 KiB
Bash
Executable File

#!/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" <<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
}
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 "$@"