From cc38057c3c1c4de460baa3328238615889f307a1 Mon Sep 17 00:00:00 2001 From: kdletters Date: Sun, 3 May 2026 03:38:10 +0800 Subject: [PATCH] chore: harden spacetime publish provisioning --- .../PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md | 4 +- .../Jenkinsfile.production-server-provision | 110 ++++++++++++++++++ scripts/deploy/production-stdb-publish.sh | 3 + 3 files changed, 116 insertions(+), 1 deletion(-) diff --git a/docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md b/docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md index 6a317d95..8d0b246f 100644 --- a/docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md +++ b/docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md @@ -392,6 +392,7 @@ WASM_SOURCE="${CARGO_TARGET_DIR}/wasm32-unknown-unknown/release/spacetime_module - 可选安装 Nginx 配置和维护模式 snippet。 - 安装 Nginx 配置时执行 `nginx -t`,通过后必须执行 `nginx -s reload`,确保新配置对当前 Nginx master/worker 生效。 - 启用并启动 `spacetimedb.service` 与 `genarrative-api.service`;重启 `spacetimedb.service` 后必须等待 `http://127.0.0.1:3101/v1/ping` 确认就绪,不能只依赖 `systemctl restart` 的返回码。`` 下所有运行态文件必须归属 `spacetimedb:spacetimedb`。不要在 root 身份下对同一个 `` 执行 `spacetime --root-dir= server ping`,否则会生成 root-owned CLI 配置,导致 `spacetimedb` 服务用户后续启动时遇到权限错误。 +- 首次初始化时,如果 `/etc/genarrative/api-server.env` 里还没有 `GENARRATIVE_SPACETIME_TOKEN`,流水线会在 `spacetimedb.service` 就绪后调用本机 `POST http://127.0.0.1:3101/v1/identity` 生成 client identity/token,只把 token 写入环境文件,并只在日志里显示 identity 前缀。随后流水线会以 `spacetimedb` 用户执行 `/bin/current/spacetimedb-cli --root-dir login --token [REDACTED]`,确保后续首次 `Stdb publish` 使用同一个 client identity 创建数据库;这个 identity 才会成为后台读取 private 表所需的 owner。若环境文件已有 `GENARRATIVE_SPACETIME_TOKEN`,初始化必须保留该值,只同步 CLI 登录态,不重新生成或覆盖。 该流水线属于高风险操作,默认要求人工确认后执行。 已落地的 Jenkinsfile 为 `jenkins/Jenkinsfile.production-server-provision`。该流水线默认 `DRY_RUN=true`,只打印将执行的初始化动作;真正写入系统用户、目录、systemd、环境文件并启动服务时,必须设置 `DRY_RUN=false` 且勾选 `CONFIRM_PROVISION`。当 `DEPLOY_TARGET=release` 时,还必须勾选 `CONFIRM_RELEASE_DEPLOY_AGENT`,并通过 `linux && genarrative-release-deploy` 调度到独立 release 部署 agent。 @@ -452,8 +453,9 @@ WASM_SOURCE="${CARGO_TARGET_DIR}/wasm32-unknown-unknown/release/spacetime_module - 先按 `SOURCE_BRANCH` / `COMMIT_HASH` 解析并 checkout 发布脚本源码,默认使用 `origin/master` 最新 commit;上游构建触发时使用上游传入的实际构建 commit。 - 进入维护模式。 - 将 wasm 上传到生产实例。 -- 在生产实例本机执行 `spacetime --root-dir=/stdb publish --server http://127.0.0.1:3101 --bin-path spacetime_module.wasm --yes`。 +- 在生产实例本机执行 `spacetime --root-dir=/stdb publish --server http://127.0.0.1:3101 --bin-path spacetime_module.wasm --yes --no-config`。 - 发布动作默认以 `spacetimedb` 服务用户执行,避免 root 默认 CLI 身份对自托管数据库验签失败,也避免 root 写入 `/stdb/config` 造成后续服务用户启动权限错误。 +- `Stdb publish` 固定追加 `--no-config`,只依赖显式传入的 `--root-dir`、`--server`、`--bin-path` 与数据库名,避免 agent 工作区、本机用户目录或仓库内 `spacetime` 配置干扰发布目标。 - 成功后执行必要 smoke test。 - 成功后解除维护模式。 - 失败时保留维护模式并发邮件。 diff --git a/jenkins/Jenkinsfile.production-server-provision b/jenkins/Jenkinsfile.production-server-provision index 3c604947..631bcc30 100644 --- a/jenkins/Jenkinsfile.production-server-provision +++ b/jenkins/Jenkinsfile.production-server-provision @@ -284,6 +284,115 @@ pipeline { exit 1 } + read_env_value() { + local file="$1" + local key="$2" + local line value + + 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}" == \"* && "${value}" == *\" ]]; then + value="${value#\"}" + value="${value%\"}" + 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}" + } + + 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_https_config() { sed "s/genarrative.example.com/${SERVER_NAME}/g" deploy/nginx/genarrative.conf } @@ -506,6 +615,7 @@ pipeline { run_cmd systemctl enable spacetimedb.service genarrative-api.service 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 diff --git a/scripts/deploy/production-stdb-publish.sh b/scripts/deploy/production-stdb-publish.sh index c14028f9..38b3e73f 100644 --- a/scripts/deploy/production-stdb-publish.sh +++ b/scripts/deploy/production-stdb-publish.sh @@ -11,6 +11,7 @@ usage() { 进入维护模式,校验 spacetime_module.wasm.sha256,并在生产实例本机执行 spacetime publish。 默认使用 http://127.0.0.1:3101,避免与部署机本机 Git/Web 服务的 3000 端口冲突。 默认使用 /stdb 作为 spacetime CLI root-dir,并以 spacetimedb 用户发布,避免 root CLI 身份污染自托管实例。 + 发布时固定追加 --no-config,只使用显式参数,避免工作区或用户目录里的 spacetime 配置干扰目标。 失败时保留维护模式。 EOF } @@ -141,6 +142,7 @@ PUBLISH_ARGS=( "${DATABASE}" --bin-path "${SOURCE_DIR}/spacetime_module.wasm" --yes + --no-config ) if [[ -n "${SERVER_URL}" ]]; then @@ -173,6 +175,7 @@ if [[ -n "${RUN_AS_USER}" && "$(id -u)" -eq 0 ]]; then "${DATABASE}" --bin-path "${PUBLISH_TMP_DIR}/spacetime_module.wasm" --yes + --no-config ) if [[ -n "${SERVER_URL}" ]]; then PUBLISH_ARGS+=(--server "${SERVER_URL}")