Files
Genarrative/scripts/jenkins-server-provision.sh
2026-06-05 19:45:23 +08:00

846 lines
30 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
PROVISION_TOOLS_DIR="${PROVISION_TOOLS_DIR:-provision-tools}"
SPACETIME_BIN_SOURCE="${SPACETIME_BIN_SOURCE:-${PROVISION_TOOLS_DIR}/spacetime/spacetime}"
OTELCOL_BIN_SOURCE="${OTELCOL_BIN_SOURCE:-${PROVISION_TOOLS_DIR}/otelcol-contrib}"
GENARRATIVE_OPENSSL_VERSION="${GENARRATIVE_OPENSSL_VERSION:-3.2.0}"
GENARRATIVE_OPENSSL_PREFIX="${GENARRATIVE_OPENSSL_PREFIX:-/opt/genarrative/openssl-3.2.0}"
GENARRATIVE_OPENSSL_SOURCE_URL="${GENARRATIVE_OPENSSL_SOURCE_URL:-https://github.com/openssl/openssl/releases/download/openssl-${GENARRATIVE_OPENSSL_VERSION}/openssl-${GENARRATIVE_OPENSSL_VERSION}.tar.gz}"
GENARRATIVE_OPENSSL_SOURCE_SHA256="${GENARRATIVE_OPENSSL_SOURCE_SHA256:-14c826f07c7e433706fb5c69fa9e25dab95684844b4c962a2cf1bf183eb4690e}"
require_non_root_relative_path() {
local label="$1"
local path="$2"
if [[ -z "${path}" ]]; then
echo "[server-provision] ${label} 不能为空。" >&2
exit 1
fi
if [[ "${path}" == /* || "${path}" == *..* ]]; then
echo "[server-provision] ${label} 只能是工作区内的相对路径: ${path}" >&2
exit 1
fi
}
require_path() {
local path="$1"
if [[ ! -e "${path}" ]]; then
echo "[server-provision] 缺少必要文件: ${path}" >&2
exit 1
fi
}
require_cmd() {
local name="$1"
if ! command -v "${name}" >/dev/null 2>&1; then
echo "[server-provision] 缺少命令: ${name}" >&2
exit 1
fi
}
normalize_server_aliases() {
printf "%s" "${SERVER_ALIASES:-}" | tr ',' ' ' | xargs
}
validate_server_names() {
local alias_name
if [[ -z "${SERVER_NAME:-}" ]]; then
echo "[server-provision] SERVER_NAME 不能为空。" >&2
exit 1
fi
if [[ ! "${SERVER_NAME}" =~ ^[A-Za-z0-9][A-Za-z0-9.-]*$ ]]; then
echo "[server-provision] SERVER_NAME 只能填写单个域名或 IP不能包含空格、路径或协议: ${SERVER_NAME}" >&2
exit 1
fi
for alias_name in $(normalize_server_aliases); do
if [[ ! "${alias_name}" =~ ^[A-Za-z0-9][A-Za-z0-9.-]*$ ]]; then
echo "[server-provision] SERVER_ALIASES 只能填写域名或 IP多个用空格或逗号分隔: ${alias_name}" >&2
exit 1
fi
done
}
run_cmd() {
echo "+ $*"
if [[ "${DRY_RUN}" != "true" ]]; then
"$@"
fi
}
require_root_for_real_provision() {
if [[ "${DRY_RUN}" == "true" ]]; then
return
fi
if [[ "$(id -u)" != "0" ]]; then
echo "[server-provision] 非 dry-run 会安装系统包、写入 systemd/Nginx 和创建系统用户,必须在 root agent 上执行。" >&2
echo "[server-provision] 当前用户: $(id -un) uid=$(id -u)。请确认 DEPLOY_TARGET=${DEPLOY_TARGET:-} 对应的目标服务器 agent 以 root 运行,或保持 DRY_RUN=true。" >&2
exit 1
fi
}
install_file() {
local source="$1"
local target="$2"
local mode="$3"
echo "+ install -m ${mode} ${source} ${target}"
if [[ "${DRY_RUN}" != "true" ]]; then
install -m "${mode}" "${source}" "${target}"
fi
}
install_nginx_brotli_modules() {
echo "[server-provision] 安装 Nginx Brotli 动态模块依赖"
if command -v apt-get >/dev/null 2>&1; then
run_cmd apt-get install -y libnginx-mod-http-brotli-filter libnginx-mod-http-brotli-static
else
echo "[server-provision] 当前系统未使用 apt无法自动安装 Nginx Brotli 动态模块;将继续通过 nginx -t 能力探测决定是否启用 Brotli。"
fi
}
download_file() {
local url="$1"
local output="$2"
if command -v curl >/dev/null 2>&1; then
curl -fsSL --retry 3 --retry-delay 2 "${url}" -o "${output}"
elif command -v wget >/dev/null 2>&1; then
wget -O "${output}" "${url}"
else
echo "[server-provision] 需要 curl 或 wget 下载: ${url}" >&2
exit 1
fi
}
openssl_lib_dir_candidates() {
printf "%s\n" \
"${GENARRATIVE_OPENSSL_PREFIX}/lib64" \
"${GENARRATIVE_OPENSSL_PREFIX}/lib"
}
find_genarrative_openssl_lib_dir() {
local lib_dir
while IFS= read -r lib_dir; do
if [[ -f "${lib_dir}/libssl.so.3" && -f "${lib_dir}/libcrypto.so.3" ]]; then
printf "%s" "${lib_dir}"
return 0
fi
done < <(openssl_lib_dir_candidates)
return 1
}
genarrative_openssl_has_required_symbol() {
local lib_dir
lib_dir="$(find_genarrative_openssl_lib_dir 2>/dev/null || true)"
if [[ -z "${lib_dir}" ]]; then
return 1
fi
grep -a -q "OPENSSL_${GENARRATIVE_OPENSSL_VERSION}" "${lib_dir}/libssl.so.3"
}
verify_genarrative_openssl_install() {
local lib_dir
lib_dir="$(find_genarrative_openssl_lib_dir 2>/dev/null || true)"
if [[ -z "${lib_dir}" ]]; then
echo "[server-provision] OpenSSL ${GENARRATIVE_OPENSSL_VERSION} 安装后缺少 libssl.so.3/libcrypto.so.3: ${GENARRATIVE_OPENSSL_PREFIX}" >&2
exit 1
fi
if ! grep -a -q "OPENSSL_${GENARRATIVE_OPENSSL_VERSION}" "${lib_dir}/libssl.so.3"; then
echo "[server-provision] OpenSSL 动态库缺少 OPENSSL_${GENARRATIVE_OPENSSL_VERSION} 符号: ${lib_dir}/libssl.so.3" >&2
exit 1
fi
if ! env "LD_LIBRARY_PATH=${lib_dir}" "${GENARRATIVE_OPENSSL_PREFIX}/bin/openssl" version | grep -q "OpenSSL ${GENARRATIVE_OPENSSL_VERSION}"; then
echo "[server-provision] OpenSSL ${GENARRATIVE_OPENSSL_VERSION} 安装后命令验证失败: ${GENARRATIVE_OPENSSL_PREFIX}/bin/openssl" >&2
exit 1
fi
echo "[server-provision] OpenSSL ${GENARRATIVE_OPENSSL_VERSION} 已就绪: ${lib_dir}"
}
install_genarrative_openssl_runtime() {
local tmp_dir archive source_dir jobs lib_dir
echo "[server-provision] 检查 api-server/libcurl 运行时 OpenSSL ${GENARRATIVE_OPENSSL_VERSION}"
if [[ "${DRY_RUN}" == "true" ]]; then
echo "+ install OpenSSL ${GENARRATIVE_OPENSSL_VERSION} into ${GENARRATIVE_OPENSSL_PREFIX}"
echo "+ verify OPENSSL_${GENARRATIVE_OPENSSL_VERSION} symbol for api-server/libcurl"
return
fi
if genarrative_openssl_has_required_symbol; then
verify_genarrative_openssl_install
return
fi
if command -v apt-get >/dev/null 2>&1; then
run_cmd apt-get install -y build-essential ca-certificates curl perl tar
else
echo "[server-provision] 当前系统未使用 apt无法自动构建 OpenSSL ${GENARRATIVE_OPENSSL_VERSION};请手动安装到 ${GENARRATIVE_OPENSSL_PREFIX}" >&2
exit 1
fi
require_cmd sha256sum
require_cmd tar
tmp_dir="$(mktemp -d)"
archive="${tmp_dir}/openssl-${GENARRATIVE_OPENSSL_VERSION}.tar.gz"
echo "[server-provision] 下载 OpenSSL ${GENARRATIVE_OPENSSL_VERSION}: ${GENARRATIVE_OPENSSL_SOURCE_URL}"
download_file "${GENARRATIVE_OPENSSL_SOURCE_URL}" "${archive}"
printf "%s %s\n" "${GENARRATIVE_OPENSSL_SOURCE_SHA256}" "${archive}" | sha256sum -c -
tar -xzf "${archive}" -C "${tmp_dir}"
source_dir="${tmp_dir}/openssl-${GENARRATIVE_OPENSSL_VERSION}"
jobs="$(nproc 2>/dev/null || echo 2)"
(
cd "${source_dir}"
./config --prefix="${GENARRATIVE_OPENSSL_PREFIX}" --openssldir="${GENARRATIVE_OPENSSL_PREFIX}/ssl" shared
make -j "${jobs}"
make install_sw
)
rm -rf "${tmp_dir}"
lib_dir="$(find_genarrative_openssl_lib_dir 2>/dev/null || true)"
if [[ -n "${lib_dir}" ]]; then
chmod 0755 "${GENARRATIVE_OPENSSL_PREFIX}" "${lib_dir}" || true
chmod 0644 "${lib_dir}/libssl.so.3" "${lib_dir}/libcrypto.so.3" || true
fi
verify_genarrative_openssl_install
}
sync_otelcol_install() {
local target_bin="/usr/local/bin/otelcol-contrib"
local source_bin="${OTELCOL_BIN_SOURCE}"
local version="${OTELCOL_VERSION:-0.151.0}"
local resolved_source="${source_bin}"
if [[ "${ENABLE_OTELCOL:-true}" != "true" ]]; then
echo "[server-provision] ENABLE_OTELCOL=${ENABLE_OTELCOL:-},跳过 otelcol-contrib 配置。"
return
fi
if command -v readlink >/dev/null 2>&1; then
resolved_source="$(readlink -f "${source_bin}" 2>/dev/null || echo "${source_bin}")"
fi
if [[ ! -x "${resolved_source}" ]]; then
echo "[server-provision] otelcol-contrib 不存在或不可执行: ${source_bin}" >&2
echo "[server-provision] 请确认 Prepare Provision Tools 已在目标 agent 生成 otelcol-contrib ${version}: ${source_bin}" >&2
exit 1
fi
if [[ "${DRY_RUN}" == "true" ]]; then
echo "+ install -m 0755 ${resolved_source} ${target_bin}"
return
fi
install -m 0755 "${resolved_source}" "${target_bin}"
if ! "${target_bin}" --version >/dev/null 2>&1; then
echo "[server-provision] otelcol-contrib 安装后无法执行: ${target_bin}" >&2
exit 1
fi
if ! "${target_bin}" --version 2>/dev/null | grep -q "${version}"; then
echo "[server-provision] 警告: otelcol-contrib 版本不是期望的 ${version}: $("${target_bin}" --version 2>/dev/null || true)" >&2
fi
}
sync_spacetime_install() {
local root_dir="$1"
local target_bin_dir="${root_dir}/bin/current"
local target_cli="${target_bin_dir}/spacetimedb-cli"
local target_standalone="${target_bin_dir}/spacetimedb-standalone"
local resolved_command="${SPACETIME_BIN_SOURCE}"
local install_dir=""
local root_bin="${root_dir}/bin"
echo "[server-provision] 同步 SpacetimeDB current 目录到 ${target_bin_dir}"
if [[ "${DRY_RUN}" == "true" ]]; then
echo "+ mkdir -p ${target_bin_dir}"
echo "+ copy spacetimedb-cli and spacetimedb-standalone into ${target_bin_dir}"
return
fi
if command -v readlink >/dev/null 2>&1; then
resolved_command="$(readlink -f "${SPACETIME_BIN_SOURCE}" 2>/dev/null || echo "${SPACETIME_BIN_SOURCE}")"
fi
install_dir="$(cd -- "$(dirname -- "${resolved_command}")" && pwd)"
mkdir -p "${root_bin}"
if [[ -d "${install_dir}/bin" ]]; then
echo "[server-provision] 同步 SpacetimeDB 安装: ${install_dir}/bin -> ${root_bin}"
rm -rf "${root_bin}"
mkdir -p "${root_bin}"
cp -a "${install_dir}/bin/." "${root_bin}/"
elif [[ -x "${install_dir}/spacetimedb-cli" && -x "${install_dir}/spacetimedb-standalone" ]]; then
echo "[server-provision] 同步 SpacetimeDB 安装: ${install_dir} -> ${target_bin_dir}"
rm -rf "${target_bin_dir}"
mkdir -p "${target_bin_dir}"
cp -f "${install_dir}/spacetimedb-cli" "${target_cli}"
cp -f "${install_dir}/spacetimedb-standalone" "${target_standalone}"
chmod +x "${target_cli}" "${target_standalone}"
else
echo "[server-provision] 未能从 SpacetimeDB 交付包推断完整安装目录: ${resolved_command}" >&2
fi
if [[ ! -x "${target_cli}" || ! -x "${target_standalone}" ]]; then
echo "[server-provision] 同步 SpacetimeDB 安装后仍缺少 current 目录。" >&2
echo "[server-provision] 需要同时存在: ${target_cli}${target_standalone}" >&2
exit 1
fi
chown -R spacetimedb:spacetimedb "${root_bin}"
}
is_spacetimedb_ready() {
local server_url="http://127.0.0.1:3101"
if command -v curl >/dev/null 2>&1 && curl -fsS "${server_url}/v1/ping" >/dev/null 2>&1; then
return 0
fi
return 1
}
wait_for_spacetimedb_service() {
local deadline=$((SECONDS + 60))
if [[ "${DRY_RUN}" == "true" ]]; then
echo "+ wait for spacetimedb.service on http://127.0.0.1:3101"
return
fi
while ((SECONDS < deadline)); do
if is_spacetimedb_ready; then
echo "[server-provision] spacetimedb.service 已就绪: http://127.0.0.1:3101"
return
fi
sleep 1
done
echo "[server-provision] 等待 spacetimedb.service 就绪超时。" >&2
systemctl status spacetimedb.service --no-pager -l >&2 || true
journalctl -u spacetimedb.service --no-pager -n 80 >&2 || true
ss -ltnp >&2 || true
exit 1
}
read_env_value() {
local file="$1"
local key="$2"
local line value quote_char
quote_char='"'
if [[ ! -f "${file}" ]]; then
return
fi
while IFS= read -r line || [[ -n "${line}" ]]; do
if [[ "${line}" == "${key}="* ]]; then
value="${line#*=}"
value="$(printf "%s" "${value}" | tr -d "\r")"
if [[ ${#value} -ge 2 && "${value:0:1}" == "${quote_char}" && "${value: -1}" == "${quote_char}" ]]; then
value="${value:1:${#value}-2}"
fi
printf "%s" "${value}"
return
fi
done <"${file}"
}
write_env_value() {
local file="$1"
local key="$2"
local value="$3"
local tmp updated line
tmp="$(mktemp)"
updated="false"
while IFS= read -r line || [[ -n "${line}" ]]; do
if [[ "${line}" == "${key}="* ]]; then
if [[ "${updated}" != "true" ]]; then
printf "%s=%s\\n" "${key}" "${value}" >>"${tmp}"
updated="true"
fi
else
printf "%s\\n" "${line}" >>"${tmp}"
fi
done <"${file}"
if [[ "${updated}" != "true" ]]; then
printf "%s=%s\\n" "${key}" "${value}" >>"${tmp}"
fi
cat "${tmp}" >"${file}"
rm -f "${tmp}"
chmod 0600 "${file}"
chown root:root "${file}"
}
ensure_env_value() {
local file="$1"
local key="$2"
local default_value="$3"
local current_value
current_value="$(read_env_value "${file}" "${key}")"
if [[ -n "${current_value}" ]]; then
return
fi
echo "[server-provision] 补齐 api-server 环境变量: ${key} -> ${file}"
if [[ "${DRY_RUN}" != "true" ]]; then
write_env_value "${file}" "${key}" "${default_value}"
fi
}
ensure_api_runtime_env_defaults() {
if [[ "${DRY_RUN}" == "true" ]]; then
echo "+ ensure api-server runtime env defaults in ${API_ENV_FILE}"
return
fi
if [[ ! -f "${API_ENV_FILE}" ]]; then
echo "[server-provision] 环境文件不存在,无法补齐 api-server 运行态目录变量: ${API_ENV_FILE}" >&2
exit 1
fi
# 已存在的生产 env 会被保留,不会整文件覆盖;这里仅补后续版本新增的运行态写入路径。
ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_TRACKING_OUTBOX_ENABLED" "true"
ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_TRACKING_OUTBOX_DIR" "/var/lib/genarrative/tracking-outbox"
ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_TRACKING_OUTBOX_BATCH_SIZE" "500"
ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_TRACKING_OUTBOX_FLUSH_INTERVAL_MS" "1000"
ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_TRACKING_OUTBOX_MAX_BYTES" "268435456"
}
parse_json_string_field() {
local json="$1"
local key="$2"
printf "%s" "${json}" | sed -n "s/.*\\\"${key}\\\"[[:space:]]*:[[:space:]]*\\\"\\([^\\\"]*\\)\\\".*/\\1/p" | head -n 1
}
ensure_spacetime_owner_client_token() {
local server_url="http://127.0.0.1:3101"
local cli_path="${SPACETIME_ROOT}/bin/current/spacetimedb-cli"
local token identity response login_output existing_token identity_preview
if [[ "${DRY_RUN}" == "true" ]]; then
echo "+ ensure GENARRATIVE_SPACETIME_TOKEN in ${API_ENV_FILE}"
echo "+ generate SpacetimeDB client identity when token is missing"
echo "+ runuser -u spacetimedb -- ${cli_path} --root-dir ${SPACETIME_ROOT} login --token [REDACTED]"
return
fi
if [[ ! -f "${API_ENV_FILE}" ]]; then
echo "[server-provision] 环境文件不存在,无法写入 GENARRATIVE_SPACETIME_TOKEN: ${API_ENV_FILE}" >&2
exit 1
fi
if [[ ! -x "${cli_path}" ]]; then
echo "[server-provision] SpacetimeDB CLI 不存在或不可执行: ${cli_path}" >&2
exit 1
fi
existing_token="$(read_env_value "${API_ENV_FILE}" "GENARRATIVE_SPACETIME_TOKEN")"
if [[ -n "${existing_token}" ]]; then
token="${existing_token}"
echo "[server-provision] GENARRATIVE_SPACETIME_TOKEN 已存在,保留并同步 SpacetimeDB CLI 登录态。"
else
response="$(curl -fsS -X POST "${server_url}/v1/identity")"
identity="$(parse_json_string_field "${response}" "identity")"
identity="${identity:-$(parse_json_string_field "${response}" "Identity")}"
identity="${identity:-$(parse_json_string_field "${response}" "identity_hex")}"
identity="${identity:-$(parse_json_string_field "${response}" "identityHex")}"
token="$(parse_json_string_field "${response}" "token")"
token="${token:-$(parse_json_string_field "${response}" "Token")}"
if [[ -z "${identity}" || -z "${token}" ]]; then
echo "[server-provision] 生成 SpacetimeDB client identity 失败,响应缺少 identity/token。" >&2
exit 1
fi
write_env_value "${API_ENV_FILE}" "GENARRATIVE_SPACETIME_TOKEN" "${token}"
identity_preview="${identity:0:12}"
echo "[server-provision] 已生成 SpacetimeDB client identity 并写入 GENARRATIVE_SPACETIME_TOKEN: ${identity_preview}..."
fi
if ! login_output="$(runuser -u spacetimedb -- "${cli_path}" --root-dir "${SPACETIME_ROOT}" login --token "${token}" 2>&1)"; then
echo "[server-provision] 使用 GENARRATIVE_SPACETIME_TOKEN 登录 SpacetimeDB CLI 失败。" >&2
printf "%s\\n" "${login_output}" | sed -E "s/[A-Za-z0-9_.=-]{24,}/[REDACTED]/g" >&2
exit 1
fi
echo "[server-provision] 已同步 SpacetimeDB CLI 登录态;后续首次 publish 将使用同一 client identity。"
}
render_nginx_brotli_directives() {
if ! command -v nginx >/dev/null 2>&1; then
echo " # Brotli 未启用:目标服务器未找到 nginx 命令。"
return
fi
local brotli_snippet
brotli_snippet="$(mktemp)"
cat >"${brotli_snippet}" <<'EOF'
include /etc/nginx/modules-enabled/*.conf;
events {}
http {
brotli on;
brotli_comp_level 4;
brotli_min_length 1024;
brotli_types application/json;
}
EOF
if nginx -t -c "${brotli_snippet}" >/dev/null 2>&1; then
cat <<'EOF'
brotli on;
brotli_comp_level 4;
brotli_min_length 1024;
brotli_types
text/plain
text/css
text/javascript
application/javascript
application/json
application/xml
application/xml+rss
image/svg+xml;
EOF
else
echo " # Brotli 未启用nginx -t 不接受 brotli 指令。"
fi
rm -f "${brotli_snippet}"
}
render_nginx_template() {
local template="$1"
local rendered_brotli server_names
rendered_brotli="$(render_nginx_brotli_directives)"
server_names="${SERVER_NAME}"
if [[ -n "${SERVER_ALIASES:-}" ]]; then
server_names="${server_names} $(normalize_server_aliases)"
fi
sed \
-e "s/server_name genarrative.example.com;/server_name ${server_names};/g" \
-e "s|/etc/letsencrypt/live/genarrative.example.com/|/etc/letsencrypt/live/${SERVER_NAME}/|g" \
-e "/# __GENARRATIVE_BROTLI_DIRECTIVES__/r /dev/stdin" \
-e "/# __GENARRATIVE_BROTLI_DIRECTIVES__/d" \
"${template}" <<<"${rendered_brotli}"
}
render_nginx_https_config() {
render_nginx_template deploy/nginx/genarrative.conf
}
render_nginx_development_http_config() {
render_nginx_template deploy/nginx/genarrative-dev-http.conf
}
render_api_env_example() {
sed \
-e "s|^GENARRATIVE_API_PORT=.*|GENARRATIVE_API_PORT=${API_PORT}|" \
-e "s|^GENARRATIVE_SPACETIME_SERVER_URL=.*|GENARRATIVE_SPACETIME_SERVER_URL=http://127.0.0.1:3101|" \
deploy/env/api-server.env.example
}
render_otelcol_service() {
cat deploy/systemd/otelcol-contrib.service
}
validate_nginx_tls() {
local cert_dir="/etc/letsencrypt/live/${SERVER_NAME}"
if [[ "${SERVER_NAME}" == "genarrative.example.com" ]]; then
echo "[server-provision] SERVER_NAME 仍是占位域名,拒绝写入 Nginx HTTPS 配置。请填写真实域名,或先设置 NGINX_CONFIG_MODE=none。" >&2
exit 1
fi
if [[ ! -f "${cert_dir}/fullchain.pem" || ! -f "${cert_dir}/privkey.pem" ]]; then
echo "[server-provision] 未找到 Nginx HTTPS 证书: ${cert_dir}/fullchain.pem 或 ${cert_dir}/privkey.pem" >&2
echo "[server-provision] 请先完成证书申请,或首次初始化时设置 NGINX_CONFIG_MODE=none避免写入无法通过 nginx -t 的配置。" >&2
exit 1
fi
}
disable_nginx_default_sites_enabled() {
local moves_file="$1"
local sites_enabled="/etc/nginx/sites-enabled"
local sites_disabled="/etc/nginx/sites-disabled"
local stamp source target base
local candidates=("${sites_enabled}/default" "${sites_enabled}/default."*)
stamp="$(date +%Y%m%d%H%M%S)"
for source in "${candidates[@]}"; do
if [[ ! -e "${source}" && ! -L "${source}" ]]; then
continue
fi
base="$(basename "${source}")"
target="${sites_disabled}/${base}.disabled-${stamp}"
echo "[server-provision] 禁用 Debian 默认 Nginx 站点,避免与 Genarrative server_name 冲突: ${source} -> ${target}"
mkdir -p "${sites_disabled}"
mv "${source}" "${target}"
printf "%s\t%s\n" "${target}" "${source}" >>"${moves_file}"
done
}
restore_nginx_default_sites_enabled() {
local moves_file="$1"
local target source
if [[ ! -f "${moves_file}" ]]; then
return
fi
while IFS=$'\t' read -r target source || [[ -n "${target:-}" ]]; do
if [[ -z "${target:-}" || -z "${source:-}" ]]; then
continue
fi
if [[ -e "${target}" || -L "${target}" ]]; then
mkdir -p "$(dirname "${source}")"
if [[ ! -e "${source}" && ! -L "${source}" ]]; then
echo "[server-provision] 恢复 Debian 默认 Nginx 站点: ${target} -> ${source}"
mv "${target}" "${source}"
fi
fi
done <"${moves_file}"
}
install_nginx_config_with_rollback() {
local config_target="/etc/nginx/conf.d/genarrative.conf"
local snippet_target="/etc/nginx/snippets/genarrative-maintenance.conf"
local config_source
local rendered_config rendered_snippet config_backup snippet_backup disabled_sites
local had_config="false"
local had_snippet="false"
run_cmd mkdir -p /etc/nginx/snippets /etc/nginx/conf.d
if [[ "${NGINX_CONFIG_MODE}" == "production-https" ]]; then
config_source="deploy/nginx/genarrative.conf"
elif [[ "${NGINX_CONFIG_MODE}" == "development-http" ]]; then
config_source="deploy/nginx/genarrative-dev-http.conf"
else
echo "[server-provision] NGINX_CONFIG_MODE=${NGINX_CONFIG_MODE} 不需要安装 Nginx 配置。"
return
fi
echo "+ render ${config_source} -> ${config_target}"
echo "+ install -m 0644 deploy/nginx/snippets/genarrative-maintenance.conf ${snippet_target}"
if [[ "${DRY_RUN}" == "true" ]]; then
echo "+ disable /etc/nginx/sites-enabled/default* if present"
echo "+ nginx -t"
echo "+ nginx -s reload"
return
fi
rendered_config="$(mktemp)"
rendered_snippet="$(mktemp)"
config_backup="$(mktemp)"
snippet_backup="$(mktemp)"
disabled_sites="$(mktemp)"
if [[ "${NGINX_CONFIG_MODE}" == "production-https" ]]; then
validate_nginx_tls
render_nginx_https_config >"${rendered_config}"
else
render_nginx_development_http_config >"${rendered_config}"
fi
cp deploy/nginx/snippets/genarrative-maintenance.conf "${rendered_snippet}"
if [[ -f "${config_target}" ]]; then
cp -p "${config_target}" "${config_backup}"
had_config="true"
fi
if [[ -f "${snippet_target}" ]]; then
cp -p "${snippet_target}" "${snippet_backup}"
had_snippet="true"
fi
install -m 0644 "${rendered_config}" "${config_target}"
install -m 0644 "${rendered_snippet}" "${snippet_target}"
disable_nginx_default_sites_enabled "${disabled_sites}"
if ! nginx -t; then
echo "[server-provision] nginx -t 失败,恢复写入前的 Nginx 配置。" >&2
if [[ "${had_config}" == "true" ]]; then
cp -p "${config_backup}" "${config_target}"
else
rm -f "${config_target}"
fi
if [[ "${had_snippet}" == "true" ]]; then
cp -p "${snippet_backup}" "${snippet_target}"
else
rm -f "${snippet_target}"
fi
restore_nginx_default_sites_enabled "${disabled_sites}"
rm -f "${rendered_config}" "${rendered_snippet}" "${config_backup}" "${snippet_backup}" "${disabled_sites}"
exit 1
fi
echo "+ nginx -s reload"
nginx -s reload
rm -f "${rendered_config}" "${rendered_snippet}" "${config_backup}" "${snippet_backup}" "${disabled_sites}"
}
cleanup_placeholder_nginx_config() {
local config_target="/etc/nginx/conf.d/genarrative.conf"
local disabled_target
if [[ ! -f "${config_target}" ]]; then
return
fi
if ! grep -q "/etc/letsencrypt/live/genarrative.example.com/" "${config_target}"; then
return
fi
disabled_target="${config_target}.disabled-placeholder-$(date +%Y%m%d%H%M%S)"
echo "[server-provision] 发现上一轮初始化留下的占位域名 Nginx 配置,禁用: ${config_target} -> ${disabled_target}"
if [[ "${DRY_RUN}" != "true" ]]; then
mv "${config_target}" "${disabled_target}"
if command -v nginx >/dev/null 2>&1; then
if ! nginx -t; then
echo "[server-provision] 占位配置已禁用,但 nginx -t 仍失败;请检查其他 Nginx 配置。" >&2
else
echo "+ nginx -s reload"
nginx -s reload
fi
fi
fi
}
escape_sed_replacement() {
printf "%s" "$1" | sed "s/[&|]/\\\\&/g"
}
render_spacetimedb_service() {
local root_escaped
root_escaped="$(escape_sed_replacement "${SPACETIME_ROOT}")"
sed \
-e "s|/stdb|${root_escaped}|g" \
deploy/systemd/spacetimedb.service
}
render_api_service() {
local current_escaped env_escaped
current_escaped="$(escape_sed_replacement "${CURRENT_LINK}")"
env_escaped="$(escape_sed_replacement "${API_ENV_FILE}")"
sed \
-e "s|/opt/genarrative/current|${current_escaped}|g" \
-e "s|/etc/genarrative/api-server.env|${env_escaped}|g" \
deploy/systemd/genarrative-api.service
}
render_database_backup_service() {
local current_escaped env_escaped
current_escaped="$(escape_sed_replacement "${CURRENT_LINK}")"
env_escaped="$(escape_sed_replacement "${API_ENV_FILE}")"
sed \
-e "s|/opt/genarrative/current|${current_escaped}|g" \
-e "s|/etc/genarrative/api-server.env|${env_escaped}|g" \
deploy/systemd/genarrative-database-backup.service
}
require_path deploy/systemd/spacetimedb.service
require_path deploy/systemd/genarrative-api.service
require_path deploy/systemd/genarrative-database-backup.service
require_path deploy/systemd/genarrative-database-backup.timer
require_path deploy/systemd/otelcol-contrib.service
require_path deploy/otelcol/genarrative-debug.yaml
require_path deploy/nginx/genarrative.conf
require_path deploy/nginx/genarrative-dev-http.conf
require_path deploy/nginx/snippets/genarrative-maintenance.conf
require_path deploy/env/api-server.env.example
require_path scripts/deploy/maintenance-on.sh
require_path scripts/deploy/maintenance-off.sh
require_path scripts/deploy/maintenance-status.sh
validate_server_names
require_non_root_relative_path "PROVISION_TOOLS_DIR" "${PROVISION_TOOLS_DIR}"
echo "[server-provision] target=${DEPLOY_TARGET}, dry_run=${DRY_RUN}, nginx_config_mode=${NGINX_CONFIG_MODE}, source_commit=$(cat .jenkins-source-commit)"
run_cmd id
require_root_for_real_provision
install_nginx_brotli_modules
run_cmd mkdir -p "${SPACETIME_ROOT}" "${RELEASE_ROOT}" "$(dirname "${CURRENT_LINK}")" "$(dirname "${WEB_LINK}")" /etc/genarrative /var/lib/genarrative/maintenance /var/lib/genarrative/auth /var/lib/genarrative/tracking-outbox /var/lib/genarrative/database-backups
if ! id spacetimedb >/dev/null 2>&1; then
run_cmd useradd --system --home-dir "${SPACETIME_ROOT}" --shell /usr/sbin/nologin spacetimedb
else
echo "[server-provision] 用户已存在: spacetimedb"
fi
if ! id genarrative >/dev/null 2>&1; then
run_cmd useradd --system --home-dir /opt/genarrative --shell /usr/sbin/nologin genarrative
else
echo "[server-provision] 用户已存在: genarrative"
fi
run_cmd chown -R spacetimedb:spacetimedb "${SPACETIME_ROOT}"
run_cmd chown -R genarrative:genarrative /opt/genarrative /var/lib/genarrative /srv/genarrative
install_genarrative_openssl_runtime
if [[ ! -x "${SPACETIME_BIN_SOURCE}" ]]; then
echo "[server-provision] spacetime CLI 不存在或不可执行: ${SPACETIME_BIN_SOURCE}" >&2
exit 1
fi
echo "+ install -m 0755 ${SPACETIME_BIN_SOURCE} ${SPACETIME_ROOT}/spacetime"
if [[ "${DRY_RUN}" != "true" ]]; then
install -m 0755 "${SPACETIME_BIN_SOURCE}" "${SPACETIME_ROOT}/spacetime"
chown spacetimedb:spacetimedb "${SPACETIME_ROOT}/spacetime"
fi
sync_spacetime_install "${SPACETIME_ROOT}"
spacetimedb_service="$(mktemp)"
api_service="$(mktemp)"
database_backup_service="$(mktemp)"
render_spacetimedb_service >"${spacetimedb_service}"
render_api_service >"${api_service}"
render_database_backup_service >"${database_backup_service}"
install_file "${spacetimedb_service}" /etc/systemd/system/spacetimedb.service 0644
install_file "${api_service}" /etc/systemd/system/genarrative-api.service 0644
install_file "${database_backup_service}" /etc/systemd/system/genarrative-database-backup.service 0644
install_file deploy/systemd/genarrative-database-backup.timer /etc/systemd/system/genarrative-database-backup.timer 0644
rm -f "${spacetimedb_service}" "${api_service}" "${database_backup_service}"
if [[ ! -f "${API_ENV_FILE}" ]]; then
echo "+ create ${API_ENV_FILE} from example"
if [[ "${DRY_RUN}" != "true" ]]; then
render_api_env_example >"${API_ENV_FILE}"
chmod 0600 "${API_ENV_FILE}"
chown root:root "${API_ENV_FILE}"
fi
else
echo "[server-provision] 已存在环境文件,保留不覆盖: ${API_ENV_FILE}"
fi
ensure_api_runtime_env_defaults
if [[ "${ENABLE_OTELCOL:-true}" == "true" ]]; then
sync_otelcol_install
otelcol_service="$(mktemp)"
render_otelcol_service >"${otelcol_service}"
install_file "${otelcol_service}" /etc/systemd/system/otelcol-contrib.service 0644
rm -f "${otelcol_service}"
else
echo "[server-provision] ENABLE_OTELCOL=${ENABLE_OTELCOL:-},跳过 otelcol-contrib service 安装。"
fi
if [[ "${NGINX_CONFIG_MODE}" != "none" ]]; then
install_nginx_config_with_rollback
else
cleanup_placeholder_nginx_config
fi
run_cmd systemctl daemon-reload
if [[ "${ENABLE_SERVICES}" == "true" ]]; then
if [[ "${ENABLE_OTELCOL:-true}" == "true" ]]; then
run_cmd systemctl enable otelcol-contrib.service
fi
run_cmd systemctl enable spacetimedb.service genarrative-api.service genarrative-database-backup.timer
if [[ "${ENABLE_OTELCOL:-true}" == "true" ]]; then
run_cmd systemctl restart otelcol-contrib.service
fi
run_cmd systemctl restart spacetimedb.service
wait_for_spacetimedb_service
ensure_spacetime_owner_client_token
if [[ -x "${CURRENT_LINK}/api-server" ]]; then
run_cmd systemctl restart genarrative-api.service
else
echo "[server-provision] 尚未发现 ${CURRENT_LINK}/api-server跳过 api-server 首次启动。后续 API deploy 会重启服务。"
fi
fi
echo "[server-provision] 完成。若是首次初始化,请补齐 ${API_ENV_FILE} 的真实密钥后再启动 api-server。"