149 lines
6.5 KiB
Bash
149 lines
6.5 KiB
Bash
#!/bin/sh
|
|
# Run in a disposable Linux container, never on a production host.
|
|
set -eu
|
|
REPO="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
|
|
fixture="$(mktemp -d /tmp/caiwu-release-guard.XXXXXX)"
|
|
cleanup() {
|
|
for legacy in activate-release.sh activate-incremental-release.sh; do
|
|
chattr -i "$fixture/legacy-entrypoints/$legacy" 2>/dev/null || true
|
|
done
|
|
rm -rf "$fixture"
|
|
}
|
|
trap cleanup EXIT
|
|
mkdir -p "$fixture/bin"
|
|
cat > "$fixture/bin/docker" <<'EOF'
|
|
#!/bin/sh
|
|
printf '%s\n' "$*" >> "$DOCKER_CALLS"
|
|
if [ "$1" = inspect ]; then
|
|
case "$*" in
|
|
*Config.Image*) printf 'caiwu_akaunting:old\n' ;;
|
|
*) printf '%s\n' "${FAKE_RUNNING_SHA:-}" ;;
|
|
esac
|
|
exit 0
|
|
fi
|
|
exit 91
|
|
EOF
|
|
chmod +x "$fixture/bin/docker"
|
|
PATH="$fixture/bin:$PATH"
|
|
export PATH
|
|
old=1111111111111111111111111111111111111111
|
|
new=2222222222222222222222222222222222222222
|
|
requested=3333333333333333333333333333333333333333
|
|
passed=0
|
|
pass() { passed=$((passed + 1)); printf 'PASS: %s\n' "$*"; }
|
|
fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; }
|
|
|
|
new_runtime() {
|
|
REMOTE_DIR="$fixture/$1"
|
|
mkdir -p "$REMOTE_DIR/runtime"
|
|
: > "$REMOTE_DIR/runtime/.env"
|
|
: > "$REMOTE_DIR/runtime/config-db.env"
|
|
printf 'CLICKHOUSE_PASSWORD=fixture\nBUSINESS_CLICKHOUSE_PASSWORD=fixture\n' > "$REMOTE_DIR/runtime/clickhouse.env"
|
|
printf 'BUSINESS_STORAGE_DRIVER=qiniu\nQINIU_ACCESS_KEY=fixture\nQINIU_SECRET_KEY=fixture\n' > "$REMOTE_DIR/runtime/qiniu-storage.env"
|
|
printf '%s\n' "$old" > "$REMOTE_DIR/.deployed-sha"
|
|
printf 'previous runtime configuration\n' > "$REMOTE_DIR/compose.jiangling.yml"
|
|
DOCKER_CALLS="$REMOTE_DIR/docker-calls"
|
|
: > "$DOCKER_CALLS"
|
|
FAKE_RUNNING_SHA="$old"
|
|
export DOCKER_CALLS FAKE_RUNNING_SHA
|
|
}
|
|
|
|
for activation in activate-release.sh activate-incremental-release.sh; do
|
|
for scenario in stale missing-protocol marker-image-mismatch unchanged; do
|
|
new_runtime "$activation-$scenario"
|
|
: > "$REMOTE_DIR/upload.tar.gz"
|
|
expected="$old"
|
|
case "$scenario" in
|
|
stale) printf '%s\n' "$new" > "$REMOTE_DIR/.deployed-sha"; FAKE_RUNNING_SHA="$new" ;;
|
|
marker-image-mismatch) FAKE_RUNNING_SHA="$new" ;;
|
|
esac
|
|
set +e
|
|
if [ "$scenario" = missing-protocol ]; then
|
|
sh "$REPO/deploy/$activation" "$REMOTE_DIR/upload.tar.gz" tag "$REMOTE_DIR" "$requested" 0 2 > "$REMOTE_DIR/output" 2>&1
|
|
else
|
|
sh "$REPO/deploy/$activation" "$REMOTE_DIR/upload.tar.gz" tag "$REMOTE_DIR" "$requested" 0 2 "$expected" fixture > "$REMOTE_DIR/output" 2>&1
|
|
fi
|
|
status=$?
|
|
set -e
|
|
if [ "$scenario" = unchanged ]; then
|
|
[ "$status" -eq 91 ] || { cat "$REMOTE_DIR/output"; fail 'accepted release did not reach image loading'; }
|
|
else
|
|
[ "$status" -ne 0 ] || fail "$activation accepted $scenario"
|
|
if grep -Ev '^inspect ' "$DOCKER_CALLS" | grep -q .; then
|
|
fail "$activation mutated Docker before rejecting $scenario"
|
|
fi
|
|
[ "$(cat "$REMOTE_DIR/compose.jiangling.yml")" = 'previous runtime configuration' ] || fail 'rejected release overwrote runtime configuration'
|
|
fi
|
|
if [ "$scenario" != missing-protocol ]; then
|
|
[ -s "$REMOTE_DIR/runtime/private/deployments.tsv" ] || fail 'deployment receipt missing'
|
|
if [ "$scenario" != unchanged ]; then
|
|
grep -q 'blocked' "$REMOTE_DIR/runtime/private/deployments.tsv" || fail 'rejection not recorded'
|
|
fi
|
|
fi
|
|
pass "$activation: $scenario"
|
|
done
|
|
done
|
|
|
|
. "$REPO/deploy/release-guard.sh"
|
|
new_runtime bootstrap
|
|
rm "$REMOTE_DIR/.deployed-sha"
|
|
FAKE_RUNNING_SHA=''
|
|
EXPECTED_PREVIOUS_SHA=none
|
|
DEPLOY_SHA="$new"
|
|
caiwu_verify_release_base
|
|
pass 'first deployment with no running application'
|
|
EXPECTED_PREVIOUS_SHA=''
|
|
if caiwu_verify_release_base 2>/dev/null; then fail 'empty baseline accepted'; fi
|
|
pass 'empty baseline rejected'
|
|
|
|
new_runtime ordered-uploaders
|
|
EXPECTED_PREVIOUS_SHA="$old"
|
|
DEPLOY_SHA="$new"
|
|
# Both uploaders checked the same old revision. The newer one activates first.
|
|
(
|
|
exec 9> "$REMOTE_DIR/.deploy.lock"
|
|
flock -x 9
|
|
caiwu_verify_release_base
|
|
printf '%s\n' "$new" > "$REMOTE_DIR/.deployed-sha"
|
|
)
|
|
FAKE_RUNNING_SHA="$new"
|
|
DEPLOY_SHA="$requested"
|
|
if (
|
|
exec 9> "$REMOTE_DIR/.deploy.lock"
|
|
flock -x 9
|
|
caiwu_verify_release_base
|
|
) 2>/dev/null; then fail 'delayed uploader overwrote the newer release'; fi
|
|
[ "$(cat "$REMOTE_DIR/.deployed-sha")" = "$new" ] || fail 'production marker regressed'
|
|
pass 'delayed uploader is rejected after a newer activation'
|
|
|
|
new_runtime staged-config
|
|
RELEASE_DIR="$fixture/staging"
|
|
RELEASE_APP_URL=''
|
|
mkdir -p "$RELEASE_DIR"
|
|
for file in compose.jiangling.yml activate-release.sh activate-incremental-release.sh release-guard.sh retired-activation.sh install-release-protection.sh config-db-init.sh run-finance-batch.sh run-finance-import.py finance-batch-imports.json clickhouse-resource-policy.xml update-runtime-url.sh configure-dingtalk-callback.sh host-nginx-8442-dingtalk.conf setup-incremental-registry.sh registry-config.yml; do
|
|
printf 'new staged runtime configuration\n' > "$RELEASE_DIR/$file"
|
|
done
|
|
printf '#!/bin/sh\nexit 0\n' > "$RELEASE_DIR/install-release-protection.sh"
|
|
rm "$RELEASE_DIR/registry-config.yml"
|
|
if caiwu_install_release_files 2>/dev/null; then fail 'partial upload accepted'; fi
|
|
[ "$(cat "$REMOTE_DIR/compose.jiangling.yml")" = 'previous runtime configuration' ] || fail 'partial upload replaced runtime files'
|
|
pass 'incomplete staging directory leaves runtime files untouched'
|
|
printf 'complete\n' > "$RELEASE_DIR/registry-config.yml"
|
|
caiwu_install_release_files
|
|
[ "$(cat "$REMOTE_DIR/compose.jiangling.yml")" = 'new staged runtime configuration' ] || fail 'staged configuration not installed'
|
|
pass 'complete isolated upload installs the intended runtime files'
|
|
|
|
new_runtime legacy-entrypoints
|
|
for legacy in activate-release.sh activate-incremental-release.sh; do
|
|
printf '#!/bin/sh\nexit 0\n' > "$REMOTE_DIR/$legacy"
|
|
done
|
|
sh "$REPO/deploy/install-release-protection.sh" "$REMOTE_DIR"
|
|
for legacy in activate-release.sh activate-incremental-release.sh; do
|
|
if cp "$REPO/deploy/$legacy" "$REMOTE_DIR/$legacy" 2>/dev/null; then fail 'old uploader replaced the protected entry point'; fi
|
|
if sh "$REMOTE_DIR/$legacy" old-arguments 2>/dev/null; then fail 'old activation path accepted a release'; fi
|
|
pass "$legacy: legacy uploader and activation both rejected"
|
|
done
|
|
sh "$REPO/deploy/install-release-protection.sh" "$REMOTE_DIR"
|
|
pass 'installing entry point protection again is idempotent'
|
|
printf 'PASS: %s release protection scenarios\n' "$passed"
|