272 lines
13 KiB
Bash
Executable File
272 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
|
|
umask 077
|
|
|
|
SERVICE_USER=kaidi
|
|
SERVICE_GROUP=kaidi
|
|
CONFIG_ROOT=/etc/kaidi
|
|
STATE_ROOT=/var/lib/kaidi
|
|
UPDATE_STATE_ROOT=/var/lib/kaidi-update
|
|
LOG_ROOT=/var/log/kaidi
|
|
PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9
|
|
RELEASE_API_URL=https://git.awaioi.com/api/v1/repos/ERP-Team/kaidi/releases/latest
|
|
INIT_ARMED=false
|
|
INIT_COMMITTED=false
|
|
INIT_RELEASE_ROOT=
|
|
INIT_APP_ROOT=
|
|
LOG_LOCALE=${KAIDI_LOG_LOCALE:-zh-CN}
|
|
SERVICE_USER_CREATED=false
|
|
SERVICE_GROUP_CREATED=false
|
|
CREATED_PATHS=()
|
|
|
|
localize_message() {
|
|
local message=$1
|
|
[ "$LOG_LOCALE" = en ] && { printf '%s' "$message"; return; }
|
|
case "$message" in
|
|
"Run with sudo or as root") printf '请使用 sudo 或 root 运行' ;;
|
|
"Baota initialization only supports Linux") printf '宝塔初始化仅支持 Linux' ;;
|
|
"Extract the release"*) printf '请先将发布包解压到 APP_ROOT/releases/VERSION' ;;
|
|
"The supported Baota project root"*) printf '宝塔项目根目录必须是 /www/wwwroot/kaidi' ;;
|
|
"The extracted release is incomplete") printf '解压后的发布包不完整' ;;
|
|
"The extracted operations scripts are incomplete") printf '发布包中的运维脚本不完整' ;;
|
|
"The release public key is missing") printf '缺少发布公钥' ;;
|
|
"The release public-key fingerprint is invalid") printf '发布公钥指纹不匹配' ;;
|
|
"Existing user "*" has unexpected home directory "*) printf '现有用户目录不符合 Kaidi 约定:%s' "${message#Existing user }" ;;
|
|
"Existing user "*" is not a member"*) printf '现有 Kaidi 用户不属于服务用户组' ;;
|
|
"A nologin shell is required") printf '服务用户必须使用 nologin shell' ;;
|
|
*" contains a line break") printf '配置项包含换行符,已拒绝:%s' "${message%% contains a line break*}" ;;
|
|
"find is required"|"openssl is required"|"sha256sum is required") printf '缺少初始化依赖命令:%s' "${message%% is required}" ;;
|
|
"Kaidi is already initialized"*) printf 'Kaidi 已初始化,请先执行卸载流程再重新安装' ;;
|
|
"The old kaidi-finance.service"*) printf '检测到旧 kaidi-finance.service,请先执行卸载流程' ;;
|
|
"Old Kaidi update units"*) printf '检测到旧 Kaidi 更新单元,请先执行卸载流程' ;;
|
|
"The Baota current release link"*) printf '宝塔 current 发布链接已存在,请先执行卸载流程' ;;
|
|
"Port must be an integer"*) printf '端口必须是 1024 到 65535 的整数' ;;
|
|
"Initialization failed"*) printf '初始化失败,本次创建的文件已回滚,可以修正原因后重试' ;;
|
|
"Manual deployment is initialized"*) printf '宝塔手动部署初始化完成:%s' "${message#Manual deployment is initialized for Kaidi Finance }" ;;
|
|
"Create the Baota Spring Boot project"*) printf '请在宝塔创建 Spring Boot 项目,项目路径:%s' "${message#Create the Baota Spring Boot project with }" ;;
|
|
"Baota environment variables: leave empty") printf '宝塔环境变量请全部留空' ;;
|
|
"Setup code: "*) printf '安装码位置:%s' "${message#Setup code: }" ;;
|
|
*) printf '%s' "$message" ;;
|
|
esac
|
|
}
|
|
|
|
log() { printf '[kaidi-baota-init] %s\n' "$(localize_message "$*")"; }
|
|
die() { printf '[kaidi-baota-init] 错误:%s\n' "$(localize_message "$*")" >&2; exit 1; }
|
|
|
|
rollback_initialization() {
|
|
local rc=$? index path service_uid
|
|
trap - EXIT HUP INT TERM
|
|
if [ "$rc" -ne 0 ] && [ "$INIT_ARMED" = true ] && [ "$INIT_COMMITTED" != true ]; then
|
|
systemctl disable --now kaidi-update.path >/dev/null 2>&1 || true
|
|
systemctl stop kaidi-update.service >/dev/null 2>&1 || true
|
|
rm -f /etc/systemd/system/kaidi-update.service /etc/systemd/system/kaidi-update.path
|
|
systemctl daemon-reload >/dev/null 2>&1 || true
|
|
rm -f "$CONFIG_ROOT/kaidi.env" "$CONFIG_ROOT/update.env" \
|
|
"$CONFIG_ROOT/release-public.pem" \
|
|
"$UPDATE_STATE_ROOT/status.json" /root/kaidi-first-login.txt
|
|
rm -f "$INIT_APP_ROOT/current.next"
|
|
if [ -n "$INIT_RELEASE_ROOT" ] \
|
|
&& [ "$(readlink "$INIT_APP_ROOT/current" 2>/dev/null || true)" = "$INIT_RELEASE_ROOT" ]; then
|
|
rm -f "$INIT_APP_ROOT/current"
|
|
fi
|
|
rm -f "$CONFIG_ROOT"/*.next.* "$UPDATE_STATE_ROOT"/*.next.* "$STATE_ROOT/setup"/*.next.*
|
|
if [ "$SERVICE_USER_CREATED" = true ]; then
|
|
service_uid=$(id -u "$SERVICE_USER" 2>/dev/null || true)
|
|
[ -z "$service_uid" ] || userdel --force "$SERVICE_USER" >/dev/null 2>&1 || true
|
|
fi
|
|
if [ "$SERVICE_GROUP_CREATED" = true ]; then
|
|
groupdel "$SERVICE_GROUP" >/dev/null 2>&1 || true
|
|
fi
|
|
for ((index=${#CREATED_PATHS[@]} - 1; index >= 0; index--)); do
|
|
path=${CREATED_PATHS[$index]}
|
|
rmdir -- "$path" >/dev/null 2>&1 || true
|
|
done
|
|
log "Initialization failed; files created by this attempt were rolled back and the command can be retried"
|
|
fi
|
|
exit "$rc"
|
|
}
|
|
trap rollback_initialization EXIT
|
|
trap 'exit 129' HUP
|
|
trap 'exit 130' INT
|
|
trap 'exit 143' TERM
|
|
|
|
sha256_file() {
|
|
sha256sum "$1" | awk '{print $1}'
|
|
}
|
|
|
|
record_path() {
|
|
[ -e "$1" ] || [ -L "$1" ] || CREATED_PATHS+=("$1")
|
|
}
|
|
|
|
random_secret() {
|
|
openssl rand -base64 36 | tr -d '\n/+=' | cut -c1-36
|
|
}
|
|
|
|
write_env_file() {
|
|
local output=$1 temporary name value escaped
|
|
shift
|
|
temporary="${output}.next.$$"
|
|
: > "$temporary"
|
|
while [ "$#" -gt 0 ]; do
|
|
name=$1
|
|
value=$2
|
|
shift 2
|
|
case "$value" in *$'\n'*|*$'\r'*) die "$name contains a line break" ;; esac
|
|
escaped=${value//\\/\\\\}
|
|
escaped=${escaped//\"/\\\"}
|
|
printf '%s="%s"\n' "$name" "$escaped" >> "$temporary"
|
|
done
|
|
mv -f "$temporary" "$output"
|
|
}
|
|
|
|
ensure_service_identity() {
|
|
local existing_home nologin_path
|
|
if ! getent group "$SERVICE_GROUP" >/dev/null 2>&1; then
|
|
groupadd --system "$SERVICE_GROUP"
|
|
SERVICE_GROUP_CREATED=true
|
|
fi
|
|
if id "$SERVICE_USER" >/dev/null 2>&1; then
|
|
existing_home=$(getent passwd "$SERVICE_USER" | awk -F: '{print $6}')
|
|
[ "$existing_home" = "$STATE_ROOT" ] \
|
|
|| die "Existing user $SERVICE_USER has unexpected home directory $existing_home"
|
|
id -nG "$SERVICE_USER" | tr ' ' '\n' | grep -Fxq "$SERVICE_GROUP" \
|
|
|| die "Existing user $SERVICE_USER is not a member of group $SERVICE_GROUP"
|
|
return 0
|
|
fi
|
|
nologin_path=$(command -v nologin 2>/dev/null || true)
|
|
[ -n "$nologin_path" ] || nologin_path=/usr/sbin/nologin
|
|
[ -x "$nologin_path" ] || die "A nologin shell is required"
|
|
useradd --system --gid "$SERVICE_GROUP" --home-dir "$STATE_ROOT" --shell "$nologin_path" "$SERVICE_USER"
|
|
SERVICE_USER_CREATED=true
|
|
}
|
|
|
|
main() {
|
|
local script_dir release_root releases_root app_root version app_port field_key setup_code setup_hash
|
|
[ "$(id -u)" -eq 0 ] || die "Run with sudo or as root"
|
|
[ "$(uname -s)" = Linux ] || die "Baota initialization only supports Linux"
|
|
for command in find openssl sha256sum; do
|
|
command -v "$command" >/dev/null 2>&1 || die "$command is required"
|
|
done
|
|
|
|
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)
|
|
release_root=$(cd "$script_dir/.." && pwd -P)
|
|
releases_root=$(dirname "$release_root")
|
|
app_root=$(dirname "$releases_root")
|
|
[ "$(basename "$releases_root")" = releases ] \
|
|
|| die "Extract the release into APP_ROOT/releases/VERSION before initialization"
|
|
[ "$app_root" = /www/wwwroot/kaidi ] \
|
|
|| die "The supported Baota project root is /www/wwwroot/kaidi"
|
|
INIT_RELEASE_ROOT=$release_root
|
|
INIT_APP_ROOT=$app_root
|
|
version=$(tr -d '\r\n' < "$release_root/VERSION")
|
|
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.+-]+)?$ ]] \
|
|
&& [ -s "$release_root/app.jar" ] && [ -s "$release_root/public/index.html" ] \
|
|
|| die "The extracted release is incomplete"
|
|
[ -x "$release_root/ops/update.sh" ] && [ -x "$release_root/ops/baota-start.sh" ] \
|
|
&& [ -x "$release_root/ops/baota-init.sh" ] \
|
|
|| die "The extracted operations scripts are incomplete"
|
|
[ -s "$release_root/ops/release-public.pem" ] \
|
|
|| die "The release public key is missing"
|
|
[ "$(sha256_file "$release_root/ops/release-public.pem")" = "$PUBLIC_KEY_SHA256" ] \
|
|
|| die "The release public-key fingerprint is invalid"
|
|
[ ! -e "$CONFIG_ROOT/kaidi.env" ] && [ ! -e "$CONFIG_ROOT/update.env" ] \
|
|
&& [ ! -e "$STATE_ROOT/setup/locked" ] \
|
|
|| die "Kaidi is already initialized; run the published purge workflow before a fresh installation"
|
|
[ ! -e /etc/systemd/system/kaidi-finance.service ] \
|
|
|| die "The old kaidi-finance.service still exists; run the published purge workflow first"
|
|
[ ! -e /etc/systemd/system/kaidi-update.service ] \
|
|
&& [ ! -e /etc/systemd/system/kaidi-update.path ] \
|
|
|| die "Old Kaidi update units still exist; run the published purge workflow first"
|
|
[ ! -e "$app_root/current" ] && [ ! -L "$app_root/current" ] \
|
|
|| die "The Baota current release link already exists; run the published purge workflow first"
|
|
|
|
app_port=${1:-18080}
|
|
[[ "$app_port" =~ ^[0-9]{1,5}$ ]] && [ "$app_port" -ge 1024 ] && [ "$app_port" -le 65535 ] \
|
|
|| die "Port must be an integer between 1024 and 65535"
|
|
|
|
INIT_ARMED=true
|
|
ensure_service_identity
|
|
for path in "$app_root" "$releases_root" "$STATE_ROOT" "$STATE_ROOT/files" "$STATE_ROOT/tmp" \
|
|
"$LOG_ROOT" "$STATE_ROOT/setup" "$UPDATE_STATE_ROOT" "$UPDATE_STATE_ROOT/inbox" "$CONFIG_ROOT"; do
|
|
record_path "$path"
|
|
done
|
|
install -d -o root -g "$SERVICE_GROUP" -m 0750 "$app_root" "$releases_root"
|
|
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0750 \
|
|
"$STATE_ROOT" "$STATE_ROOT/files" "$STATE_ROOT/tmp" "$LOG_ROOT"
|
|
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0700 "$STATE_ROOT/setup"
|
|
install -d -o root -g "$SERVICE_GROUP" -m 0750 "$UPDATE_STATE_ROOT"
|
|
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0750 "$UPDATE_STATE_ROOT/inbox"
|
|
install -d -o root -g "$SERVICE_GROUP" -m 0750 "$CONFIG_ROOT"
|
|
|
|
chown -R root:"$SERVICE_GROUP" "$release_root"
|
|
find "$release_root" -type d -exec chmod 0750 {} +
|
|
find "$release_root" -type f -exec chmod 0640 {} +
|
|
chmod 0750 "$release_root/ops/update.sh" "$release_root/ops/baota-start.sh" "$release_root/ops/baota-init.sh"
|
|
ln -sfn "$release_root" "$app_root/current.next"
|
|
mv -Tf "$app_root/current.next" "$app_root/current"
|
|
|
|
field_key=$(openssl rand -base64 32 | tr -d '\n')
|
|
setup_code="KD-$(random_secret)"
|
|
setup_hash=$(printf '%s' "$setup_code" | sha256sum | awk '{print $1}')
|
|
# The Baota process manager does not own a restartable application unit.
|
|
# Keep its updater files for diagnostics, but do not expose online update
|
|
# actions until the panel lifecycle is integrated with the transaction state machine.
|
|
# The setup context has no datasource. Empty values avoid Baota treating
|
|
# a development placeholder as a real local MySQL dependency.
|
|
write_env_file "$CONFIG_ROOT/kaidi.env" \
|
|
SPRING_PROFILES_ACTIVE production \
|
|
SERVER_ADDRESS 127.0.0.1 \
|
|
SERVER_PORT "$app_port" \
|
|
SESSION_COOKIE_SECURE false \
|
|
DB_URL '' \
|
|
DB_USERNAME '' \
|
|
DB_PASSWORD '' \
|
|
FIELD_ENCRYPTION_KEY "$field_key" \
|
|
FILE_STORAGE_ROOT "$STATE_ROOT/files" \
|
|
FILE_STORAGE_TEMP "$STATE_ROOT/tmp" \
|
|
FILE_SCANNER_ENABLED false \
|
|
FINANCE_BOOTSTRAP_ENABLED false \
|
|
FINANCE_BOOTSTRAP_PASSWORD '' \
|
|
FINANCE_SETUP_ENABLED true \
|
|
FINANCE_SETUP_TOKEN_SHA256 "$setup_hash" \
|
|
FINANCE_SETUP_ENV_FILE "$STATE_ROOT/setup/application.env" \
|
|
FINANCE_SETUP_MARKER_FILE "$STATE_ROOT/setup/locked" \
|
|
FINANCE_SETUP_RESTART_AFTER_COMPLETE true \
|
|
FINANCE_UPDATE_ENABLED false \
|
|
UPDATE_RELEASE_BASE_URL '' \
|
|
UPDATE_RELEASE_API_URL "$RELEASE_API_URL" \
|
|
UPDATE_RELEASE_TOKEN '' \
|
|
UPDATE_REQUEST_FILE "$UPDATE_STATE_ROOT/inbox/request.json" \
|
|
UPDATE_STATUS_FILE "$UPDATE_STATE_ROOT/status.json" \
|
|
KAIDI_PID_FILE "$STATE_ROOT/kaidi.pid"
|
|
chown root:"$SERVICE_GROUP" "$CONFIG_ROOT/kaidi.env"
|
|
chmod 0640 "$CONFIG_ROOT/kaidi.env"
|
|
|
|
# Baota owns the Java process in this mode. Do not install a second
|
|
# systemd updater; online update is intentionally systemd-only until the
|
|
# panel lifecycle can participate in the transaction state machine.
|
|
install -m 0644 "$release_root/ops/release-public.pem" "$CONFIG_ROOT/release-public.pem"
|
|
|
|
printf '{"state":"CURRENT","message":"宝塔发布已准备","targetVersion":"%s","updatedAt":"%s"}\n' \
|
|
"$version" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$UPDATE_STATE_ROOT/status.json"
|
|
chmod 0644 "$UPDATE_STATE_ROOT/status.json"
|
|
cat > /root/kaidi-first-login.txt <<EOF
|
|
项目路径:$app_root/current
|
|
启动命令:$app_root/current/ops/baota-start.sh
|
|
反向代理目标:http://127.0.0.1:$app_port
|
|
安装向导地址:/setup
|
|
安装码:$setup_code
|
|
版本:$version
|
|
EOF
|
|
chmod 0600 /root/kaidi-first-login.txt
|
|
|
|
INIT_COMMITTED=true
|
|
log "Manual deployment is initialized for Kaidi Finance $version"
|
|
log "Create the Baota Spring Boot project with $app_root/current"
|
|
log "Baota environment variables: leave empty"
|
|
log "Setup code: /root/kaidi-first-login.txt"
|
|
}
|
|
|
|
main "$@"
|