Files
geo/deploy/scripts/load-and-start.sh
T
root 7d8e82c69f feat(ops): add object storage management and site-mapping CSV import/export
Ship the Ops backstage 对象存储 page (browse / upload / move / delete /
folder ops) backed by a new object-storage service + handler, with the
shared storage client extended with List/Stat/Copy/Usage/DownloadURL so
both MinIO and Aliyun providers cover the new surface. Add
ForcePathStyle to the object-storage config and treat r2/s3/custom_s3
as external providers so Cloudflare R2 and other S3-compatibles can
share the MinIO client path without spinning up the bundled MinIO.

Also add CSV import/export + template download to the site-domain
mappings page, raise gin MaxMultipartMemory to 8 MiB for the new
multipart endpoints, register Ops swagger routes, and extend the
descriptions-parity test to cover the ops router.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 01:19:01 +08:00

293 lines
9.9 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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|r2|cloudflare_r2|cloudflare-r2|s3|aws_s3|aws-s3|custom|custom_s3|custom-s3)
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" <<EOF
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../$(basename "${base_dir}")
secretGenerator:
- name: geo-rankly-app-secret
behavior: merge
files:
- config.local.yaml=config.local.yaml
- ops-config.local.yaml=ops-config.local.yaml
generatorOptions:
disableNameSuffixHash: true
EOF
printf '%s' "${runtime_dir}"
}
deploy_compose() {
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"
local compose_files=("-f" "docker-compose.yaml" "-f" "docker-compose.offline.yaml")
local minio_enabled="true"
if ! use_minio_for_object_storage; then
compose_files+=("-f" "docker-compose.aliyun-oss.yaml")
minio_enabled="false"
fi
# ─── Step 1: 加载镜像 ───────────────────────────────────────────────────────
echo "==> [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://<server-ip>:$(env_value FRONTEND_PORT 18080)"
echo " Ops Web: http://<server-ip>:$(env_value OPS_WEB_PORT 18082)"
echo " RabbitMQ: http://<server-ip>:$(env_value RABBITMQ_MGMT_PORT 15673) (geo / geo_dev)"
if [[ "${minio_enabled}" == "true" ]]; then
echo " MinIO: http://<server-ip>:$(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