mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 16:06:08 +00:00
Limit application data and log ownership changes to their top-level directories. Add a container check that reapplies installer permissions and verifies application startup. Validation: first-install and repeated-install filesystem permission checks, Bash syntax, ShellCheck, Actionlint, and diff checks passed. Full local container validation was blocked by Docker Desktop bind-mount permission mapping. Co-authored-by: Codex <noreply@openai.com>
370 lines
11 KiB
Bash
Executable File
370 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
set -Eeuo pipefail
|
||
umask 0077
|
||
|
||
# codex-proxy-rs 一键安装脚本。
|
||
# 新安装使用同一个 Release 的 compose.yaml 与 config.example.yaml;
|
||
# 已存在 deploy/config.yaml 时保留整套部署文件,不执行版本升级。
|
||
|
||
REPO="zyycn/codex-proxy-rs"
|
||
CPR_RELEASE_TAG="${CPR_RELEASE_TAG:-}"
|
||
INSTALL_DIR="${INSTALL_DIR:-$PWD/codex-proxy-rs}"
|
||
DEPLOY_DIR="${INSTALL_DIR}/deploy"
|
||
RUNTIME_DIR="${INSTALL_DIR}/.runtime"
|
||
|
||
CONFIG_EXAMPLE="${DEPLOY_DIR}/config.example.yaml"
|
||
CONFIG_FILE="${DEPLOY_DIR}/config.yaml"
|
||
COMPOSE_FILE="${DEPLOY_DIR}/compose.yaml"
|
||
|
||
ADMIN_PASSWORD="${ADMIN_PASSWORD:-}"
|
||
|
||
GREEN='\033[1;32m'
|
||
YELLOW='\033[1;33m'
|
||
RED='\033[1;31m'
|
||
CYAN='\033[1;36m'
|
||
NC='\033[0m'
|
||
|
||
log() {
|
||
printf '%b[+]%b %s\n' "$GREEN" "$NC" "$*"
|
||
}
|
||
|
||
info() {
|
||
printf '%b[*]%b %s\n' "$CYAN" "$NC" "$*"
|
||
}
|
||
|
||
warn() {
|
||
printf '%b[!]%b %s\n' "$YELLOW" "$NC" "$*" >&2
|
||
}
|
||
|
||
die() {
|
||
printf '%b[x]%b %s\n' "$RED" "$NC" "$*" >&2
|
||
exit 1
|
||
}
|
||
|
||
on_error() {
|
||
local code=$?
|
||
|
||
printf '\n' >&2
|
||
warn "部署失败,退出码:${code}"
|
||
warn "安装目录已保留:${INSTALL_DIR}"
|
||
if [[ -f "$COMPOSE_FILE" ]]; then
|
||
warn "可执行以下命令查看状态:"
|
||
printf ' cd %q && docker compose -f deploy/compose.yaml ps\n' "$INSTALL_DIR" >&2
|
||
printf ' cd %q && docker compose -f deploy/compose.yaml logs --tail=200\n' "$INSTALL_DIR" >&2
|
||
fi
|
||
exit "$code"
|
||
}
|
||
trap on_error ERR
|
||
|
||
need_cmd() {
|
||
command -v "$1" >/dev/null 2>&1 || die "缺少命令:$1"
|
||
}
|
||
|
||
run_root() {
|
||
if [[ "$(id -u)" -eq 0 ]]; then
|
||
"$@"
|
||
elif command -v sudo >/dev/null 2>&1; then
|
||
sudo "$@"
|
||
else
|
||
die "该步骤需要 root 权限,但系统没有 sudo;请使用 root 用户运行。"
|
||
fi
|
||
}
|
||
|
||
prepare_install_dir() {
|
||
if ! mkdir -p -- "$INSTALL_DIR" 2>/dev/null; then
|
||
run_root install -d -m 0750 -o "$(id -u)" -g "$(id -g)" -- "$INSTALL_DIR"
|
||
fi
|
||
mkdir -p -- "$DEPLOY_DIR"
|
||
}
|
||
|
||
resolve_release_tag() {
|
||
local release_url
|
||
|
||
if [[ -z "$CPR_RELEASE_TAG" ]]; then
|
||
release_url="$(curl -fsSL -o /dev/null -w '%{url_effective}' \
|
||
"https://github.com/${REPO}/releases/latest")"
|
||
CPR_RELEASE_TAG="${release_url%/}"
|
||
CPR_RELEASE_TAG="${CPR_RELEASE_TAG##*/}"
|
||
fi
|
||
|
||
[[ "$CPR_RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]] \
|
||
|| die "无法确定有效的发布版本:${CPR_RELEASE_TAG:-<空>}"
|
||
}
|
||
|
||
download() {
|
||
local url="$1"
|
||
local output="$2"
|
||
|
||
curl \
|
||
--fail \
|
||
--location \
|
||
--silent \
|
||
--show-error \
|
||
--retry 3 \
|
||
--connect-timeout 15 \
|
||
"$url" \
|
||
--output "$output"
|
||
}
|
||
|
||
download_release_files() {
|
||
local base_url
|
||
|
||
resolve_release_tag
|
||
base_url="https://github.com/${REPO}/releases/download/${CPR_RELEASE_TAG}"
|
||
|
||
log "下载 ${CPR_RELEASE_TAG} compose.yaml"
|
||
download "${base_url}/compose.yaml" "$COMPOSE_FILE"
|
||
log "下载 ${CPR_RELEASE_TAG} config.example.yaml"
|
||
download "${base_url}/config.example.yaml" "$CONFIG_EXAMPLE"
|
||
|
||
grep -q '^name: codex-proxy-rs$' "$COMPOSE_FILE" \
|
||
|| die "下载到的 compose.yaml 内容异常。"
|
||
grep -q '^schema_version:' "$CONFIG_EXAMPLE" \
|
||
|| die "下载到的 config.example.yaml 内容异常。"
|
||
}
|
||
|
||
random_password() {
|
||
openssl rand -hex 24
|
||
}
|
||
|
||
validate_admin_password() {
|
||
local password="$1"
|
||
local normalized
|
||
|
||
if (( ${#password} < 12 )); then
|
||
die "ADMIN_PASSWORD 至少需要 12 个字符。"
|
||
fi
|
||
if [[ "$password" == *'$'* ]]; then
|
||
die 'ADMIN_PASSWORD 不能包含 $ 字符。'
|
||
fi
|
||
if [[ "$password" =~ [[:cntrl:]] ]]; then
|
||
die "ADMIN_PASSWORD 不能包含换行或其他控制字符。"
|
||
fi
|
||
normalized="$(printf '%s' "$password" | tr '[:upper:]' '[:lower:]')"
|
||
if [[ "$normalized" == "codex-proxy-rs" ]]; then
|
||
die "ADMIN_PASSWORD 不能使用常见弱口令。"
|
||
fi
|
||
}
|
||
|
||
yaml_single_quote() {
|
||
local value="${1//\'/\'\'}"
|
||
printf "'%s'" "$value"
|
||
}
|
||
|
||
patch_config() {
|
||
local postgres_password redis_password admin_password
|
||
local postgres_count=0 redis_count=0 admin_count=0
|
||
local line rendered=''
|
||
local postgres_pattern="^([[:blank:]]*password:[[:blank:]]*&postgres_password[[:blank:]]*)''[[:blank:]]*$"
|
||
local redis_pattern="^([[:blank:]]*password:[[:blank:]]*&redis_password[[:blank:]]*)''[[:blank:]]*$"
|
||
local admin_pattern="^([[:blank:]]*default_password:[[:blank:]]*)''[[:blank:]]*$"
|
||
|
||
postgres_password="$(yaml_single_quote "$1")"
|
||
redis_password="$(yaml_single_quote "$2")"
|
||
admin_password="$(yaml_single_quote "$3")"
|
||
|
||
# 按模板中的空密码占位替换;先完整检查,再写文件,避免留下半份配置。
|
||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||
if [[ "$line" =~ $postgres_pattern ]]; then
|
||
line="${BASH_REMATCH[1]}${postgres_password}"
|
||
postgres_count=$((postgres_count + 1))
|
||
elif [[ "$line" =~ $redis_pattern ]]; then
|
||
line="${BASH_REMATCH[1]}${redis_password}"
|
||
redis_count=$((redis_count + 1))
|
||
elif [[ "$line" =~ $admin_pattern ]]; then
|
||
line="${BASH_REMATCH[1]}${admin_password}"
|
||
admin_count=$((admin_count + 1))
|
||
fi
|
||
rendered+="$line"$'\n'
|
||
done < "$CONFIG_EXAMPLE"
|
||
|
||
if (( postgres_count != 1 || redis_count != 1 || admin_count != 1 )); then
|
||
die "config.example.yaml 结构与安装脚本预期不一致,已停止以免生成错误配置。"
|
||
fi
|
||
printf '%s' "$rendered" > "$CONFIG_FILE"
|
||
}
|
||
|
||
service_user_ids() {
|
||
local service="$1"
|
||
local user="$2"
|
||
local uid
|
||
local gid
|
||
|
||
uid="$(
|
||
cd "$INSTALL_DIR"
|
||
docker compose -f deploy/compose.yaml run --rm --no-deps --entrypoint id "$service" -u "$user"
|
||
)"
|
||
gid="$(
|
||
cd "$INSTALL_DIR"
|
||
docker compose -f deploy/compose.yaml run --rm --no-deps --entrypoint id "$service" -g "$user"
|
||
)"
|
||
|
||
[[ "$uid" =~ ^[0-9]+$ ]] || die "无法读取 ${service} 容器用户 ${user} 的 UID。"
|
||
[[ "$gid" =~ ^[0-9]+$ ]] || die "无法读取 ${service} 容器用户 ${user} 的 GID。"
|
||
printf '%s:%s\n' "$uid" "$gid"
|
||
}
|
||
|
||
fix_runtime_permissions() {
|
||
local postgres_ids
|
||
local redis_ids
|
||
local app_ids
|
||
local app_gid
|
||
|
||
log "读取容器用户 UID/GID"
|
||
postgres_ids="$(service_user_ids postgres postgres)"
|
||
redis_ids="$(service_user_ids redis redis)"
|
||
app_ids="$(service_user_ids codex-proxy-rs cpr)"
|
||
app_gid="${app_ids#*:}"
|
||
|
||
info "PostgreSQL postgres = ${postgres_ids}"
|
||
info "Redis redis = ${redis_ids}"
|
||
info "codex-proxy-rs cpr = ${app_ids}"
|
||
|
||
log "设置运行目录权限"
|
||
run_root chown -R "$postgres_ids" -- "${RUNTIME_DIR}/postgres"
|
||
run_root chmod 0750 -- "${RUNTIME_DIR}/postgres"
|
||
run_root chown -R "$redis_ids" -- "${RUNTIME_DIR}/redis"
|
||
run_root chmod 0750 -- "${RUNTIME_DIR}/redis"
|
||
|
||
# 只设置顶层目录,保留应用创建的私有目录与文件的所有权,确保重复安装后仍可读写
|
||
run_root chown "$(id -u):${app_gid}" -- "${RUNTIME_DIR}/data" "${RUNTIME_DIR}/logs"
|
||
run_root chmod 0770 -- "${RUNTIME_DIR}/data" "${RUNTIME_DIR}/logs"
|
||
run_root chown "$(id -u):${app_gid}" -- "$CONFIG_FILE"
|
||
run_root chmod 0640 -- "$CONFIG_FILE"
|
||
}
|
||
|
||
check_health() {
|
||
local status
|
||
|
||
status="$(curl \
|
||
--silent \
|
||
--output /dev/null \
|
||
--write-out '%{http_code}' \
|
||
--max-time 10 \
|
||
http://127.0.0.1:8080/healthz || true)"
|
||
[[ "$status" == "204" ]]
|
||
}
|
||
|
||
main() {
|
||
local existing_config=0
|
||
local postgres_password
|
||
local redis_password
|
||
|
||
printf '\n'
|
||
printf '%s\n' '============================================='
|
||
printf '%s\n' ' codex-proxy-rs 一键部署'
|
||
printf '%s\n' ' 新安装使用同一 Release 的官方部署文件'
|
||
printf '%s\n' '============================================='
|
||
printf '\n'
|
||
|
||
[[ "$(uname -s)" == "Linux" ]] || die "此脚本仅支持 Linux。"
|
||
need_cmd curl
|
||
need_cmd docker
|
||
need_cmd install
|
||
need_cmd openssl
|
||
|
||
docker info >/dev/null 2>&1 \
|
||
|| die "Docker daemon 不可用;请先安装、启动 Docker,并确认当前用户可以访问。"
|
||
docker compose version >/dev/null 2>&1 \
|
||
|| die "未检测到 Docker Compose v2;请确保 'docker compose' 可用。"
|
||
|
||
prepare_install_dir
|
||
if [[ -e "$CONFIG_FILE" ]]; then
|
||
existing_config=1
|
||
[[ -f "$COMPOSE_FILE" && -f "$CONFIG_EXAMPLE" ]] \
|
||
|| die "已有 config.yaml,但 compose.yaml 或 config.example.yaml 缺失。"
|
||
warn "发现已有配置:${CONFIG_FILE}"
|
||
info "将保留现有 config.yaml、compose.yaml 和 config.example.yaml,不执行版本升级。"
|
||
if [[ -n "$ADMIN_PASSWORD" ]]; then
|
||
warn "已有 config.yaml,因此忽略本次传入的 ADMIN_PASSWORD。"
|
||
fi
|
||
else
|
||
download_release_files
|
||
postgres_password="$(random_password)"
|
||
redis_password="$(random_password)"
|
||
if [[ -z "$ADMIN_PASSWORD" ]]; then
|
||
ADMIN_PASSWORD="$(openssl rand -hex 16)"
|
||
fi
|
||
validate_admin_password "$ADMIN_PASSWORD"
|
||
log "生成 PostgreSQL、Redis 和管理员密码"
|
||
patch_config "$postgres_password" "$redis_password" "$ADMIN_PASSWORD"
|
||
fi
|
||
|
||
log "创建运行目录"
|
||
mkdir -p -- \
|
||
"${RUNTIME_DIR}/postgres" \
|
||
"${RUNTIME_DIR}/redis" \
|
||
"${RUNTIME_DIR}/data" \
|
||
"${RUNTIME_DIR}/logs"
|
||
|
||
log "验证 Docker Compose 配置"
|
||
(
|
||
cd "$INSTALL_DIR"
|
||
docker compose -f deploy/compose.yaml config --quiet
|
||
)
|
||
|
||
log "拉取容器镜像"
|
||
(
|
||
cd "$INSTALL_DIR"
|
||
docker compose -f deploy/compose.yaml pull
|
||
)
|
||
|
||
fix_runtime_permissions
|
||
|
||
log "启动服务"
|
||
(
|
||
cd "$INSTALL_DIR"
|
||
docker compose -f deploy/compose.yaml up -d --no-build --wait
|
||
)
|
||
|
||
log "检查容器状态"
|
||
(
|
||
cd "$INSTALL_DIR"
|
||
docker compose -f deploy/compose.yaml ps
|
||
)
|
||
|
||
info "验证健康检查接口"
|
||
if ! check_health; then
|
||
warn "Compose 已启动,但 http://127.0.0.1:8080/healthz 未返回 204。"
|
||
warn "最近日志:"
|
||
(
|
||
cd "$INSTALL_DIR"
|
||
docker compose -f deploy/compose.yaml logs --tail=100 codex-proxy-rs
|
||
) || true
|
||
return 1
|
||
fi
|
||
|
||
printf '\n'
|
||
printf '%b%s%b\n' "$GREEN" '=============================================' "$NC"
|
||
printf '%b%s%b\n' "$GREEN" ' 部署完成' "$NC"
|
||
printf '%b%s%b\n' "$GREEN" '=============================================' "$NC"
|
||
printf '\n'
|
||
printf '访问地址: http://127.0.0.1:8080\n'
|
||
printf '管理员用户名: admin@cpr.local\n'
|
||
if [[ "$existing_config" -eq 0 ]]; then
|
||
printf '管理员密码: %s\n' "$ADMIN_PASSWORD"
|
||
else
|
||
printf '管理员密码: 保留现有配置(未修改)\n'
|
||
fi
|
||
printf '安装目录: %s\n' "$INSTALL_DIR"
|
||
printf '配置文件: %s\n' "$CONFIG_FILE"
|
||
printf '\n'
|
||
printf '常用命令:\n'
|
||
printf ' cd %q\n' "$INSTALL_DIR"
|
||
printf ' docker compose -f deploy/compose.yaml ps\n'
|
||
printf ' docker compose -f deploy/compose.yaml logs -f codex-proxy-rs\n'
|
||
printf ' docker compose -f deploy/compose.yaml restart codex-proxy-rs\n'
|
||
printf ' docker compose -f deploy/compose.yaml down\n'
|
||
printf '\n'
|
||
if [[ "$existing_config" -eq 0 ]]; then
|
||
printf '%b请立即保存管理员密码。%b\n' "$YELLOW" "$NC"
|
||
fi
|
||
printf '%b%s%b\n' "$YELLOW" \
|
||
'Compose 默认只把 8080 绑定到 127.0.0.1;公网访问请配置 HTTPS 反向代理。' "$NC"
|
||
printf '\n'
|
||
}
|
||
|
||
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
||
main "$@"
|
||
fi
|