Files
kaidi/scripts/test-install-fixture.sh
T
Qiufeng 2c73ad7eb0
Release / release (push) Canceled after 0s
fix: support panel-managed deployment safely
2026-08-18 00:02:45 +08:00

391 lines
19 KiB
Bash
Executable File

#!/usr/bin/env bash
set -Eeuo pipefail
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
fail() {
printf 'Install fixture failed: %s\n' "$1" >&2
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"
sed -n '/^read_env_file()/,/^}/p' "$ROOT/deploy/install.sh"
sed -n '/^read_existing_env()/,/^}/p' "$ROOT/deploy/install.sh"
sed -n '/^read_setup_env()/,/^}/p' "$ROOT/deploy/install.sh"
sed -n '/^read_reinstall_env()/,/^}/p' "$ROOT/deploy/install.sh"
sed -n '/^port_is_listening()/,/^}/p' "$ROOT/deploy/install.sh"
sed -n '/^valid_app_port()/,/^}/p' "$ROOT/deploy/install.sh"
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"
sed -n '/^download_release_url()/,/^}/p' "$ROOT/deploy/install.sh"
sed -n '/^release_asset_url()/,/^}/p' "$ROOT/deploy/install.sh"
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"
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"
printf '#!/usr/bin/env bash\n' > "$release_permissions/ops/baota-start.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.
WORK_DIR=$WORK
# shellcheck disable=SC2034 # Referenced by the extracted installer helper.
CONFIG_ROOT=$WORK
STATE_ROOT=$WORK/state
mkdir -p "$STATE_ROOT/setup"
env_file="$WORK/kaidi.env"
cat > "$env_file" <<'ENV'
# operator-owned values must survive a reinstall
FINANCE_SESSION_ABSOLUTE_TIMEOUT="45m"
FILE_SCANNER_HOST="scanner.internal"
FILE_SCANNER_PORT="3310"
DB_URL="jdbc:mysql://old/kaidi_finance"
APP_VERSION="old"
ENV
# shellcheck disable=SC2016 # These shell characters must remain literal data.
literal_password='pw$HOME-$(touch PLACEHOLDER)-`id`-back\slash-"quote"'
literal_password=${literal_password/PLACEHOLDER/$WORK\/executed}
write_env_file_preserving_unknown "$env_file" \
DB_URL 'jdbc:mysql://new/kaidi_finance' \
DB_PASSWORD "$literal_password" \
APP_VERSION '1.0.0-preview.1'
grep -qx 'FINANCE_SESSION_ABSOLUTE_TIMEOUT="45m"' "$env_file" \
|| fail 'reinstall removed the custom session timeout'
grep -qx 'FILE_SCANNER_HOST="scanner.internal"' "$env_file" \
|| fail 'reinstall removed the custom scanner host'
grep -qx 'DB_URL="jdbc:mysql://new/kaidi_finance"' "$env_file" \
|| fail 'reinstall did not replace the managed database URL'
[ "$(grep -c '^DB_URL=' "$env_file")" -eq 1 ] || fail 'reinstall duplicated a managed key'
[ "$(grep -c '^APP_VERSION=' "$env_file")" -eq 1 ] || fail 'reinstall duplicated the application version'
[ "$(read_existing_env DB_PASSWORD)" = "$literal_password" ] \
|| fail 'reinstall changed literal shell characters in the database password'
[ ! -e "$WORK/executed" ] || fail 'reinstall executed database password content'
cat > "$STATE_ROOT/setup/application.env" <<'ENV'
DB_URL="jdbc:mysql://runtime/kaidi_finance"
DB_USERNAME="runtime-user"
DB_PASSWORD="runtime-password"
ENV
grep -qx 'jdbc:mysql://runtime/kaidi_finance' <(read_reinstall_env DB_URL) \
|| fail 'reinstall did not prefer the completed setup runtime database URL'
grep -qx 'runtime-user' <(read_reinstall_env DB_USERNAME) \
|| fail 'reinstall did not prefer the completed setup runtime database user'
for port in 1024 18080 65535; do
valid_app_port "$port" || fail "installer rejected valid application port $port"
done
for port in 0 80 1023 65536 invalid 18080.0; do
! valid_app_port "$port" || fail "installer accepted invalid application port $port"
done
port_is_listening() { return 1; }
log() { printf '%s\n' "$*" >/dev/null; }
# shellcheck disable=SC2329 # Invoked by the extracted installer helper.
die() { printf '%s\n' "$*" >&2; return 1; }
export APP_PORT=19090 SERVER_ADDRESS=127.0.0.1 HEALTH_URL='' APP_INDEX_URL='' REINSTALL=false
configure_app_port
[ "$HEALTH_URL" = 'http://127.0.0.1:19090/actuator/health' ] \
|| fail 'selected application port did not reach the health URL'
[ "$APP_INDEX_URL" = 'http://127.0.0.1:19090/' ] \
|| fail 'selected application port did not reach the frontend URL'
export APP_PORT=19091 SERVER_ADDRESS=::1 HEALTH_URL='' APP_INDEX_URL=''
configure_app_port
[ "$HEALTH_URL" = 'http://[::1]:19091/actuator/health' ] \
|| fail 'IPv6 bind address did not produce a bracketed health URL'
[ "$APP_INDEX_URL" = 'http://[::1]:19091/' ] \
|| fail 'IPv6 bind address did not produce a bracketed frontend URL'
[ "$PROXY_TARGET" = 'http://[::1]:19091' ] \
|| fail 'IPv6 bind address did not produce a bracketed reverse-proxy target'
for version in 0.0.0 1.2.3-alpha- 1.2.3--alpha 1.2.3-alpha+build.07; do
is_semver "$version" || fail "installer rejected valid SemVer $version"
"$ROOT/scripts/check-semver.sh" "$version" || fail "release workflow rejected valid SemVer $version"
done
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 "installer accepted invalid SemVer $version"
! "$ROOT/scripts/check-semver.sh" "$version" >/dev/null 2>&1 \
|| fail "release workflow accepted invalid SemVer $version"
done
ARCH_FIXTURE=
uname() {
if [ "${1:-}" = -m ]; then
printf '%s\n' "$ARCH_FIXTURE"
else
command uname "$@"
fi
}
die() {
return 1
}
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"
tar -czf "$WORK/java-fixture.tar.gz" -C "$WORK" fake-jre
java_fixture_sha=$(sha256sum "$WORK/java-fixture.tar.gz" | awk '{print $1}')
cat > "$WORK/java-list.json" <<'JSON'
[{"name":"zulu17-fixture-linux_i686.tar.gz","package_uuid":"fixture-package"}]
JSON
cat > "$WORK/java-detail.json" <<JSON
{"download_url":"https://cdn.azul.com/zulu/bin/zulu17-fixture-linux_i686.tar.gz",
"sha256_hash":"$java_fixture_sha"}
JSON
download() {
case "$1" in
*'/packages/?'*) cp "$WORK/java-list.json" "$2" ;;
*/packages/fixture-package) cp "$WORK/java-detail.json" "$2" ;;
https://cdn.azul.com/*) cp "$WORK/java-fixture.tar.gz" "$2" ;;
*) fail "unexpected Java fixture URL: $1" ;;
esac
}
ARCH_FIXTURE=i686
export KAIDI_JAVA_HOME="$WORK/missing-java-home"
prepare_java >/dev/null 2>&1
[ -x "$JAVA_STAGED_DIR/bin/java" ] || fail 'verified i686 Java runtime was not staged'
rm -rf "$JAVA_STAGED_DIR"
export KAIDI_JAVA_HOME="$WORK/fake-jre"
prepare_java >/dev/null 2>&1
[ -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
RELEASE_AUTH_HEADER_FILE=$WORK/release-auth-header
printf 'Authorization: token %s\n' "$RELEASE_TOKEN" > "$RELEASE_AUTH_HEADER_FILE"
chmod 0600 "$RELEASE_AUTH_HEADER_FILE"
cat > "$WORK/release-api.json" <<'JSON'
{"tag_name":"v1.0.0-preview.11","assets":[
{"name":"release-manifest.json","browser_download_url":"https://gitea.fixture.invalid/assets/release-manifest.json"}
]}
JSON
printf 'signed manifest fixture\n' > "$WORK/release-manifest.fixture"
: > "$WORK/release-curl.log"
curl() {
local output='' url='' header_file='' argument=''
printf '%s\n' "$*" >> "$WORK/release-curl.log"
for argument in "$@"; do
case "$argument" in
@*) header_file=${argument#@} ;;
https://*) url=$argument ;;
esac
done
while [ "$#" -gt 0 ]; do
case "$1" in
-o) shift; output=$1 ;;
esac
shift
done
[ "$header_file" = "$RELEASE_AUTH_HEADER_FILE" ] || return 90
grep -Fqx "Authorization: token $RELEASE_TOKEN" "$header_file" || return 91
[ "$url" = https://gitea.fixture.invalid/ERP-Team/kaidi/releases/download/v1.0.0-preview.11/release-manifest.json ] \
|| return 92
cp "$WORK/release-manifest.fixture" "$output"
}
[ "$(release_asset_url release-manifest.json)" = \
https://gitea.fixture.invalid/ERP-Team/kaidi/releases/download/v1.0.0-preview.11/release-manifest.json ] \
|| fail 'installer did not construct the trusted Gitea release asset URL'
download_release_asset release-manifest.json "$WORK/downloaded-manifest.json"
cmp -s "$WORK/release-manifest.fixture" "$WORK/downloaded-manifest.json" \
|| fail 'installer did not download the private Gitea release asset'
! grep -Fq "$RELEASE_TOKEN" "$WORK/release-curl.log" \
|| fail 'installer leaked the private Gitea token into curl process arguments'
! grep -Fq -- '--location' "$WORK/release-curl.log" \
|| fail 'installer allowed authenticated Gitea redirects'
jq '.assets[0].browser_download_url = "https://assets.fixture.invalid/release-manifest.json"' \
"$WORK/release-api.json" > "$WORK/release-api.cross-origin.json"
mv "$WORK/release-api.cross-origin.json" "$WORK/release-api.json"
[ "$(release_asset_url release-manifest.json)" = \
https://gitea.fixture.invalid/ERP-Team/kaidi/releases/download/v1.0.0-preview.11/release-manifest.json ] \
|| fail 'installer trusted the cross-origin browser download URL'
# shellcheck disable=SC2016 # Match literal installer source.
grep -Fq '[ "$APP_ROOT" = /opt/kaidi ]' "$ROOT/deploy/install.sh" \
|| fail 'installer no longer rejects unsupported custom roots'
grep -Fq '8.4.*) ;;' "$ROOT/deploy/install.sh" \
|| fail 'installer no longer enforces MySQL 8.4.x'
# shellcheck disable=SC2016 # Match literal installer source.
grep -Fq 'write_env_file_preserving_unknown "$CONFIG_ROOT/kaidi.env"' "$ROOT/deploy/install.sh" \
|| fail 'installer no longer uses the reinstall-safe environment writer'
# shellcheck disable=SC2016 # Match literal installer source.
grep -Fq 'download "$api" "$java_metadata"' "$ROOT/deploy/install.sh" \
|| fail 'installer no longer applies the restricted downloader to Java metadata'
# 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 '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'
grep -Fqx 'Restart=no' "$ROOT/deploy/systemd/kaidi-update.service" \
|| fail 'update service can automatically repeat a failed switching transaction'
! grep -Fq '/var/lib/kaidi-update/processing' "$ROOT/deploy/systemd/kaidi-update.path" \
|| fail 'update path can automatically repeat a claimed switching transaction'
# 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" \
|| fail 'installer default release source is not the public Gitea latest API'
# shellcheck disable=SC2016 # Match literal installer source.
grep -Fq 'TRUSTED_PUBLIC_KEY_SHA256=${KAIDI_RELEASE_PUBLIC_KEY_SHA256:-807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9}' "$ROOT/deploy/install.sh" \
|| fail 'installer does not pin the default release public-key fingerprint'
# shellcheck disable=SC2016 # Match literal installer source.
grep -Fq 'if [ "${KAIDI_SETUP_WIZARD+x}" = x ]; then' "$ROOT/deploy/install.sh" \
|| fail 'installer no longer distinguishes an explicit setup-wizard selection'
# shellcheck disable=SC2016 # Match literal installer source.
grep -Fq 'elif [ "$REINSTALL" = true ]; then' "$ROOT/deploy/install.sh" \
|| fail 'repair reinstall no longer defaults to production mode'
! grep -Fq 'docker run' "$ROOT/deploy/install.sh" \
|| fail 'installer must not create a MySQL container'
! grep -Fq 'install_docker' "$ROOT/deploy/install.sh" \
|| fail 'installer must not install Docker or MySQL automatically'
package_log="$WORK/package-manager.log"
for package_manager in apt-get dnf yum; do
package_bin="$WORK/package-manager-$package_manager"
mkdir "$package_bin"
cat > "$package_bin/$package_manager" <<'SH'
#!/bin/sh
printf '%s %s\n' "${0##*/}" "$*" >> "$PACKAGE_LOG"
SH
chmod +x "$package_bin/$package_manager"
: > "$package_log"
(
PATH="$package_bin"
PACKAGE_LOG="$package_log"
export PATH PACKAGE_LOG
install_packages
)
if grep -Eqi '(^|[[:space:]])(default-mysql-client|mysql(-client|-server)?|mariadb(-client|-server)?)([[:space:]]|$)' "$package_log"; then
fail "installer asked $package_manager to install a database package"
fi
done
grep -Fq 'An external MySQL 8.4 database is required' "$ROOT/deploy/install.sh" \
|| fail 'installer does not require an operator-managed external MySQL database'
grep -Fq 'KAIDI_RELEASE_TOKEN_FILE' "$ROOT/deploy/install.sh" \
|| fail 'installer no longer supports a protected initial Gitea token file'
grep -Fq 'KAIDI_SETUP_WIZARD' "$ROOT/deploy/install.sh" \
|| fail 'installer no longer supports first-run setup mode'
grep -Fq 'FINANCE_SETUP_TOKEN_SHA256' "$ROOT/deploy/install.sh" \
|| fail 'installer no longer writes the one-time setup-code hash'
# shellcheck disable=SC2016 # Match literal installer source.
grep -Fq 'SERVER_PORT "$APP_PORT"' "$ROOT/deploy/install.sh" \
|| fail 'installer does not persist the selected application port'
# shellcheck disable=SC2016 # Match literal installer source.
grep -Fq 'SERVER_ADDRESS "$SERVER_ADDRESS"' "$ROOT/deploy/install.sh" \
|| fail 'installer does not persist the selected bind address'
! grep -Eqi 'nginx|/etc/nginx/' "$ROOT/deploy/install.sh" \
|| fail 'installer must not install, start, or modify Nginx'
grep -Fq 'EnvironmentFile=-/var/lib/kaidi/setup/application.env' \
"$ROOT/deploy/systemd/kaidi-finance.service" \
|| fail 'application service no longer loads the setup-completion environment'
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 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'
printf 'Install configuration, custom port, public Gitea, optional private token, i686, setup wizard, and MySQL 8.4 fixtures passed\n'