#!/usr/bin/env bash # load-and-start.sh — 在【离线目标服务器】上运行 # 加载镜像并使用 Docker Compose 或 k3s 启动服务 # # 用法(解压部署包后): # bash deploy.sh compose # 默认,Docker Compose 部署 # bash deploy.sh k3s # k3s / Kubernetes 部署 set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" MODE="${1:-compose}" # 读取打包时记录的元数据 if [[ -f "${SCRIPT_DIR}/.package-meta" ]]; then source "${SCRIPT_DIR}/.package-meta" fi IMAGE_TAG="${IMAGE_TAG:-latest}" LOCAL_PREFIX="${LOCAL_PREFIX:-geo-rankly}" # ─── 检查 .env ──────────────────────────────────────────────────────────────── if [[ ! -f "${SCRIPT_DIR}/.env" ]]; then echo "==> 未找到 .env,已从 .env.example 自动创建。" cp "${SCRIPT_DIR}/.env.example" "${SCRIPT_DIR}/.env" echo " 如需填写真实 API Key 或修改端口,可编辑 ${SCRIPT_DIR}/.env 后再次执行 bash deploy.sh" fi ensure_local_config() { local file="$1" example="$2" if [[ -f "${SCRIPT_DIR}/${file}" ]]; then return fi if [[ -f "${SCRIPT_DIR}/${example}" ]]; then cp "${SCRIPT_DIR}/${example}" "${SCRIPT_DIR}/${file}" echo "==> 已从 ${example} 创建 ${file}。" echo " 生产多副本部署前请把其中的 password_cipher.private_key_pem 替换为同一把 RSA 私钥。" fi } ensure_config_aliases() { local yaml yml for yaml in config.yaml config.local.yaml ops-config.yaml ops-config.local.yaml; do yml="${yaml%.yaml}.yml" if [[ ! -f "${SCRIPT_DIR}/${yaml}" && -f "${SCRIPT_DIR}/${yml}" ]]; then cp "${SCRIPT_DIR}/${yml}" "${SCRIPT_DIR}/${yaml}" echo "==> 已从 ${yml} 创建 ${yaml},供部署清单挂载使用。" fi done if [[ ! -f "${SCRIPT_DIR}/prompts.yml" && -f "${SCRIPT_DIR}/prompts.yaml" ]]; then cp "${SCRIPT_DIR}/prompts.yaml" "${SCRIPT_DIR}/prompts.yml" echo "==> 已从 prompts.yaml 创建 prompts.yml,供部署清单挂载使用。" fi } env_value() { local key="$1" default_value="$2" value value="$(grep -E "^${key}=" "${SCRIPT_DIR}/.env" | tail -1 | cut -d= -f2- || true)" value="${value%\"}" value="${value#\"}" printf '%s' "${value:-${default_value}}" } config_file_path() { local base="$1" for file in "${SCRIPT_DIR}/${base}.yaml" "${SCRIPT_DIR}/${base}.yml"; do if [[ -f "${file}" ]]; then printf '%s' "${file}" return fi done } read_object_storage_provider() { local provider="minio" file for file in "$(config_file_path config)" "$(config_file_path config.local)"; do if [[ -z "${file}" || ! -f "${file}" ]]; then continue fi local next next="$( awk ' /^[[:space:]]*object_storage:[[:space:]]*$/ { in_section=1 base=match($0, /[^[:space:]]/) - 1 next } in_section { indent=match($0, /[^[:space:]]/) - 1 if ($0 ~ /^[[:space:]]*$/ || $0 ~ /^[[:space:]]*#/) { next } if (indent <= base) { in_section=0 next } if ($0 ~ /^[[:space:]]*provider[[:space:]]*:/) { value=$0 sub(/^[[:space:]]*provider[[:space:]]*:[[:space:]]*/, "", value) sub(/[[:space:]]+#.*$/, "", value) gsub(/^["'\'']|["'\'']$/, "", value) print value } } ' "${file}" | tail -1 )" if [[ -n "${next}" ]]; then provider="${next}" fi done printf '%s' "$(printf '%s' "${provider}" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')" } use_minio_for_object_storage() { case "$(read_object_storage_provider)" in ""|minio|mino) return 0 ;; aliyun|aliyun_oss|aliyun-oss|oss) return 1 ;; *) return 0 ;; esac } sync_k3s_config() { mkdir -p "${SCRIPT_DIR}/k3s/config" for name in config ops-config prompts; do local output="${name}.yaml" if [[ "${name}" == "prompts" ]]; then output="prompts.yml" fi local source="" for candidate in "${SCRIPT_DIR}/${name}.yaml" "${SCRIPT_DIR}/${name}.yml"; do if [[ -f "${candidate}" ]]; then source="${candidate}" break fi done if [[ -n "${source}" ]]; then cp "${source}" "${SCRIPT_DIR}/k3s/config/${output}" fi done } prepare_k3s_runtime_overlay() { local base_dir="$1" local runtime_dir="${SCRIPT_DIR}/.k3s-runtime" rm -rf "${runtime_dir}" mkdir -p "${runtime_dir}" cp "$(config_file_path config.local)" "${runtime_dir}/config.local.yaml" cp "$(config_file_path ops-config.local)" "${runtime_dir}/ops-config.local.yaml" cat > "${runtime_dir}/kustomization.yaml" < [1/3] 加载 Docker 镜像(可能需要几分钟)..." docker load -i "${SCRIPT_DIR}/images.tar" echo " 镜像加载完成" docker images | grep -E "geo-rankly|postgres|rabbitmq|redis|qdrant|minio" | head -20 # ─── Step 2: 设置 IMAGE_REGISTRY 为本地前缀 ──────────────────────────────── echo "==> [2/3] 配置离线镜像引用..." export IMAGE_REGISTRY="${LOCAL_PREFIX}" export IMAGE_TAG="${IMAGE_TAG}" # ─── Step 3: 启动服务 ─────────────────────────────────────────────────────── echo "==> [3/3] 启动所有服务..." if [[ "${minio_enabled}" == "true" ]]; then echo " object_storage.provider=$(read_object_storage_provider),将部署 MinIO。" else echo " object_storage.provider=$(read_object_storage_provider),将跳过 MinIO。" export COMPOSE_PROFILES="" docker rm -f geo-minio geo-minio-init >/dev/null 2>&1 || true fi cd "${SCRIPT_DIR}" docker compose "${compose_files[@]}" --env-file .env up -d echo "" echo "✅ Docker Compose 部署完成!" echo "" echo "服务状态:" docker compose "${compose_files[@]}" ps echo "" echo "查看日志: docker compose ${compose_files[*]} logs -f tenant-api" echo "停止服务: docker compose ${compose_files[*]} down" echo "管理界面:" echo " Tenant Web: http://:$(env_value FRONTEND_PORT 18080)" echo " Ops Web: http://:$(env_value OPS_WEB_PORT 18082)" echo " RabbitMQ: http://:$(env_value RABBITMQ_MGMT_PORT 15673) (geo / geo_dev)" if [[ "${minio_enabled}" == "true" ]]; then echo " MinIO: http://:$(env_value MINIO_CONSOLE_PORT 19001) ($(env_value MINIO_ROOT_USER minioadmin) / $(env_value MINIO_ROOT_PASSWORD minioadmin))" fi } deploy_k3s() { local kubectl_cmd="kubectl" local kustomize_dir="${SCRIPT_DIR}/k3s" local minio_enabled="true" ensure_config_aliases ensure_local_config "config.local.yaml" "config.local.yaml.example" ensure_local_config "ops-config.local.yaml" "ops-config.local.yaml.example" if ! use_minio_for_object_storage; then kustomize_dir="${SCRIPT_DIR}/k3s-aliyun-oss" minio_enabled="false" fi if command -v k3s >/dev/null 2>&1; then echo "==> [1/3] 导入镜像到 k3s containerd..." sudo k3s ctr images import "${SCRIPT_DIR}/images.tar" kubectl_cmd="sudo k3s kubectl" elif command -v kubectl >/dev/null 2>&1 && command -v ctr >/dev/null 2>&1; then echo "==> [1/3] 导入镜像到 containerd..." sudo ctr -n k8s.io images import "${SCRIPT_DIR}/images.tar" else echo "❌ 未找到 k3s/kubectl/ctr。请先安装 k3s,或改用:bash deploy.sh compose" exit 1 fi echo "==> [2/3] 应用 k3s 清单..." sync_k3s_config kustomize_dir="$(prepare_k3s_runtime_overlay "${kustomize_dir}")" if [[ "${minio_enabled}" == "true" ]]; then echo " object_storage.provider=$(read_object_storage_provider),将部署 MinIO。" else echo " object_storage.provider=$(read_object_storage_provider),将跳过 MinIO。" ${kubectl_cmd} -n geo-rankly delete statefulset minio --ignore-not-found ${kubectl_cmd} -n geo-rankly delete service minio --ignore-not-found ${kubectl_cmd} -n geo-rankly delete job minio-init --ignore-not-found for svc in tenant-api worker-generate kol-assist-worker scheduler; do ${kubectl_cmd} -n geo-rankly set env "deployment/${svc}" OBJECT_STORAGE_ACCESS_KEY- OBJECT_STORAGE_SECRET_KEY- >/dev/null 2>&1 || true done fi ${kubectl_cmd} apply -k "${kustomize_dir}" echo "==> [3/3] 查看服务状态..." ${kubectl_cmd} -n geo-rankly get pods,svc echo "" echo "✅ k3s 部署命令已执行。等待 Pod 全部 Running:" echo " ${kubectl_cmd} -n geo-rankly get pods -w" echo "" echo "查看日志:" echo " ${kubectl_cmd} -n geo-rankly logs deploy/tenant-api -f" } case "${MODE}" in compose|docker-compose|docker) deploy_compose ;; k3s|kubernetes) deploy_k3s ;; *) echo "用法:bash deploy.sh [compose|k3s]" exit 1 ;; esac