#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" INSTALLER_SOURCE="$(cd "$SCRIPT_DIR/.." && pwd)" TEST_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/ours-rp-installer-env-contract.XXXXXX")" cleanup() { rm -rf "$TEST_ROOT" } trap cleanup EXIT fail() { printf 'FAIL: %s\n' "$*" >&2 exit 1 } assert_equals() { local expected="$1" local actual="$2" local message="$3" [[ "$actual" == "$expected" ]] || fail "$message (expected=$expected actual=$actual)" } assert_not_contains() { local needle="$1" local path="$2" ! grep -Fq "$needle" "$path" || fail "unexpected value in $path: $needle" } expect_exit() { local expected_status="$1" local output_path="$2" shift 2 local actual_status set +e "$@" >"$output_path" 2>&1 actual_status=$? set -e [[ "$actual_status" == "$expected_status" ]] || { cat "$output_path" >&2 || true fail "unexpected exit status (expected=$expected_status actual=$actual_status)" } } host_arch() { case "$(uname -m)" in x86_64) printf 'amd64\n' ;; aarch64) printf 'arm64\n' ;; *) fail "unsupported test host architecture: $(uname -m)" ;; esac } env_value() { local env_path="$1" local key="$2" ( # shellcheck disable=SC1090 source "$INSTALLER_SOURCE/scripts/common.sh" env_file_value "$env_path" "$key" ) } create_fixture() { local name="$1" local fixture="$TEST_ROOT/$name" local arch arch="$(host_arch)" mkdir -p "$fixture" cp -a "$INSTALLER_SOURCE"/. "$fixture"/ rm -rf "$fixture/tests" mkdir -p "$fixture/images" "$fixture/fake-bin" sed -i \ -e "s|__PACKAGE_ARCH__|$arch|g" \ -e "s|__PACKAGE_PLATFORM__|linux/$arch|g" \ -e "s|__RUNTIME_IMAGE__|ours-rp-runtime-$arch:new|g" \ -e "s|__METRICS_IMAGE__|ours-rp-metrics-$arch:new|g" \ -e "s|__HOST_DATA_DIR__|$fixture/data|g" \ "$fixture/.env.example" cat > "$fixture/PACKAGE-MANIFEST.env" < "$fixture/fake-bin/docker" <<'EOF' #!/usr/bin/env bash printf '%s\n' "$*" >> "$FAKE_DOCKER_LOG" if [[ "${1:-}" == "compose" && "${2:-}" == "version" ]]; then exit 0 fi exit 99 EOF chmod +x "$fixture/fake-bin/docker" for command in jq rsync curl gzip tar; do cat > "$fixture/fake-bin/$command" <<'EOF' #!/usr/bin/env bash exit 0 EOF chmod +x "$fixture/fake-bin/$command" done printf '%s\n' "$fixture" } run_upgrade() { local fixture="$1" local old_env="$2" shift 2 ( cd "$fixture" export PATH="$fixture/fake-bin:$PATH" export FAKE_DOCKER_LOG="$fixture/fake-docker.log" "$@" ./scripts/upgrade.sh --reuse-env-from "$old_env" ) } run_upgrade_without_reuse() { local fixture="$1" ( cd "$fixture" export PATH="$fixture/fake-bin:$PATH" export FAKE_DOCKER_LOG="$fixture/fake-docker.log" ./scripts/upgrade.sh ) } new_fixture_with_old_env() { local name="$1" local fixture fixture="$(create_fixture "$name")" cat > "$fixture/old.env" <<'EOF' INTERVAL_SECS=120 RPKI_IMAGE=ours-rp-runtime-old:old REMOVED_LEGACY_KEY=legacy-value EOF printf '%s\n' "$fixture" } fixture="$(create_fixture template-defaults)" expect_exit 99 "$fixture/template.log" run_upgrade_without_reuse "$fixture" assert_equals "600" "$(env_value "$fixture/.env" INTERVAL_SECS)" "template default must be retained without reuse" fixture="$(new_fixture_with_old_env default-old)" expect_exit 99 "$fixture/default.log" run_upgrade "$fixture" "$fixture/old.env" assert_equals "120" "$(env_value "$fixture/.env" INTERVAL_SECS)" "old value must override template default" assert_equals "ours-rp-runtime-$(host_arch):new" "$(env_value "$fixture/.env" RPKI_IMAGE)" "standard upgrade must retain package image" assert_equals "10" "$(env_value "$fixture/.env" METRICS_POLL_SECS)" "template default must fill a key absent from old environment" ! grep -q '^REMOVED_LEGACY_KEY=' "$fixture/.env" || fail "removed old key was copied into target environment" grep -q 'reused_keys=INTERVAL_SECS' "$fixture/default.log" || fail "reused key summary missing" grep -q 'ignored_removed_old_keys=REMOVED_LEGACY_KEY' "$fixture/default.log" || fail "removed key summary missing" grep -q 'template_only_keys=' "$fixture/default.log" || fail "template key summary missing" fixture="$(new_fixture_with_old_env invocation-overrides)" expect_exit 99 "$fixture/override.log" run_upgrade "$fixture" "$fixture/old.env" env INTERVAL_SECS=300 GRAFANA_ADMIN_PASSWORD='secret with space' assert_equals "300" "$(env_value "$fixture/.env" INTERVAL_SECS)" "invocation value must override old value" assert_equals "secret with space" "$(env_value "$fixture/.env" GRAFANA_ADMIN_PASSWORD)" "invocation value must preserve shell-sensitive content" grep -q 'command_override_keys=INTERVAL_SECS,GRAFANA_ADMIN_PASSWORD' "$fixture/override.log" || fail "override key summary missing" assert_not_contains 'secret with space' "$fixture/override.log" fixture="$(new_fixture_with_old_env invocation-special-characters)" special_password="O'Reilly" special_password+=' $literal `tick` slash\' expect_exit 99 "$fixture/special.log" run_upgrade "$fixture" "$fixture/old.env" env GRAFANA_ADMIN_PASSWORD="$special_password" assert_equals "$special_password" "$(env_value "$fixture/.env" GRAFANA_ADMIN_PASSWORD)" "invocation value with dotenv-sensitive characters must round-trip" assert_not_contains "$special_password" "$fixture/special.log" fixture="$(create_fixture existing-target-env)" cp "$fixture/.env.example" "$fixture/.env" sed -i 's/^INTERVAL_SECS=.*/INTERVAL_SECS=180/' "$fixture/.env" cat > "$fixture/old.env" <<'EOF' INTERVAL_SECS=120 EOF expect_exit 99 "$fixture/existing.log" run_upgrade "$fixture" "$fixture/old.env" env INTERVAL_SECS=300 assert_equals "300" "$(env_value "$fixture/.env" INTERVAL_SECS)" "invocation value must override an existing target environment" fixture="$(new_fixture_with_old_env explicit-empty)" expect_exit 1 "$fixture/empty.log" run_upgrade "$fixture" "$fixture/old.env" env INTERVAL_SECS= [[ ! -s "$fixture/fake-docker.log" ]] || fail "explicit empty value reached Docker" grep -q 'INTERVAL_SECS' "$fixture/empty.log" || fail "empty-value error did not identify key" fixture="$(create_fixture empty-old-value)" cat > "$fixture/old.env" <<'EOF' INTERVAL_SECS= EOF expect_exit 1 "$fixture/old-empty.log" run_upgrade "$fixture" "$fixture/old.env" [[ ! -s "$fixture/fake-docker.log" ]] || fail "empty reused value reached Docker" grep -q 'INTERVAL_SECS' "$fixture/old-empty.log" || fail "empty reused value error did not identify key" fixture="$(create_fixture empty-template)" sed -i 's/^INTERVAL_SECS=.*/INTERVAL_SECS=/' "$fixture/.env.example" cat > "$fixture/old.env" <<'EOF' INTERVAL_SECS=120 EOF expect_exit 1 "$fixture/template-empty.log" run_upgrade "$fixture" "$fixture/old.env" [[ ! -s "$fixture/fake-docker.log" ]] || fail "empty template default reached Docker" grep -q 'INTERVAL_SECS' "$fixture/template-empty.log" || fail "empty template error did not identify key" fixture="$(new_fixture_with_old_env package-guard)" arch="$(host_arch)" other_arch="amd64" [[ "$arch" == "amd64" ]] && other_arch="arm64" expect_exit 1 "$fixture/arch.log" run_upgrade "$fixture" "$fixture/old.env" env PACKAGE_ARCH="$other_arch" [[ ! -s "$fixture/fake-docker.log" ]] || fail "package architecture mismatch reached Docker" grep -q 'mismatches manifest PACKAGE_ARCH' "$fixture/arch.log" || fail "package architecture guard did not reject override" printf 'PASS: Docker installer upgrade environment contract\n'