fix: make Linux installation permissions deterministic
This commit is contained in:
@@ -59,14 +59,14 @@ PostgreSQL 18。PostgreSQL 兼容开发已冻结,不属于本次 Preview.12
|
||||
### 直接 curl 安装
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.15/install.sh | sudo bash
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.16/install.sh | sudo bash
|
||||
```
|
||||
|
||||
这条命令会提示填写 Java 应用端口,直接回车使用 `18080`;随后安装最新签名 Release,并默认进入 `/setup`
|
||||
安装向导。Java 默认只监听 `127.0.0.1:所选端口`,前端页面、API 和健康检查均由同一端口提供。无人值守安装可直接指定:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.15/install.sh \
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.16/install.sh \
|
||||
| sudo env KAIDI_APP_PORT=19090 bash
|
||||
```
|
||||
|
||||
@@ -74,8 +74,8 @@ curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-previe
|
||||
时才设置 `KAIDI_SERVER_ADDRESS=0.0.0.0`,通常应保持默认回环绑定并由本机反向代理访问。需要在执行前独立校验安装脚本时使用:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.15/install.sh -o /tmp/kaidi-install.sh
|
||||
printf '%s %s\n' 8a75ee99a1c2f426c11ea18c8ef65f4f4ac3f8d52321c358f3ff645baa6ad99c /tmp/kaidi-install.sh | sha256sum -c -
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.16/install.sh -o /tmp/kaidi-install.sh
|
||||
printf '%s %s\n' bc9001197af843dc323907a98023064ccc4f184e000543d2086b8a6f8161dbc7 /tmp/kaidi-install.sh | sha256sum -c -
|
||||
sudo bash /tmp/kaidi-install.sh
|
||||
rm -f /tmp/kaidi-install.sh
|
||||
```
|
||||
@@ -86,7 +86,7 @@ rm -f /tmp/kaidi-install.sh
|
||||
包装器会在 `sudo` 前校验 `deploy/install.sh` 的固定 SHA-256,再按同一公钥信任链安装最新签名 Release。
|
||||
|
||||
```bash
|
||||
git clone --branch v1.0.0-preview.15 --depth 1 https://git.awaioi.com/ERP-Team/kaidi.git kaidi-preview
|
||||
git clone --branch v1.0.0-preview.16 --depth 1 https://git.awaioi.com/ERP-Team/kaidi.git kaidi-preview
|
||||
cd kaidi-preview
|
||||
./deploy/install-from-git.sh
|
||||
```
|
||||
@@ -141,7 +141,7 @@ sudo cat /root/kaidi-first-login.txt
|
||||
32 位服务端镜像,因此 32 位主机需要预先连接一台 MySQL 8.4 数据库,之后仍然只执行一个安装命令:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.15/install.sh | sudo bash
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.16/install.sh | sudo bash
|
||||
```
|
||||
|
||||
无论主机架构如何,安装器都不会安装 MySQL、数据库客户端或创建数据库容器。在打开向导前,需要预先创建 `kaidi_finance`,并授予安装账号该库的
|
||||
@@ -161,7 +161,7 @@ GRANT ALL PRIVILEGES ON kaidi_finance.* TO 'kaidi'@'KAIDI_SERVER_IP';
|
||||
管理员数据和运维人员新增的环境变量:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.15/install.sh \
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.16/install.sh \
|
||||
| sudo env KAIDI_REINSTALL=true KAIDI_SETUP_WIZARD=false bash
|
||||
```
|
||||
|
||||
@@ -171,7 +171,7 @@ curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-previe
|
||||
如果安装器已完成但向导尚未提交,可执行下面的命令重新生成一次性安装码;该恢复路径只接受仍处于向导模式且未锁定的安装,正式模式不会被覆盖。
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.15/install.sh \
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.16/install.sh \
|
||||
| sudo env KAIDI_REINSTALL=true bash
|
||||
```
|
||||
|
||||
@@ -207,8 +207,8 @@ act_runner 提供 `ubuntu-24.04` 标签,并在 tag 发布时执行后端、前
|
||||
Preview 属性由 SemVer 版本名表达。之后推送 tag 即会构建、测试、签名并发布:
|
||||
|
||||
```bash
|
||||
git tag v1.0.0-preview.15
|
||||
git push origin v1.0.0-preview.15
|
||||
git tag v1.0.0-preview.16
|
||||
git push origin v1.0.0-preview.16
|
||||
```
|
||||
|
||||
在线更新使用独立的 TDesign 页面:隔离的系统管理员进入“系统治理 → 系统更新”。权限与配置页只管理用户、角色、数据范围、表单模板和参数版本,不配置系统名称或域名。
|
||||
@@ -260,7 +260,7 @@ cat /var/lib/kaidi-update/status.json
|
||||
```bash
|
||||
KAIDI_RELEASE_SIGNING_KEY=/secure/release-signing-private.pem \
|
||||
KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 \
|
||||
./scripts/package-release.sh 1.0.0-preview.15
|
||||
./scripts/package-release.sh 1.0.0-preview.16
|
||||
KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 \
|
||||
./scripts/verify-release.sh dist/release
|
||||
```
|
||||
|
||||
@@ -5,7 +5,7 @@ umask 077
|
||||
|
||||
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
INSTALLER="$ROOT/deploy/install.sh"
|
||||
INSTALLER_SHA256=${KAIDI_INSTALLER_SHA256:-8a75ee99a1c2f426c11ea18c8ef65f4f4ac3f8d52321c358f3ff645baa6ad99c}
|
||||
INSTALLER_SHA256=${KAIDI_INSTALLER_SHA256:-bc9001197af843dc323907a98023064ccc4f184e000543d2086b8a6f8161dbc7}
|
||||
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:-}
|
||||
|
||||
+167
-34
@@ -28,6 +28,9 @@ BACKUP_DIR=
|
||||
JAVA_STAGED_DIR=
|
||||
JAVA_PREVIOUS_DIR=
|
||||
JAVA_ACTIVATED=false
|
||||
SERVICE_USER=kaidi
|
||||
SERVICE_GROUP=kaidi
|
||||
HOST_ARCH=
|
||||
|
||||
log() { printf '[kaidi-install] %s\n' "$*"; }
|
||||
die() { printf '[kaidi-install] ERROR: %s\n' "$*" >&2; exit 1; }
|
||||
@@ -36,14 +39,42 @@ sha256_file() {
|
||||
sha256sum "$1" | awk '{print $1}'
|
||||
}
|
||||
|
||||
normalized_host_arch() {
|
||||
local machine host_bits
|
||||
machine=$(uname -m)
|
||||
host_bits=$(getconf LONG_BIT 2>/dev/null || true)
|
||||
case "$machine" in
|
||||
x86_64|amd64)
|
||||
[ "$host_bits" = 32 ] && printf x86 || printf x86_64
|
||||
;;
|
||||
i386|i486|i586|i686) printf x86 ;;
|
||||
aarch64|arm64)
|
||||
[ "$host_bits" = 32 ] && printf arm || printf aarch64
|
||||
;;
|
||||
armv7l|armv6l) printf arm ;;
|
||||
*) die "Unsupported CPU architecture: $machine" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
preflight_host() {
|
||||
local host_bits expected_bits
|
||||
[ "$(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 configuration"
|
||||
command -v systemctl >/dev/null 2>&1 || die "systemd is required"
|
||||
command -v systemd-analyze >/dev/null 2>&1 || die "systemd-analyze is required"
|
||||
command -v getconf >/dev/null 2>&1 || die "getconf is required"
|
||||
[ -d /run/systemd/system ] || die "systemd is not running as PID 1"
|
||||
[ -d /etc/systemd/system ] || die "/etc/systemd/system is missing"
|
||||
HOST_ARCH=$(normalized_host_arch)
|
||||
host_bits=$(getconf LONG_BIT 2>/dev/null || true)
|
||||
case "$HOST_ARCH" in
|
||||
x86_64|aarch64) expected_bits=64 ;;
|
||||
x86|arm) expected_bits=32 ;;
|
||||
esac
|
||||
[ "$host_bits" = "$expected_bits" ] \
|
||||
|| die "CPU architecture and userspace word size do not match: $HOST_ARCH/$host_bits-bit"
|
||||
[[ "$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
|
||||
@@ -62,28 +93,31 @@ preflight_host() {
|
||||
[ ! -e "$STATE_ROOT/setup/locked" ] \
|
||||
|| die "The setup wizard is already locked; use a normal repair reinstall"
|
||||
fi
|
||||
log "Environment verified: $(uname -sr), architecture=$HOST_ARCH, userspace=${host_bits}-bit, systemd"
|
||||
}
|
||||
|
||||
[ "$(id -u)" -eq 0 ] || die "Run with sudo or as root"
|
||||
[ -z "$RELEASE_TOKEN" ] || [ -z "$RELEASE_TOKEN_FILE" ] \
|
||||
validate_inputs() {
|
||||
[ "$(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
|
||||
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" ] \
|
||||
fi
|
||||
[ "$REINSTALL" = "true" ] || [ ! -e "$APP_ROOT/current" ] \
|
||||
|| die "Kaidi Finance is already installed; use the system update page"
|
||||
if [ -z "$RELEASE_API_URL" ]; then
|
||||
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 ] \
|
||||
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
|
||||
@@ -170,12 +204,26 @@ install_packages() {
|
||||
}
|
||||
|
||||
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)" ;;
|
||||
case "${HOST_ARCH:-$(normalized_host_arch)}" in
|
||||
x86_64) printf x86 ;;
|
||||
x86) printf i686 ;;
|
||||
aarch64) printf arm_64 ;;
|
||||
arm) printf arm ;;
|
||||
esac
|
||||
}
|
||||
|
||||
java_arch_matches_host() {
|
||||
local java_bin=$1 java_arch host_arch
|
||||
host_arch=${HOST_ARCH:-$(normalized_host_arch)}
|
||||
java_arch=$(
|
||||
"$java_bin" -XshowSettings:properties -version 2>&1 \
|
||||
| sed -n 's/^[[:space:]]*os\.arch = [[:space:]]*//p' \
|
||||
| head -n 1
|
||||
)
|
||||
case "$host_arch:$java_arch" in
|
||||
x86_64:amd64|x86_64:x86_64|x86:x86|x86:i386|x86:i486|x86:i586|x86:i686|\
|
||||
aarch64:aarch64|aarch64:arm64|arm:arm) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
@@ -194,6 +242,7 @@ system_java_home() {
|
||||
| head -n 1
|
||||
)
|
||||
[[ "$java_major" =~ ^[0-9]+$ ]] && [ "$java_major" -ge 17 ] || return 1
|
||||
java_arch_matches_host "$java_bin" || return 1
|
||||
resolved_java=$(readlink -f "$java_bin" 2>/dev/null || printf '%s' "$java_bin")
|
||||
case "$resolved_java" in
|
||||
*/bin/java) home=${resolved_java%/bin/java} ;;
|
||||
@@ -204,12 +253,14 @@ system_java_home() {
|
||||
}
|
||||
|
||||
prepare_java() {
|
||||
local api java_metadata package_metadata package_uuid java_url java_sha256 actual_sha256 system_home
|
||||
local api java_metadata package_metadata package_uuid java_url java_sha256 actual_sha256 system_home java_line
|
||||
if system_home=$(system_java_home); then
|
||||
JAVA_STAGED_DIR="$WORK_DIR/java.next"
|
||||
ln -s "$system_home" "$JAVA_STAGED_DIR"
|
||||
log "Using existing Java 17 runtime at $system_home"
|
||||
"$JAVA_STAGED_DIR/bin/java" -version
|
||||
mkdir -p "$JAVA_STAGED_DIR"
|
||||
log "Copying existing Java 17 runtime from $system_home into the managed application directory"
|
||||
COPYFILE_DISABLE=1 cp -R "$system_home/." "$JAVA_STAGED_DIR/"
|
||||
java_line=$("$JAVA_STAGED_DIR/bin/java" -version 2>&1 | head -n 1)
|
||||
log "Local Java verified: $java_line"
|
||||
return 0
|
||||
fi
|
||||
log "No local Java 17 runtime found; downloading the official Azul Java 17 runtime"
|
||||
@@ -231,18 +282,88 @@ prepare_java() {
|
||||
[ "$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
|
||||
java_arch_matches_host "$JAVA_STAGED_DIR/bin/java" \
|
||||
|| die "Downloaded Java runtime architecture does not match $HOST_ARCH"
|
||||
java_line=$("$JAVA_STAGED_DIR/bin/java" -version 2>&1 | head -n 1)
|
||||
log "Downloaded Java verified: $java_line"
|
||||
}
|
||||
|
||||
activate_java() {
|
||||
mkdir -p "$APP_ROOT/runtime"
|
||||
JAVA_PREVIOUS_DIR="$WORK_DIR/java.previous"
|
||||
if [ -e "$APP_ROOT/runtime/java" ]; then
|
||||
if [ -e "$APP_ROOT/runtime/java" ] || [ -L "$APP_ROOT/runtime/java" ]; then
|
||||
mv "$APP_ROOT/runtime/java" "$JAVA_PREVIOUS_DIR"
|
||||
fi
|
||||
mv "$JAVA_STAGED_DIR" "$APP_ROOT/runtime/java"
|
||||
chown -R root:"$SERVICE_GROUP" "$APP_ROOT/runtime/java"
|
||||
chmod -R u=rwX,g=rX,o= "$APP_ROOT/runtime/java"
|
||||
JAVA_ACTIVATED=true
|
||||
"$APP_ROOT/runtime/java/bin/java" -version
|
||||
}
|
||||
|
||||
ensure_service_identity() {
|
||||
local existing_home nologin_path
|
||||
command -v getent >/dev/null 2>&1 || die "getent is required"
|
||||
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"
|
||||
if ! id -nG "$SERVICE_USER" | tr ' ' '\n' | grep -Fxq "$SERVICE_GROUP"; then
|
||||
usermod --append --groups "$SERVICE_GROUP" "$SERVICE_USER"
|
||||
fi
|
||||
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"
|
||||
}
|
||||
|
||||
prepare_managed_layout() {
|
||||
command -v runuser >/dev/null 2>&1 || die "runuser is required"
|
||||
install -d -o root -g "$SERVICE_GROUP" -m 0750 \
|
||||
"$APP_ROOT" "$APP_ROOT/releases" "$APP_ROOT/runtime" "$APP_ROOT/bin"
|
||||
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0750 \
|
||||
"$STATE_ROOT" "$STATE_ROOT/files" "$STATE_ROOT/tmp" /var/log/kaidi
|
||||
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 root -m 0700 "$CONFIG_ROOT"
|
||||
}
|
||||
|
||||
secure_release_tree() {
|
||||
local release_dir=$1
|
||||
chown -R root:"$SERVICE_GROUP" "$release_dir"
|
||||
find "$release_dir" -type d -exec chmod 0750 {} +
|
||||
find "$release_dir" -type f -exec chmod 0640 {} +
|
||||
chmod 0750 "$release_dir/ops/update.sh"
|
||||
}
|
||||
|
||||
restore_security_contexts() {
|
||||
command -v restorecon >/dev/null 2>&1 || return 0
|
||||
restorecon -RF "$APP_ROOT" "$STATE_ROOT" "$UPDATE_STATE_ROOT" /var/log/kaidi \
|
||||
/etc/systemd/system/kaidi-finance.service \
|
||||
/etc/systemd/system/kaidi-update.service \
|
||||
/etc/systemd/system/kaidi-update.path >/dev/null 2>&1 \
|
||||
|| log "SELinux context restoration reported a warning; service access checks will decide whether installation can continue"
|
||||
}
|
||||
|
||||
verify_service_access() {
|
||||
# shellcheck disable=SC2016 # Positional parameters are expanded by the child shell.
|
||||
runuser -u "$SERVICE_USER" -- sh -c \
|
||||
'cd "$1" && test -r app.jar && test -r public/index.html' sh "$APP_ROOT/current" \
|
||||
|| die "$SERVICE_USER cannot traverse or read the active release"
|
||||
runuser -u "$SERVICE_USER" -- test -x "$APP_ROOT/runtime/java/bin/java" \
|
||||
|| die "$SERVICE_USER cannot execute the managed Java runtime"
|
||||
runuser -u "$SERVICE_USER" -- "$APP_ROOT/runtime/java/bin/java" -version >/dev/null 2>&1 \
|
||||
|| die "The managed Java runtime cannot execute as $SERVICE_USER"
|
||||
runuser -u "$SERVICE_USER" -- test -w "$STATE_ROOT/files" \
|
||||
|| die "$SERVICE_USER cannot write the file-storage directory"
|
||||
runuser -u "$SERVICE_USER" -- test -w "$UPDATE_STATE_ROOT/inbox" \
|
||||
|| die "$SERVICE_USER cannot write the update inbox"
|
||||
log "Service-user filesystem and Java access checks passed"
|
||||
}
|
||||
|
||||
random_secret() { openssl rand -base64 36 | tr -d '\n/+=' | cut -c1-36; }
|
||||
@@ -599,6 +720,12 @@ backup_managed_state() {
|
||||
|
||||
restore_unit_state() {
|
||||
local unit=$1 was_enabled=$2 was_active=$3
|
||||
if ! systemctl cat "$unit" >/dev/null 2>&1; then
|
||||
systemctl reset-failed "$unit" >/dev/null 2>&1 || true
|
||||
[ "$was_enabled" = false ] && [ "$was_active" = false ]
|
||||
return
|
||||
fi
|
||||
systemctl reset-failed "$unit" >/dev/null 2>&1 || true
|
||||
if [ "$was_enabled" = true ]; then
|
||||
systemctl enable "$unit" >/dev/null 2>&1
|
||||
else
|
||||
@@ -631,7 +758,7 @@ 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
|
||||
systemctl stop kaidi-update.path kaidi-finance.service >/dev/null 2>&1 || true
|
||||
for path in "${MANAGED_PATHS[@]}"; do
|
||||
restore_managed_path "$path" "$index" || rollback_failed=true
|
||||
index=$((index + 1))
|
||||
@@ -639,7 +766,7 @@ rollback_install() {
|
||||
if [ -n "$RELEASE_DIR" ]; then
|
||||
rm -rf -- "$RELEASE_DIR" || rollback_failed=true
|
||||
fi
|
||||
if [ -d "$JAVA_PREVIOUS_DIR" ]; then
|
||||
if [ -d "$JAVA_PREVIOUS_DIR" ] || [ -L "$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
|
||||
@@ -666,8 +793,10 @@ cleanup() {
|
||||
exit "$result"
|
||||
}
|
||||
|
||||
main() {
|
||||
trap cleanup EXIT
|
||||
|
||||
validate_inputs
|
||||
configure_app_port
|
||||
preflight_host
|
||||
install_packages
|
||||
@@ -714,25 +843,18 @@ find "$WORK_DIR/extracted" -type l -print -quit | grep -q . \
|
||||
[ "$(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"
|
||||
ensure_service_identity
|
||||
prepare_managed_layout
|
||||
prepare_java
|
||||
|
||||
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"
|
||||
secure_release_tree "$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"
|
||||
@@ -824,7 +946,15 @@ 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
|
||||
restore_security_contexts
|
||||
verify_service_access
|
||||
systemd-analyze verify \
|
||||
/etc/systemd/system/kaidi-finance.service \
|
||||
/etc/systemd/system/kaidi-update.service \
|
||||
/etc/systemd/system/kaidi-update.path >/dev/null \
|
||||
|| die "Installed systemd units failed validation"
|
||||
systemctl daemon-reload
|
||||
systemctl reset-failed kaidi-finance.service >/dev/null 2>&1 || true
|
||||
systemctl enable kaidi-finance.service
|
||||
systemctl restart kaidi-finance.service
|
||||
wait_for_health
|
||||
@@ -875,3 +1005,6 @@ 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
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
Description=Kaidi Finance System
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
StartLimitIntervalSec=60
|
||||
StartLimitBurst=3
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
|
||||
+53
-5
@@ -18,6 +18,8 @@ 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}
|
||||
SERVICE_USER=${KAIDI_SERVICE_USER:-kaidi}
|
||||
SERVICE_GROUP=${KAIDI_SERVICE_GROUP:-kaidi}
|
||||
HEALTH_URL=${KAIDI_HEALTH_URL:-http://127.0.0.1:18080/actuator/health}
|
||||
APP_INDEX_URL=${KAIDI_APP_INDEX_URL:-http://127.0.0.1:18080/}
|
||||
LOCK_FILE=$STATE_ROOT/update.lock
|
||||
@@ -38,9 +40,10 @@ case "$HEALTH_ATTEMPTS:$HEALTH_INTERVAL_SECONDS" in
|
||||
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"
|
||||
bootstrap_die() {
|
||||
printf '%s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
status() {
|
||||
state=$1
|
||||
@@ -77,6 +80,43 @@ fail() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
prepare_update_layout() {
|
||||
id "$SERVICE_USER" >/dev/null 2>&1 || bootstrap_die "Service user $SERVICE_USER is missing"
|
||||
id -nG "$SERVICE_USER" | tr ' ' '\n' | grep -Fxq "$SERVICE_GROUP" \
|
||||
|| bootstrap_die "Service user $SERVICE_USER is not a member of group $SERVICE_GROUP"
|
||||
command -v runuser >/dev/null 2>&1 || bootstrap_die "runuser is required"
|
||||
install -d -o root -g "$SERVICE_GROUP" -m 0750 \
|
||||
"$APP_ROOT" "$APP_ROOT/releases" "$APP_ROOT/runtime" "$APP_ROOT/bin" "$STATE_ROOT" \
|
||||
|| bootstrap_die "Managed application or update directories could not be prepared"
|
||||
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0750 "$STATE_ROOT/inbox" "$LOG_ROOT" \
|
||||
|| bootstrap_die "Writable update directories could not be prepared"
|
||||
install -d -o root -g root -m 0700 \
|
||||
"$PROCESSING_DIR" "$FAILED_REQUEST_ROOT" "$TRANSACTION_ROOT" "$STATE_ROOT/work" \
|
||||
"$BACKUP_ROOT" "$CACHE_ROOT" \
|
||||
|| bootstrap_die "Private update directories could not be prepared"
|
||||
}
|
||||
|
||||
secure_release_tree() {
|
||||
release_dir=$1
|
||||
chown -R root:"$SERVICE_GROUP" "$release_dir" || return 1
|
||||
find "$release_dir" -type d -exec chmod 0750 {} + || return 1
|
||||
find "$release_dir" -type f -exec chmod 0640 {} + || return 1
|
||||
chmod 0750 "$release_dir/ops/update.sh"
|
||||
}
|
||||
|
||||
verify_release_access() {
|
||||
release_dir=$1
|
||||
# shellcheck disable=SC2016 # Positional parameters are expanded by the child shell.
|
||||
runuser -u "$SERVICE_USER" -- sh -c \
|
||||
'cd "$1" && test -r app.jar && test -r public/index.html' sh "$release_dir" \
|
||||
|| return 1
|
||||
runuser -u "$SERVICE_USER" -- test -x "$APP_ROOT/runtime/java/bin/java" \
|
||||
|| return 1
|
||||
runuser -u "$SERVICE_USER" -- "$APP_ROOT/runtime/java/bin/java" -version >/dev/null 2>&1
|
||||
}
|
||||
|
||||
prepare_update_layout
|
||||
|
||||
# shellcheck disable=SC2329 # Invoked by the EXIT trap below.
|
||||
cleanup() {
|
||||
rc=$?
|
||||
@@ -329,6 +369,7 @@ rollback_active_transaction() {
|
||||
rollback_ok=false
|
||||
fi
|
||||
restore_operations || rollback_ok=false
|
||||
systemctl reset-failed "$SERVICE_NAME" >/dev/null 2>&1 || true
|
||||
systemctl start "$SERVICE_NAME" || rollback_ok=false
|
||||
if [ "$rollback_ok" = true ] && verify_app_surface; then
|
||||
remove_failed_release "$failed_release"
|
||||
@@ -520,8 +561,14 @@ if [ -e "$RELEASE_DIR" ]; then
|
||||
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"
|
||||
secure_release_tree "$RELEASE_DIR" || fail "Release ownership or permissions could not be secured"
|
||||
if command -v restorecon >/dev/null 2>&1; then
|
||||
restorecon -RF "$APP_ROOT" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if ! verify_release_access "$RELEASE_DIR"; then
|
||||
rm -rf "$RELEASE_DIR"
|
||||
fail "Service user cannot access the release or managed Java runtime"
|
||||
fi
|
||||
|
||||
PREVIOUS_TARGET=$(readlink "$APP_ROOT/current" 2>/dev/null || true)
|
||||
mkdir "$ACTIVE_TRANSACTION"
|
||||
@@ -552,6 +599,7 @@ write_transaction_value phase APP_SWITCHED
|
||||
|
||||
status RUNNING "Starting and verifying release $TARGET_VERSION" "$TARGET_VERSION"
|
||||
write_transaction_value phase HEALTH_CHECKING
|
||||
systemctl reset-failed "$SERVICE_NAME" >/dev/null 2>&1 || true
|
||||
if systemctl start "$SERVICE_NAME" \
|
||||
&& systemctl is-active --quiet kaidi-update.path \
|
||||
&& verify_app_surface; then
|
||||
|
||||
@@ -10,6 +10,10 @@ fail() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
mode_of() {
|
||||
stat -c '%a' "$1" 2>/dev/null || stat -f '%Lp' "$1"
|
||||
}
|
||||
|
||||
# Load only pure helper functions. The installer itself must never run in this fixture.
|
||||
{
|
||||
sed -n '/^decode_env_value()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
@@ -22,7 +26,9 @@ fail() {
|
||||
sed -n '/^configure_app_port()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^is_semver()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^write_env_file_preserving_unknown()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^normalized_host_arch()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^azul_arch()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^java_arch_matches_host()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^system_java_home()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^sha256_file()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^prepare_java()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
@@ -31,6 +37,8 @@ fail() {
|
||||
sed -n '/^download_release_asset()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^install_packages()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^preflight_database()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^secure_release_tree()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^restore_unit_state()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
} > "$WORK/helpers.sh"
|
||||
# shellcheck disable=SC1090,SC1091
|
||||
source "$WORK/helpers.sh"
|
||||
@@ -38,6 +46,35 @@ source "$WORK/helpers.sh"
|
||||
export SETUP_WIZARD=true
|
||||
preflight_database || fail 'setup wizard database preflight returned a failure status'
|
||||
|
||||
release_permissions="$WORK/release-permissions"
|
||||
mkdir -p "$release_permissions/public" "$release_permissions/ops"
|
||||
printf 'jar\n' > "$release_permissions/app.jar"
|
||||
printf 'html\n' > "$release_permissions/public/index.html"
|
||||
printf '#!/bin/sh\n' > "$release_permissions/ops/update.sh"
|
||||
chmod -R 0777 "$release_permissions"
|
||||
(
|
||||
# shellcheck disable=SC2329 # Invoked indirectly by the sourced installer helper.
|
||||
chown() { return 0; }
|
||||
export SERVICE_GROUP=fixture
|
||||
secure_release_tree "$release_permissions"
|
||||
)
|
||||
[ "$(mode_of "$release_permissions")" = 750 ] || fail 'release root mode is not 0750'
|
||||
[ "$(mode_of "$release_permissions/public")" = 750 ] || fail 'release directory mode is not 0750'
|
||||
[ "$(mode_of "$release_permissions/app.jar")" = 640 ] || fail 'release file mode is not 0640'
|
||||
[ "$(mode_of "$release_permissions/ops/update.sh")" = 750 ] || fail 'release updater mode is not 0750'
|
||||
|
||||
(
|
||||
# shellcheck disable=SC2329 # Invoked indirectly by the sourced installer helper.
|
||||
systemctl() {
|
||||
case "$1" in
|
||||
cat) return 1 ;;
|
||||
reset-failed) return 0 ;;
|
||||
*) return 97 ;;
|
||||
esac
|
||||
}
|
||||
restore_unit_state missing.service false false
|
||||
) || fail 'rollback treated an absent first-install unit as an incomplete restoration'
|
||||
|
||||
# shellcheck disable=SC2034 # Referenced by the extracted installer helper.
|
||||
REINSTALL=true
|
||||
# shellcheck disable=SC2034 # Referenced by the extracted installer helper.
|
||||
@@ -137,10 +174,23 @@ for arch in i386 i486 i586 i686; do
|
||||
ARCH_FIXTURE=$arch
|
||||
[ "$(azul_arch)" = i686 ] || fail "$arch did not map to the Azul i686 runtime"
|
||||
done
|
||||
(
|
||||
ARCH_FIXTURE=x86_64
|
||||
# shellcheck disable=SC2329 # Invoked indirectly by the sourced architecture helper.
|
||||
getconf() { printf '32\n'; }
|
||||
[ "$(normalized_host_arch)" = x86 ] \
|
||||
|| fail '32-bit userspace on an x86_64 kernel was not normalized to x86'
|
||||
export HOST_ARCH=x86
|
||||
[ "$(azul_arch)" = i686 ] \
|
||||
|| fail '32-bit userspace on an x86_64 kernel did not select the i686 Java runtime'
|
||||
)
|
||||
|
||||
mkdir -p "$WORK/fake-jre/bin"
|
||||
cat > "$WORK/fake-jre/bin/java" <<'JAVA'
|
||||
#!/usr/bin/env sh
|
||||
if [ "${1:-}" = '-XshowSettings:properties' ]; then
|
||||
printf ' os.arch = x86\n' >&2
|
||||
fi
|
||||
printf 'openjdk version "17-fixture"\n' >&2
|
||||
JAVA
|
||||
chmod 0755 "$WORK/fake-jre/bin/java"
|
||||
@@ -168,9 +218,10 @@ prepare_java >/dev/null 2>&1
|
||||
rm -rf "$JAVA_STAGED_DIR"
|
||||
export KAIDI_JAVA_HOME="$WORK/fake-jre"
|
||||
prepare_java >/dev/null 2>&1
|
||||
[ -L "$JAVA_STAGED_DIR" ] || fail 'existing Java 17 runtime was not reused'
|
||||
[ "$(readlink -f "$JAVA_STAGED_DIR")" = "$(readlink -f "$WORK/fake-jre")" ] \
|
||||
|| fail 'installer linked an unexpected local Java runtime'
|
||||
[ -d "$JAVA_STAGED_DIR" ] && [ ! -L "$JAVA_STAGED_DIR" ] \
|
||||
|| fail 'existing Java 17 runtime was not copied into managed storage'
|
||||
cmp -s "$WORK/fake-jre/bin/java" "$JAVA_STAGED_DIR/bin/java" \
|
||||
|| fail 'installer staged an unexpected local Java runtime'
|
||||
|
||||
export RELEASE_API_URL=https://gitea.fixture.invalid/api/v1/repos/ERP-Team/kaidi/releases/latest
|
||||
RELEASE_TOKEN=fixture-read-only-token
|
||||
@@ -237,14 +288,35 @@ grep -Fq 'download "$api" "$java_metadata"' "$ROOT/deploy/install.sh" \
|
||||
# shellcheck disable=SC2016 # Match literal installer source.
|
||||
grep -Fq 'java_sha256=$(jq -er' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer no longer obtains the Java runtime SHA-256'
|
||||
grep -Fq 'Using existing Java 17 runtime' "$ROOT/deploy/install.sh" \
|
||||
grep -Fq 'Copying existing Java 17 runtime' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer no longer reuses a local Java 17 runtime'
|
||||
# shellcheck disable=SC2016 # Match literal installer source.
|
||||
grep -Fq 'cp -R "$system_home/." "$JAVA_STAGED_DIR/"' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer no longer copies a local Java runtime into managed storage'
|
||||
# shellcheck disable=SC2016 # Match literal installer source.
|
||||
! grep -Fq 'ln -s "$system_home"' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer must not link the service to an externally managed Java directory'
|
||||
# shellcheck disable=SC2016 # Match the literal installer command.
|
||||
grep -Fq -- '--connect-timeout 1 --max-time 2 "$HEALTH_URL" 2>/dev/null' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer no longer performs a quiet bounded health check'
|
||||
grep -Fq 'restart_count" -ge 3' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer no longer stops early after repeated service restarts'
|
||||
# shellcheck disable=SC2016 # Match literal installer source.
|
||||
grep -Fq 'install -d -o root -g "$SERVICE_GROUP" -m 0750' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer no longer creates traversable root-owned application directories'
|
||||
# shellcheck disable=SC2016 # Match literal installer source.
|
||||
grep -Fq 'find "$release_dir" -type d -exec chmod 0750 {} +' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer no longer secures release directory traversal permissions'
|
||||
# shellcheck disable=SC2016 # Match literal installer source.
|
||||
grep -Fq 'runuser -u "$SERVICE_USER" -- sh -c' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer no longer validates the release as the service user'
|
||||
grep -Fq 'systemctl reset-failed kaidi-finance.service' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer no longer resets stale systemd failure state'
|
||||
[ "$(tail -n 1 "$ROOT/deploy/install.sh")" = 'main "$@"' ] \
|
||||
|| fail 'installer can execute before the complete curl stream is parsed'
|
||||
grep -Fqx 'StartLimitBurst=3' "$ROOT/deploy/systemd/kaidi-finance.service" \
|
||||
|| fail 'application service no longer has a bounded restart burst'
|
||||
# shellcheck disable=SC2016 # Match literal installer source.
|
||||
grep -Fq '[ "$actual_sha256" = "$java_sha256" ]' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer no longer verifies the Java runtime SHA-256'
|
||||
grep -Fq 'https://git.awaioi.com/api/v1/repos/ERP-Team/kaidi/releases/latest' "$ROOT/deploy/install.sh" \
|
||||
@@ -301,8 +373,8 @@ grep -Fq 'EnvironmentFile=-/var/lib/kaidi/setup/application.env' \
|
||||
grep -Fq 'EnvironmentFile=-/var/lib/kaidi/setup/application.env' \
|
||||
"$ROOT/deploy/systemd/kaidi-update.service" \
|
||||
|| fail 'update service no longer loads setup database overrides'
|
||||
# shellcheck disable=SC2016 # Match the literal installer source.
|
||||
grep -Fq 'chown root:kaidi "$UPDATE_STATE_ROOT"' "$ROOT/deploy/install.sh" \
|
||||
# shellcheck disable=SC2016 # Match literal installer source.
|
||||
grep -Fq 'install -d -o root -g "$SERVICE_GROUP" -m 0750 "$UPDATE_STATE_ROOT"' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'update state parent is not group-accessible to the application user'
|
||||
grep -Fq 'install.sh | sudo bash' "$ROOT/README.md" \
|
||||
|| fail 'README does not document the public one-line setup-wizard install path'
|
||||
|
||||
@@ -11,6 +11,10 @@ fail() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
mode_of() {
|
||||
stat -c '%a' "$1" 2>/dev/null || stat -f '%Lp' "$1"
|
||||
}
|
||||
|
||||
sed -n '/^is_semver()/,/^}/p' "$ROOT/deploy/update.sh" > "$WORK/update-semver.sh"
|
||||
# shellcheck disable=SC1090,SC1091
|
||||
source "$WORK/update-semver.sh"
|
||||
@@ -21,6 +25,20 @@ for version in 01.2.3 1.02.3 1.2.03 1.2.3-01 1.2.3-alpha..1; do
|
||||
! is_semver "$version" || fail "updater accepted invalid SemVer $version"
|
||||
done
|
||||
|
||||
missing_service_user="kaidi-fixture-missing-$$"
|
||||
if KAIDI_APP_ROOT="$WORK/bootstrap/app" \
|
||||
KAIDI_UPDATE_STATE_ROOT="$WORK/bootstrap/state" \
|
||||
KAIDI_LOG_ROOT="$WORK/bootstrap/log" \
|
||||
KAIDI_SERVICE_USER="$missing_service_user" \
|
||||
KAIDI_SERVICE_GROUP="$missing_service_user" \
|
||||
sh "$ROOT/deploy/update.sh" > "$WORK/bootstrap.log" 2>&1; then
|
||||
fail 'updater accepted a missing service identity during bootstrap'
|
||||
fi
|
||||
grep -Fq "Service user $missing_service_user is missing" "$WORK/bootstrap.log" \
|
||||
|| fail 'updater bootstrap failure did not preserve its diagnostic'
|
||||
! grep -Eq 'No such file|nonexistent directory|cannot create' "$WORK/bootstrap.log" \
|
||||
|| fail 'updater bootstrap failure was masked by an unavailable status directory'
|
||||
|
||||
write_mock_commands() {
|
||||
local mock_bin=$1
|
||||
mkdir -p "$mock_bin"
|
||||
@@ -99,6 +117,33 @@ SH
|
||||
cat > "$mock_bin/chown" <<'SH'
|
||||
#!/bin/sh
|
||||
exit 0
|
||||
SH
|
||||
|
||||
cat > "$mock_bin/runuser" <<'SH'
|
||||
#!/bin/sh
|
||||
[ "${1:-}" = -u ] || exit 2
|
||||
shift 2
|
||||
[ "${1:-}" = -- ] && shift
|
||||
exec "$@"
|
||||
SH
|
||||
|
||||
cat > "$mock_bin/install" <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
if [[ " $* " != *" -d "* ]]; then
|
||||
exec /usr/bin/install "$@"
|
||||
fi
|
||||
mode=0755
|
||||
paths=()
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
-d) shift ;;
|
||||
-o|-g) shift 2 ;;
|
||||
-m) mode=$2; shift 2 ;;
|
||||
*) paths+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
mkdir -p "${paths[@]}"
|
||||
chmod "$mode" "${paths[@]}"
|
||||
SH
|
||||
|
||||
cat > "$mock_bin/mv" <<'SH'
|
||||
@@ -170,13 +215,18 @@ prepare_installation() {
|
||||
local fixture=$1
|
||||
local version=$2
|
||||
mkdir -p "$fixture/app/releases/1.0.0-preview.1/public" "$fixture/app/bin" \
|
||||
"$fixture/state/inbox" "$fixture/systemd" "$fixture/log"
|
||||
"$fixture/app/runtime/java/bin" "$fixture/state/inbox" "$fixture/systemd" "$fixture/log"
|
||||
printf 'old application\n' > "$fixture/app/releases/1.0.0-preview.1/app.jar"
|
||||
printf '<!doctype html><title>old</title>\n' > "$fixture/app/releases/1.0.0-preview.1/public/index.html"
|
||||
printf '1.0.0-preview.1\n' > "$fixture/app/releases/1.0.0-preview.1/VERSION"
|
||||
ln -s "$fixture/app/releases/1.0.0-preview.1" "$fixture/app/current"
|
||||
printf 'old update.sh\n' > "$fixture/app/bin/update.sh"
|
||||
chmod 0755 "$fixture/app/bin/update.sh"
|
||||
cat > "$fixture/app/runtime/java/bin/java" <<'SH'
|
||||
#!/bin/sh
|
||||
exit 0
|
||||
SH
|
||||
chmod 0755 "$fixture/app/runtime/java/bin/java"
|
||||
for name in kaidi-finance.service kaidi-update.service kaidi-update.path; do
|
||||
printf 'old %s\n' "$name" > "$fixture/systemd/$name"
|
||||
done
|
||||
@@ -222,6 +272,8 @@ run_update() {
|
||||
MOCK_RELEASE_ORIGIN="${FIXTURE_RELEASE_ORIGIN:-https://release.fixture.invalid}" \
|
||||
MOCK_EXPECT_RELEASE_TOKEN="${FIXTURE_RELEASE_TOKEN-}" \
|
||||
MOCK_SYSTEMCTL_LOG="$fixture/systemctl.log" \
|
||||
KAIDI_SERVICE_USER="$(id -un)" \
|
||||
KAIDI_SERVICE_GROUP="$(id -gn)" \
|
||||
KAIDI_APP_ROOT="$fixture/app" \
|
||||
KAIDI_UPDATE_STATE_ROOT="$fixture/state" \
|
||||
KAIDI_LOG_ROOT="$fixture/log" \
|
||||
@@ -310,6 +362,10 @@ assert_success_case() {
|
||||
|| fail 'success case left a claimed request behind'
|
||||
grep -qx 'daemon-reload' "$fixture/systemctl.log" || fail 'systemd units were not reloaded'
|
||||
! grep -qi nginx "$fixture/systemctl.log" || fail 'updater unexpectedly managed Nginx'
|
||||
[ "$(mode_of "$fixture/app")" = 750 ] || fail 'application root is not traversable by the service group'
|
||||
[ "$(mode_of "$fixture/app/releases/$version")" = 750 ] || fail 'release root mode is not 0750'
|
||||
[ "$(mode_of "$fixture/app/current/app.jar")" = 640 ] || fail 'release file mode is not 0640'
|
||||
[ "$(mode_of "$fixture/app/current/ops/update.sh")" = 750 ] || fail 'release updater mode is not 0750'
|
||||
}
|
||||
|
||||
assert_rollback_case() {
|
||||
|
||||
Reference in New Issue
Block a user