Files
ctms/install.sh
T
Cheng Zhou b83c900109 环境菜单标题显示已识别的环境名
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 22:23:50 +08:00

296 lines
10 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
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=scripts/common.sh
source "$ROOT_DIR/scripts/common.sh"
SELECTED_INDEX=0
SELECTED_ENV=""
SELECTED_BASE_URL=""
ENV_STATUS_LINES=""
UI_BOX_WIDTH=76
usage() {
cat <<EOF
${CC_PRIMARY}${C_BOLD}═══════════════════════════════════════════════════════════════${C_RESET}
${CC_PRIMARY}${C_BOLD} CTMS 部署控制台 - 使用帮助${C_RESET}
${CC_PRIMARY}${C_BOLD}═══════════════════════════════════════════════════════════════${C_RESET}
${C_BOLD}【主入口】${C_RESET} ${CC_INFO}./install.sh${C_RESET} ${CC_MUTED}↑/↓ 选择 Enter 确认 Q 退出${C_RESET}
${C_BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C_RESET}
${C_BOLD}【底层脚本(CI/CD)】${C_RESET}
${CC_INFO}bash scripts/install.sh <env> [--base-url <url>] [--yes] [--skip-build] [--skip-migrate]${C_RESET}
${CC_INFO}bash scripts/update.sh <env> [--repo-url <url>] [--branch <name>] [--yes] [--force]${C_RESET}
${CC_INFO}bash scripts/uninstall.sh <env>${C_RESET}
${CC_INFO}bash scripts/status.sh <env>${C_RESET}
${CC_MUTED}env: dev(开发)| main(内网测试)| release(生产)${C_RESET}
${CC_MUTED}update.sh 认证:export GIT_USERNAME=xxx GIT_PASSWORD=yyy${C_RESET}
${CC_MUTED}查看脚本帮助:${C_RESET} bash scripts/<script>.sh --help
${CC_PRIMARY}${C_BOLD}═══════════════════════════════════════════════════════════════${C_RESET}
EOF
}
require_interactive_terminal() {
[[ -t 0 && -t 1 ]] || ctms_fail "install.sh 需要在交互式终端中运行"
}
banner() {
printf '\n'
ctms_box_top "${C_BLUE}${C_BOLD}" "CTMS DEPLOYMENT CONSOLE" "$UI_BOX_WIDTH"
ctms_box_line "${C_BLUE}${C_BOLD}" "$UI_BOX_WIDTH" "${ICON_BULLET} 安装 · 更新 · 卸载 · 资源状态" "${CC_INFO}${C_BOLD}${ICON_BULLET}${C_RESET} 安装 · 更新 · 卸载 · 资源状态"
ctms_box_line "${C_BLUE}${C_BOLD}" "$UI_BOX_WIDTH" "↑/↓ 选择 Enter 确认 Q 退出" "${CC_MUTED}↑/↓ 选择 Enter 确认 Q 退出${C_RESET}"
ctms_box_bottom "${C_BLUE}${C_BOLD}" "$UI_BOX_WIDTH"
printf '\n'
render_status_panel
}
render_status_panel() {
ctms_box_top "${C_MAGENTA}${C_BOLD}" "环境状态" "$UI_BOX_WIDTH"
printf '%b\n' "$ENV_STATUS_LINES" | while IFS= read -r line; do
ctms_box_line "${C_MAGENTA}${C_BOLD}" "$UI_BOX_WIDTH" "$line" "$line"
done
ctms_box_bottom "${C_MAGENTA}${C_BOLD}" "$UI_BOX_WIDTH"
printf '\n'
}
build_env_status() {
local output=""
local detected=0
local docker_error=""
if ! command -v docker >/dev/null 2>&1; then
ENV_STATUS_LINES=" ${C_YELLOW}Docker 未安装或 docker 命令不可用${C_RESET}"
return
fi
if ! docker ps >/dev/null 2>&1; then
docker_error="$(docker ps 2>&1 >/dev/null || true)"
ENV_STATUS_LINES=" ${C_YELLOW}Docker 未运行或当前用户无权限访问 Docker${C_RESET}"
[[ -n "$docker_error" ]] && ENV_STATUS_LINES="${ENV_STATUS_LINES}\n ${C_DIM}${docker_error}${C_RESET}"
return
fi
local env project rows total running stopped status_label
for env in dev main release; do
project="$(ctms_project_name "$env")"
rows="$(docker ps -a \
--filter "label=com.docker.compose.project=$project" \
--format '{{.Names}}|{{.State}}|{{.Status}}' 2>/dev/null || true)"
[[ -n "$rows" ]] || continue
detected=1
total="$(printf '%s\n' "$rows" | awk 'NF { count++ } END { print count + 0 }')"
running="$(printf '%s\n' "$rows" | awk -F'|' '$2 == "running" { count++ } END { print count + 0 }')"
stopped=$(( total - running ))
if [[ "$running" -eq "$total" && "$total" -gt 0 ]]; then
status_label="${CC_SUCCESS}${ICON_RUNNING} 运行中${C_RESET}"
elif [[ "$running" -gt 0 ]]; then
status_label="${CC_WARN}${ICON_RUNNING} 部分运行${C_RESET}"
else
status_label="${CC_DANGER}${ICON_STOPPED} 已停止${C_RESET}"
fi
output="${output} ${CC_INFO}${C_BOLD}${env}${C_RESET} ${CC_MUTED}(${project})${C_RESET} ${status_label} ${CC_MUTED}${running}/${total} 运行"
[[ "$stopped" -gt 0 ]] && output="${output}${stopped} 已停止"
output="${output}${C_RESET}\n"
output="${output}$(printf '%s\n' "$rows" | awk -F'|' '{ printf " %-18s %-8s %s\n", $1, $2, $3 }')\n"
done
if [[ "$detected" -eq 0 ]]; then
rows="$(docker ps -a \
--filter "name=ctms_" \
--format '{{.Names}}|{{.State}}|{{.Status}}' 2>/dev/null || true)"
if [[ -n "$rows" ]]; then
total="$(printf '%s\n' "$rows" | awk 'NF { count++ } END { print count + 0 }')"
running="$(printf '%s\n' "$rows" | awk -F'|' '$2 == "running" { count++ } END { print count + 0 }')"
ENV_STATUS_LINES=" ${C_YELLOW}检测到 CTMS 命名容器,但无法从 Compose 标签确认环境${C_RESET}\n"
ENV_STATUS_LINES="${ENV_STATUS_LINES} ${running}/${total} 运行\n"
ENV_STATUS_LINES="${ENV_STATUS_LINES}$(printf '%s\n' "$rows" | awk -F'|' '{ printf " - %-18s %-8s %s\n", $1, $2, $3 }')"
else
ENV_STATUS_LINES=" ${C_DIM}未检测到 CTMS Compose 容器(ctms_dev / ctms_main / ctms_release${C_RESET}"
fi
else
ENV_STATUS_LINES="${output%\\n}"
fi
}
select_menu() {
local title="$1"
local default_index="$2"
shift 2
local options=("$@")
local selected="${default_index:-0}"
local key=""
[[ "${#options[@]}" -gt 0 ]] || ctms_fail "菜单没有可选项"
trap ctms_restore_cursor EXIT INT TERM
printf '\033[?25l\033[2J'
while true; do
printf '\033[H'
banner
ctms_box_top "${C_BLUE}${C_BOLD}" "$title" "$UI_BOX_WIDTH"
local i
for i in "${!options[@]}"; do
if [[ "$i" -eq "$selected" ]]; then
ctms_box_line "${C_BLUE}${C_BOLD}" "$UI_BOX_WIDTH" "${ICON_BULLET} ${options[$i]}" "${CC_INFO}${C_BOLD}${ICON_BULLET} ${options[$i]}${C_RESET}"
else
ctms_box_line "${C_BLUE}${C_BOLD}" "$UI_BOX_WIDTH" "${ICON_BULLET_OFF} ${options[$i]}" "${CC_MUTED}${ICON_BULLET_OFF} ${options[$i]}${C_RESET}"
fi
done
ctms_box_bottom "${C_BLUE}${C_BOLD}" "$UI_BOX_WIDTH"
printf '\033[J'
key="$(ctms_read_key)"
case "$key" in
"[A") selected=$(( (selected - 1 + ${#options[@]}) % ${#options[@]} )) ;;
"[B") selected=$(( (selected + 1) % ${#options[@]} )) ;;
"") ctms_restore_cursor; SELECTED_INDEX="$selected"; return ;;
q|Q) ctms_restore_cursor; printf '\033[2J\033[H'; exit 0 ;;
esac
done
}
# 读取 .ctms-env 中的 KEY = VALUE 配置
ctms_config_get() {
local key="$1" file="$ROOT_DIR/.ctms-env"
[[ -f "$file" ]] || return 1
awk -F' *= *' -v k="$key" 'NF==2 && $1==k { print $2; exit }' "$file"
}
pick_env() {
local env default_index=0
env="$(ctms_config_get "env" || true)"
case "$env" in
main) default_index=1 ;;
release) default_index=2 ;;
esac
local -a opts=(
"dev 本地开发环境 Vite 热加载 / 默认密钥"
"main 内网测试环境 预发布验证 / 生产模式"
"release 生产环境 HTTPS / RSA 密钥"
)
if [[ -n "$env" ]]; then
case "$default_index" in
0) opts[0]="dev(默认) 本地开发环境 Vite 热加载 / 默认密钥" ;;
1) opts[1]="main(默认) 内网测试环境 预发布验证 / 生产模式" ;;
2) opts[2]="release(默认) 生产环境 HTTPS / RSA 密钥" ;;
esac
fi
local menu_title="选择环境"
[[ -n "$env" ]] && menu_title="选择环境 (已识别: ${env}"
select_menu "$menu_title" "$default_index" "${opts[@]}"
case "$SELECTED_INDEX" in
0) SELECTED_ENV="dev" ;;
1) SELECTED_ENV="main" ;;
2) SELECTED_ENV="release" ;;
esac
}
collect_install_options() {
local env="$1"
SELECTED_BASE_URL="$(ctms_config_get "base-url" || true)"
if [[ "$env" == "release" && -z "$SELECTED_BASE_URL" ]]; then
SELECTED_BASE_URL="$(ctms_ask_line "请输入健康检查地址,例如 https://ctms.example.com")"
[[ -n "$SELECTED_BASE_URL" ]] || ctms_fail "release 环境必须提供健康检查地址"
elif [[ "$env" == "main" && -z "$SELECTED_BASE_URL" ]]; then
SELECTED_BASE_URL="$(ctms_ask_line "请输入健康检查地址" "http://127.0.0.1:8888")"
fi
}
run_script() {
local script="$1"
shift
[[ -x "$ROOT_DIR/scripts/$script" ]] || ctms_fail "脚本不可执行: $ROOT_DIR/scripts/$script"
"$ROOT_DIR/scripts/$script" "$@"
}
run_install_like_script() {
local script="$1"
local env="$2"
if [[ -n "$SELECTED_BASE_URL" ]]; then
run_script "$script" "$env" --base-url "$SELECTED_BASE_URL"
else
run_script "$script" "$env"
fi
}
# 操作完成后暂停,等用户确认再返回菜单
pause_return() {
printf '\n %s%s%s 按 Enter 返回主菜单…' "${CC_INFO}${C_BOLD}" "$ICON_PROMPT" "${C_RESET}"
read -r _ || true
}
interactive_main() {
select_menu "选择操作" "" \
"安装部署 配置环境、构建容器、迁移数据库、健康检查" \
"滚动更新 重建服务、执行迁移、复用安装健康检查" \
"安全卸载 停止并移除容器,保留 .env 与数据文件" \
"资源状态 查看 CPU、内存、网络与磁盘 I/O 快照" \
"使用帮助 查看入口和底层脚本说明" \
"退出控制台"
case "$SELECTED_INDEX" in
0)
pick_env
collect_install_options "$SELECTED_ENV"
run_install_like_script "install.sh" "$SELECTED_ENV"
pause_return
;;
1)
pick_env
collect_install_options "$SELECTED_ENV"
run_install_like_script "update.sh" "$SELECTED_ENV"
pause_return
;;
2)
pick_env
run_script "uninstall.sh" "$SELECTED_ENV"
pause_return
;;
3)
pick_env
run_script "status.sh" "$SELECTED_ENV"
pause_return
;;
4)
usage
pause_return
;;
5)
return 1
;;
esac
return 0
}
main() {
if [[ $# -gt 0 ]]; then
usage
ctms_fail "install.sh 不接受参数,请直接执行 ./install.sh"
fi
require_interactive_terminal
while true; do
build_env_status
interactive_main || break
done
printf '\033[2J\033[H'
exit 0
}
main "$@"