Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9d5d678dc | ||
|
|
e21978c25c | ||
|
|
f9545a9d0f | ||
|
|
4d6a9307e5 | ||
|
|
b792f3f21b |
@@ -58,6 +58,11 @@ jobs:
|
|||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
printf '%s' "$RELEASE_PRIVATE_KEY_B64" | base64 --decode > "$key_file"
|
printf '%s' "$RELEASE_PRIVATE_KEY_B64" | base64 --decode > "$key_file"
|
||||||
export ERP_RELEASE_PRIVATE_KEY_FILE="$key_file"
|
export ERP_RELEASE_PRIVATE_KEY_FILE="$key_file"
|
||||||
|
bash tests/release-scripts.test.sh
|
||||||
|
(
|
||||||
|
cd oa-backend
|
||||||
|
./gradlew test installerTest
|
||||||
|
)
|
||||||
bash scripts/package-release.sh "$version"
|
bash scripts/package-release.sh "$version"
|
||||||
|
|
||||||
owner="${repository%%/*}"
|
owner="${repository%%/*}"
|
||||||
@@ -97,6 +102,9 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
release_id="$(python3 -c 'import json; print(json.load(open("release.json"))["id"])')"
|
release_id="$(python3 -c 'import json; print(json.load(open("release.json"))["id"])')"
|
||||||
|
curl "${curl_protocols[@]}" --fail-with-body --silent --show-error --location --retry 3 \
|
||||||
|
--header "$auth_header" --header 'Accept: application/json' \
|
||||||
|
--output release.json "$release_api/$release_id"
|
||||||
for asset in "dist/kaidi-erp-$version.tar.gz" "dist/kaidi-erp-installer-$version.jar" dist/SHA256SUMS dist/SHA256SUMS.sig; do
|
for asset in "dist/kaidi-erp-$version.tar.gz" "dist/kaidi-erp-installer-$version.jar" dist/SHA256SUMS dist/SHA256SUMS.sig; do
|
||||||
name="$(basename "$asset")"
|
name="$(basename "$asset")"
|
||||||
encoded_name="$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$name")"
|
encoded_name="$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$name")"
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ Administrator -> System Update UI -> erp-update helper
|
|||||||
|- README.md
|
|- README.md
|
||||||
|- run.command # macOS 本地预览与 ngrok 启动器
|
|- run.command # macOS 本地预览与 ngrok 启动器
|
||||||
|- install.sh # 非 Docker 一键安装器
|
|- install.sh # 非 Docker 一键安装器
|
||||||
|
|- uninstall.sh # Linux 完整卸载与数据库 schema 重置
|
||||||
|- distribution/
|
|- distribution/
|
||||||
| |- bin/erp-run # 正式环境应用启动器
|
| |- bin/erp-run # 正式环境应用启动器
|
||||||
| `- bin/erp-update # 下载、校验、切换和回滚助手
|
| `- bin/erp-update # 下载、校验、切换和回滚助手
|
||||||
@@ -188,6 +189,8 @@ Setup URL: http://192.168.1.20:8091/?token=<one-time-token>
|
|||||||
|
|
||||||
首次打开该地址进入网页向导,依次完成环境检查、PostgreSQL 地址/端口/库名/账号/密码/SSL 测试、管理员账号/姓名/密码设置、数据库迁移和初始化。项目当前没有 Redis 依赖,因此向导不会显示 Redis 配置项。正式服务真实健康检查通过后,启动器才会原子写入安装锁并物理删除 `installer/` 和 `install.pending`。
|
首次打开该地址进入网页向导,依次完成环境检查、PostgreSQL 地址/端口/库名/账号/密码/SSL 测试、管理员账号/姓名/密码设置、数据库迁移和初始化。项目当前没有 Redis 依赖,因此向导不会显示 Redis 配置项。正式服务真实健康检查通过后,启动器才会原子写入安装锁并物理删除 `installer/` 和 `install.pending`。
|
||||||
|
|
||||||
|
PostgreSQL 必须使用专用空数据库,网页中填写的账号必须是该数据库的所有者。该约束保证账号拥有 `public` schema 建表权限,并能持有安装器创建的 `pg_trgm` 扩展;只授予 `CONNECT` 权限不足以完成迁移。
|
||||||
|
|
||||||
### macOS
|
### macOS
|
||||||
|
|
||||||
macOS 需要 Homebrew,并使用已有 PostgreSQL:
|
macOS 需要 Homebrew,并使用已有 PostgreSQL:
|
||||||
@@ -204,14 +207,37 @@ curl -fsSL https://git.example.com/awaioi/ERP/raw/branch/main/install.sh \
|
|||||||
当前 Gitea 地址 `http://38.76.196.225:10099` 只允许用于开发验收:
|
当前 Gitea 地址 `http://38.76.196.225:10099` 只允许用于开发验收:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL http://38.76.196.225:10099/awaioi/ERP/raw/branch/main/install.sh \
|
(
|
||||||
| sudo -E bash -s -- \
|
set -e
|
||||||
--gitea-url http://38.76.196.225:10099 \
|
tmp="$(mktemp)"
|
||||||
--repository awaioi/ERP \
|
trap 'rm -f -- "$tmp"' EXIT
|
||||||
--allow-insecure
|
curl -fsSL http://38.76.196.225:10099/awaioi/ERP/raw/tag/v0.3.1/install.sh -o "$tmp"
|
||||||
|
printf '%s %s\n' '89a3c45e76f500c9475cb596ea29e3518bfafdc59f36dd3c7b316ed3cdd0448c' "$tmp" | sha256sum -c -
|
||||||
|
sudo -E bash "$tmp" \
|
||||||
|
--gitea-url http://38.76.196.225:10099 \
|
||||||
|
--repository awaioi/ERP \
|
||||||
|
--allow-insecure
|
||||||
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
只有首个 Release 发布后这条命令才可下载安装包。HTTP 会暴露请求、Release 元数据和可能使用的访问令牌,不得作为生产方案。
|
只有 `v0.3.1` Release 发布后这条命令才可下载安装包。固定 tag 和 SHA-256 只用于保护当前 HTTP 引导脚本不被传输途中篡改;Release 资产仍会继续执行 Ed25519 和 SHA-256 双重校验。HTTP 会暴露请求、Release 元数据和可能使用的访问令牌,不得作为生产方案。
|
||||||
|
|
||||||
|
### 完整卸载后重装
|
||||||
|
|
||||||
|
以下命令具有破坏性:它会先停止服务,使用现有配置中的 ERP 数据库账号删除并重建目标数据库的 `public` schema,然后删除 systemd unit、程序、配置、状态和日志。脚本只允许数据库所有者执行 schema 清理,并拒绝 `postgres`、`template0`、`template1` 和危险文件路径。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
(
|
||||||
|
set -e
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
trap 'rm -f -- "$tmp"' EXIT
|
||||||
|
curl -fsSL http://38.76.196.225:10099/awaioi/ERP/raw/tag/v0.3.1/uninstall.sh -o "$tmp"
|
||||||
|
printf '%s %s\n' '98c56fed2fd4d01874e4ab5a1a4f3ec42ec3e29b315ffd87385a95488d587546' "$tmp" | sha256sum -c -
|
||||||
|
sudo -E bash "$tmp" --purge-database --yes
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
卸载器会先核对安装配置、路径和 systemd 停止状态,再清理数据库和文件。卸载成功后,再执行上面的 Linux 一键安装命令。不要对包含其他系统数据的共享数据库运行此命令。
|
||||||
|
|
||||||
### 正式安装目录
|
### 正式安装目录
|
||||||
|
|
||||||
@@ -263,7 +289,7 @@ ERP_UPDATE_BACKUP_MODE=pg_dump
|
|||||||
|
|
||||||
## Gitea Release 发布
|
## Gitea Release 发布
|
||||||
|
|
||||||
推送 `v*` tag 会触发 `.gitea/workflows/release.yml`。流水线会构建前端、生成 PostgreSQL-only JAR、打包、签名,并创建或更新对应 Gitea Release。
|
推送 `v*` tag 会触发 `.gitea/workflows/release.yml`。流水线会先执行 shell、后端和独立安装器测试,再构建前端、生成 PostgreSQL-only JAR、打包、签名,并创建或更新对应 Gitea Release。
|
||||||
|
|
||||||
### Actions 前置配置
|
### Actions 前置配置
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ load_configuration() {
|
|||||||
INSTALLER_JAR="${ERP_INSTALLER_JAR:-$INSTALL_ROOT/installer/kaidi-erp-installer.jar}"
|
INSTALLER_JAR="${ERP_INSTALLER_JAR:-$INSTALL_ROOT/installer/kaidi-erp-installer.jar}"
|
||||||
PENDING_FILE="${ERP_INSTALL_PENDING_FILE:-$INSTALL_ROOT/state/install.pending}"
|
PENDING_FILE="${ERP_INSTALL_PENDING_FILE:-$INSTALL_ROOT/state/install.pending}"
|
||||||
INSTALL_LOCK_FILE="${ERP_INSTALL_LOCK_FILE:-$INSTALL_ROOT/state/install.lock}"
|
INSTALL_LOCK_FILE="${ERP_INSTALL_LOCK_FILE:-$INSTALL_ROOT/state/install.lock}"
|
||||||
|
INSTALL_LOG_FILE="${ERP_INSTALL_LOG_FILE:-$(dirname "$PENDING_FILE")/install-formal.log}"
|
||||||
HEALTH_URL="${ERP_HEALTH_URL:-http://127.0.0.1:${SERVER_PORT:-8091}/api/oa/health}"
|
HEALTH_URL="${ERP_HEALTH_URL:-http://127.0.0.1:${SERVER_PORT:-8091}/api/oa/health}"
|
||||||
HEALTH_TIMEOUT_SECONDS="${ERP_INSTALL_HEALTH_TIMEOUT_SECONDS:-240}"
|
HEALTH_TIMEOUT_SECONDS="${ERP_INSTALL_HEALTH_TIMEOUT_SECONDS:-240}"
|
||||||
HEALTH_POLL_SECONDS="${ERP_UPDATE_HEALTH_POLL_SECONDS:-2}"
|
HEALTH_POLL_SECONDS="${ERP_UPDATE_HEALTH_POLL_SECONDS:-2}"
|
||||||
@@ -98,10 +99,14 @@ run_pending_formal_application() {
|
|||||||
[[ "$HEALTH_POLL_SECONDS" =~ ^[1-9][0-9]*$ ]] || fail 'invalid health poll interval'
|
[[ "$HEALTH_POLL_SECONDS" =~ ^[1-9][0-9]*$ ]] || fail 'invalid health poll interval'
|
||||||
|
|
||||||
say 'Starting the formal PostgreSQL application'
|
say 'Starting the formal PostgreSQL application'
|
||||||
|
mkdir -p "$(dirname "$INSTALL_LOG_FILE")"
|
||||||
|
: > "$INSTALL_LOG_FILE"
|
||||||
|
chmod 600 "$INSTALL_LOG_FILE"
|
||||||
"$JAVA_BIN" "${JAVA_OPTS[@]}" \
|
"$JAVA_BIN" "${JAVA_OPTS[@]}" \
|
||||||
-jar "$JAR_PATH" \
|
-jar "$JAR_PATH" \
|
||||||
--spring.profiles.active=postgres \
|
--spring.profiles.active=postgres \
|
||||||
--server.port="${SERVER_PORT:-8091}" &
|
--server.port="${SERVER_PORT:-8091}" \
|
||||||
|
> >(tee -a "$INSTALL_LOG_FILE") 2>&1 &
|
||||||
APP_PID=$!
|
APP_PID=$!
|
||||||
write_pid "$APP_PID"
|
write_pid "$APP_PID"
|
||||||
|
|
||||||
@@ -118,6 +123,7 @@ run_pending_formal_application() {
|
|||||||
local status=$?
|
local status=$?
|
||||||
set -e
|
set -e
|
||||||
clear_pid "$APP_PID"
|
clear_pid "$APP_PID"
|
||||||
|
say "Formal application diagnostics: $INSTALL_LOG_FILE"
|
||||||
fail "formal application exited before becoming healthy (status $status)"
|
fail "formal application exited before becoming healthy (status $status)"
|
||||||
fi
|
fi
|
||||||
if curl -fsS --connect-timeout 2 --max-time 5 "$HEALTH_URL" >/dev/null 2>&1; then
|
if curl -fsS --connect-timeout 2 --max-time 5 "$HEALTH_URL" >/dev/null 2>&1; then
|
||||||
@@ -137,6 +143,7 @@ run_pending_formal_application() {
|
|||||||
wait "$APP_PID"
|
wait "$APP_PID"
|
||||||
set -e
|
set -e
|
||||||
clear_pid "$APP_PID"
|
clear_pid "$APP_PID"
|
||||||
|
say "Formal application diagnostics: $INSTALL_LOG_FILE"
|
||||||
fail "formal application did not become healthy within ${HEALTH_TIMEOUT_SECONDS} seconds"
|
fail "formal application did not become healthy within ${HEALTH_TIMEOUT_SECONDS} seconds"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,11 +42,42 @@ curl -fsSL https://git.example.com/awaioi/ERP/raw/branch/main/install.sh \
|
|||||||
--repository awaioi/ERP
|
--repository awaioi/ERP
|
||||||
```
|
```
|
||||||
|
|
||||||
命令行只检查并安装 Java 17+、curl、tar、Python 3 和 OpenSSL 3,然后启动独立安装器并输出带一次性 token 的网页地址。数据库、管理员和密码全部在首次网页向导填写;安装器会真实测试 PostgreSQL 15+ 和 `pg_trgm`,迁移完成并确认正式服务健康后才写 `install.lock`,随后物理删除安装器目录。
|
命令行只检查并安装 Java 17+、curl、tar、Python 3 和 OpenSSL 3,然后启动独立安装器并输出带一次性 token 的网页地址。数据库、管理员和密码全部在首次网页向导填写;安装器会真实测试 PostgreSQL 15+、数据库所有权、`public` schema 建表权限和 `pg_trgm` 所有权,迁移完成并确认正式服务健康后才写 `install.lock`,随后物理删除安装器目录。
|
||||||
|
|
||||||
|
目标 PostgreSQL 必须是专用空数据库,网页中填写的账号必须是该数据库的所有者。只拥有连接权限的账号会在网页连接测试阶段被拒绝,不再等到 Flyway 迁移后才显示笼统错误。
|
||||||
|
|
||||||
Linux 生产服务要求主机使用 systemd;没有 systemd 的容器、WSL 或精简系统只能显式使用 `--no-service` 做开发验收,在线更新也会保持关闭。
|
Linux 生产服务要求主机使用 systemd;没有 systemd 的容器、WSL 或精简系统只能显式使用 `--no-service` 做开发验收,在线更新也会保持关闭。
|
||||||
|
|
||||||
当前 `http://38.76.196.225:10099` 仅用于开发测试,必须同时传入 `--allow-insecure`。HTTP 会暴露安装脚本、Release 元数据和 Gitea token,不应作为生产部署方式。
|
当前 `http://38.76.196.225:10099` 仅用于开发测试,安装器必须同时传入 `--allow-insecure`。在没有 HTTPS 的情况下,必须从固定 tag 下载引导脚本并验证本版本记录的 SHA-256,禁止把可变的 `main` 分支脚本直接管道给 root。HTTP 仍会暴露请求、Release 元数据和 Gitea token,不应作为生产部署方式。
|
||||||
|
|
||||||
|
当前 `v0.3.1` 安装命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
(
|
||||||
|
set -e
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
trap 'rm -f -- "$tmp"' EXIT
|
||||||
|
curl -fsSL http://38.76.196.225:10099/awaioi/ERP/raw/tag/v0.3.1/install.sh -o "$tmp"
|
||||||
|
printf '%s %s\n' '89a3c45e76f500c9475cb596ea29e3518bfafdc59f36dd3c7b316ed3cdd0448c' "$tmp" | sha256sum -c -
|
||||||
|
sudo -E bash "$tmp" --gitea-url http://38.76.196.225:10099 \
|
||||||
|
--repository awaioi/ERP --allow-insecure
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
完整卸载并清空本项目数据库 schema 后重装:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
(
|
||||||
|
set -e
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
trap 'rm -f -- "$tmp"' EXIT
|
||||||
|
curl -fsSL http://38.76.196.225:10099/awaioi/ERP/raw/tag/v0.3.1/uninstall.sh -o "$tmp"
|
||||||
|
printf '%s %s\n' '98c56fed2fd4d01874e4ab5a1a4f3ec42ec3e29b315ffd87385a95488d587546' "$tmp" | sha256sum -c -
|
||||||
|
sudo -E bash "$tmp" --purge-database --yes
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
该命令会先确认安装路径与 `erp.env` 一致、systemd 服务已经停止,再删除目标数据库 `public` schema 中的全部对象以及 `/opt/kaidi-erp`、`/etc/kaidi-erp`、`/var/lib/kaidi-erp`、`/var/log/kaidi-erp`。只允许对 Kaidi ERP 专用数据库执行。
|
||||||
|
|
||||||
## 在线更新
|
## 在线更新
|
||||||
|
|
||||||
|
|||||||
+10
-4
@@ -394,6 +394,8 @@ write_bootstrap_configuration() {
|
|||||||
shell_setting ERP_INSTALL_PENDING_FILE "$PENDING_FILE"
|
shell_setting ERP_INSTALL_PENDING_FILE "$PENDING_FILE"
|
||||||
shell_setting ERP_INSTALL_LOCK_FILE "$INSTALL_LOCK_FILE"
|
shell_setting ERP_INSTALL_LOCK_FILE "$INSTALL_LOCK_FILE"
|
||||||
shell_setting ERP_INSTALL_OPERATION_LOCK_FILE "$OPERATION_LOCK_FILE"
|
shell_setting ERP_INSTALL_OPERATION_LOCK_FILE "$OPERATION_LOCK_FILE"
|
||||||
|
shell_setting ERP_INSTALL_LOG_FILE "$STATE_ROOT/install-formal.log"
|
||||||
|
shell_setting ERP_INSTALL_HEALTH_TIMEOUT_SECONDS "${ERP_INSTALL_HEALTH_TIMEOUT_SECONDS:-240}"
|
||||||
shell_setting ERP_SETUP_TOKEN "$SETUP_TOKEN"
|
shell_setting ERP_SETUP_TOKEN "$SETUP_TOKEN"
|
||||||
shell_setting ERP_INSTALLER_JAR "$INSTALL_ROOT/installer/kaidi-erp-installer.jar"
|
shell_setting ERP_INSTALLER_JAR "$INSTALL_ROOT/installer/kaidi-erp-installer.jar"
|
||||||
shell_setting ERP_JAR_PATH "$INSTALL_ROOT/current/app/kaidi-erp.jar"
|
shell_setting ERP_JAR_PATH "$INSTALL_ROOT/current/app/kaidi-erp.jar"
|
||||||
@@ -442,8 +444,8 @@ User=$ERP_USER
|
|||||||
Group=$ERP_GROUP
|
Group=$ERP_GROUP
|
||||||
Environment="ERP_INSTALL_ROOT=$INSTALL_ROOT"
|
Environment="ERP_INSTALL_ROOT=$INSTALL_ROOT"
|
||||||
Environment="ERP_CONFIG_FILE=$CONFIG_FILE"
|
Environment="ERP_CONFIG_FILE=$CONFIG_FILE"
|
||||||
WorkingDirectory="$INSTALL_ROOT"
|
WorkingDirectory=$INSTALL_ROOT
|
||||||
ExecStart="$INSTALL_ROOT/current/bin/erp-run"
|
ExecStart=$INSTALL_ROOT/current/bin/erp-run
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=3
|
RestartSec=3
|
||||||
TimeoutStopSec=90
|
TimeoutStopSec=90
|
||||||
@@ -451,14 +453,18 @@ SuccessExitStatus=143
|
|||||||
UMask=0077
|
UMask=0077
|
||||||
NoNewPrivileges=true
|
NoNewPrivileges=true
|
||||||
PrivateTmp=true
|
PrivateTmp=true
|
||||||
ProtectSystem=strict
|
ProtectSystem=full
|
||||||
ProtectHome=true
|
ProtectHome=true
|
||||||
ReadWritePaths="$INSTALL_ROOT" "$CONFIG_ROOT" "$STATE_ROOT" "$LOG_ROOT"
|
ReadWritePaths=$INSTALL_ROOT $CONFIG_ROOT $STATE_ROOT $LOG_ROOT
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
EOF
|
EOF
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
|
if command -v systemd-analyze >/dev/null 2>&1; then
|
||||||
|
systemd-analyze verify /etc/systemd/system/kaidi-erp.service \
|
||||||
|
|| fail 'generated systemd unit is invalid; run systemd-analyze verify /etc/systemd/system/kaidi-erp.service'
|
||||||
|
fi
|
||||||
systemctl enable kaidi-erp.service
|
systemctl enable kaidi-erp.service
|
||||||
systemctl restart kaidi-erp.service
|
systemctl restart kaidi-erp.service
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ group = 'com.kaidi'
|
|||||||
version = providers.gradleProperty('releaseVersion')
|
version = providers.gradleProperty('releaseVersion')
|
||||||
.orElse(System.getenv('ERP_RELEASE_VERSION') ?: '0.1.0')
|
.orElse(System.getenv('ERP_RELEASE_VERSION') ?: '0.1.0')
|
||||||
.get()
|
.get()
|
||||||
|
def flywayVersion = '11.20.3'
|
||||||
|
def postgresqlDriverVersion = '42.7.13'
|
||||||
def productionBuild = providers.gradleProperty('productionBuild')
|
def productionBuild = providers.gradleProperty('productionBuild')
|
||||||
.map { it.toBoolean() }
|
.map { it.toBoolean() }
|
||||||
.orElse(false)
|
.orElse(false)
|
||||||
@@ -47,9 +49,9 @@ dependencies {
|
|||||||
implementation 'org.springframework.boot:spring-boot-starter-websocket'
|
implementation 'org.springframework.boot:spring-boot-starter-websocket'
|
||||||
|
|
||||||
// Production database and versioned schema migrations.
|
// Production database and versioned schema migrations.
|
||||||
implementation 'org.flywaydb:flyway-core:10.22.0'
|
implementation "org.flywaydb:flyway-core:$flywayVersion"
|
||||||
runtimeOnly 'org.flywaydb:flyway-database-postgresql:10.22.0'
|
runtimeOnly "org.flywaydb:flyway-database-postgresql:$flywayVersion"
|
||||||
runtimeOnly 'org.postgresql:postgresql'
|
runtimeOnly "org.postgresql:postgresql:$postgresqlDriverVersion"
|
||||||
|
|
||||||
// SQLite remains available to source-tree development and tests, but is
|
// SQLite remains available to source-tree development and tests, but is
|
||||||
// deliberately absent from PostgreSQL-only production release artifacts.
|
// deliberately absent from PostgreSQL-only production release artifacts.
|
||||||
@@ -69,9 +71,9 @@ dependencies {
|
|||||||
|
|
||||||
installerImplementation 'org.springframework.boot:spring-boot-starter-web'
|
installerImplementation 'org.springframework.boot:spring-boot-starter-web'
|
||||||
installerImplementation 'org.springframework.boot:spring-boot-starter-validation'
|
installerImplementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||||
installerImplementation 'org.flywaydb:flyway-core:10.22.0'
|
installerImplementation "org.flywaydb:flyway-core:$flywayVersion"
|
||||||
installerRuntimeOnly 'org.flywaydb:flyway-database-postgresql:10.22.0'
|
installerRuntimeOnly "org.flywaydb:flyway-database-postgresql:$flywayVersion"
|
||||||
installerRuntimeOnly 'org.postgresql:postgresql'
|
installerRuntimeOnly "org.postgresql:postgresql:$postgresqlDriverVersion"
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named('test') {
|
tasks.named('test') {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package com.kaidi.oa.install;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.flywaydb.core.Flyway;
|
import org.flywaydb.core.Flyway;
|
||||||
import org.flywaydb.core.api.output.MigrateResult;
|
import org.flywaydb.core.api.output.MigrateResult;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.context.ConfigurableApplicationContext;
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
@@ -36,6 +38,8 @@ import java.util.Set;
|
|||||||
@Service
|
@Service
|
||||||
public class InstallerService {
|
public class InstallerService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(InstallerService.class);
|
||||||
|
|
||||||
private static final Set<PosixFilePermission> OWNER_ONLY = EnumSet.of(
|
private static final Set<PosixFilePermission> OWNER_ONLY = EnumSet.of(
|
||||||
PosixFilePermission.OWNER_READ,
|
PosixFilePermission.OWNER_READ,
|
||||||
PosixFilePermission.OWNER_WRITE);
|
PosixFilePermission.OWNER_WRITE);
|
||||||
@@ -117,6 +121,11 @@ public class InstallerService {
|
|||||||
.load()
|
.load()
|
||||||
.migrate();
|
.migrate();
|
||||||
} catch (RuntimeException exception) {
|
} catch (RuntimeException exception) {
|
||||||
|
log.error(
|
||||||
|
"PostgreSQL migration failed for database {} as user {}",
|
||||||
|
request.database().database().strip(),
|
||||||
|
request.database().username().strip(),
|
||||||
|
exception);
|
||||||
throw new InstallApiException(
|
throw new InstallApiException(
|
||||||
HttpStatus.UNPROCESSABLE_ENTITY,
|
HttpStatus.UNPROCESSABLE_ENTITY,
|
||||||
42203,
|
42203,
|
||||||
@@ -170,6 +179,7 @@ public class InstallerService {
|
|||||||
42201,
|
42201,
|
||||||
"PostgreSQL 版本过低,需要 15 或更高版本");
|
"PostgreSQL 版本过低,需要 15 或更高版本");
|
||||||
}
|
}
|
||||||
|
requireDatabaseOwnership(connection);
|
||||||
try (Statement statement = connection.createStatement()) {
|
try (Statement statement = connection.createStatement()) {
|
||||||
statement.execute("SELECT 1");
|
statement.execute("SELECT 1");
|
||||||
statement.execute("CREATE EXTENSION IF NOT EXISTS pg_trgm");
|
statement.execute("CREATE EXTENSION IF NOT EXISTS pg_trgm");
|
||||||
@@ -181,10 +191,16 @@ public class InstallerService {
|
|||||||
throw new SQLException("pg_trgm is unavailable");
|
throw new SQLException("pg_trgm is unavailable");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
requirePgTrgmOwnership(connection);
|
||||||
return new DatabaseCheck(major, connection.getMetaData().getDatabaseProductVersion());
|
return new DatabaseCheck(major, connection.getMetaData().getDatabaseProductVersion());
|
||||||
} catch (InstallApiException exception) {
|
} catch (InstallApiException exception) {
|
||||||
throw exception;
|
throw exception;
|
||||||
} catch (SQLException | NumberFormatException exception) {
|
} catch (SQLException | NumberFormatException exception) {
|
||||||
|
log.warn(
|
||||||
|
"PostgreSQL verification failed for database {} as user {}: {}",
|
||||||
|
database.database().strip(),
|
||||||
|
database.username().strip(),
|
||||||
|
exception.getMessage());
|
||||||
throw new InstallApiException(
|
throw new InstallApiException(
|
||||||
HttpStatus.UNPROCESSABLE_ENTITY,
|
HttpStatus.UNPROCESSABLE_ENTITY,
|
||||||
42202,
|
42202,
|
||||||
@@ -192,6 +208,40 @@ public class InstallerService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void requireDatabaseOwnership(Connection connection) throws SQLException {
|
||||||
|
try (Statement statement = connection.createStatement();
|
||||||
|
ResultSet result = statement.executeQuery(
|
||||||
|
"SELECT "
|
||||||
|
+ "pg_get_userbyid(datdba) = current_user AS owns_database, "
|
||||||
|
+ "has_schema_privilege(current_user, 'public', 'USAGE') AS can_use_schema, "
|
||||||
|
+ "has_schema_privilege(current_user, 'public', 'CREATE') AS can_create_in_schema "
|
||||||
|
+ "FROM pg_database WHERE datname = current_database()")) {
|
||||||
|
if (!result.next()
|
||||||
|
|| !result.getBoolean("owns_database")
|
||||||
|
|| !result.getBoolean("can_use_schema")
|
||||||
|
|| !result.getBoolean("can_create_in_schema")) {
|
||||||
|
throw new InstallApiException(
|
||||||
|
HttpStatus.UNPROCESSABLE_ENTITY,
|
||||||
|
42206,
|
||||||
|
"数据库账号必须是该空数据库的所有者,并拥有 public schema 建表权限");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requirePgTrgmOwnership(Connection connection) throws SQLException {
|
||||||
|
try (Statement statement = connection.createStatement();
|
||||||
|
ResultSet result = statement.executeQuery(
|
||||||
|
"SELECT pg_get_userbyid(extowner) = current_user "
|
||||||
|
+ "FROM pg_extension WHERE extname = 'pg_trgm'")) {
|
||||||
|
if (!result.next() || !result.getBoolean(1)) {
|
||||||
|
throw new InstallApiException(
|
||||||
|
HttpStatus.UNPROCESSABLE_ENTITY,
|
||||||
|
42207,
|
||||||
|
"数据库账号必须拥有 pg_trgm 扩展;请由管理员删除预建扩展后重新测试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void rejectExistingInstallation(InstallRequest.Database database) {
|
private void rejectExistingInstallation(InstallRequest.Database database) {
|
||||||
try (Connection connection = openConnection(database);
|
try (Connection connection = openConnection(database);
|
||||||
PreparedStatement tableQuery = connection.prepareStatement(
|
PreparedStatement tableQuery = connection.prepareStatement(
|
||||||
@@ -272,6 +322,11 @@ public class InstallerService {
|
|||||||
} catch (InstallApiException exception) {
|
} catch (InstallApiException exception) {
|
||||||
throw exception;
|
throw exception;
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
|
log.error(
|
||||||
|
"Administrator initialization failed for database {} as user {}",
|
||||||
|
database.database().strip(),
|
||||||
|
database.username().strip(),
|
||||||
|
exception);
|
||||||
throw new InstallApiException(HttpStatus.UNPROCESSABLE_ENTITY, 42205, "管理员账号初始化失败");
|
throw new InstallApiException(HttpStatus.UNPROCESSABLE_ENTITY, 42205, "管理员账号初始化失败");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -326,6 +381,8 @@ public class InstallerService {
|
|||||||
values.put("ERP_INSTALL_PENDING_FILE", properties.pendingFile().toAbsolutePath().normalize().toString());
|
values.put("ERP_INSTALL_PENDING_FILE", properties.pendingFile().toAbsolutePath().normalize().toString());
|
||||||
values.put("ERP_INSTALL_LOCK_FILE", properties.lockFile().toAbsolutePath().normalize().toString());
|
values.put("ERP_INSTALL_LOCK_FILE", properties.lockFile().toAbsolutePath().normalize().toString());
|
||||||
values.put("ERP_INSTALL_OPERATION_LOCK_FILE", properties.operationLockFile().toAbsolutePath().normalize().toString());
|
values.put("ERP_INSTALL_OPERATION_LOCK_FILE", properties.operationLockFile().toAbsolutePath().normalize().toString());
|
||||||
|
values.put("ERP_INSTALL_LOG_FILE", stateRoot.resolve("install-formal.log").toString());
|
||||||
|
values.put("ERP_INSTALL_HEALTH_TIMEOUT_SECONDS", environment("ERP_INSTALL_HEALTH_TIMEOUT_SECONDS", "240"));
|
||||||
values.put("ERP_SETUP_TOKEN", properties.token());
|
values.put("ERP_SETUP_TOKEN", properties.token());
|
||||||
values.put("ERP_INSTALLER_JAR", installRoot.resolve("installer/kaidi-erp-installer.jar").toString());
|
values.put("ERP_INSTALLER_JAR", installRoot.resolve("installer/kaidi-erp-installer.jar").toString());
|
||||||
values.put("ERP_JAR_PATH", installRoot.resolve("current/app/kaidi-erp.jar").toString());
|
values.put("ERP_JAR_PATH", installRoot.resolve("current/app/kaidi-erp.jar").toString());
|
||||||
|
|||||||
@@ -440,7 +440,7 @@
|
|||||||
} catch { /* Service is changing from installer to the formal application. */ }
|
} catch { /* Service is changing from installer to the formal application. */ }
|
||||||
}
|
}
|
||||||
$('#install-message').className = 'notice error';
|
$('#install-message').className = 'notice error';
|
||||||
$('#install-message').textContent = '正式服务尚未就绪。请在服务器查看 kaidi-erp 服务日志,安装文件和安装锁不会被误删。';
|
$('#install-message').textContent = '正式服务尚未就绪。请查看 systemctl 日志或 /var/lib/kaidi-erp/install-formal.log;安装文件和待确认状态均已保留。';
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#start-button').addEventListener('click', () => go(2));
|
$('#start-button').addEventListener('click', () => go(2));
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
TARGET=/root/kaidi-erp-reinstall-v031.sh
|
||||||
|
URL=http://38.76.196.225:10099/awaioi/ERP/raw/tag/recovery-v0.3.1-1/reinstall-centos9.sh
|
||||||
|
SHA256=0370fb17926f3b53d5ae849310fdc6cbbf019150415365a464b0972b8edf5d9a
|
||||||
|
|
||||||
|
curl -fsSL --connect-timeout 15 --max-time 300 "$URL" -o "$TARGET"
|
||||||
|
printf '%s %s\n' "$SHA256" "$TARGET" | sha256sum -c -
|
||||||
|
exec bash "$TARGET"
|
||||||
Executable
+105
@@ -0,0 +1,105 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
GITEA_BASE_URL="http://38.76.196.225:10099"
|
||||||
|
REPOSITORY="awaioi/ERP"
|
||||||
|
VERSION="0.3.1"
|
||||||
|
TAG="v${VERSION}"
|
||||||
|
INSTALL_SHA256="89a3c45e76f500c9475cb596ea29e3518bfafdc59f36dd3c7b316ed3cdd0448c"
|
||||||
|
UNINSTALL_SHA256="98c56fed2fd4d01874e4ab5a1a4f3ec42ec3e29b315ffd87385a95488d587546"
|
||||||
|
PG_ROOT="${ERP_RECOVERY_PG_ROOT:-/www/server/pgsql}"
|
||||||
|
PG_DATA="${ERP_RECOVERY_PG_DATA:-/www/server/pgsql/data}"
|
||||||
|
PG_INIT_SCRIPT="${ERP_RECOVERY_PG_INIT_SCRIPT:-/etc/init.d/pgsql}"
|
||||||
|
TMP_DIR=""
|
||||||
|
|
||||||
|
say() { printf '[ERP Recovery] %s\n' "$*"; }
|
||||||
|
fail() { printf '[ERP Recovery] ERROR: %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
[[ -z "$TMP_DIR" || ! -d "$TMP_DIR" ]] || rm -rf -- "$TMP_DIR"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
postgres_port_open() {
|
||||||
|
timeout 2 bash -c 'exec 3<>/dev/tcp/127.0.0.1/5432' >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_postgres() {
|
||||||
|
local _
|
||||||
|
for _ in {1..60}; do
|
||||||
|
postgres_port_open && return 0
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
start_erp_postgres() {
|
||||||
|
if postgres_port_open; then
|
||||||
|
say 'PostgreSQL is already listening on 127.0.0.1:5432'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -f "$PG_DATA/PG_VERSION" ]] \
|
||||||
|
|| fail "PostgreSQL data directory is missing: $PG_DATA"
|
||||||
|
|
||||||
|
if [[ -x "$PG_INIT_SCRIPT" ]]; then
|
||||||
|
say "Starting PostgreSQL with $PG_INIT_SCRIPT"
|
||||||
|
if ! "$PG_INIT_SCRIPT" start; then
|
||||||
|
say 'The init script returned an error; checking whether PostgreSQL still started'
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
local pg_ctl pg_user
|
||||||
|
pg_ctl="$(find "$PG_ROOT" -type f -name pg_ctl -print -quit 2>/dev/null || true)"
|
||||||
|
[[ -n "$pg_ctl" && -x "$pg_ctl" ]] || fail "pg_ctl was not found below $PG_ROOT"
|
||||||
|
pg_user="$(stat -c '%U' "$PG_DATA")" || fail 'Unable to determine the PostgreSQL owner'
|
||||||
|
id "$pg_user" >/dev/null 2>&1 || fail "PostgreSQL owner does not exist: $pg_user"
|
||||||
|
say "Starting PostgreSQL as $pg_user"
|
||||||
|
runuser -u "$pg_user" -- "$pg_ctl" \
|
||||||
|
-D "$PG_DATA" \
|
||||||
|
-l "$PG_DATA/startup.log" \
|
||||||
|
-w -t 60 start
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! wait_for_postgres; then
|
||||||
|
[[ ! -r "$PG_DATA/startup.log" ]] || tail -n 100 "$PG_DATA/startup.log" >&2
|
||||||
|
fail 'PostgreSQL did not listen on 127.0.0.1:5432 within 60 seconds'
|
||||||
|
fi
|
||||||
|
say 'PostgreSQL is ready on 127.0.0.1:5432'
|
||||||
|
}
|
||||||
|
|
||||||
|
download_verified_script() {
|
||||||
|
local name="$1" expected="$2" destination
|
||||||
|
destination="$TMP_DIR/$name"
|
||||||
|
curl -fsSL --connect-timeout 15 --max-time 300 \
|
||||||
|
"$GITEA_BASE_URL/$REPOSITORY/raw/tag/$TAG/$name" \
|
||||||
|
-o "$destination"
|
||||||
|
printf '%s %s\n' "$expected" "$destination" | sha256sum -c - >&2
|
||||||
|
printf '%s' "$destination"
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
[[ "$(id -u)" == "0" ]] || fail 'Run this recovery command as root'
|
||||||
|
for command in bash curl find runuser sha256sum stat timeout; do
|
||||||
|
command -v "$command" >/dev/null 2>&1 || fail "Missing command: $command"
|
||||||
|
done
|
||||||
|
|
||||||
|
start_erp_postgres
|
||||||
|
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/kaidi-erp-recovery.XXXXXX")"
|
||||||
|
|
||||||
|
local uninstaller installer
|
||||||
|
uninstaller="$(download_verified_script uninstall.sh "$UNINSTALL_SHA256")"
|
||||||
|
say 'Removing the pending installation and its PostgreSQL schema safely'
|
||||||
|
bash "$uninstaller" --purge-database --yes
|
||||||
|
|
||||||
|
installer="$(download_verified_script install.sh "$INSTALL_SHA256")"
|
||||||
|
say "Installing Kaidi ERP $VERSION"
|
||||||
|
bash "$installer" \
|
||||||
|
--gitea-url "$GITEA_BASE_URL" \
|
||||||
|
--repository "$REPOSITORY" \
|
||||||
|
--version "$VERSION" \
|
||||||
|
--allow-insecure
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${BASH_SOURCE[0]:-$0}" == "$0" ]]; then
|
||||||
|
main "$@"
|
||||||
|
fi
|
||||||
@@ -56,6 +56,83 @@ test_erp_run_preserves_java_option_arguments() (
|
|||||||
grep -Fqx -- "$tmp/current/app/kaidi-erp.jar" <<< "$output"
|
grep -Fqx -- "$tmp/current/app/kaidi-erp.jar" <<< "$output"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
test_erp_run_finalizes_healthy_pending_install() (
|
||||||
|
local tmp
|
||||||
|
tmp="$(mktemp -d "${TMPDIR:-/tmp}/erp-handoff-success.XXXXXX")" || return 1
|
||||||
|
trap 'rm -rf "$tmp"' EXIT
|
||||||
|
mkdir -p "$tmp/current/app" "$tmp/run" "$tmp/state" "$tmp/installer" "$tmp/bin"
|
||||||
|
: > "$tmp/current/app/kaidi-erp.jar"
|
||||||
|
: > "$tmp/installer/kaidi-erp-installer.jar"
|
||||||
|
printf 'pending\n' > "$tmp/state/install.pending"
|
||||||
|
printf '%s\n' \
|
||||||
|
'#!/usr/bin/env bash' \
|
||||||
|
'if [[ "${1:-}" == "-version" ]]; then printf '\''openjdk version "17.0.12"\n'\'' >&2; exit 0; fi' \
|
||||||
|
'printf '\''formal application output\n'\''' \
|
||||||
|
'sleep 0.1' \
|
||||||
|
'exit 0' > "$tmp/java"
|
||||||
|
printf '%s\n' '#!/usr/bin/env bash' 'exit 0' > "$tmp/bin/curl"
|
||||||
|
chmod +x "$tmp/java" "$tmp/bin/curl"
|
||||||
|
{
|
||||||
|
printf 'ERP_JAVA_BIN=%q\n' "$tmp/java"
|
||||||
|
printf 'ERP_RUN_DIR=%q\n' "$tmp/run"
|
||||||
|
printf 'ERP_JAR_PATH=%q\n' "$tmp/current/app/kaidi-erp.jar"
|
||||||
|
printf 'ERP_INSTALLER_JAR=%q\n' "$tmp/installer/kaidi-erp-installer.jar"
|
||||||
|
printf 'ERP_INSTALL_PENDING_FILE=%q\n' "$tmp/state/install.pending"
|
||||||
|
printf 'ERP_INSTALL_LOCK_FILE=%q\n' "$tmp/state/install.lock"
|
||||||
|
printf 'ERP_INSTALL_LOG_FILE=%q\n' "$tmp/state/install-formal.log"
|
||||||
|
printf 'ERP_INSTALL_HEALTH_TIMEOUT_SECONDS=3\n'
|
||||||
|
printf 'ERP_UPDATE_HEALTH_POLL_SECONDS=1\n'
|
||||||
|
} > "$tmp/erp.env"
|
||||||
|
|
||||||
|
PATH="$tmp/bin:$PATH" ERP_INSTALL_ROOT="$tmp" ERP_CONFIG_FILE="$tmp/erp.env" \
|
||||||
|
"$PROJECT_ROOT/distribution/bin/erp-run" > "$tmp/run.log" 2>&1 || return 1
|
||||||
|
|
||||||
|
[[ -f "$tmp/state/install.lock" ]] || return 1
|
||||||
|
[[ ! -e "$tmp/state/install.pending" ]] || return 1
|
||||||
|
[[ ! -d "$tmp/installer" ]] || return 1
|
||||||
|
[[ ! -e "$tmp/run/app.pid" ]] || return 1
|
||||||
|
grep -Fq 'formal application output' "$tmp/state/install-formal.log"
|
||||||
|
)
|
||||||
|
|
||||||
|
test_erp_run_preserves_failed_pending_install() (
|
||||||
|
local tmp output status=0
|
||||||
|
tmp="$(mktemp -d "${TMPDIR:-/tmp}/erp-handoff-failure.XXXXXX")" || return 1
|
||||||
|
trap 'rm -rf "$tmp"' EXIT
|
||||||
|
mkdir -p "$tmp/current/app" "$tmp/run" "$tmp/state" "$tmp/installer" "$tmp/bin"
|
||||||
|
: > "$tmp/current/app/kaidi-erp.jar"
|
||||||
|
: > "$tmp/installer/kaidi-erp-installer.jar"
|
||||||
|
printf 'pending\n' > "$tmp/state/install.pending"
|
||||||
|
printf '%s\n' \
|
||||||
|
'#!/usr/bin/env bash' \
|
||||||
|
'if [[ "${1:-}" == "-version" ]]; then printf '\''openjdk version "17.0.12"\n'\'' >&2; exit 0; fi' \
|
||||||
|
'printf '\''formal application exploded\n'\'' >&2' \
|
||||||
|
'sleep 0.1' \
|
||||||
|
'exit 23' > "$tmp/java"
|
||||||
|
printf '%s\n' '#!/usr/bin/env bash' 'exit 1' > "$tmp/bin/curl"
|
||||||
|
chmod +x "$tmp/java" "$tmp/bin/curl"
|
||||||
|
{
|
||||||
|
printf 'ERP_JAVA_BIN=%q\n' "$tmp/java"
|
||||||
|
printf 'ERP_RUN_DIR=%q\n' "$tmp/run"
|
||||||
|
printf 'ERP_JAR_PATH=%q\n' "$tmp/current/app/kaidi-erp.jar"
|
||||||
|
printf 'ERP_INSTALLER_JAR=%q\n' "$tmp/installer/kaidi-erp-installer.jar"
|
||||||
|
printf 'ERP_INSTALL_PENDING_FILE=%q\n' "$tmp/state/install.pending"
|
||||||
|
printf 'ERP_INSTALL_LOCK_FILE=%q\n' "$tmp/state/install.lock"
|
||||||
|
printf 'ERP_INSTALL_LOG_FILE=%q\n' "$tmp/state/install-formal.log"
|
||||||
|
printf 'ERP_INSTALL_HEALTH_TIMEOUT_SECONDS=3\n'
|
||||||
|
printf 'ERP_UPDATE_HEALTH_POLL_SECONDS=1\n'
|
||||||
|
} > "$tmp/erp.env"
|
||||||
|
|
||||||
|
output="$(PATH="$tmp/bin:$PATH" ERP_INSTALL_ROOT="$tmp" ERP_CONFIG_FILE="$tmp/erp.env" \
|
||||||
|
"$PROJECT_ROOT/distribution/bin/erp-run" 2>&1)" || status=$?
|
||||||
|
|
||||||
|
[[ "$status" -ne 0 ]] || return 1
|
||||||
|
[[ -f "$tmp/state/install.pending" ]] || return 1
|
||||||
|
[[ ! -e "$tmp/state/install.lock" ]] || return 1
|
||||||
|
[[ -d "$tmp/installer" ]] || return 1
|
||||||
|
grep -Fq 'formal application exploded' "$tmp/state/install-formal.log" || return 1
|
||||||
|
[[ "$output" == *"Formal application diagnostics: $tmp/state/install-formal.log"* ]]
|
||||||
|
)
|
||||||
|
|
||||||
prepare_signed_archive() {
|
prepare_signed_archive() {
|
||||||
local tmp="$1" unsafe="${2:-0}"
|
local tmp="$1" unsafe="${2:-0}"
|
||||||
mkdir -p "$tmp/package/kaidi-erp-1.2.3/app"
|
mkdir -p "$tmp/package/kaidi-erp-1.2.3/app"
|
||||||
@@ -469,6 +546,112 @@ test_linux_service_preflight_requires_systemd() (
|
|||||||
NO_SERVICE=1 PATH="$tmp/bin" validate_service_manager
|
NO_SERVICE=1 PATH="$tmp/bin" validate_service_manager
|
||||||
)
|
)
|
||||||
|
|
||||||
|
test_systemd_unit_uses_compatible_protection() (
|
||||||
|
grep -Fqx 'ProtectSystem=full' <(sed -n '/^ cat > \/etc\/systemd\/system\/kaidi-erp.service/,/^ systemctl daemon-reload/p' "$PROJECT_ROOT/install.sh")
|
||||||
|
! grep -Fq 'ProtectSystem=strict' "$PROJECT_ROOT/install.sh"
|
||||||
|
)
|
||||||
|
|
||||||
|
test_systemd_unit_uses_unquoted_legacy_paths() (
|
||||||
|
local unit
|
||||||
|
unit="$(sed -n '/^ cat > \/etc\/systemd\/system\/kaidi-erp.service/,/^ systemctl daemon-reload/p' "$PROJECT_ROOT/install.sh")"
|
||||||
|
grep -Fqx 'WorkingDirectory=$INSTALL_ROOT' <<< "$unit"
|
||||||
|
grep -Fqx 'ExecStart=$INSTALL_ROOT/current/bin/erp-run' <<< "$unit"
|
||||||
|
grep -Fqx 'ReadWritePaths=$INSTALL_ROOT $CONFIG_ROOT $STATE_ROOT $LOG_ROOT' <<< "$unit"
|
||||||
|
! grep -Fq 'WorkingDirectory="$INSTALL_ROOT"' <<< "$unit"
|
||||||
|
)
|
||||||
|
|
||||||
|
test_uninstaller_purges_database_before_removing_files() (
|
||||||
|
local tmp
|
||||||
|
tmp="$(mktemp -d "${TMPDIR:-/tmp}/erp-uninstall-test.XXXXXX")" || return 1
|
||||||
|
trap 'rm -rf "$tmp"' EXIT
|
||||||
|
mkdir -p "$tmp/bin" "$tmp/install" "$tmp/config" "$tmp/state" "$tmp/logs"
|
||||||
|
: > "$tmp/unit.service"
|
||||||
|
{
|
||||||
|
printf 'ERP_PGHOST=127.0.0.1\n'
|
||||||
|
printf 'ERP_PGPORT=5432\n'
|
||||||
|
printf 'ERP_PGDATABASE=kaidi_test\n'
|
||||||
|
printf 'ERP_PGSSLMODE=disable\n'
|
||||||
|
printf 'OA_DB_USERNAME=kaidi_test\n'
|
||||||
|
printf 'OA_DB_PASSWORD='\''test-password'\''\n'
|
||||||
|
printf 'ERP_INSTALL_ROOT=%q\n' "$tmp/install"
|
||||||
|
printf 'ERP_INSTALL_LOCK_FILE=%q\n' "$tmp/state/install.lock"
|
||||||
|
} > "$tmp/config/erp.env"
|
||||||
|
printf '%s\n' \
|
||||||
|
'#!/usr/bin/env bash' \
|
||||||
|
'printf '\''%s\n'\'' "$*" >> "$MOCK_SYSTEMCTL_LOG"' \
|
||||||
|
'if [[ "${1:-}" == "is-active" ]]; then exit 3; fi' \
|
||||||
|
'exit 0' > "$tmp/bin/systemctl"
|
||||||
|
printf '%s\n' \
|
||||||
|
'#!/usr/bin/env bash' \
|
||||||
|
'printf '\''ARGS %s\n'\'' "$*" >> "$MOCK_PSQL_LOG"' \
|
||||||
|
'if [[ " $* " == *" -c "* ]]; then printf '\''t\n'\''; exit 0; fi' \
|
||||||
|
'cat >> "$MOCK_PSQL_LOG"' \
|
||||||
|
'exit 0' > "$tmp/bin/psql"
|
||||||
|
chmod +x "$tmp/bin/systemctl" "$tmp/bin/psql"
|
||||||
|
|
||||||
|
PATH="$tmp/bin:$PATH" \
|
||||||
|
MOCK_SYSTEMCTL_LOG="$tmp/systemctl.log" \
|
||||||
|
MOCK_PSQL_LOG="$tmp/psql.log" \
|
||||||
|
ERP_UNINSTALL_TEST_MODE=1 \
|
||||||
|
ERP_UNINSTALL_SYSTEMD_UNIT_FILE="$tmp/unit.service" \
|
||||||
|
"$PROJECT_ROOT/uninstall.sh" --purge-database --yes \
|
||||||
|
--config-file "$tmp/config/erp.env" \
|
||||||
|
--install-root "$tmp/install" \
|
||||||
|
--config-root "$tmp/config" \
|
||||||
|
--state-root "$tmp/state" \
|
||||||
|
--log-root "$tmp/logs" > "$tmp/uninstall.log" 2>&1 || return 1
|
||||||
|
|
||||||
|
[[ ! -e "$tmp/install" && ! -e "$tmp/config" && ! -e "$tmp/state" && ! -e "$tmp/logs" ]] || return 1
|
||||||
|
[[ ! -e "$tmp/unit.service" ]] || return 1
|
||||||
|
grep -Fq 'DROP SCHEMA IF EXISTS public CASCADE;' "$tmp/psql.log" || return 1
|
||||||
|
grep -Fq 'stop kaidi-erp.service' "$tmp/systemctl.log" || return 1
|
||||||
|
grep -Fq 'disable kaidi-erp.service' "$tmp/systemctl.log"
|
||||||
|
)
|
||||||
|
|
||||||
|
test_uninstaller_rejects_unsafe_paths() (
|
||||||
|
! (source "$PROJECT_ROOT/uninstall.sh"; validate_remove_path /etc) >/dev/null 2>&1 || return 1
|
||||||
|
! (source "$PROJECT_ROOT/uninstall.sh"; validate_remove_path /etc/ssh) >/dev/null 2>&1 || return 1
|
||||||
|
! (source "$PROJECT_ROOT/uninstall.sh"; validate_remove_path /opt/kaidi-erp/../../etc) >/dev/null 2>&1
|
||||||
|
)
|
||||||
|
|
||||||
|
test_uninstaller_aborts_when_service_remains_active() (
|
||||||
|
local tmp output status=0
|
||||||
|
tmp="$(mktemp -d "${TMPDIR:-/tmp}/erp-uninstall-active.XXXXXX")" || return 1
|
||||||
|
trap 'rm -rf "$tmp"' EXIT
|
||||||
|
mkdir -p "$tmp/bin" "$tmp/install" "$tmp/config" "$tmp/state" "$tmp/logs"
|
||||||
|
{
|
||||||
|
printf 'ERP_PGHOST=127.0.0.1\n'
|
||||||
|
printf 'ERP_PGPORT=5432\n'
|
||||||
|
printf 'ERP_PGDATABASE=kaidi_test\n'
|
||||||
|
printf 'ERP_PGSSLMODE=disable\n'
|
||||||
|
printf 'OA_DB_USERNAME=kaidi_test\n'
|
||||||
|
printf 'OA_DB_PASSWORD=test-password\n'
|
||||||
|
printf 'ERP_INSTALL_ROOT=%q\n' "$tmp/install"
|
||||||
|
printf 'ERP_INSTALL_LOCK_FILE=%q\n' "$tmp/state/install.lock"
|
||||||
|
} > "$tmp/config/erp.env"
|
||||||
|
printf '%s\n' \
|
||||||
|
'#!/usr/bin/env bash' \
|
||||||
|
'if [[ "${1:-}" == "stop" ]]; then exit 1; fi' \
|
||||||
|
'if [[ "${1:-}" == "show" ]]; then printf '\''loaded\n'\''; exit 0; fi' \
|
||||||
|
'if [[ "${1:-}" == "is-active" ]]; then exit 0; fi' \
|
||||||
|
'exit 0' > "$tmp/bin/systemctl"
|
||||||
|
printf '%s\n' '#!/usr/bin/env bash' 'touch "$MOCK_PSQL_CALLED"' 'exit 0' > "$tmp/bin/psql"
|
||||||
|
chmod +x "$tmp/bin/systemctl" "$tmp/bin/psql"
|
||||||
|
|
||||||
|
output="$(PATH="$tmp/bin:$PATH" MOCK_PSQL_CALLED="$tmp/psql.called" \
|
||||||
|
ERP_UNINSTALL_TEST_MODE=1 ERP_UNINSTALL_SYSTEMD_UNIT_FILE="$tmp/unit.service" \
|
||||||
|
"$PROJECT_ROOT/uninstall.sh" --purge-database --yes \
|
||||||
|
--config-file "$tmp/config/erp.env" \
|
||||||
|
--install-root "$tmp/install" \
|
||||||
|
--config-root "$tmp/config" \
|
||||||
|
--state-root "$tmp/state" \
|
||||||
|
--log-root "$tmp/logs" 2>&1)" || status=$?
|
||||||
|
|
||||||
|
[[ "$status" -ne 0 && "$output" == *'unable to stop kaidi-erp.service'* ]] || return 1
|
||||||
|
[[ ! -e "$tmp/psql.called" ]] || return 1
|
||||||
|
[[ -d "$tmp/install" && -d "$tmp/config" && -d "$tmp/state" && -d "$tmp/logs" ]]
|
||||||
|
)
|
||||||
|
|
||||||
test_no_service_install_disables_online_update() (
|
test_no_service_install_disables_online_update() (
|
||||||
local tmp
|
local tmp
|
||||||
tmp="$(mktemp -d "${TMPDIR:-/tmp}/erp-no-service-test.XXXXXX")" || return 1
|
tmp="$(mktemp -d "${TMPDIR:-/tmp}/erp-no-service-test.XXXXXX")" || return 1
|
||||||
@@ -510,6 +693,8 @@ test_installer_runs_when_piped_to_bash() (
|
|||||||
)
|
)
|
||||||
|
|
||||||
run_test 'erp-run preserves Java option arguments' test_erp_run_preserves_java_option_arguments
|
run_test 'erp-run preserves Java option arguments' test_erp_run_preserves_java_option_arguments
|
||||||
|
run_test 'erp-run finalizes a healthy pending installation' test_erp_run_finalizes_healthy_pending_install
|
||||||
|
run_test 'erp-run preserves a failed pending installation' test_erp_run_preserves_failed_pending_install
|
||||||
run_test 'installer accepts a correctly signed archive' test_installer_verifies_signed_safe_archive
|
run_test 'installer accepts a correctly signed archive' test_installer_verifies_signed_safe_archive
|
||||||
run_test 'installer rejects symlinks in release archives' test_installer_rejects_archive_symlinks
|
run_test 'installer rejects symlinks in release archives' test_installer_rejects_archive_symlinks
|
||||||
run_test 'stable update channel rejects prerelease tags' test_stable_channel_rejects_prerelease_tag
|
run_test 'stable update channel rejects prerelease tags' test_stable_channel_rejects_prerelease_tag
|
||||||
@@ -520,6 +705,11 @@ run_test 'update helper rejects a concurrent process' test_update_lock_rejects_c
|
|||||||
run_test 'installer moves database setup to the web wizard' test_installer_moves_database_setup_to_web_wizard
|
run_test 'installer moves database setup to the web wizard' test_installer_moves_database_setup_to_web_wizard
|
||||||
run_test 'installer requires an explicit Gitea URL' test_installer_requires_explicit_gitea_url
|
run_test 'installer requires an explicit Gitea URL' test_installer_requires_explicit_gitea_url
|
||||||
run_test 'Linux production install requires systemd' test_linux_service_preflight_requires_systemd
|
run_test 'Linux production install requires systemd' test_linux_service_preflight_requires_systemd
|
||||||
|
run_test 'systemd unit uses compatible protection' test_systemd_unit_uses_compatible_protection
|
||||||
|
run_test 'systemd unit uses unquoted legacy paths' test_systemd_unit_uses_unquoted_legacy_paths
|
||||||
|
run_test 'uninstaller purges database before removing files' test_uninstaller_purges_database_before_removing_files
|
||||||
|
run_test 'uninstaller rejects unsafe paths' test_uninstaller_rejects_unsafe_paths
|
||||||
|
run_test 'uninstaller aborts while the service remains active' test_uninstaller_aborts_when_service_remains_active
|
||||||
run_test 'no-service install disables online update' test_no_service_install_disables_online_update
|
run_test 'no-service install disables online update' test_no_service_install_disables_online_update
|
||||||
run_test 'release workflow uses the scoped Gitea job token' test_release_workflow_uses_scoped_job_token
|
run_test 'release workflow uses the scoped Gitea job token' test_release_workflow_uses_scoped_job_token
|
||||||
run_test 'installer starts correctly when piped to bash' test_installer_runs_when_piped_to_bash
|
run_test 'installer starts correctly when piped to bash' test_installer_runs_when_piped_to_bash
|
||||||
|
|||||||
Executable
+242
@@ -0,0 +1,242 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CONFIG_FILE="${ERP_CONFIG_FILE:-/etc/kaidi-erp/erp.env}"
|
||||||
|
INSTALL_ROOT="${ERP_INSTALL_ROOT:-/opt/kaidi-erp}"
|
||||||
|
CONFIG_ROOT="${ERP_CONFIG_ROOT:-/etc/kaidi-erp}"
|
||||||
|
STATE_ROOT="${ERP_STATE_ROOT:-/var/lib/kaidi-erp}"
|
||||||
|
LOG_ROOT="${ERP_LOG_ROOT:-/var/log/kaidi-erp}"
|
||||||
|
PURGE_DATABASE=0
|
||||||
|
ASSUME_YES=0
|
||||||
|
|
||||||
|
say() { printf '[ERP Uninstall] %s\n' "$*"; }
|
||||||
|
fail() { printf '[ERP Uninstall] ERROR: %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: uninstall.sh [options]
|
||||||
|
--purge-database Drop and recreate the public schema using the configured ERP account
|
||||||
|
--yes Confirm destructive removal without an interactive prompt
|
||||||
|
--config-file PATH Installer-generated erp.env file
|
||||||
|
--install-root PATH Installation root (default: /opt/kaidi-erp)
|
||||||
|
--config-root PATH Configuration root (default: /etc/kaidi-erp)
|
||||||
|
--state-root PATH State root (default: /var/lib/kaidi-erp)
|
||||||
|
--log-root PATH Log root (default: /var/log/kaidi-erp)
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--purge-database) PURGE_DATABASE=1; shift ;;
|
||||||
|
--yes) ASSUME_YES=1; shift ;;
|
||||||
|
--config-file) [[ $# -ge 2 ]] || fail '--config-file requires a value'; CONFIG_FILE="$2"; shift 2 ;;
|
||||||
|
--install-root) [[ $# -ge 2 ]] || fail '--install-root requires a value'; INSTALL_ROOT="$2"; shift 2 ;;
|
||||||
|
--config-root) [[ $# -ge 2 ]] || fail '--config-root requires a value'; CONFIG_ROOT="$2"; shift 2 ;;
|
||||||
|
--state-root) [[ $# -ge 2 ]] || fail '--state-root requires a value'; STATE_ROOT="$2"; shift 2 ;;
|
||||||
|
--log-root) [[ $# -ge 2 ]] || fail '--log-root requires a value'; LOG_ROOT="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) fail "unknown option: $1" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
require_root() {
|
||||||
|
if [[ "${ERP_UNINSTALL_TEST_MODE:-0}" != "1" && "$(id -u)" != "0" ]]; then
|
||||||
|
fail 'Linux uninstall requires root'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
require_confirmation() {
|
||||||
|
[[ "$PURGE_DATABASE" == "1" ]] || fail '--purge-database is required for a clean reinstall'
|
||||||
|
[[ "$ASSUME_YES" == "1" ]] || fail '--yes is required to confirm database and file removal'
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_remove_path() {
|
||||||
|
local path="$1"
|
||||||
|
[[ "$path" == /* && "$path" != "/" && ${#path} -ge 8 ]] \
|
||||||
|
|| fail "refusing unsafe removal path: $path"
|
||||||
|
[[ "$path" =~ ^/[A-Za-z0-9._/@:+-]+$ ]] \
|
||||||
|
|| fail "refusing removal path with unsafe characters: $path"
|
||||||
|
|
||||||
|
local resolved
|
||||||
|
resolved="$(python3 - "$path" <<'PY'
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
print(os.path.realpath(sys.argv[1]), end="")
|
||||||
|
PY
|
||||||
|
)" || fail "unable to resolve removal path: $path"
|
||||||
|
if [[ "${ERP_UNINSTALL_TEST_MODE:-0}" != "1" && "$resolved" != "$path" ]]; then
|
||||||
|
fail "removal path must be canonical and cannot traverse links: $path"
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$resolved" in
|
||||||
|
/bin|/boot|/dev|/etc|/home|/lib|/lib64|/opt|/proc|/root|/run|/sbin|/srv|/sys|/tmp|/usr|/var)
|
||||||
|
fail "refusing unsafe removal path: $path"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [[ "${ERP_UNINSTALL_TEST_MODE:-0}" != "1" ]]; then
|
||||||
|
case "$resolved" in
|
||||||
|
/etc/kaidi-erp|/var/lib/kaidi-erp|/var/log/kaidi-erp) ;;
|
||||||
|
/bin/*|/boot/*|/dev/*|/etc/*|/lib/*|/lib64/*|/proc/*|/root/*|/run/*|/sbin/*|/sys/*|/tmp/*|/usr/*|/var/*)
|
||||||
|
fail "refusing removal below a protected system directory: $path"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s' "$resolved"
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_paths() {
|
||||||
|
command -v python3 >/dev/null 2>&1 || fail 'python3 is required'
|
||||||
|
INSTALL_ROOT="$(validate_remove_path "$INSTALL_ROOT")"
|
||||||
|
CONFIG_ROOT="$(validate_remove_path "$CONFIG_ROOT")"
|
||||||
|
STATE_ROOT="$(validate_remove_path "$STATE_ROOT")"
|
||||||
|
LOG_ROOT="$(validate_remove_path "$LOG_ROOT")"
|
||||||
|
|
||||||
|
local config_resolved
|
||||||
|
config_resolved="$(python3 - "$CONFIG_FILE" <<'PY'
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
print(os.path.realpath(sys.argv[1]), end="")
|
||||||
|
PY
|
||||||
|
)" || fail "unable to resolve configuration path: $CONFIG_FILE"
|
||||||
|
[[ "$config_resolved" == "$CONFIG_ROOT/erp.env" ]] \
|
||||||
|
|| fail "configuration file must be $CONFIG_ROOT/erp.env"
|
||||||
|
CONFIG_FILE="$config_resolved"
|
||||||
|
}
|
||||||
|
|
||||||
|
read_config_value() {
|
||||||
|
local key="$1"
|
||||||
|
python3 - "$CONFIG_FILE" "$key" <<'PY'
|
||||||
|
import re
|
||||||
|
import shlex
|
||||||
|
import sys
|
||||||
|
|
||||||
|
path, wanted = sys.argv[1:]
|
||||||
|
pattern = re.compile(r"[A-Z][A-Z0-9_]*")
|
||||||
|
seen = set()
|
||||||
|
matches = []
|
||||||
|
with open(path, encoding="utf-8") as handle:
|
||||||
|
for raw in handle:
|
||||||
|
line = raw.rstrip("\r\n")
|
||||||
|
if not line or line.startswith("#") or "=" not in line:
|
||||||
|
continue
|
||||||
|
key, encoded = line.split("=", 1)
|
||||||
|
if not pattern.fullmatch(key):
|
||||||
|
raise SystemExit("invalid configuration key")
|
||||||
|
if key in seen:
|
||||||
|
raise SystemExit(f"duplicate configuration key: {key}")
|
||||||
|
seen.add(key)
|
||||||
|
if key != wanted:
|
||||||
|
continue
|
||||||
|
values = shlex.split(encoded, posix=True)
|
||||||
|
if len(values) != 1:
|
||||||
|
raise SystemExit(f"invalid value for {wanted}")
|
||||||
|
matches.append(values[0])
|
||||||
|
if len(matches) != 1:
|
||||||
|
raise SystemExit(f"missing configuration value: {wanted}")
|
||||||
|
print(matches[0], end="")
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
verify_installation_identity() {
|
||||||
|
[[ -f "$CONFIG_FILE" && ! -L "$CONFIG_FILE" ]] \
|
||||||
|
|| fail "configuration file is missing or unsafe: $CONFIG_FILE"
|
||||||
|
|
||||||
|
local configured_root configured_lock configured_state
|
||||||
|
configured_root="$(read_config_value ERP_INSTALL_ROOT)" \
|
||||||
|
|| fail 'unable to verify the configured installation root'
|
||||||
|
configured_lock="$(read_config_value ERP_INSTALL_LOCK_FILE)" \
|
||||||
|
|| fail 'unable to verify the configured installation state'
|
||||||
|
configured_root="$(validate_remove_path "$configured_root")"
|
||||||
|
configured_state="$(validate_remove_path "$(dirname "$configured_lock")")"
|
||||||
|
[[ "$configured_root" == "$INSTALL_ROOT" ]] \
|
||||||
|
|| fail 'requested installation root does not match erp.env'
|
||||||
|
[[ "$configured_state" == "$STATE_ROOT" ]] \
|
||||||
|
|| fail 'requested state root does not match erp.env'
|
||||||
|
}
|
||||||
|
|
||||||
|
purge_database() {
|
||||||
|
command -v psql >/dev/null 2>&1 || fail 'PostgreSQL client psql is required'
|
||||||
|
|
||||||
|
local host port database sslmode username password owner_check
|
||||||
|
host="$(read_config_value ERP_PGHOST)"
|
||||||
|
port="$(read_config_value ERP_PGPORT)"
|
||||||
|
database="$(read_config_value ERP_PGDATABASE)"
|
||||||
|
sslmode="$(read_config_value ERP_PGSSLMODE)"
|
||||||
|
username="$(read_config_value OA_DB_USERNAME)"
|
||||||
|
password="$(read_config_value OA_DB_PASSWORD)"
|
||||||
|
|
||||||
|
[[ "$host" != *[[:space:]]* && "$port" =~ ^[0-9]+$ && "$port" -ge 1 && "$port" -le 65535 ]] \
|
||||||
|
|| fail 'invalid database endpoint in configuration'
|
||||||
|
[[ "$database" =~ ^[A-Za-z_][A-Za-z0-9_-]{0,62}$ ]] || fail 'invalid database name in configuration'
|
||||||
|
[[ "$username" =~ ^[A-Za-z_][A-Za-z0-9_.-]{0,127}$ ]] || fail 'invalid database user in configuration'
|
||||||
|
case "$database" in postgres|template0|template1) fail "refusing to purge protected database: $database" ;; esac
|
||||||
|
case "$sslmode" in disable|allow|prefer|require|verify-ca|verify-full) ;; *) fail 'invalid database SSL mode' ;; esac
|
||||||
|
|
||||||
|
owner_check="$(PGPASSWORD="$password" PGSSLMODE="$sslmode" psql -XAt \
|
||||||
|
-h "$host" -p "$port" -U "$username" -d "$database" -v ON_ERROR_STOP=1 \
|
||||||
|
-c "SELECT pg_get_userbyid(datdba) = current_user FROM pg_database WHERE datname = current_database()")" \
|
||||||
|
|| fail 'unable to verify database ownership'
|
||||||
|
[[ "$owner_check" == "t" ]] \
|
||||||
|
|| fail 'configured ERP account is not the database owner; database was not changed'
|
||||||
|
|
||||||
|
say "Purging PostgreSQL schema $database/public"
|
||||||
|
PGPASSWORD="$password" PGSSLMODE="$sslmode" psql -X \
|
||||||
|
-h "$host" -p "$port" -U "$username" -d "$database" -v ON_ERROR_STOP=1 <<'SQL'
|
||||||
|
DROP SCHEMA IF EXISTS public CASCADE;
|
||||||
|
SELECT format('CREATE SCHEMA public AUTHORIZATION %I', current_user) \gexec
|
||||||
|
SQL
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_service() {
|
||||||
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
|
local load_state
|
||||||
|
if ! systemctl stop kaidi-erp.service >/dev/null 2>&1; then
|
||||||
|
load_state="$(systemctl show kaidi-erp.service --property=LoadState --value 2>/dev/null || true)"
|
||||||
|
[[ "$load_state" == "not-found" ]] \
|
||||||
|
|| fail 'unable to stop kaidi-erp.service; database and files were not changed'
|
||||||
|
fi
|
||||||
|
if systemctl is-active --quiet kaidi-erp.service; then
|
||||||
|
fail 'kaidi-erp.service is still active; database and files were not changed'
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_service() {
|
||||||
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
|
systemctl disable kaidi-erp.service >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
local unit_file="${ERP_UNINSTALL_SYSTEMD_UNIT_FILE:-/etc/systemd/system/kaidi-erp.service}"
|
||||||
|
if [[ "${ERP_UNINSTALL_TEST_MODE:-0}" != "1" && "$unit_file" != "/etc/systemd/system/kaidi-erp.service" ]]; then
|
||||||
|
fail 'custom systemd unit path is only available in test mode'
|
||||||
|
fi
|
||||||
|
rm -f -- "$unit_file"
|
||||||
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
|
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||||
|
systemctl reset-failed kaidi-erp.service >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_files() {
|
||||||
|
local path
|
||||||
|
for path in "$INSTALL_ROOT" "$CONFIG_ROOT" "$STATE_ROOT" "$LOG_ROOT"; do
|
||||||
|
[[ ! -e "$path" && ! -L "$path" ]] || rm -rf -- "$path"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
require_root
|
||||||
|
require_confirmation
|
||||||
|
prepare_paths
|
||||||
|
verify_installation_identity
|
||||||
|
stop_service
|
||||||
|
purge_database
|
||||||
|
remove_service
|
||||||
|
remove_files
|
||||||
|
say 'Kaidi ERP service, files, state, logs, and public database schema were removed'
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${BASH_SOURCE[0]:-$0}" == "$0" ]]; then
|
||||||
|
main "$@"
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user