This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.4
|
||||
container_name: kaidi-finance-mysql
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_DATABASE: kaidi_finance
|
||||
MYSQL_USER: kaidi
|
||||
MYSQL_PASSWORD: kaidi_local_2026
|
||||
MYSQL_ROOT_PASSWORD: root_local_2026
|
||||
TZ: Asia/Shanghai
|
||||
command:
|
||||
- --character-set-server=utf8mb4
|
||||
- --collation-server=utf8mb4_0900_ai_ci
|
||||
- --default-time-zone=+00:00
|
||||
ports:
|
||||
- "3307:3306"
|
||||
volumes:
|
||||
- kaidi_mysql_data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mysqladmin ping -h localhost -uroot -p$$MYSQL_ROOT_PASSWORD --silent"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
|
||||
volumes:
|
||||
kaidi_mysql_data:
|
||||
@@ -0,0 +1,25 @@
|
||||
SPRING_PROFILES_ACTIVE=production
|
||||
SERVER_PORT=18080
|
||||
SESSION_COOKIE_SECURE=false
|
||||
|
||||
DB_URL=jdbc:mysql://127.0.0.1:3307/kaidi_finance?useUnicode=true&characterEncoding=utf8&connectionTimeZone=UTC&serverTimezone=UTC
|
||||
DB_USERNAME=kaidi
|
||||
DB_PASSWORD=CHANGE_ME
|
||||
FIELD_ENCRYPTION_KEY=BASE64_32_BYTE_KEY
|
||||
|
||||
FILE_STORAGE_ROOT=/var/lib/kaidi/files
|
||||
FILE_STORAGE_TEMP=/var/lib/kaidi/tmp
|
||||
FILE_SCANNER_ENABLED=false
|
||||
|
||||
FINANCE_BOOTSTRAP_ENABLED=false
|
||||
FINANCE_BOOTSTRAP_PASSWORD=
|
||||
|
||||
APP_VERSION=1.0.0-preview.8
|
||||
UPDATE_CURRENT_VERSION_FILE=/opt/kaidi/current/VERSION
|
||||
FINANCE_UPDATE_ENABLED=true
|
||||
# Use either a stable direct asset base URL or the private Gitea latest-release API.
|
||||
UPDATE_RELEASE_BASE_URL=
|
||||
UPDATE_RELEASE_API_URL=https://git.awaioi.com/api/v1/repos/ERP-Team/kaidi/releases/latest
|
||||
UPDATE_RELEASE_TOKEN=READ_ONLY_GITEA_TOKEN
|
||||
UPDATE_REQUEST_FILE=/var/lib/kaidi-update/inbox/request.json
|
||||
UPDATE_STATUS_FILE=/var/lib/kaidi-update/status.json
|
||||
Executable
+85
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
umask 077
|
||||
|
||||
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
INSTALLER="$ROOT/deploy/install.sh"
|
||||
INSTALLER_SHA256=${KAIDI_INSTALLER_SHA256:-33d4921bcad7ef2be12f20bc0512a5f6df4227ea6dd3b7e6225164077d9cd9e7}
|
||||
RELEASE_API_URL=${KAIDI_RELEASE_API_URL:-https://git.awaioi.com/api/v1/repos/ERP-Team/kaidi/releases/latest}
|
||||
PUBLIC_KEY_SHA256=${KAIDI_RELEASE_PUBLIC_KEY_SHA256:-807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9}
|
||||
TOKEN_FILE=${KAIDI_RELEASE_TOKEN_FILE:-}
|
||||
TEMP_TOKEN_FILE=
|
||||
|
||||
log() { printf '[kaidi-git-install] %s\n' "$*"; }
|
||||
die() { printf '[kaidi-git-install] ERROR: %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
cleanup() {
|
||||
[ -z "$TEMP_TOKEN_FILE" ] || rm -f "$TEMP_TOKEN_FILE"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
[ -f "$INSTALLER" ] || die "deploy/install.sh is missing; run this command from the Kaidi Git checkout"
|
||||
command -v sha256sum >/dev/null 2>&1 || die "sha256sum is required"
|
||||
[[ "$INSTALLER_SHA256" =~ ^[0-9A-Fa-f]{64}$ ]] || die "The installer SHA-256 is invalid"
|
||||
[[ "$PUBLIC_KEY_SHA256" =~ ^[0-9A-Fa-f]{64}$ ]] || die "The release public-key SHA-256 is invalid"
|
||||
actual_installer_sha256=$(sha256sum "$INSTALLER" | awk '{print $1}')
|
||||
normalized_actual_installer_sha256=$(printf '%s' "$actual_installer_sha256" | tr '[:upper:]' '[:lower:]')
|
||||
normalized_installer_sha256=$(printf '%s' "$INSTALLER_SHA256" | tr '[:upper:]' '[:lower:]')
|
||||
[ "$normalized_actual_installer_sha256" = "$normalized_installer_sha256" ] \
|
||||
|| die "deploy/install.sh does not match the trusted SHA-256 for this Git tag"
|
||||
|
||||
if [ -z "$TOKEN_FILE" ]; then
|
||||
TOKEN_FILE=$(mktemp)
|
||||
TEMP_TOKEN_FILE=$TOKEN_FILE
|
||||
chmod 0600 "$TOKEN_FILE"
|
||||
if [ -t 0 ]; then
|
||||
read -rsp 'Gitea read-only token: ' token
|
||||
printf '\n'
|
||||
printf '%s' "$token" > "$TOKEN_FILE"
|
||||
unset token
|
||||
else
|
||||
die "Set KAIDI_RELEASE_TOKEN_FILE or run interactively to enter the Gitea read-only token"
|
||||
fi
|
||||
fi
|
||||
|
||||
[ -f "$TOKEN_FILE" ] && [ ! -L "$TOKEN_FILE" ] || die "The Gitea token file must be a regular file"
|
||||
token_size=$(wc -c < "$TOKEN_FILE" | tr -d '[:space:]')
|
||||
[ "$token_size" -gt 0 ] && [ "$token_size" -le 512 ] || die "The Gitea token file must contain 1 to 512 bytes"
|
||||
LC_ALL=C grep -q '[[:cntrl:]]' "$TOKEN_FILE" && die "The Gitea token file contains invalid control characters"
|
||||
|
||||
root_command=(env)
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
command -v sudo >/dev/null 2>&1 || die "sudo is required when not running as root"
|
||||
root_command=(sudo env)
|
||||
fi
|
||||
|
||||
install_env=(
|
||||
"KAIDI_RELEASE_API_URL=$RELEASE_API_URL"
|
||||
"KAIDI_RELEASE_TOKEN_FILE=$TOKEN_FILE"
|
||||
"KAIDI_RELEASE_PUBLIC_KEY_SHA256=$PUBLIC_KEY_SHA256"
|
||||
)
|
||||
|
||||
KAIDI_REINSTALL=${KAIDI_REINSTALL:-}
|
||||
KAIDI_SETUP_WIZARD=${KAIDI_SETUP_WIZARD:-}
|
||||
KAIDI_DB_URL=${KAIDI_DB_URL:-}
|
||||
KAIDI_DB_USERNAME=${KAIDI_DB_USERNAME:-}
|
||||
KAIDI_DB_PASSWORD=${KAIDI_DB_PASSWORD:-}
|
||||
KAIDI_DB_HOST=${KAIDI_DB_HOST:-}
|
||||
KAIDI_DB_PORT=${KAIDI_DB_PORT:-}
|
||||
KAIDI_DB_NAME=${KAIDI_DB_NAME:-}
|
||||
KAIDI_DB_CONTAINER=${KAIDI_DB_CONTAINER:-}
|
||||
KAIDI_SESSION_COOKIE_SECURE=${KAIDI_SESSION_COOKIE_SECURE:-}
|
||||
KAIDI_FILE_SCANNER_ENABLED=${KAIDI_FILE_SCANNER_ENABLED:-}
|
||||
|
||||
for name in KAIDI_REINSTALL KAIDI_SETUP_WIZARD KAIDI_DB_URL KAIDI_DB_USERNAME KAIDI_DB_PASSWORD \
|
||||
KAIDI_DB_HOST KAIDI_DB_PORT KAIDI_DB_NAME KAIDI_DB_CONTAINER \
|
||||
KAIDI_SESSION_COOKIE_SECURE KAIDI_FILE_SCANNER_ENABLED; do
|
||||
value=${!name}
|
||||
if [ -n "$value" ]; then
|
||||
install_env+=("$name=$value")
|
||||
fi
|
||||
done
|
||||
|
||||
log "Installing the latest signed release from $RELEASE_API_URL"
|
||||
"${root_command[@]}" "${install_env[@]}" bash "$INSTALLER"
|
||||
Executable
+820
@@ -0,0 +1,820 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
umask 077
|
||||
|
||||
APP_ROOT=${KAIDI_APP_ROOT:-/opt/kaidi}
|
||||
STATE_ROOT=${KAIDI_STATE_ROOT:-/var/lib/kaidi}
|
||||
UPDATE_STATE_ROOT=${KAIDI_UPDATE_STATE_ROOT:-/var/lib/kaidi-update}
|
||||
CONFIG_ROOT=${KAIDI_CONFIG_ROOT:-/etc/kaidi}
|
||||
RELEASE_BASE_URL=${KAIDI_RELEASE_BASE_URL:-}
|
||||
RELEASE_API_URL=${KAIDI_RELEASE_API_URL:-https://git.awaioi.com/api/v1/repos/ERP-Team/kaidi/releases/latest}
|
||||
RELEASE_TOKEN=${KAIDI_RELEASE_TOKEN:-}
|
||||
RELEASE_TOKEN_FILE=${KAIDI_RELEASE_TOKEN_FILE:-}
|
||||
HEALTH_URL=${KAIDI_HEALTH_URL:-http://127.0.0.1:18080/actuator/health}
|
||||
TRUSTED_PUBLIC_KEY_SHA256=${KAIDI_RELEASE_PUBLIC_KEY_SHA256:-}
|
||||
REINSTALL=${KAIDI_REINSTALL:-false}
|
||||
SETUP_WIZARD=${KAIDI_SETUP_WIZARD:-false}
|
||||
WORK_DIR=
|
||||
RELEASE_AUTH_HEADER_FILE=
|
||||
INSTALL_TRANSACTION_ARMED=false
|
||||
INSTALL_COMMITTED=false
|
||||
RELEASE_DIR=
|
||||
BACKUP_DIR=
|
||||
JAVA_STAGED_DIR=
|
||||
JAVA_PREVIOUS_DIR=
|
||||
JAVA_ACTIVATED=false
|
||||
CREATED_DB_CONTAINER=false
|
||||
CREATED_DB_VOLUME=false
|
||||
|
||||
log() { printf '[kaidi-install] %s\n' "$*"; }
|
||||
die() { printf '[kaidi-install] ERROR: %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
sha256_file() {
|
||||
sha256sum "$1" | awk '{print $1}'
|
||||
}
|
||||
|
||||
preflight_host() {
|
||||
[ "$(uname -s)" = Linux ] || die "The installer only supports Linux"
|
||||
[ "$APP_ROOT" = /opt/kaidi ] && [ "$STATE_ROOT" = /var/lib/kaidi ] \
|
||||
&& [ "$UPDATE_STATE_ROOT" = /var/lib/kaidi-update ] && [ "$CONFIG_ROOT" = /etc/kaidi ] \
|
||||
|| die "Custom installation roots are not supported by the packaged systemd and Nginx configuration"
|
||||
command -v systemctl >/dev/null 2>&1 || die "systemd is required"
|
||||
[ -d /run/systemd/system ] || die "systemd is not running as PID 1"
|
||||
[ -d /etc/systemd/system ] || die "/etc/systemd/system is missing"
|
||||
[[ "$TRUSTED_PUBLIC_KEY_SHA256" =~ ^[0-9A-Fa-f]{64}$ ]] \
|
||||
|| die "Set KAIDI_RELEASE_PUBLIC_KEY_SHA256 to the trusted release public-key SHA-256"
|
||||
case "$REINSTALL" in
|
||||
true|false) ;;
|
||||
*) die "KAIDI_REINSTALL must be true or false" ;;
|
||||
esac
|
||||
case "$SETUP_WIZARD" in
|
||||
true|false) ;;
|
||||
*) die "KAIDI_SETUP_WIZARD must be true or false" ;;
|
||||
esac
|
||||
if [ "$SETUP_WIZARD" = true ] && [ "$REINSTALL" = true ]; then
|
||||
[ -s "$CONFIG_ROOT/kaidi.env" ] \
|
||||
|| die "A setup-wizard repair needs the existing $CONFIG_ROOT/kaidi.env"
|
||||
[ "$(read_existing_env FINANCE_SETUP_ENABLED)" = true ] \
|
||||
|| die "The existing installation is already in production mode; use a normal repair reinstall"
|
||||
[ ! -e "$STATE_ROOT/setup/locked" ] \
|
||||
|| die "The setup wizard is already locked; use a normal repair reinstall"
|
||||
fi
|
||||
}
|
||||
|
||||
[ "$(id -u)" -eq 0 ] || die "Run with sudo or as root"
|
||||
[ -z "$RELEASE_TOKEN" ] || [ -z "$RELEASE_TOKEN_FILE" ] \
|
||||
|| die "Set only one of KAIDI_RELEASE_TOKEN or KAIDI_RELEASE_TOKEN_FILE"
|
||||
if [ -n "$RELEASE_TOKEN_FILE" ]; then
|
||||
[ -f "$RELEASE_TOKEN_FILE" ] && [ ! -L "$RELEASE_TOKEN_FILE" ] \
|
||||
|| die "KAIDI_RELEASE_TOKEN_FILE must be a regular file"
|
||||
[ "$(wc -c < "$RELEASE_TOKEN_FILE" | tr -d '[:space:]')" -le 512 ] \
|
||||
|| die "KAIDI_RELEASE_TOKEN_FILE is too large"
|
||||
RELEASE_TOKEN=$(cat "$RELEASE_TOKEN_FILE")
|
||||
fi
|
||||
[ "$REINSTALL" = "true" ] || [ ! -e "$APP_ROOT/current" ] \
|
||||
|| die "Kaidi Finance is already installed; use the system update page"
|
||||
if [ -z "$RELEASE_API_URL" ]; then
|
||||
case "$RELEASE_BASE_URL" in
|
||||
*OWNER/REPO*) die "Set KAIDI_RELEASE_BASE_URL or KAIDI_RELEASE_API_URL to the Git release source" ;;
|
||||
esac
|
||||
fi
|
||||
[ -z "$RELEASE_TOKEN" ] || { [ "${#RELEASE_TOKEN}" -le 512 ] \
|
||||
&& ! printf '%s' "$RELEASE_TOKEN" | grep -q '[[:cntrl:]]'; } \
|
||||
|| die "KAIDI_RELEASE_TOKEN is invalid"
|
||||
|
||||
download() {
|
||||
local url=$1 output=$2
|
||||
case "$url" in
|
||||
https://*)
|
||||
curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' \
|
||||
--tlsv1.2 "$url" -o "$output"
|
||||
;;
|
||||
*)
|
||||
[ "${KAIDI_ALLOW_INSECURE_UPDATE:-false}" = "true" ] || die "Release URLs must use HTTPS"
|
||||
curl --fail --silent --show-error --location --proto '=http,https' \
|
||||
--proto-redir '=http,https' "$url" -o "$output"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
download_release_url() {
|
||||
local url=$1 output=$2
|
||||
case "$url" in
|
||||
https://*)
|
||||
if [ -n "$RELEASE_TOKEN" ]; then
|
||||
[ -s "$RELEASE_AUTH_HEADER_FILE" ] || die "Release authentication header is unavailable"
|
||||
curl --fail --silent --show-error --proto '=https' --tlsv1.2 \
|
||||
--header "@$RELEASE_AUTH_HEADER_FILE" "$url" -o "$output"
|
||||
else
|
||||
download "$url" "$output"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
[ "${KAIDI_ALLOW_INSECURE_UPDATE:-false}" = "true" ] || die "Release URLs must use HTTPS"
|
||||
if [ -n "$RELEASE_TOKEN" ]; then
|
||||
[ -s "$RELEASE_AUTH_HEADER_FILE" ] || die "Release authentication header is unavailable"
|
||||
curl --fail --silent --show-error --proto '=http,https' \
|
||||
--header "@$RELEASE_AUTH_HEADER_FILE" "$url" -o "$output"
|
||||
else
|
||||
download "$url" "$output"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
release_asset_url() {
|
||||
local asset_name=$1 asset_url api_origin
|
||||
if [ -n "$RELEASE_API_URL" ]; then
|
||||
asset_url=$(jq -er --arg name "$asset_name" \
|
||||
'.assets[] | select(.name == $name) | .browser_download_url
|
||||
| strings | select(startswith("https://") or startswith("http://"))' \
|
||||
"$WORK_DIR/release-api.json" | head -n 1) || die "Release asset $asset_name is missing"
|
||||
api_origin=$(printf '%s' "$RELEASE_API_URL" | sed -E 's#^(https?://[^/]+).*$#\1#')
|
||||
case "$asset_url" in
|
||||
"$api_origin"/*) printf '%s\n' "$asset_url" ;;
|
||||
*) die "Release asset $asset_name must use the Gitea API host" ;;
|
||||
esac
|
||||
else
|
||||
printf '%s/%s\n' "${RELEASE_BASE_URL%/}" "$asset_name"
|
||||
fi
|
||||
}
|
||||
|
||||
download_release_asset() {
|
||||
local asset_name=$1 output=$2 url
|
||||
url=$(release_asset_url "$asset_name")
|
||||
download_release_url "$url" "$output"
|
||||
}
|
||||
|
||||
install_packages() {
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq ca-certificates coreutils curl findutils gzip jq openssl tar nginx util-linux default-mysql-client
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
dnf install -y ca-certificates coreutils curl findutils gzip jq openssl tar nginx util-linux mysql
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
yum install -y ca-certificates coreutils curl findutils gzip jq openssl tar nginx util-linux mysql
|
||||
else
|
||||
die "Supported package managers are apt, dnf, and yum"
|
||||
fi
|
||||
}
|
||||
|
||||
install_docker() {
|
||||
command -v docker >/dev/null 2>&1 && return
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
apt-get install -y -qq docker.io
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
dnf install -y docker
|
||||
else
|
||||
yum install -y docker
|
||||
fi
|
||||
systemctl enable --now docker
|
||||
}
|
||||
|
||||
azul_arch() {
|
||||
case "$(uname -m)" in
|
||||
x86_64|amd64) printf x86 ;;
|
||||
i386|i486|i586|i686) printf i686 ;;
|
||||
aarch64|arm64) printf arm_64 ;;
|
||||
armv7l|armv6l) printf arm ;;
|
||||
*) die "Unsupported CPU architecture: $(uname -m)" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
prepare_java() {
|
||||
local api java_metadata package_metadata package_uuid java_url java_sha256 actual_sha256
|
||||
api="https://api.azul.com/metadata/v1/zulu/packages/?java_version=17&os=linux&arch=$(azul_arch)&archive_type=tar.gz&java_package_type=jre&release_status=ga&availability_types=CA&latest=true"
|
||||
java_metadata="$WORK_DIR/zulu-metadata.json"
|
||||
download "$api" "$java_metadata"
|
||||
package_uuid=$(jq -er '[.[] | select(.name | contains("-fx-") | not)][0].package_uuid' \
|
||||
"$java_metadata") \
|
||||
|| die "No Java 17 runtime is published for this Linux architecture"
|
||||
package_metadata="$WORK_DIR/zulu-package.json"
|
||||
download "https://api.azul.com/metadata/v1/zulu/packages/$package_uuid" "$package_metadata"
|
||||
java_url=$(jq -er '.download_url | strings | select(startswith("https://cdn.azul.com/"))' \
|
||||
"$package_metadata") || die "Java 17 runtime download URL is invalid"
|
||||
java_sha256=$(jq -er '.sha256_hash | strings | ascii_downcase | select(test("^[0-9a-f]{64}$"))' \
|
||||
"$package_metadata") || die "Java 17 runtime SHA-256 is unavailable"
|
||||
JAVA_STAGED_DIR="$WORK_DIR/java.next"
|
||||
download "$java_url" "$WORK_DIR/java.tar.gz"
|
||||
actual_sha256=$(sha256_file "$WORK_DIR/java.tar.gz")
|
||||
[ "$actual_sha256" = "$java_sha256" ] || die "Java 17 runtime SHA-256 verification failed"
|
||||
mkdir -p "$JAVA_STAGED_DIR"
|
||||
tar -xzf "$WORK_DIR/java.tar.gz" --strip-components=1 -C "$JAVA_STAGED_DIR"
|
||||
"$JAVA_STAGED_DIR/bin/java" -version
|
||||
}
|
||||
|
||||
activate_java() {
|
||||
mkdir -p "$APP_ROOT/runtime"
|
||||
JAVA_PREVIOUS_DIR="$WORK_DIR/java.previous"
|
||||
if [ -e "$APP_ROOT/runtime/java" ]; then
|
||||
mv "$APP_ROOT/runtime/java" "$JAVA_PREVIOUS_DIR"
|
||||
fi
|
||||
mv "$JAVA_STAGED_DIR" "$APP_ROOT/runtime/java"
|
||||
JAVA_ACTIVATED=true
|
||||
"$APP_ROOT/runtime/java/bin/java" -version
|
||||
}
|
||||
|
||||
random_secret() { openssl rand -base64 36 | tr -d '\n/+=' | cut -c1-36; }
|
||||
|
||||
decode_env_value() {
|
||||
local raw=$1 result='' char next index=0 length
|
||||
if [[ "$raw" == \"*\" && ${#raw} -ge 2 ]]; then
|
||||
raw=${raw:1:${#raw}-2}
|
||||
length=${#raw}
|
||||
while [ "$index" -lt "$length" ]; do
|
||||
char=${raw:index:1}
|
||||
if [ "$char" = "\\" ] && [ $((index + 1)) -lt "$length" ]; then
|
||||
next=${raw:index+1:1}
|
||||
if [ "$next" = "\\" ] || [ "$next" = '"' ]; then
|
||||
result+="$next"
|
||||
index=$((index + 2))
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
result+="$char"
|
||||
index=$((index + 1))
|
||||
done
|
||||
printf '%s' "$result"
|
||||
elif [[ "$raw" == \'*\' && ${#raw} -ge 2 ]]; then
|
||||
printf '%s' "${raw:1:${#raw}-2}"
|
||||
else
|
||||
printf '%s' "$raw"
|
||||
fi
|
||||
}
|
||||
|
||||
read_existing_env() {
|
||||
read_env_file "$CONFIG_ROOT/kaidi.env" "$1"
|
||||
}
|
||||
|
||||
read_env_file() {
|
||||
local file=$1 name=$2 line value='' found=false
|
||||
[ -s "$file" ] || return 0
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
case "$line" in
|
||||
"$name="*)
|
||||
value=${line#*=}
|
||||
found=true
|
||||
;;
|
||||
esac
|
||||
done < "$file"
|
||||
if [ "$found" = true ]; then
|
||||
decode_env_value "$value"
|
||||
fi
|
||||
}
|
||||
|
||||
read_setup_env() {
|
||||
read_env_file "$STATE_ROOT/setup/application.env" "$1"
|
||||
}
|
||||
|
||||
read_reinstall_env() {
|
||||
local value
|
||||
value=$(read_setup_env "$1")
|
||||
if [ -n "$value" ]; then
|
||||
printf '%s' "$value"
|
||||
else
|
||||
read_existing_env "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
is_semver() {
|
||||
[ "${#1}" -le 128 ] \
|
||||
&& LC_ALL=C grep -Eq '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$' \
|
||||
<<EOF
|
||||
$1
|
||||
EOF
|
||||
}
|
||||
|
||||
configure_database() {
|
||||
if [ "$SETUP_WIZARD" = true ]; then
|
||||
[ -z "${KAIDI_DB_URL:-}${KAIDI_DB_USERNAME:-}${KAIDI_DB_PASSWORD:-}" ] \
|
||||
|| die "Do not pass database credentials when KAIDI_SETUP_WIZARD=true; enter them in the browser wizard"
|
||||
DB_URL='jdbc:mysql://setup.invalid:3306/kaidi_finance'
|
||||
DB_USERNAME=setup_pending
|
||||
DB_PASSWORD=setup_pending
|
||||
return
|
||||
fi
|
||||
if [ "$REINSTALL" = true ]; then
|
||||
[ -s "$CONFIG_ROOT/kaidi.env" ] || die "KAIDI_REINSTALL needs the existing $CONFIG_ROOT/kaidi.env"
|
||||
DB_USERNAME=${KAIDI_DB_USERNAME:-$(read_reinstall_env DB_USERNAME)}
|
||||
DB_PASSWORD=${KAIDI_DB_PASSWORD:-$(read_reinstall_env DB_PASSWORD)}
|
||||
DB_URL=${KAIDI_DB_URL:-$(read_reinstall_env DB_URL)}
|
||||
else
|
||||
DB_USERNAME=${KAIDI_DB_USERNAME:-}
|
||||
DB_PASSWORD=${KAIDI_DB_PASSWORD:-}
|
||||
DB_URL=${KAIDI_DB_URL:-}
|
||||
fi
|
||||
if [ -n "$DB_URL" ]; then
|
||||
[ -n "$DB_USERNAME" ] && [ -n "$DB_PASSWORD" ] \
|
||||
|| die "KAIDI_DB_USERNAME and KAIDI_DB_PASSWORD are required with KAIDI_DB_URL"
|
||||
return
|
||||
fi
|
||||
[ "$REINSTALL" != true ] \
|
||||
|| die "KAIDI_REINSTALL needs database credentials in the existing installation or command environment"
|
||||
case "$(uname -m)" in
|
||||
i386|i486|i586|i686)
|
||||
die "32-bit Linux needs an external MySQL 8.4 database; set KAIDI_DB_URL, KAIDI_DB_USERNAME, and KAIDI_DB_PASSWORD"
|
||||
;;
|
||||
esac
|
||||
install_docker
|
||||
DB_USERNAME=kaidi
|
||||
DB_PASSWORD=$(random_secret)
|
||||
DB_ROOT_PASSWORD=$(random_secret)
|
||||
! docker inspect kaidi-finance-mysql >/dev/null 2>&1 \
|
||||
|| die "Docker container kaidi-finance-mysql already exists; provide explicit database parameters"
|
||||
! docker volume inspect kaidi_mysql_data >/dev/null 2>&1 \
|
||||
|| die "Docker volume kaidi_mysql_data already exists; provide explicit database parameters"
|
||||
docker volume create kaidi_mysql_data >/dev/null
|
||||
CREATED_DB_VOLUME=true
|
||||
docker run -d \
|
||||
--name kaidi-finance-mysql \
|
||||
--restart unless-stopped \
|
||||
-e MYSQL_DATABASE=kaidi_finance \
|
||||
-e MYSQL_USER="$DB_USERNAME" \
|
||||
-e MYSQL_PASSWORD="$DB_PASSWORD" \
|
||||
-e MYSQL_ROOT_PASSWORD="$DB_ROOT_PASSWORD" \
|
||||
-e TZ=Asia/Shanghai \
|
||||
-p 127.0.0.1:3307:3306 \
|
||||
-v kaidi_mysql_data:/var/lib/mysql \
|
||||
--health-cmd='mysqladmin ping -h localhost -uroot -p$MYSQL_ROOT_PASSWORD --silent' \
|
||||
--health-interval=5s --health-timeout=5s --health-retries=30 \
|
||||
mysql:8.4 \
|
||||
--character-set-server=utf8mb4 \
|
||||
--collation-server=utf8mb4_0900_ai_ci \
|
||||
--default-time-zone=+00:00 >/dev/null
|
||||
CREATED_DB_CONTAINER=true
|
||||
local attempts=0 health
|
||||
while [ "$attempts" -lt 60 ]; do
|
||||
health=$(docker inspect --format '{{.State.Health.Status}}' kaidi-finance-mysql 2>/dev/null || true)
|
||||
[ "$health" = healthy ] && break
|
||||
[ "$health" = unhealthy ] && die "MySQL container failed its health check"
|
||||
attempts=$((attempts + 1))
|
||||
sleep 2
|
||||
done
|
||||
[ "$health" = healthy ] || die "MySQL did not become ready"
|
||||
DB_URL='jdbc:mysql://127.0.0.1:3307/kaidi_finance?useUnicode=true&characterEncoding=utf8&connectionTimeZone=UTC&serverTimezone=UTC'
|
||||
}
|
||||
|
||||
database_host() {
|
||||
local endpoint host_port
|
||||
endpoint=${DB_URL#jdbc:mysql://}
|
||||
host_port=${endpoint%%/*}
|
||||
printf '%s' "${KAIDI_DB_HOST:-${host_port%%:*}}"
|
||||
}
|
||||
|
||||
database_port() {
|
||||
local endpoint host_port candidate
|
||||
endpoint=${DB_URL#jdbc:mysql://}
|
||||
host_port=${endpoint%%/*}
|
||||
candidate=${host_port##*:}
|
||||
[ "$candidate" != "$host_port" ] || candidate=3306
|
||||
printf '%s' "${KAIDI_DB_PORT:-$candidate}"
|
||||
}
|
||||
|
||||
database_name() {
|
||||
local endpoint name
|
||||
endpoint=${DB_URL#jdbc:mysql://}
|
||||
name=${endpoint#*/}
|
||||
name=${name%%\?*}
|
||||
printf '%s' "${KAIDI_DB_NAME:-$name}"
|
||||
}
|
||||
|
||||
preflight_database() {
|
||||
local client host port name version table
|
||||
[ "$SETUP_WIZARD" != true ] || return
|
||||
case "$DB_URL" in
|
||||
jdbc:mysql://*) ;;
|
||||
*) die "KAIDI_DB_URL must start with jdbc:mysql://" ;;
|
||||
esac
|
||||
client=$(command -v mysql || command -v mariadb || true)
|
||||
[ -n "$client" ] || die "A MySQL command-line client is required"
|
||||
host=$(database_host)
|
||||
port=$(database_port)
|
||||
name=$(database_name)
|
||||
[ -n "$host" ] || die "The database host is empty"
|
||||
[[ "$port" =~ ^[0-9]{1,5}$ ]] && [ "$port" -ge 1 ] && [ "$port" -le 65535 ] \
|
||||
|| die "The database port is invalid"
|
||||
[[ "$name" =~ ^[A-Za-z0-9_$]+$ ]] || die "The database name is invalid"
|
||||
|
||||
version=$(MYSQL_PWD="$DB_PASSWORD" "$client" --protocol=TCP --connect-timeout=10 \
|
||||
-h "$host" -P "$port" -u "$DB_USERNAME" "$name" --batch --skip-column-names \
|
||||
-e 'SELECT VERSION()' 2>/dev/null) || die "Cannot connect to the configured MySQL database"
|
||||
case "$version" in
|
||||
8.4.*) ;;
|
||||
*) die "MySQL 8.4.x is required; server reported $version" ;;
|
||||
esac
|
||||
|
||||
table="kaidi_install_preflight_$$"
|
||||
if ! MYSQL_PWD="$DB_PASSWORD" "$client" --protocol=TCP --connect-timeout=10 \
|
||||
-h "$host" -P "$port" -u "$DB_USERNAME" "$name" >/dev/null <<SQL
|
||||
DROP TABLE IF EXISTS $table;
|
||||
CREATE TABLE $table (id INT NOT NULL PRIMARY KEY);
|
||||
INSERT INTO $table (id) VALUES (1);
|
||||
UPDATE $table SET id = 2 WHERE id = 1;
|
||||
DELETE FROM $table WHERE id = 2;
|
||||
DROP TABLE $table;
|
||||
SQL
|
||||
then
|
||||
MYSQL_PWD="$DB_PASSWORD" "$client" --protocol=TCP --connect-timeout=10 \
|
||||
-h "$host" -P "$port" -u "$DB_USERNAME" "$name" \
|
||||
-e "DROP TABLE IF EXISTS $table" >/dev/null 2>&1 || true
|
||||
die "The database account needs DDL and DML permissions on $name"
|
||||
fi
|
||||
log "MySQL $version connectivity and DDL/DML permissions verified"
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
write_env_file_preserving_unknown() {
|
||||
local output=$1 existing='' temporary name value escaped line key managed_keys='|'
|
||||
shift
|
||||
if [ "$REINSTALL" = true ] && [ -f "$output" ]; then
|
||||
existing="$WORK_DIR/existing-kaidi.env"
|
||||
cp -p "$output" "$existing"
|
||||
fi
|
||||
local arguments=("$@")
|
||||
while [ "$#" -gt 0 ]; do
|
||||
managed_keys+="$1|"
|
||||
shift 2
|
||||
done
|
||||
temporary="${output}.next.$$"
|
||||
: > "$temporary"
|
||||
if [ -n "$existing" ]; then
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)= ]]; then
|
||||
key=${BASH_REMATCH[1]}
|
||||
case "$managed_keys" in
|
||||
*"|$key|"*) continue ;;
|
||||
esac
|
||||
fi
|
||||
printf '%s\n' "$line" >> "$temporary"
|
||||
done < "$existing"
|
||||
fi
|
||||
set -- "${arguments[@]}"
|
||||
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"
|
||||
}
|
||||
|
||||
wait_for_health() {
|
||||
local attempts=0
|
||||
while [ "$attempts" -lt 60 ]; do
|
||||
if curl -fsS "$HEALTH_URL" | jq -e '.status == "UP"' >/dev/null 2>&1; then
|
||||
return
|
||||
fi
|
||||
attempts=$((attempts + 1))
|
||||
sleep 2
|
||||
done
|
||||
journalctl -u kaidi-finance.service -n 80 --no-pager >&2 || true
|
||||
die "Application health check did not become UP"
|
||||
}
|
||||
|
||||
backup_managed_state() {
|
||||
local index=0 path
|
||||
MANAGED_PATHS=(
|
||||
"$APP_ROOT/current"
|
||||
"$APP_ROOT/bin/update.sh"
|
||||
"$CONFIG_ROOT/kaidi.env"
|
||||
"$CONFIG_ROOT/update.env"
|
||||
"$CONFIG_ROOT/release-public.pem"
|
||||
"$UPDATE_STATE_ROOT/status.json"
|
||||
/root/kaidi-first-login.txt
|
||||
/etc/systemd/system/kaidi-finance.service
|
||||
/etc/systemd/system/kaidi-update.service
|
||||
/etc/systemd/system/kaidi-update.path
|
||||
/etc/nginx/conf.d/kaidi-finance.conf
|
||||
/etc/nginx/sites-enabled/default
|
||||
)
|
||||
BACKUP_DIR="$WORK_DIR/rollback"
|
||||
mkdir -p "$BACKUP_DIR/files"
|
||||
for path in "${MANAGED_PATHS[@]}"; do
|
||||
if [ -e "$path" ] || [ -L "$path" ]; then
|
||||
cp -a -- "$path" "$BACKUP_DIR/files/$index"
|
||||
: > "$BACKUP_DIR/files/$index.exists"
|
||||
fi
|
||||
index=$((index + 1))
|
||||
done
|
||||
systemctl is-active --quiet kaidi-finance.service && PREVIOUS_APP_ACTIVE=true || PREVIOUS_APP_ACTIVE=false
|
||||
systemctl is-enabled --quiet kaidi-finance.service && PREVIOUS_APP_ENABLED=true || PREVIOUS_APP_ENABLED=false
|
||||
systemctl is-active --quiet kaidi-update.path && PREVIOUS_UPDATE_ACTIVE=true || PREVIOUS_UPDATE_ACTIVE=false
|
||||
systemctl is-enabled --quiet kaidi-update.path && PREVIOUS_UPDATE_ENABLED=true || PREVIOUS_UPDATE_ENABLED=false
|
||||
systemctl is-active --quiet nginx && PREVIOUS_NGINX_ACTIVE=true || PREVIOUS_NGINX_ACTIVE=false
|
||||
systemctl is-enabled --quiet nginx && PREVIOUS_NGINX_ENABLED=true || PREVIOUS_NGINX_ENABLED=false
|
||||
INSTALL_TRANSACTION_ARMED=true
|
||||
}
|
||||
|
||||
restore_unit_state() {
|
||||
local unit=$1 was_enabled=$2 was_active=$3
|
||||
if [ "$was_enabled" = true ]; then
|
||||
systemctl enable "$unit" >/dev/null 2>&1
|
||||
else
|
||||
systemctl disable "$unit" >/dev/null 2>&1
|
||||
fi
|
||||
if [ "$was_active" = true ]; then
|
||||
systemctl start "$unit" >/dev/null 2>&1
|
||||
else
|
||||
systemctl stop "$unit" >/dev/null 2>&1
|
||||
fi
|
||||
}
|
||||
|
||||
restore_managed_path() {
|
||||
local path=$1 index=$2 temporary
|
||||
if [ -e "$BACKUP_DIR/files/$index.exists" ]; then
|
||||
mkdir -p "$(dirname "$path")"
|
||||
temporary="${path}.rollback.$$"
|
||||
rm -rf -- "$temporary"
|
||||
cp -a -- "$BACKUP_DIR/files/$index" "$temporary" || return 1
|
||||
mv -Tf -- "$temporary" "$path" || {
|
||||
rm -rf -- "$temporary"
|
||||
return 1
|
||||
}
|
||||
else
|
||||
rm -rf -- "$path"
|
||||
fi
|
||||
}
|
||||
|
||||
rollback_install() {
|
||||
local index=0 path rollback_failed=false
|
||||
set +e
|
||||
log "Installation failed; restoring the previous managed state"
|
||||
systemctl stop kaidi-update.path kaidi-finance.service >/dev/null 2>&1 || rollback_failed=true
|
||||
for path in "${MANAGED_PATHS[@]}"; do
|
||||
restore_managed_path "$path" "$index" || rollback_failed=true
|
||||
index=$((index + 1))
|
||||
done
|
||||
if [ -n "$RELEASE_DIR" ]; then
|
||||
rm -rf -- "$RELEASE_DIR" || rollback_failed=true
|
||||
fi
|
||||
if [ -d "$JAVA_PREVIOUS_DIR" ]; then
|
||||
rm -rf -- "$APP_ROOT/runtime/java" || rollback_failed=true
|
||||
mkdir -p "$APP_ROOT/runtime" || rollback_failed=true
|
||||
mv "$JAVA_PREVIOUS_DIR" "$APP_ROOT/runtime/java" || rollback_failed=true
|
||||
elif [ "$JAVA_ACTIVATED" = true ]; then
|
||||
rm -rf -- "$APP_ROOT/runtime/java" || rollback_failed=true
|
||||
fi
|
||||
systemctl daemon-reload >/dev/null 2>&1 || rollback_failed=true
|
||||
restore_unit_state kaidi-finance.service "$PREVIOUS_APP_ENABLED" "$PREVIOUS_APP_ACTIVE" \
|
||||
|| rollback_failed=true
|
||||
restore_unit_state kaidi-update.path "$PREVIOUS_UPDATE_ENABLED" "$PREVIOUS_UPDATE_ACTIVE" \
|
||||
|| rollback_failed=true
|
||||
restore_unit_state nginx "$PREVIOUS_NGINX_ENABLED" "$PREVIOUS_NGINX_ACTIVE" || rollback_failed=true
|
||||
if [ "$PREVIOUS_NGINX_ACTIVE" = true ]; then
|
||||
nginx -t >/dev/null 2>&1 && systemctl reload nginx >/dev/null 2>&1 || rollback_failed=true
|
||||
fi
|
||||
set -e
|
||||
[ "$rollback_failed" = false ]
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
local result=$?
|
||||
trap - EXIT
|
||||
if [ "$result" -ne 0 ] && [ "$INSTALL_TRANSACTION_ARMED" = true ] \
|
||||
&& [ "$INSTALL_COMMITTED" != true ]; then
|
||||
rollback_install || log "ERROR: rollback was incomplete; inspect systemd and Nginx state"
|
||||
fi
|
||||
if [ "$result" -ne 0 ] && [ "$INSTALL_COMMITTED" != true ] \
|
||||
&& [ "$CREATED_DB_CONTAINER" = true ]; then
|
||||
docker rm -f kaidi-finance-mysql >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [ "$result" -ne 0 ] && [ "$INSTALL_COMMITTED" != true ] \
|
||||
&& [ "$CREATED_DB_VOLUME" = true ]; then
|
||||
docker volume rm kaidi_mysql_data >/dev/null 2>&1 || true
|
||||
fi
|
||||
[ -z "$WORK_DIR" ] || rm -rf "$WORK_DIR"
|
||||
exit "$result"
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
preflight_host
|
||||
install_packages
|
||||
|
||||
BASE=${RELEASE_BASE_URL%/}
|
||||
WORK_DIR=$(mktemp -d)
|
||||
if [ -n "$RELEASE_TOKEN" ]; then
|
||||
RELEASE_AUTH_HEADER_FILE="$WORK_DIR/release-auth-header"
|
||||
printf 'Authorization: token %s\n' "$RELEASE_TOKEN" > "$RELEASE_AUTH_HEADER_FILE"
|
||||
chmod 0600 "$RELEASE_AUTH_HEADER_FILE"
|
||||
fi
|
||||
if [ -n "$RELEASE_API_URL" ]; then
|
||||
download_release_url "$RELEASE_API_URL" "$WORK_DIR/release-api.json"
|
||||
fi
|
||||
download_release_asset release-public.pem "$WORK_DIR/release-public.pem"
|
||||
ACTUAL_PUBLIC_KEY_SHA256=$(sha256_file "$WORK_DIR/release-public.pem" | tr '[:upper:]' '[:lower:]')
|
||||
EXPECTED_PUBLIC_KEY_SHA256=$(printf '%s' "$TRUSTED_PUBLIC_KEY_SHA256" | tr '[:upper:]' '[:lower:]')
|
||||
[ "$ACTUAL_PUBLIC_KEY_SHA256" = "$EXPECTED_PUBLIC_KEY_SHA256" ] \
|
||||
|| die "Release public-key SHA-256 does not match the trusted fingerprint"
|
||||
download_release_asset release-manifest.json "$WORK_DIR/release-manifest.json"
|
||||
download_release_asset release-manifest.sig "$WORK_DIR/release-manifest.sig"
|
||||
openssl dgst -sha256 -verify "$WORK_DIR/release-public.pem" -signature "$WORK_DIR/release-manifest.sig" \
|
||||
"$WORK_DIR/release-manifest.json" >/dev/null 2>&1 || die "Release manifest signature verification failed"
|
||||
|
||||
VERSION=$(jq -er '.version | strings | select(length > 0 and length <= 128)' \
|
||||
"$WORK_DIR/release-manifest.json") || die "Release version is invalid"
|
||||
is_semver "$VERSION" || die "Release version is invalid"
|
||||
ARTIFACT=$(jq -er '.artifact | strings | select(test("^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}\\.tar\\.gz$"))' \
|
||||
"$WORK_DIR/release-manifest.json") || die "Release artifact name is invalid"
|
||||
EXPECTED_SHA=$(jq -er '.sha256 | strings | ascii_downcase | select(test("^[0-9a-f]{64}$"))' \
|
||||
"$WORK_DIR/release-manifest.json") || die "Release SHA-256 is invalid"
|
||||
download_release_asset "$ARTIFACT" "$WORK_DIR/release.tar.gz"
|
||||
ACTUAL_SHA=$(sha256sum "$WORK_DIR/release.tar.gz" | awk '{print $1}')
|
||||
[ "$ACTUAL_SHA" = "$EXPECTED_SHA" ] || die "Release artifact SHA-256 verification failed"
|
||||
tar -tzf "$WORK_DIR/release.tar.gz" > "$WORK_DIR/archive.list"
|
||||
grep -Eq '(^/|(^|/)\.\.(/|$))' "$WORK_DIR/archive.list" \
|
||||
&& die "Release archive contains an unsafe path"
|
||||
mkdir "$WORK_DIR/extracted"
|
||||
tar -xzf "$WORK_DIR/release.tar.gz" -C "$WORK_DIR/extracted"
|
||||
find "$WORK_DIR/extracted" -type l -print -quit | grep -q . \
|
||||
&& die "Release archive must not contain symbolic links"
|
||||
[ -s "$WORK_DIR/extracted/app.jar" ] || die "Release app.jar is missing"
|
||||
[ -s "$WORK_DIR/extracted/public/index.html" ] || die "Release frontend is missing"
|
||||
[ "$(cat "$WORK_DIR/extracted/VERSION")" = "$VERSION" ] || die "Release version mismatch"
|
||||
[ -x "$WORK_DIR/extracted/ops/update.sh" ] || die "Release updater is missing"
|
||||
|
||||
prepare_java
|
||||
configure_database
|
||||
preflight_database
|
||||
|
||||
id kaidi >/dev/null 2>&1 || useradd --system --home "$STATE_ROOT" --shell /usr/sbin/nologin kaidi
|
||||
mkdir -p "$APP_ROOT/releases" "$APP_ROOT/bin" "$STATE_ROOT/files" "$STATE_ROOT/tmp" \
|
||||
"$STATE_ROOT/setup" "$UPDATE_STATE_ROOT/inbox" "$CONFIG_ROOT" /var/log/kaidi
|
||||
chown -R kaidi:kaidi "$STATE_ROOT" "$UPDATE_STATE_ROOT/inbox" /var/log/kaidi
|
||||
chown root:kaidi "$UPDATE_STATE_ROOT"
|
||||
chmod 0750 "$STATE_ROOT" "$STATE_ROOT/files" "$STATE_ROOT/tmp" "$UPDATE_STATE_ROOT" "$UPDATE_STATE_ROOT/inbox"
|
||||
chmod 0700 "$STATE_ROOT/setup"
|
||||
|
||||
RELEASE_DIR="$APP_ROOT/releases/$VERSION"
|
||||
[ ! -e "$RELEASE_DIR" ] || RELEASE_DIR="$APP_ROOT/releases/${VERSION}-reinstall-$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
backup_managed_state
|
||||
activate_java
|
||||
mv "$WORK_DIR/extracted" "$RELEASE_DIR"
|
||||
chown -R root:root "$RELEASE_DIR"
|
||||
chmod -R go-w "$RELEASE_DIR"
|
||||
ln -sfn "$RELEASE_DIR" "$APP_ROOT/current.next"
|
||||
mv -Tf "$APP_ROOT/current.next" "$APP_ROOT/current"
|
||||
install -m 0755 "$RELEASE_DIR/ops/update.sh" "$APP_ROOT/bin/update.sh"
|
||||
install -m 0644 "$WORK_DIR/release-public.pem" "$CONFIG_ROOT/release-public.pem"
|
||||
|
||||
if [ "$REINSTALL" = true ]; then
|
||||
FIELD_KEY=$(read_existing_env FIELD_ENCRYPTION_KEY)
|
||||
[ -n "$FIELD_KEY" ] || die "The existing FIELD_ENCRYPTION_KEY is missing"
|
||||
ADMIN_PASSWORD=
|
||||
SESSION_COOKIE_SECURE=${KAIDI_SESSION_COOKIE_SECURE:-$(read_existing_env SESSION_COOKIE_SECURE)}
|
||||
FILE_STORAGE_ROOT_VALUE=$(read_existing_env FILE_STORAGE_ROOT)
|
||||
FILE_STORAGE_TEMP_VALUE=$(read_existing_env FILE_STORAGE_TEMP)
|
||||
FILE_SCANNER_ENABLED_VALUE=$(read_existing_env FILE_SCANNER_ENABLED)
|
||||
[ -n "$SESSION_COOKIE_SECURE" ] || SESSION_COOKIE_SECURE=false
|
||||
[ -n "$FILE_STORAGE_ROOT_VALUE" ] || FILE_STORAGE_ROOT_VALUE="$STATE_ROOT/files"
|
||||
[ -n "$FILE_STORAGE_TEMP_VALUE" ] || FILE_STORAGE_TEMP_VALUE="$STATE_ROOT/tmp"
|
||||
[ -n "$FILE_SCANNER_ENABLED_VALUE" ] || FILE_SCANNER_ENABLED_VALUE=false
|
||||
else
|
||||
FIELD_KEY=$(openssl rand -base64 32 | tr -d '\n')
|
||||
if [ "$SETUP_WIZARD" = true ]; then
|
||||
ADMIN_PASSWORD=
|
||||
else
|
||||
ADMIN_PASSWORD="Kaidi!$(random_secret)Aa1"
|
||||
fi
|
||||
SESSION_COOKIE_SECURE=${KAIDI_SESSION_COOKIE_SECURE:-false}
|
||||
FILE_STORAGE_ROOT_VALUE="$STATE_ROOT/files"
|
||||
FILE_STORAGE_TEMP_VALUE="$STATE_ROOT/tmp"
|
||||
FILE_SCANNER_ENABLED_VALUE=${KAIDI_FILE_SCANNER_ENABLED:-false}
|
||||
fi
|
||||
BOOTSTRAP_ENABLED=true
|
||||
SETUP_CODE=
|
||||
SETUP_TOKEN_SHA256=
|
||||
if [ "$REINSTALL" = true ] || [ "$SETUP_WIZARD" = true ]; then
|
||||
BOOTSTRAP_ENABLED=false
|
||||
fi
|
||||
if [ "$SETUP_WIZARD" = true ]; then
|
||||
SETUP_CODE="KD-$(random_secret)"
|
||||
SETUP_TOKEN_SHA256=$(printf '%s' "$SETUP_CODE" | sha256sum | awk '{print $1}')
|
||||
fi
|
||||
write_env_file_preserving_unknown "$CONFIG_ROOT/kaidi.env" \
|
||||
SPRING_PROFILES_ACTIVE production \
|
||||
SERVER_PORT 18080 \
|
||||
SESSION_COOKIE_SECURE "$SESSION_COOKIE_SECURE" \
|
||||
DB_URL "$DB_URL" \
|
||||
DB_USERNAME "$DB_USERNAME" \
|
||||
DB_PASSWORD "$DB_PASSWORD" \
|
||||
FIELD_ENCRYPTION_KEY "$FIELD_KEY" \
|
||||
FILE_STORAGE_ROOT "$FILE_STORAGE_ROOT_VALUE" \
|
||||
FILE_STORAGE_TEMP "$FILE_STORAGE_TEMP_VALUE" \
|
||||
FILE_SCANNER_ENABLED "$FILE_SCANNER_ENABLED_VALUE" \
|
||||
FINANCE_BOOTSTRAP_ENABLED "$BOOTSTRAP_ENABLED" \
|
||||
FINANCE_BOOTSTRAP_PASSWORD "$ADMIN_PASSWORD" \
|
||||
FINANCE_SETUP_ENABLED "$SETUP_WIZARD" \
|
||||
FINANCE_SETUP_TOKEN_SHA256 "$SETUP_TOKEN_SHA256" \
|
||||
FINANCE_SETUP_ENV_FILE "$STATE_ROOT/setup/application.env" \
|
||||
FINANCE_SETUP_MARKER_FILE "$STATE_ROOT/setup/locked" \
|
||||
FINANCE_SETUP_RESTART_AFTER_COMPLETE true \
|
||||
APP_VERSION "$VERSION" \
|
||||
UPDATE_CURRENT_VERSION_FILE "$APP_ROOT/current/VERSION" \
|
||||
FINANCE_UPDATE_ENABLED true \
|
||||
UPDATE_RELEASE_BASE_URL "$([ -n "$RELEASE_API_URL" ] && printf '' || printf '%s/' "$BASE")" \
|
||||
UPDATE_RELEASE_API_URL "$RELEASE_API_URL" \
|
||||
UPDATE_RELEASE_TOKEN "$RELEASE_TOKEN" \
|
||||
UPDATE_REQUEST_FILE "$UPDATE_STATE_ROOT/inbox/request.json" \
|
||||
UPDATE_STATUS_FILE "$UPDATE_STATE_ROOT/status.json"
|
||||
chmod 0600 "$CONFIG_ROOT/kaidi.env"
|
||||
|
||||
write_env_file "$CONFIG_ROOT/update.env" \
|
||||
UPDATE_RELEASE_BASE_URL "$([ -n "$RELEASE_API_URL" ] && printf '' || printf '%s/' "$BASE")" \
|
||||
UPDATE_RELEASE_API_URL "$RELEASE_API_URL" \
|
||||
UPDATE_RELEASE_TOKEN "$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_SERVICE_NAME kaidi-finance.service \
|
||||
KAIDI_HEALTH_URL "$HEALTH_URL" \
|
||||
KAIDI_DB_CONTAINER "${KAIDI_DB_CONTAINER:-kaidi-finance-mysql}" \
|
||||
KAIDI_DB_HOST "$(database_host)" \
|
||||
KAIDI_DB_PORT "$(database_port)" \
|
||||
KAIDI_DB_NAME "$(database_name)" \
|
||||
KAIDI_DB_USERNAME "$DB_USERNAME" \
|
||||
KAIDI_DB_PASSWORD "$DB_PASSWORD"
|
||||
chmod 0600 "$CONFIG_ROOT/update.env"
|
||||
|
||||
install -m 0644 "$RELEASE_DIR/ops/kaidi-finance.service" /etc/systemd/system/kaidi-finance.service
|
||||
install -m 0644 "$RELEASE_DIR/ops/kaidi-update.service" /etc/systemd/system/kaidi-update.service
|
||||
install -m 0644 "$RELEASE_DIR/ops/kaidi-update.path" /etc/systemd/system/kaidi-update.path
|
||||
install -m 0644 "$RELEASE_DIR/ops/kaidi-finance.conf" /etc/nginx/conf.d/kaidi-finance.conf
|
||||
rm -f /etc/nginx/sites-enabled/default
|
||||
nginx -t
|
||||
systemctl daemon-reload
|
||||
systemctl enable kaidi-finance.service
|
||||
systemctl restart kaidi-finance.service
|
||||
wait_for_health
|
||||
systemctl enable --now nginx
|
||||
systemctl reload nginx
|
||||
curl -fsS http://127.0.0.1/actuator/health | jq -e '.status == "UP"' >/dev/null \
|
||||
|| die "Nginx proxy health check failed"
|
||||
|
||||
if [ "$SETUP_WIZARD" != true ]; then
|
||||
sed -i 's/^FINANCE_BOOTSTRAP_ENABLED="true"$/FINANCE_BOOTSTRAP_ENABLED="false"/' "$CONFIG_ROOT/kaidi.env"
|
||||
sed -i 's/^FINANCE_BOOTSTRAP_PASSWORD=.*$/FINANCE_BOOTSTRAP_PASSWORD=""/' "$CONFIG_ROOT/kaidi.env"
|
||||
fi
|
||||
jq -n --arg version "$VERSION" --arg updatedAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||
'{state:"CURRENT",message:"Initial release is running",targetVersion:$version,updatedAt:$updatedAt}' \
|
||||
> "$UPDATE_STATE_ROOT/status.json"
|
||||
chmod 0644 "$UPDATE_STATE_ROOT/status.json"
|
||||
systemctl enable --now kaidi-update.path
|
||||
|
||||
if [ "$SETUP_WIZARD" = true ]; then
|
||||
cat > /root/kaidi-first-login.txt <<EOF
|
||||
URL: http://SERVER_IP/setup
|
||||
Setup code: $SETUP_CODE
|
||||
Version: $VERSION
|
||||
EOF
|
||||
chmod 0600 /root/kaidi-first-login.txt
|
||||
elif [ "$REINSTALL" != true ]; then
|
||||
cat > /root/kaidi-first-login.txt <<EOF
|
||||
URL: http://SERVER_IP/
|
||||
Username: admin
|
||||
Temporary password: $ADMIN_PASSWORD
|
||||
Version: $VERSION
|
||||
EOF
|
||||
chmod 0600 /root/kaidi-first-login.txt
|
||||
fi
|
||||
INSTALL_COMMITTED=true
|
||||
INSTALL_TRANSACTION_ARMED=false
|
||||
|
||||
log "Kaidi Finance $VERSION is installed"
|
||||
if [ "$SETUP_WIZARD" = true ]; then
|
||||
log "Open http://SERVER_IP/setup and complete the first-run wizard"
|
||||
log "Setup code: /root/kaidi-first-login.txt"
|
||||
else
|
||||
log "Open http://SERVER_IP/ and sign in as admin"
|
||||
fi
|
||||
if [ "$REINSTALL" != true ] && [ "$SETUP_WIZARD" != true ]; then
|
||||
log "Temporary credentials: /root/kaidi-first-login.txt"
|
||||
log "Change the temporary password immediately after first sign-in"
|
||||
fi
|
||||
@@ -0,0 +1,40 @@
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
server_name _;
|
||||
|
||||
root /opt/kaidi/current/public;
|
||||
index index.html;
|
||||
client_max_body_size 500m;
|
||||
|
||||
location /api/v1/ {
|
||||
proxy_pass http://127.0.0.1:18080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_connect_timeout 10s;
|
||||
proxy_read_timeout 120s;
|
||||
}
|
||||
|
||||
location = /actuator/health {
|
||||
proxy_pass http://127.0.0.1:18080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
add_header Referrer-Policy same-origin always;
|
||||
add_header X-Frame-Options SAMEORIGIN always;
|
||||
}
|
||||
|
||||
location ~* \.(?:js|css|woff2?|png|jpe?g|gif|svg|ico)$ {
|
||||
try_files $uri =404;
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
[Unit]
|
||||
Description=Kaidi Finance System
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=kaidi
|
||||
Group=kaidi
|
||||
EnvironmentFile=/etc/kaidi/kaidi.env
|
||||
EnvironmentFile=-/var/lib/kaidi/setup/application.env
|
||||
WorkingDirectory=/opt/kaidi/current
|
||||
ExecStart=/opt/kaidi/runtime/java/bin/java -XX:MaxRAMPercentage=70 -Dfile.encoding=UTF-8 -jar /opt/kaidi/current/app.jar
|
||||
SuccessExitStatus=143
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
TimeoutStopSec=45
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectHome=true
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths=/var/lib/kaidi /var/log/kaidi /var/lib/kaidi-update/inbox
|
||||
UMask=0027
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=Watch for Kaidi Finance update requests
|
||||
|
||||
[Path]
|
||||
PathChanged=/var/lib/kaidi-update/inbox
|
||||
PathExists=/var/lib/kaidi-update/inbox/request.json
|
||||
PathChanged=/var/lib/kaidi-update/processing
|
||||
PathExists=/var/lib/kaidi-update/processing/request.json
|
||||
Unit=kaidi-update.service
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,24 @@
|
||||
[Unit]
|
||||
Description=Kaidi Finance signed release updater
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=3
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=root
|
||||
Group=root
|
||||
EnvironmentFile=/etc/kaidi/update.env
|
||||
EnvironmentFile=-/var/lib/kaidi/setup/application.env
|
||||
ExecStart=/opt/kaidi/bin/update.sh
|
||||
Restart=on-failure
|
||||
RestartSec=15
|
||||
Nice=10
|
||||
IOSchedulingClass=best-effort
|
||||
IOSchedulingPriority=6
|
||||
PrivateTmp=true
|
||||
ProtectHome=true
|
||||
ProtectSystem=full
|
||||
ReadWritePaths=/opt/kaidi /var/lib/kaidi /var/lib/kaidi-update /var/log/kaidi /etc/systemd/system /etc/nginx/conf.d
|
||||
UMask=0077
|
||||
Executable
+577
@@ -0,0 +1,577 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
umask 077
|
||||
|
||||
APP_ROOT=${KAIDI_APP_ROOT:-/opt/kaidi}
|
||||
STATE_ROOT=${KAIDI_UPDATE_STATE_ROOT:-/var/lib/kaidi-update}
|
||||
REQUEST_FILE=${UPDATE_REQUEST_FILE:-$STATE_ROOT/inbox/request.json}
|
||||
PROCESSING_DIR=${KAIDI_UPDATE_PROCESSING_DIR:-$STATE_ROOT/processing}
|
||||
PROCESSING_FILE=$PROCESSING_DIR/request.json
|
||||
FAILED_REQUEST_ROOT=${KAIDI_UPDATE_FAILED_ROOT:-$STATE_ROOT/failed}
|
||||
TRANSACTION_ROOT=${KAIDI_UPDATE_TRANSACTION_ROOT:-$STATE_ROOT/transactions}
|
||||
ACTIVE_TRANSACTION=$TRANSACTION_ROOT/active
|
||||
STATUS_FILE=${UPDATE_STATUS_FILE:-$STATE_ROOT/status.json}
|
||||
PUBLIC_KEY=${UPDATE_PUBLIC_KEY:-/etc/kaidi/release-public.pem}
|
||||
RELEASE_BASE_URL=${UPDATE_RELEASE_BASE_URL:-}
|
||||
RELEASE_API_URL=${UPDATE_RELEASE_API_URL:-}
|
||||
RELEASE_TOKEN=${UPDATE_RELEASE_TOKEN:-}
|
||||
CACHE_ROOT=${KAIDI_UPDATE_CACHE_ROOT:-$STATE_ROOT/cache}
|
||||
SERVICE_NAME=${KAIDI_SERVICE_NAME:-kaidi-finance.service}
|
||||
HEALTH_URL=${KAIDI_HEALTH_URL:-http://127.0.0.1:18080/actuator/health}
|
||||
PUBLIC_HEALTH_URL=${KAIDI_PUBLIC_HEALTH_URL:-http://127.0.0.1/actuator/health}
|
||||
PUBLIC_INDEX_URL=${KAIDI_PUBLIC_INDEX_URL:-http://127.0.0.1/}
|
||||
LOCK_FILE=$STATE_ROOT/update.lock
|
||||
BACKUP_ROOT=${KAIDI_BACKUP_ROOT:-$STATE_ROOT/backups}
|
||||
UPDATER_PATH=${KAIDI_UPDATER_PATH:-$APP_ROOT/bin/update.sh}
|
||||
SYSTEMD_ROOT=${KAIDI_SYSTEMD_ROOT:-/etc/systemd/system}
|
||||
NGINX_CONFIG=${KAIDI_NGINX_CONFIG:-/etc/nginx/conf.d/kaidi-finance.conf}
|
||||
LOG_ROOT=${KAIDI_LOG_ROOT:-/var/log/kaidi}
|
||||
HEALTH_ATTEMPTS=${KAIDI_UPDATE_HEALTH_ATTEMPTS:-60}
|
||||
HEALTH_INTERVAL_SECONDS=${KAIDI_UPDATE_HEALTH_INTERVAL_SECONDS:-2}
|
||||
WORK_DIR=
|
||||
CACHE_TEMP=
|
||||
RELEASE_AUTH_HEADER_FILE=
|
||||
TARGET_VERSION=
|
||||
TERMINAL_STATUS_WRITTEN=false
|
||||
|
||||
case "$HEALTH_ATTEMPTS:$HEALTH_INTERVAL_SECONDS" in
|
||||
*[!0-9:]* | :* | *:) printf '%s\n' "Update health-check settings must be non-negative integers" >&2; exit 1 ;;
|
||||
esac
|
||||
[ "$HEALTH_ATTEMPTS" -ge 1 ] || { printf '%s\n' "Update health-check attempts must be at least 1" >&2; exit 1; }
|
||||
|
||||
mkdir -p "$STATE_ROOT/inbox" "$PROCESSING_DIR" "$FAILED_REQUEST_ROOT" "$TRANSACTION_ROOT" \
|
||||
"$STATE_ROOT/work" "$APP_ROOT/releases" "$LOG_ROOT" "$BACKUP_ROOT" "$CACHE_ROOT"
|
||||
chmod 0700 "$PROCESSING_DIR" "$FAILED_REQUEST_ROOT" "$TRANSACTION_ROOT" "$BACKUP_ROOT" "$CACHE_ROOT"
|
||||
|
||||
status() {
|
||||
state=$1
|
||||
message=$2
|
||||
version=${3:-}
|
||||
tmp="$STATUS_FILE.tmp.$$"
|
||||
jq -n \
|
||||
--arg state "$state" \
|
||||
--arg message "$message" \
|
||||
--arg version "$version" \
|
||||
--arg updatedAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||
'{state:$state,message:$message,updatedAt:$updatedAt}
|
||||
+ (if ($version | length) > 0 then {targetVersion:$version} else {} end)' > "$tmp"
|
||||
chmod 0644 "$tmp"
|
||||
mv -f "$tmp" "$STATUS_FILE"
|
||||
}
|
||||
|
||||
archive_processing_request() {
|
||||
[ -f "$PROCESSING_FILE" ] || return 0
|
||||
archived="$FAILED_REQUEST_ROOT/request-$(date -u +%Y%m%dT%H%M%SZ)-$$.json"
|
||||
mv "$PROCESSING_FILE" "$archived"
|
||||
chmod 0600 "$archived"
|
||||
}
|
||||
|
||||
complete_request() {
|
||||
rm -f "$PROCESSING_FILE"
|
||||
}
|
||||
|
||||
fail() {
|
||||
TERMINAL_STATUS_WRITTEN=true
|
||||
status FAILED "$1" "${TARGET_VERSION:-}"
|
||||
archive_processing_request || true
|
||||
printf '%s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# shellcheck disable=SC2329 # Invoked by the EXIT trap below.
|
||||
cleanup() {
|
||||
rc=$?
|
||||
trap - EXIT HUP INT TERM
|
||||
[ -z "$WORK_DIR" ] || rm -rf "$WORK_DIR"
|
||||
[ -z "$CACHE_TEMP" ] || rm -rf "$CACHE_TEMP"
|
||||
if [ "$rc" -ne 0 ] && [ "$TERMINAL_STATUS_WRITTEN" != true ]; then
|
||||
status FAILED "Update process was interrupted; automatic recovery will run" "${TARGET_VERSION:-}" || true
|
||||
fi
|
||||
exit "$rc"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'exit 129' HUP
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
|
||||
download() {
|
||||
url=$1
|
||||
output=$2
|
||||
case "$url" in
|
||||
https://*)
|
||||
if [ -n "$RELEASE_TOKEN" ]; then
|
||||
[ -s "$RELEASE_AUTH_HEADER_FILE" ] || return 2
|
||||
curl --fail --silent --show-error --proto '=https' --tlsv1.2 \
|
||||
--header "@$RELEASE_AUTH_HEADER_FILE" "$url" -o "$output"
|
||||
else
|
||||
curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' \
|
||||
--tlsv1.2 "$url" -o "$output"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
[ "${KAIDI_ALLOW_INSECURE_UPDATE:-false}" = "true" ] || return 2
|
||||
if [ -n "$RELEASE_TOKEN" ]; then
|
||||
[ -s "$RELEASE_AUTH_HEADER_FILE" ] || return 2
|
||||
curl --fail --silent --show-error --proto '=http,https' \
|
||||
--header "@$RELEASE_AUTH_HEADER_FILE" "$url" -o "$output"
|
||||
else
|
||||
curl --fail --silent --show-error --location --proto '=http,https' \
|
||||
--proto-redir '=http,https' "$url" -o "$output"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
release_asset_url() {
|
||||
asset_name=$1
|
||||
if [ -n "$RELEASE_API_URL" ]; then
|
||||
asset_url=$(jq -er --arg name "$asset_name" \
|
||||
'.assets[] | select(.name == $name) | .browser_download_url
|
||||
| strings | select(startswith("https://") or startswith("http://"))' \
|
||||
"$WORK_DIR/release-api.json" | head -n 1) || return 1
|
||||
api_origin=$(printf '%s' "$RELEASE_API_URL" | sed -E 's#^(https?://[^/]+).*$#\1#')
|
||||
case "$asset_url" in
|
||||
"$api_origin"/*) printf '%s\n' "$asset_url" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
else
|
||||
printf '%s/%s\n' "${RELEASE_BASE_URL%/}" "$asset_name"
|
||||
fi
|
||||
}
|
||||
|
||||
is_semver() {
|
||||
[ "${#1}" -le 128 ] \
|
||||
&& LC_ALL=C grep -Eq '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$' \
|
||||
<<EOF
|
||||
$1
|
||||
EOF
|
||||
}
|
||||
|
||||
wait_for_health() {
|
||||
health_url=$1
|
||||
attempts=0
|
||||
while [ "$attempts" -lt "$HEALTH_ATTEMPTS" ]; do
|
||||
if curl -fsS "$health_url" | jq -e '.status == "UP"' >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
attempts=$((attempts + 1))
|
||||
sleep "$HEALTH_INTERVAL_SECONDS"
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
verify_public_surface() {
|
||||
wait_for_health "$PUBLIC_HEALTH_URL" || return 1
|
||||
index_file=$(mktemp "$STATE_ROOT/work/public-index.XXXXXX")
|
||||
if curl -fsS "$PUBLIC_INDEX_URL" -o "$index_file" \
|
||||
&& grep -Eiq '<!doctype|<html' "$index_file"; then
|
||||
rm -f "$index_file"
|
||||
return 0
|
||||
fi
|
||||
rm -f "$index_file"
|
||||
return 1
|
||||
}
|
||||
|
||||
backup_database() {
|
||||
[ "${KAIDI_SKIP_DB_BACKUP:-false}" = "true" ] && return
|
||||
mkdir -p "$BACKUP_ROOT"
|
||||
chmod 0700 "$BACKUP_ROOT"
|
||||
dump_file=$(mktemp "$BACKUP_ROOT/.mysql-dump.XXXXXX.sql")
|
||||
backup="$BACKUP_ROOT/mysql-$(date -u +%Y%m%dT%H%M%SZ)-${TARGET_VERSION}.sql.gz"
|
||||
backup_tmp="$backup.tmp.$$"
|
||||
chmod 0600 "$dump_file"
|
||||
|
||||
if [ -n "${KAIDI_DB_CONTAINER:-}" ] && command -v docker >/dev/null 2>&1 \
|
||||
&& docker inspect "$KAIDI_DB_CONTAINER" >/dev/null 2>&1; then
|
||||
if ! docker exec -e MYSQL_PWD="${KAIDI_DB_PASSWORD:-}" "$KAIDI_DB_CONTAINER" \
|
||||
mysqldump --single-transaction --routines --triggers \
|
||||
-u "${KAIDI_DB_USERNAME:-kaidi}" "${KAIDI_DB_NAME:-kaidi_finance}" > "$dump_file"; then
|
||||
rm -f "$dump_file" "$backup_tmp"
|
||||
fail "Database backup failed"
|
||||
fi
|
||||
else
|
||||
command -v mysqldump >/dev/null 2>&1 || {
|
||||
rm -f "$dump_file" "$backup_tmp"
|
||||
fail "mysqldump is required before installing an update"
|
||||
}
|
||||
if ! MYSQL_PWD=${KAIDI_DB_PASSWORD:-} mysqldump --single-transaction --routines --triggers \
|
||||
-h "${KAIDI_DB_HOST:-127.0.0.1}" -P "${KAIDI_DB_PORT:-3306}" \
|
||||
-u "${KAIDI_DB_USERNAME:-kaidi}" "${KAIDI_DB_NAME:-kaidi_finance}" > "$dump_file"; then
|
||||
rm -f "$dump_file" "$backup_tmp"
|
||||
fail "Database backup failed"
|
||||
fi
|
||||
fi
|
||||
|
||||
[ -s "$dump_file" ] || {
|
||||
rm -f "$dump_file" "$backup_tmp"
|
||||
fail "Database backup is empty"
|
||||
}
|
||||
if ! gzip -9 -c "$dump_file" > "$backup_tmp" || ! gzip -t "$backup_tmp"; then
|
||||
rm -f "$dump_file" "$backup_tmp"
|
||||
fail "Database backup compression failed"
|
||||
fi
|
||||
chmod 0600 "$backup_tmp"
|
||||
mv -f "$backup_tmp" "$backup"
|
||||
rm -f "$dump_file"
|
||||
find "$BACKUP_ROOT" -type f -name 'mysql-*.sql.gz' -printf '%T@ %p\n' \
|
||||
| sort -nr | awk 'NR > 5 {sub(/^[^ ]+ /, ""); print}' | xargs -r rm -f
|
||||
}
|
||||
|
||||
atomic_install() {
|
||||
install_source=$1
|
||||
install_destination=$2
|
||||
install_mode=$3
|
||||
install_temporary="${install_destination}.next.$$"
|
||||
rm -f "$install_temporary"
|
||||
install -m "$install_mode" "$install_source" "$install_temporary" || {
|
||||
rm -f "$install_temporary"
|
||||
return 1
|
||||
}
|
||||
mv -f "$install_temporary" "$install_destination"
|
||||
}
|
||||
|
||||
backup_managed_file() {
|
||||
backup_source=$1
|
||||
backup_name=$2
|
||||
if [ -f "$backup_source" ] && [ ! -L "$backup_source" ]; then
|
||||
cp -p "$backup_source" "$OPS_BACKUP/$backup_name"
|
||||
elif [ ! -e "$backup_source" ]; then
|
||||
: > "$OPS_BACKUP/$backup_name.absent"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
restore_managed_file() {
|
||||
restore_destination=$1
|
||||
restore_name=$2
|
||||
if [ -f "$OPS_BACKUP/$restore_name.absent" ]; then
|
||||
rm -f "$restore_destination"
|
||||
return
|
||||
fi
|
||||
[ -f "$OPS_BACKUP/$restore_name" ] || return 1
|
||||
restore_temporary="${restore_destination}.rollback.$$"
|
||||
rm -f "$restore_temporary"
|
||||
cp -p "$OPS_BACKUP/$restore_name" "$restore_temporary" || {
|
||||
rm -f "$restore_temporary"
|
||||
return 1
|
||||
}
|
||||
mv -f "$restore_temporary" "$restore_destination"
|
||||
}
|
||||
|
||||
backup_operations() {
|
||||
OPS_BACKUP="$ACTIVE_TRANSACTION/operations-backup"
|
||||
mkdir -p "$OPS_BACKUP"
|
||||
backup_managed_file "$UPDATER_PATH" update.sh \
|
||||
&& backup_managed_file "$SYSTEMD_ROOT/kaidi-finance.service" kaidi-finance.service \
|
||||
&& backup_managed_file "$SYSTEMD_ROOT/kaidi-update.service" kaidi-update.service \
|
||||
&& backup_managed_file "$SYSTEMD_ROOT/kaidi-update.path" kaidi-update.path \
|
||||
&& backup_managed_file "$NGINX_CONFIG" kaidi-finance.conf
|
||||
}
|
||||
|
||||
apply_operations() {
|
||||
atomic_install "$RELEASE_DIR/ops/update.sh" "$UPDATER_PATH" 0755 \
|
||||
&& atomic_install "$RELEASE_DIR/ops/kaidi-finance.service" \
|
||||
"$SYSTEMD_ROOT/kaidi-finance.service" 0644 \
|
||||
&& atomic_install "$RELEASE_DIR/ops/kaidi-update.service" \
|
||||
"$SYSTEMD_ROOT/kaidi-update.service" 0644 \
|
||||
&& atomic_install "$RELEASE_DIR/ops/kaidi-update.path" \
|
||||
"$SYSTEMD_ROOT/kaidi-update.path" 0644 \
|
||||
&& atomic_install "$RELEASE_DIR/ops/kaidi-finance.conf" "$NGINX_CONFIG" 0644 \
|
||||
&& systemctl daemon-reload \
|
||||
&& nginx -t
|
||||
}
|
||||
|
||||
restore_operations() {
|
||||
restore_failed=0
|
||||
restore_managed_file "$UPDATER_PATH" update.sh || restore_failed=1
|
||||
restore_managed_file "$SYSTEMD_ROOT/kaidi-finance.service" kaidi-finance.service || restore_failed=1
|
||||
restore_managed_file "$SYSTEMD_ROOT/kaidi-update.service" kaidi-update.service || restore_failed=1
|
||||
restore_managed_file "$SYSTEMD_ROOT/kaidi-update.path" kaidi-update.path || restore_failed=1
|
||||
restore_managed_file "$NGINX_CONFIG" kaidi-finance.conf || restore_failed=1
|
||||
systemctl daemon-reload || restore_failed=1
|
||||
if nginx -t; then
|
||||
systemctl reload nginx || restore_failed=1
|
||||
else
|
||||
restore_failed=1
|
||||
fi
|
||||
[ "$restore_failed" -eq 0 ]
|
||||
}
|
||||
|
||||
write_transaction_value() {
|
||||
name=$1
|
||||
value=$2
|
||||
tmp="$ACTIVE_TRANSACTION/$name.tmp.$$"
|
||||
printf '%s\n' "$value" > "$tmp"
|
||||
mv -f "$tmp" "$ACTIVE_TRANSACTION/$name"
|
||||
}
|
||||
|
||||
transaction_value() {
|
||||
cat "$ACTIVE_TRANSACTION/$1" 2>/dev/null || true
|
||||
}
|
||||
|
||||
remove_failed_release() {
|
||||
failed_release=$1
|
||||
case "$failed_release" in
|
||||
"$APP_ROOT"/releases/*)
|
||||
[ "$(readlink "$APP_ROOT/current" 2>/dev/null || true)" = "$failed_release" ] || rm -rf "$failed_release"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
rollback_active_transaction() {
|
||||
reason=$1
|
||||
previous_target=$(transaction_value previous-target)
|
||||
failed_release=$(transaction_value release-dir)
|
||||
OPS_BACKUP="$ACTIVE_TRANSACTION/operations-backup"
|
||||
rollback_ok=true
|
||||
|
||||
systemctl stop "$SERVICE_NAME" || true
|
||||
if [ -n "$previous_target" ] && [ -d "$previous_target" ]; then
|
||||
if ! ln -sfn "$previous_target" "$APP_ROOT/current.next" \
|
||||
|| ! mv -Tf "$APP_ROOT/current.next" "$APP_ROOT/current"; then
|
||||
rollback_ok=false
|
||||
fi
|
||||
else
|
||||
rollback_ok=false
|
||||
fi
|
||||
restore_operations || rollback_ok=false
|
||||
systemctl start "$SERVICE_NAME" || rollback_ok=false
|
||||
if [ "$rollback_ok" = true ] && wait_for_health "$HEALTH_URL" && verify_public_surface; then
|
||||
remove_failed_release "$failed_release"
|
||||
rm -rf "$ACTIVE_TRANSACTION"
|
||||
fail "$reason; previous release was restored and verified"
|
||||
fi
|
||||
TERMINAL_STATUS_WRITTEN=true
|
||||
status FAILED "$reason; rollback is incomplete and will be retried" "${TARGET_VERSION:-}"
|
||||
printf '%s\n' "$reason; rollback is incomplete and will be retried" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
recover_interrupted_transaction() {
|
||||
[ -d "$ACTIVE_TRANSACTION" ] || return 0
|
||||
TARGET_VERSION=$(transaction_value target-version)
|
||||
phase=$(transaction_value phase)
|
||||
case "$phase" in
|
||||
COMMITTED)
|
||||
complete_request
|
||||
rm -rf "$ACTIVE_TRANSACTION"
|
||||
status SUCCEEDED "Release $TARGET_VERSION is running" "$TARGET_VERSION"
|
||||
return 0
|
||||
;;
|
||||
PREPARED|'')
|
||||
remove_failed_release "$(transaction_value release-dir)"
|
||||
rm -rf "$ACTIVE_TRANSACTION"
|
||||
return 0
|
||||
;;
|
||||
*) rollback_active_transaction "Interrupted update transaction detected" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
validate_release_operations() {
|
||||
sh -n "$WORK_DIR/extracted/ops/update.sh" || return 1
|
||||
command -v systemd-analyze >/dev/null 2>&1 || return 1
|
||||
systemd-analyze verify \
|
||||
"$WORK_DIR/extracted/ops/kaidi-finance.service" \
|
||||
"$WORK_DIR/extracted/ops/kaidi-update.service" \
|
||||
"$WORK_DIR/extracted/ops/kaidi-update.path" >/dev/null || return 1
|
||||
}
|
||||
|
||||
exec 9>"$LOCK_FILE"
|
||||
flock -n 9 || exit 0
|
||||
recover_interrupted_transaction
|
||||
|
||||
if [ -e "$PROCESSING_FILE" ] || [ -L "$PROCESSING_FILE" ]; then
|
||||
if [ -L "$PROCESSING_FILE" ] || [ ! -f "$PROCESSING_FILE" ]; then
|
||||
rm -f "$PROCESSING_FILE" 2>/dev/null || true
|
||||
fail "Processing update request must be a regular file"
|
||||
fi
|
||||
else
|
||||
if [ ! -e "$REQUEST_FILE" ] && [ ! -L "$REQUEST_FILE" ]; then
|
||||
exit 0
|
||||
fi
|
||||
if [ -L "$REQUEST_FILE" ] || [ ! -f "$REQUEST_FILE" ]; then
|
||||
rm -f "$REQUEST_FILE" 2>/dev/null || true
|
||||
fail "Update request must be a regular file"
|
||||
fi
|
||||
mv "$REQUEST_FILE" "$PROCESSING_FILE" || exit 1
|
||||
if [ -L "$PROCESSING_FILE" ] || [ ! -f "$PROCESSING_FILE" ]; then
|
||||
rm -f "$PROCESSING_FILE" 2>/dev/null || true
|
||||
fail "Claimed update request must be a regular file"
|
||||
fi
|
||||
fi
|
||||
chmod 0600 "$PROCESSING_FILE"
|
||||
|
||||
if ! REQUESTED_VERSION=$(jq -er '.version | strings | select(length > 0 and length <= 128)' \
|
||||
"$PROCESSING_FILE"); then
|
||||
fail "Update request version is invalid"
|
||||
fi
|
||||
is_semver "$REQUESTED_VERSION" || fail "Update request version is invalid"
|
||||
TARGET_VERSION=$REQUESTED_VERSION
|
||||
REQUEST_ACTION=$(jq -er '(.action // "INSTALL") | strings | ascii_upcase
|
||||
| select(. == "DOWNLOAD" or . == "INSTALL")' "$PROCESSING_FILE") \
|
||||
|| fail "Update request action is invalid"
|
||||
[ -z "$RELEASE_TOKEN" ] || { [ "${#RELEASE_TOKEN}" -le 512 ] \
|
||||
&& ! printf '%s' "$RELEASE_TOKEN" | grep -q '[[:cntrl:]]'; } \
|
||||
|| fail "UPDATE_RELEASE_TOKEN is invalid"
|
||||
[ -s "$PUBLIC_KEY" ] || fail "Release verification public key is missing"
|
||||
|
||||
status VERIFYING "Validating signed release $TARGET_VERSION" "$TARGET_VERSION"
|
||||
WORK_DIR=$(mktemp -d "$STATE_ROOT/work/update.XXXXXX")
|
||||
if [ -n "$RELEASE_TOKEN" ]; then
|
||||
RELEASE_AUTH_HEADER_FILE="$WORK_DIR/release-auth-header"
|
||||
printf 'Authorization: token %s\n' "$RELEASE_TOKEN" > "$RELEASE_AUTH_HEADER_FILE"
|
||||
chmod 0600 "$RELEASE_AUTH_HEADER_FILE"
|
||||
fi
|
||||
if [ "$REQUEST_ACTION" = DOWNLOAD ]; then
|
||||
[ -n "$RELEASE_BASE_URL" ] || [ -n "$RELEASE_API_URL" ] \
|
||||
|| fail "UPDATE_RELEASE_BASE_URL or UPDATE_RELEASE_API_URL is not configured"
|
||||
if [ -n "$RELEASE_API_URL" ]; then
|
||||
download "$RELEASE_API_URL" "$WORK_DIR/release-api.json" \
|
||||
|| fail "Gitea latest Release lookup failed"
|
||||
fi
|
||||
MANIFEST_URL=$(release_asset_url release-manifest.json) \
|
||||
|| fail "Release manifest asset is missing"
|
||||
SIGNATURE_URL=$(release_asset_url release-manifest.sig) \
|
||||
|| fail "Release manifest signature asset is missing"
|
||||
download "$MANIFEST_URL" "$WORK_DIR/release-manifest.json" \
|
||||
|| fail "Release manifest download failed"
|
||||
download "$SIGNATURE_URL" "$WORK_DIR/release-manifest.sig" \
|
||||
|| fail "Release manifest signature download failed"
|
||||
else
|
||||
CACHE_DIR="$CACHE_ROOT/$TARGET_VERSION"
|
||||
[ -d "$CACHE_DIR" ] && [ ! -L "$CACHE_DIR" ] \
|
||||
|| fail "Verified release cache is missing"
|
||||
[ -f "$CACHE_DIR/release-manifest.json" ] && [ ! -L "$CACHE_DIR/release-manifest.json" ] \
|
||||
|| fail "Cached release manifest is missing"
|
||||
[ -f "$CACHE_DIR/release-manifest.sig" ] && [ ! -L "$CACHE_DIR/release-manifest.sig" ] \
|
||||
|| fail "Cached release signature is missing"
|
||||
[ -f "$CACHE_DIR/release.tar.gz" ] && [ ! -L "$CACHE_DIR/release.tar.gz" ] \
|
||||
|| fail "Cached release artifact is missing"
|
||||
cp "$CACHE_DIR/release-manifest.json" "$WORK_DIR/release-manifest.json"
|
||||
cp "$CACHE_DIR/release-manifest.sig" "$WORK_DIR/release-manifest.sig"
|
||||
cp "$CACHE_DIR/release.tar.gz" "$WORK_DIR/release.tar.gz"
|
||||
fi
|
||||
openssl dgst -sha256 -verify "$PUBLIC_KEY" -signature "$WORK_DIR/release-manifest.sig" \
|
||||
"$WORK_DIR/release-manifest.json" >/dev/null 2>&1 || fail "Release manifest signature verification failed"
|
||||
|
||||
TARGET_VERSION=$(jq -er '.version | strings | select(length > 0 and length <= 128)' \
|
||||
"$WORK_DIR/release-manifest.json") || fail "Release manifest version is invalid"
|
||||
is_semver "$TARGET_VERSION" || fail "Release manifest version is invalid"
|
||||
[ "$TARGET_VERSION" = "$REQUESTED_VERSION" ] || fail "Requested version is no longer the latest signed release"
|
||||
ARTIFACT=$(jq -er '.artifact | strings | select(test("^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}\\.tar\\.gz$"))' \
|
||||
"$WORK_DIR/release-manifest.json") || fail "Release artifact name is invalid"
|
||||
EXPECTED_SHA=$(jq -er '.sha256 | strings | ascii_downcase | select(test("^[0-9a-f]{64}$"))' \
|
||||
"$WORK_DIR/release-manifest.json") || fail "Release SHA-256 is invalid"
|
||||
|
||||
CURRENT_VERSION=$(cat "$APP_ROOT/current/VERSION" 2>/dev/null || true)
|
||||
if [ "$CURRENT_VERSION" = "$TARGET_VERSION" ]; then
|
||||
status CURRENT "Version $TARGET_VERSION is already installed" "$TARGET_VERSION"
|
||||
TERMINAL_STATUS_WRITTEN=true
|
||||
complete_request
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$REQUEST_ACTION" = DOWNLOAD ]; then
|
||||
status DOWNLOADING "Downloading signed release $TARGET_VERSION" "$TARGET_VERSION"
|
||||
ARTIFACT_URL=$(release_asset_url "$ARTIFACT") || fail "Release artifact asset is missing"
|
||||
download "$ARTIFACT_URL" "$WORK_DIR/release.tar.gz" || fail "Release artifact download failed"
|
||||
else
|
||||
status VERIFYING "Revalidating cached release $TARGET_VERSION" "$TARGET_VERSION"
|
||||
fi
|
||||
ACTUAL_SHA=$(sha256sum "$WORK_DIR/release.tar.gz" | awk '{print $1}')
|
||||
[ "$ACTUAL_SHA" = "$EXPECTED_SHA" ] || fail "Release artifact SHA-256 verification failed"
|
||||
|
||||
tar -tzf "$WORK_DIR/release.tar.gz" > "$WORK_DIR/archive.list" || fail "Release archive could not be listed"
|
||||
if grep -Eq '(^/|(^|/)\.\.(/|$))' "$WORK_DIR/archive.list"; then
|
||||
fail "Release archive contains an unsafe path"
|
||||
fi
|
||||
mkdir "$WORK_DIR/extracted"
|
||||
tar -xzf "$WORK_DIR/release.tar.gz" -C "$WORK_DIR/extracted" || fail "Release archive could not be extracted"
|
||||
if find "$WORK_DIR/extracted" -type l -print -quit | grep -q .; then
|
||||
fail "Release archive must not contain symbolic links"
|
||||
fi
|
||||
[ -s "$WORK_DIR/extracted/app.jar" ] || fail "Release app.jar is missing"
|
||||
[ -s "$WORK_DIR/extracted/public/index.html" ] || fail "Release frontend is missing"
|
||||
[ "$(cat "$WORK_DIR/extracted/VERSION" 2>/dev/null)" = "$TARGET_VERSION" ] || fail "Release version file mismatch"
|
||||
[ -x "$WORK_DIR/extracted/ops/update.sh" ] || fail "Release updater is missing"
|
||||
[ -s "$WORK_DIR/extracted/ops/kaidi-finance.service" ] || fail "Release application unit is missing"
|
||||
[ -s "$WORK_DIR/extracted/ops/kaidi-update.service" ] || fail "Release updater unit is missing"
|
||||
[ -s "$WORK_DIR/extracted/ops/kaidi-update.path" ] || fail "Release updater path unit is missing"
|
||||
[ -s "$WORK_DIR/extracted/ops/kaidi-finance.conf" ] || fail "Release Nginx configuration is missing"
|
||||
validate_release_operations || fail "Release operations validation failed"
|
||||
|
||||
if [ "$REQUEST_ACTION" = DOWNLOAD ]; then
|
||||
CACHE_DIR="$CACHE_ROOT/$TARGET_VERSION"
|
||||
CACHE_TEMP=$(mktemp -d "$CACHE_ROOT/.release.XXXXXX")
|
||||
cp "$WORK_DIR/release-manifest.json" "$CACHE_TEMP/release-manifest.json"
|
||||
cp "$WORK_DIR/release-manifest.sig" "$CACHE_TEMP/release-manifest.sig"
|
||||
cp "$WORK_DIR/release.tar.gz" "$CACHE_TEMP/release.tar.gz"
|
||||
chmod 0600 "$CACHE_TEMP/release-manifest.json" "$CACHE_TEMP/release-manifest.sig" \
|
||||
"$CACHE_TEMP/release.tar.gz"
|
||||
if [ -e "$CACHE_DIR" ] || [ -L "$CACHE_DIR" ]; then
|
||||
[ -d "$CACHE_DIR" ] && [ ! -L "$CACHE_DIR" ] || fail "Verified release cache path is unsafe"
|
||||
rm -rf "$CACHE_DIR"
|
||||
fi
|
||||
mv "$CACHE_TEMP" "$CACHE_DIR" || fail "Verified release cache could not be activated"
|
||||
CACHE_TEMP=
|
||||
status READY "Release $TARGET_VERSION is downloaded and verified; confirm installation" "$TARGET_VERSION"
|
||||
TERMINAL_STATUS_WRITTEN=true
|
||||
complete_request
|
||||
exit 0
|
||||
fi
|
||||
|
||||
RELEASE_DIR="$APP_ROOT/releases/$TARGET_VERSION"
|
||||
if [ -e "$RELEASE_DIR" ]; then
|
||||
[ "$(readlink "$APP_ROOT/current" 2>/dev/null || true)" != "$RELEASE_DIR" ] \
|
||||
|| fail "Release directory is already active"
|
||||
rm -rf "$RELEASE_DIR" || fail "Failed release staging directory could not be cleaned"
|
||||
fi
|
||||
mv "$WORK_DIR/extracted" "$RELEASE_DIR" || fail "Release directory could not be activated"
|
||||
chown -R root:root "$RELEASE_DIR" || fail "Release ownership could not be secured"
|
||||
chmod -R go-w "$RELEASE_DIR" || fail "Release permissions could not be secured"
|
||||
|
||||
PREVIOUS_TARGET=$(readlink "$APP_ROOT/current" 2>/dev/null || true)
|
||||
mkdir "$ACTIVE_TRANSACTION"
|
||||
write_transaction_value target-version "$TARGET_VERSION"
|
||||
write_transaction_value previous-target "$PREVIOUS_TARGET"
|
||||
write_transaction_value release-dir "$RELEASE_DIR"
|
||||
write_transaction_value phase PREPARED
|
||||
|
||||
status BACKING_UP "Backing up database before installing $TARGET_VERSION" "$TARGET_VERSION"
|
||||
backup_database
|
||||
if ! backup_operations; then
|
||||
remove_failed_release "$RELEASE_DIR"
|
||||
rm -rf "$ACTIVE_TRANSACTION"
|
||||
fail "Existing operations files could not be backed up"
|
||||
fi
|
||||
status INSTALLING "Installing release $TARGET_VERSION" "$TARGET_VERSION"
|
||||
write_transaction_value phase OPS_APPLYING
|
||||
apply_operations || rollback_active_transaction "Release operations files could not be activated"
|
||||
write_transaction_value phase OPS_APPLIED
|
||||
|
||||
write_transaction_value phase APP_SWITCHING
|
||||
systemctl stop "$SERVICE_NAME" || rollback_active_transaction "Application service could not be stopped"
|
||||
if ! ln -sfn "$RELEASE_DIR" "$APP_ROOT/current.next" \
|
||||
|| ! mv -Tf "$APP_ROOT/current.next" "$APP_ROOT/current"; then
|
||||
rollback_active_transaction "Release application link could not be activated"
|
||||
fi
|
||||
write_transaction_value phase APP_SWITCHED
|
||||
|
||||
status RUNNING "Starting and verifying release $TARGET_VERSION" "$TARGET_VERSION"
|
||||
write_transaction_value phase HEALTH_CHECKING
|
||||
if systemctl start "$SERVICE_NAME" \
|
||||
&& wait_for_health "$HEALTH_URL" \
|
||||
&& systemctl reload nginx \
|
||||
&& systemctl is-active --quiet kaidi-update.path \
|
||||
&& verify_public_surface; then
|
||||
write_transaction_value phase COMMITTED
|
||||
status SUCCEEDED "Release $TARGET_VERSION is running" "$TARGET_VERSION"
|
||||
TERMINAL_STATUS_WRITTEN=true
|
||||
complete_request
|
||||
rm -rf "$ACTIVE_TRANSACTION"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
rollback_active_transaction "Release health or public-surface verification failed"
|
||||
Reference in New Issue
Block a user