245 lines
10 KiB
Bash
Executable File
245 lines
10 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
|
|
UPDATER_ROOT=/opt/kaidi/bin
|
|
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() { printf '[kaidi-baota-init] %s\n' "$*"; }
|
|
die() { printf '[kaidi-baota-init] ERROR: %s\n' "$*" >&2; exit 1; }
|
|
|
|
rollback_initialization() {
|
|
local rc=$?
|
|
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" "$UPDATER_ROOT/update.sh" \
|
|
"$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
|
|
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}'
|
|
}
|
|
|
|
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"
|
|
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"
|
|
}
|
|
|
|
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"
|
|
command -v systemctl >/dev/null 2>&1 && [ -d /run/systemd/system ] \
|
|
|| die "systemd is required for the privileged background updater"
|
|
for command in find openssl setsid 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"
|
|
! systemctl cat kaidi-finance.service >/dev/null 2>&1 \
|
|
|| 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
|
|
install -d -o root -g "$SERVICE_GROUP" -m 0750 "$app_root" "$releases_root" "$UPDATER_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.
|
|
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 'jdbc:mysql://setup.invalid:3306/kaidi_finance' \
|
|
DB_USERNAME setup_pending \
|
|
DB_PASSWORD setup_pending \
|
|
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"
|
|
|
|
install -m 0644 "$release_root/ops/release-public.pem" "$CONFIG_ROOT/release-public.pem"
|
|
install -m 0755 "$release_root/ops/update.sh" "$UPDATER_ROOT/update.sh"
|
|
write_env_file "$CONFIG_ROOT/update.env" \
|
|
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" \
|
|
UPDATE_PUBLIC_KEY "$CONFIG_ROOT/release-public.pem" \
|
|
KAIDI_APP_ROOT "$app_root" \
|
|
KAIDI_UPDATE_STATE_ROOT "$UPDATE_STATE_ROOT" \
|
|
KAIDI_PROCESS_MANAGER baota \
|
|
KAIDI_PID_FILE "$STATE_ROOT/kaidi.pid" \
|
|
KAIDI_RUNTIME_ENV_FILE "$STATE_ROOT/setup/application.env" \
|
|
KAIDI_UPDATER_PATH "$UPDATER_ROOT/update.sh" \
|
|
KAIDI_SERVICE_USER "$SERVICE_USER" \
|
|
KAIDI_SERVICE_GROUP "$SERVICE_GROUP" \
|
|
KAIDI_UPDATE_PATH_NAME kaidi-update.path \
|
|
KAIDI_HEALTH_URL "http://127.0.0.1:$app_port/actuator/health" \
|
|
KAIDI_APP_INDEX_URL "http://127.0.0.1:$app_port/" \
|
|
KAIDI_DB_HOST setup.invalid \
|
|
KAIDI_DB_PORT 3306 \
|
|
KAIDI_DB_NAME kaidi_finance \
|
|
KAIDI_DB_USERNAME setup_pending \
|
|
KAIDI_DB_PASSWORD setup_pending
|
|
chmod 0600 "$CONFIG_ROOT/update.env"
|
|
|
|
install -m 0644 "$release_root/ops/kaidi-update.service" /etc/systemd/system/kaidi-update.service
|
|
install -m 0644 "$release_root/ops/kaidi-update.path" /etc/systemd/system/kaidi-update.path
|
|
systemd-analyze verify /etc/systemd/system/kaidi-update.service /etc/systemd/system/kaidi-update.path >/dev/null \
|
|
|| die "The privileged update units failed validation"
|
|
systemctl daemon-reload
|
|
systemctl enable --now kaidi-update.path
|
|
|
|
printf '{"state":"CURRENT","message":"Baota release is prepared","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
|
|
Project path: $app_root/current
|
|
Start command: $app_root/current/ops/baota-start.sh
|
|
Reverse proxy target: http://127.0.0.1:$app_port
|
|
Setup URL: /setup
|
|
Setup code: $setup_code
|
|
Version: $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 "$@"
|