feat(deploy): 在线更新 - 新增后台检查、强制备份迁移与失败自动回退
This commit is contained in:
121
scripts/install-host-updater.sh
Executable file
121
scripts/install-host-updater.sh
Executable file
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
INSTALL_DIR="${INSTALL_DIR:-/opt/open-ai-canvas}"
|
||||
REPOSITORY="${REPOSITORY:-ddcat-ai/open-ai-canvas}"
|
||||
SOCKET_DIR="${CANVAS_UPDATER_SOCKET_DIR:-/run/open-ai-canvas-updater}"
|
||||
UPDATER_BIN="/usr/local/bin/open-ai-canvas-host-updater"
|
||||
UPDATER_ENV="/etc/open-ai-canvas-updater.env"
|
||||
UPDATER_SERVICE="/etc/systemd/system/open-ai-canvas-updater.service"
|
||||
|
||||
fail() {
|
||||
printf 'Host Updater 安装失败:%s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_root() {
|
||||
[[ "${EUID}" -eq 0 ]] || fail "请使用 sudo 运行"
|
||||
[[ "$(uname -s)" == "Linux" ]] || fail "仅支持 Linux 服务器"
|
||||
command -v systemctl >/dev/null 2>&1 || fail "服务器必须使用 systemd"
|
||||
command -v curl >/dev/null 2>&1 || fail "缺少 curl"
|
||||
command -v sha256sum >/dev/null 2>&1 || fail "缺少 sha256sum"
|
||||
command -v openssl >/dev/null 2>&1 || fail "缺少 openssl"
|
||||
[[ -f "${INSTALL_DIR}/.env" ]] || fail "未找到 ${INSTALL_DIR}/.env"
|
||||
[[ -f "${INSTALL_DIR}/docker-compose.deploy.yml" ]] || fail "未找到部署 Compose"
|
||||
}
|
||||
|
||||
read_image_tag() {
|
||||
local configured
|
||||
configured="$(sed -n 's/^CANVAS_IMAGE_TAG=//p' "${INSTALL_DIR}/.env" | tail -n 1)"
|
||||
[[ -n "$configured" && "$configured" != "latest" ]] || fail "请先把 CANVAS_IMAGE_TAG 固定为已发布版本"
|
||||
if [[ "$configured" == v* ]]; then
|
||||
RELEASE_TAG="$configured"
|
||||
else
|
||||
RELEASE_TAG="v${configured}"
|
||||
fi
|
||||
}
|
||||
|
||||
install_binary() {
|
||||
local arch asset temporary checksum_file expected
|
||||
case "$(uname -m)" in
|
||||
x86_64|amd64) arch="amd64" ;;
|
||||
aarch64|arm64) arch="arm64" ;;
|
||||
*) fail "不支持的 CPU 架构:$(uname -m)" ;;
|
||||
esac
|
||||
asset="open-ai-canvas-host-updater-linux-${arch}"
|
||||
temporary="$(mktemp)"
|
||||
checksum_file="$(mktemp)"
|
||||
curl -fsSL "https://github.com/${REPOSITORY}/releases/download/${RELEASE_TAG}/${asset}" -o "$temporary"
|
||||
curl -fsSL "https://github.com/${REPOSITORY}/releases/download/${RELEASE_TAG}/SHA256SUMS" -o "$checksum_file"
|
||||
expected="$(awk -v asset="$asset" '$2 == asset { print $1 }' "$checksum_file")"
|
||||
[[ "$expected" =~ ^[a-f0-9]{64}$ ]] || fail "Release 校验清单缺少 ${asset}"
|
||||
printf '%s %s\n' "$expected" "$temporary" | sha256sum -c - >/dev/null || fail "Host Updater SHA-256 校验失败"
|
||||
install -m 0755 "$temporary" "$UPDATER_BIN"
|
||||
rm -f "$temporary" "$checksum_file"
|
||||
}
|
||||
|
||||
ensure_token() {
|
||||
local token temporary
|
||||
token="$(sed -n 's/^CANVAS_UPDATER_TOKEN=//p' "${INSTALL_DIR}/.env" | tail -n 1)"
|
||||
if [[ -z "$token" ]]; then
|
||||
token="$(openssl rand -hex 32)"
|
||||
temporary="$(mktemp "${INSTALL_DIR}/.env.XXXXXX")"
|
||||
awk -v token="$token" '
|
||||
BEGIN { updated=0 }
|
||||
/^CANVAS_UPDATER_TOKEN=/ { print "CANVAS_UPDATER_TOKEN=" token; updated=1; next }
|
||||
{ print }
|
||||
END { if (!updated) print "CANVAS_UPDATER_TOKEN=" token }
|
||||
' "${INSTALL_DIR}/.env" > "$temporary"
|
||||
chmod --reference="${INSTALL_DIR}/.env" "$temporary"
|
||||
mv "$temporary" "${INSTALL_DIR}/.env"
|
||||
fi
|
||||
[[ ${#token} -ge 32 ]] || fail "CANVAS_UPDATER_TOKEN 长度不足"
|
||||
umask 077
|
||||
printf 'CANVAS_UPDATER_TOKEN=%s\nCANVAS_UPDATER_INSTALL_DIR=%s\nCANVAS_UPDATER_SOCKET=%s/updater.sock\n' "$token" "$INSTALL_DIR" "$SOCKET_DIR" > "$UPDATER_ENV"
|
||||
}
|
||||
|
||||
install_service() {
|
||||
local temporary_service
|
||||
install -d -m 0755 "$SOCKET_DIR"
|
||||
install -d -m 0700 /var/lib/open-ai-canvas-updater "${INSTALL_DIR}/backups"
|
||||
temporary_service="$(mktemp)"
|
||||
printf '%s\n' \
|
||||
'[Unit]' \
|
||||
'Description=Open AI Canvas Host Updater' \
|
||||
'After=docker.service network-online.target' \
|
||||
'Requires=docker.service' \
|
||||
'Wants=network-online.target' \
|
||||
'' \
|
||||
'[Service]' \
|
||||
'Type=simple' \
|
||||
"EnvironmentFile=${UPDATER_ENV}" \
|
||||
"ExecStart=${UPDATER_BIN}" \
|
||||
'Restart=on-failure' \
|
||||
'RestartSec=5s' \
|
||||
'NoNewPrivileges=true' \
|
||||
'PrivateTmp=true' \
|
||||
'ProtectHome=true' \
|
||||
'ProtectSystem=full' \
|
||||
"ReadWritePaths=${INSTALL_DIR} /var/lib/open-ai-canvas-updater ${SOCKET_DIR} ${UPDATER_BIN}" \
|
||||
'' \
|
||||
'[Install]' \
|
||||
'WantedBy=multi-user.target' > "$temporary_service"
|
||||
install -m 0644 "$temporary_service" "$UPDATER_SERVICE"
|
||||
rm -f "$temporary_service"
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now open-ai-canvas-updater.service
|
||||
systemctl restart open-ai-canvas-updater.service
|
||||
}
|
||||
|
||||
main() {
|
||||
require_root
|
||||
read_image_tag
|
||||
install_binary
|
||||
ensure_token
|
||||
install_service
|
||||
printf 'Host Updater 已安装,Socket:%s/updater.sock\n' "$SOCKET_DIR"
|
||||
printf '请重建 backend 容器,使 Token 与 Socket 挂载生效。\n'
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -5,9 +5,12 @@ set -Eeuo pipefail
|
||||
REPOSITORY_REF="${REPOSITORY_REF:-main}"
|
||||
INSTALL_DIR="${INSTALL_DIR:-/opt/open-ai-canvas}"
|
||||
CANVAS_HTTP_PORT="${CANVAS_HTTP_PORT:-3000}"
|
||||
CANVAS_IMAGE_TAG="${CANVAS_IMAGE_TAG:-latest}"
|
||||
REQUESTED_IMAGE_TAG="${CANVAS_IMAGE_TAG:-}"
|
||||
CANVAS_IMAGE_TAG="${REQUESTED_IMAGE_TAG:-latest}"
|
||||
CANVAS_IMAGE_TAG="${CANVAS_IMAGE_TAG#v}"
|
||||
COMPOSE_FILE="docker-compose.deploy.yml"
|
||||
COMPOSE_URL="${COMPOSE_URL:-https://raw.githubusercontent.com/ddcat-ai/open-ai-canvas/${REPOSITORY_REF}/${COMPOSE_FILE}}"
|
||||
UPDATER_INSTALL_URL="${UPDATER_INSTALL_URL:-https://raw.githubusercontent.com/ddcat-ai/open-ai-canvas/${REPOSITORY_REF}/scripts/install-host-updater.sh}"
|
||||
|
||||
step() {
|
||||
printf '\n==> %s\n' "$1"
|
||||
@@ -91,9 +94,20 @@ prepare_environment() {
|
||||
|
||||
local configured_image_tag
|
||||
configured_image_tag="$(sed -n 's/^CANVAS_IMAGE_TAG=//p' .env | tail -n 1)"
|
||||
if [[ -n "$configured_image_tag" ]]; then
|
||||
if [[ -n "$REQUESTED_IMAGE_TAG" ]]; then
|
||||
local temporary_env
|
||||
temporary_env="$(mktemp "${INSTALL_DIR}/.env.XXXXXX")"
|
||||
awk -v image_tag="$CANVAS_IMAGE_TAG" '
|
||||
BEGIN { updated=0 }
|
||||
/^CANVAS_IMAGE_TAG=/ { print "CANVAS_IMAGE_TAG=" image_tag; updated=1; next }
|
||||
{ print }
|
||||
END { if (!updated) print "CANVAS_IMAGE_TAG=" image_tag }
|
||||
' .env > "$temporary_env"
|
||||
chmod --reference=.env "$temporary_env"
|
||||
mv "$temporary_env" .env
|
||||
elif [[ -n "$configured_image_tag" ]]; then
|
||||
[[ "$configured_image_tag" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$ ]] || fail ".env 中的 CANVAS_IMAGE_TAG 无效"
|
||||
CANVAS_IMAGE_TAG="$configured_image_tag"
|
||||
CANVAS_IMAGE_TAG="${configured_image_tag#v}"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
@@ -124,6 +138,19 @@ download_compose() {
|
||||
mv "$temporary_file" "$COMPOSE_FILE"
|
||||
}
|
||||
|
||||
install_host_updater() {
|
||||
if [[ "$CANVAS_IMAGE_TAG" == "latest" ]]; then
|
||||
printf '\n提示:CANVAS_IMAGE_TAG=latest,已跳过在线更新器安装。固定到具体发布版本后可再次运行本脚本。\n'
|
||||
return
|
||||
fi
|
||||
step "安装宿主机在线更新服务"
|
||||
local installer
|
||||
installer="$(mktemp)"
|
||||
curl -fsSL "$UPDATER_INSTALL_URL" -o "$installer"
|
||||
INSTALL_DIR="$INSTALL_DIR" bash "$installer"
|
||||
rm -f "$installer"
|
||||
}
|
||||
|
||||
start_services() {
|
||||
step "拉取并启动 GHCR 网页与后端镜像"
|
||||
if ! docker compose --env-file .env -f "$COMPOSE_FILE" pull; then
|
||||
@@ -152,6 +179,7 @@ main() {
|
||||
login_ghcr
|
||||
prepare_environment
|
||||
download_compose
|
||||
install_host_updater
|
||||
start_services
|
||||
print_result
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user