Files
Ekklesia/scripts/smoke-infrastructure.sh
T
ekklesia 2465d9c1f7
CI / verify (push) Successful in 24s
CI / infrastructure-smoke (push) Successful in 19s
runner fix
2026-07-13 20:19:30 -05:00

294 lines
8.0 KiB
Bash
Executable File

#!/usr/bin/env bash
set -Eeuo pipefail
repository_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$repository_root"
command -v docker >/dev/null || {
echo "Docker is required for the infrastructure smoke test." >&2
exit 1
}
command -v curl >/dev/null || {
echo "curl is required for the infrastructure smoke test." >&2
exit 1
}
command -v cmp >/dev/null || {
echo "cmp is required for the infrastructure smoke test." >&2
exit 1
}
docker compose version >/dev/null
curl_help="$(curl --help all)"
if [[ "$curl_help" != *"--aws-sigv4"* ]]; then
echo "curl must support --aws-sigv4 for authenticated S3 checks." >&2
exit 1
fi
raw_project="ekklesia-smoke-${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-0}-$$"
project="${raw_project//[^a-zA-Z0-9_-]/-}"
project="${project,,}"
temporary_directory="$(mktemp -d)"
payload_file="$temporary_directory/payload.txt"
download_file="$temporary_directory/download.txt"
bucket_list_file="$temporary_directory/buckets.xml"
: >"$bucket_list_file"
export POSTGRES_DB="ekklesia_smoke"
export POSTGRES_USER="ekklesia_smoke"
export POSTGRES_PASSWORD="ekklesia-smoke-postgres-password"
export POSTGRES_PORT="${EKKLESIA_SMOKE_POSTGRES_PORT:-0}"
export S3_ACCESS_KEY="ekklesia-smoke-access"
export S3_SECRET_KEY="ekklesia-smoke-secret-key"
export S3_BUCKET="ekklesia-smoke"
export S3_REGION="us-east-1"
export S3_PORT="${EKKLESIA_SMOKE_S3_PORT:-0}"
compose=(
docker compose
--project-name "$project"
--env-file .env.example
--file infrastructure/compose.yaml
)
running_in_container=false
current_container=""
compose_network=""
connected_to_compose_network=false
if [[ -f /.dockerenv ]]; then
running_in_container=true
if [[ -z "${HOSTNAME:-}" ]]; then
echo "HOSTNAME is required when the smoke test runs inside a Docker container." >&2
exit 1
fi
current_container="$HOSTNAME"
fi
cleanup() {
local exit_status=$?
local cleanup_output
trap - EXIT INT TERM
if ((exit_status != 0)); then
"${compose[@]}" logs --no-color >&2 || true
fi
if [[ "$connected_to_compose_network" == "true" ]]; then
if ! cleanup_output="$(
docker network disconnect "$compose_network" "$current_container" 2>&1
)"; then
echo "Infrastructure smoke network cleanup failed:" >&2
echo "$cleanup_output" >&2
if ((exit_status == 0)); then
exit_status=1
fi
fi
fi
if ! cleanup_output="$(
"${compose[@]}" down --volumes --remove-orphans --timeout 10 2>&1
)"; then
echo "Infrastructure smoke cleanup failed:" >&2
echo "$cleanup_output" >&2
if ((exit_status == 0)); then
exit_status=1
fi
fi
rm -rf -- "$temporary_directory"
exit "$exit_status"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
"${compose[@]}" config --quiet
"${compose[@]}" up --detach --wait --wait-timeout 120
"${compose[@]}" up --detach --wait --wait-timeout 120
database_result="$(
"${compose[@]}" exec -T \
-e "PGPASSWORD=$POSTGRES_PASSWORD" \
postgres \
psql \
--no-psqlrc \
--host 127.0.0.1 \
--username "$POSTGRES_USER" \
--dbname "$POSTGRES_DB" \
--tuples-only \
--no-align \
--set ON_ERROR_STOP=1 \
--command "SELECT current_database(), current_user"
)"
if [[ "$database_result" != "$POSTGRES_DB|$POSTGRES_USER" ]]; then
echo "PostgreSQL returned an unexpected identity: $database_result" >&2
exit 1
fi
if "${compose[@]}" exec -T \
-e "PGPASSWORD=incorrect-password" \
postgres \
psql \
--no-psqlrc \
--host 127.0.0.1 \
--username "$POSTGRES_USER" \
--dbname "$POSTGRES_DB" \
--command "SELECT 1" >/dev/null 2>&1; then
echo "PostgreSQL unexpectedly accepted an invalid password." >&2
exit 1
fi
if [[ "$running_in_container" == "true" ]]; then
object_storage_container="$("${compose[@]}" ps -q object-storage)"
if [[ -z "$object_storage_container" ]]; then
echo "Compose did not return the object-storage container ID." >&2
exit 1
fi
network_output="$(
docker inspect \
--format '{{range $networkName, $_ := .NetworkSettings.Networks}}{{println $networkName}}{{end}}' \
"$object_storage_container"
)"
object_storage_networks=()
while IFS= read -r network_name; do
if [[ -n "$network_name" ]]; then
object_storage_networks+=("$network_name")
fi
done <<<"$network_output"
if ((${#object_storage_networks[@]} != 1)); then
echo "Expected object-storage to use exactly one Compose network." >&2
exit 1
fi
compose_network="${object_storage_networks[0]}"
current_network_output="$(
docker inspect \
--format '{{range $networkName, $_ := .NetworkSettings.Networks}}{{println $networkName}}{{end}}' \
"$current_container"
)"
already_connected=false
while IFS= read -r network_name; do
if [[ "$network_name" == "$compose_network" ]]; then
already_connected=true
break
fi
done <<<"$current_network_output"
if [[ "$already_connected" != "true" ]]; then
docker network connect "$compose_network" "$current_container"
connected_to_compose_network=true
fi
s3_endpoint="http://object-storage:8333"
else
s3_binding="$("${compose[@]}" port object-storage 8333)"
s3_port="${s3_binding##*:}"
if [[ ! "$s3_port" =~ ^[0-9]+$ ]]; then
echo "Compose returned an invalid S3 port binding: $s3_binding" >&2
exit 1
fi
s3_endpoint="http://127.0.0.1:$s3_port"
fi
signed_curl=(
curl
--silent
--show-error
--aws-sigv4 "aws:amz:$S3_REGION:s3"
--user "$S3_ACCESS_KEY:$S3_SECRET_KEY"
)
s3_ready=false
for _ in {1..30}; do
status="$(
"${signed_curl[@]}" \
--output "$bucket_list_file" \
--write-out "%{http_code}" \
"$s3_endpoint/" || true
)"
bucket_list="$(<"$bucket_list_file")"
if [[ "$status" == "200" && "$bucket_list" == *"<Name>$S3_BUCKET</Name>"* ]]; then
s3_ready=true
break
fi
sleep 2
done
if [[ "$s3_ready" != "true" ]]; then
echo "The authenticated S3 endpoint and bucket did not become ready." >&2
exit 1
fi
printf '%s' "ekklesia-storage-smoke" >"$payload_file"
object_key="smoke/$project/object.txt"
object_url="$s3_endpoint/$S3_BUCKET/$object_key"
put_status="$(
"${signed_curl[@]}" \
--upload-file "$payload_file" \
--header "Content-Type: text/plain" \
--output /dev/null \
--write-out "%{http_code}" \
"$object_url"
)"
if [[ "$put_status" != "200" ]]; then
echo "S3 PUT returned $put_status instead of 200." >&2
exit 1
fi
get_status="$(
"${signed_curl[@]}" \
--output "$download_file" \
--write-out "%{http_code}" \
"$object_url"
)"
if [[ "$get_status" != "200" ]] || ! cmp --silent "$payload_file" "$download_file"; then
echo "S3 GET did not return the uploaded object unchanged." >&2
exit 1
fi
head_status="$(
"${signed_curl[@]}" --head --output /dev/null --write-out "%{http_code}" "$object_url"
)"
if [[ "$head_status" != "200" ]]; then
echo "S3 HEAD returned $head_status instead of 200." >&2
exit 1
fi
anonymous_status="$(
curl --silent --show-error --output /dev/null --write-out "%{http_code}" "$object_url"
)"
if [[ "$anonymous_status" != "403" ]]; then
echo "S3 anonymous GET returned $anonymous_status instead of 403." >&2
exit 1
fi
bad_signature_status="$(
curl \
--silent \
--show-error \
--aws-sigv4 "aws:amz:$S3_REGION:s3" \
--user "$S3_ACCESS_KEY:incorrect-secret" \
--output /dev/null \
--write-out "%{http_code}" \
"$object_url"
)"
if [[ "$bad_signature_status" != "403" ]]; then
echo "S3 invalid signature returned $bad_signature_status instead of 403." >&2
exit 1
fi
delete_status="$(
"${signed_curl[@]}" \
--request DELETE \
--output /dev/null \
--write-out "%{http_code}" \
"$object_url"
)"
if [[ "$delete_status" != "204" ]]; then
echo "S3 DELETE returned $delete_status instead of 204." >&2
exit 1
fi
missing_status="$(
"${signed_curl[@]}" --head --output /dev/null --write-out "%{http_code}" "$object_url"
)"
if [[ "$missing_status" != "404" ]]; then
echo "Deleted S3 object returned $missing_status instead of 404." >&2
exit 1
fi
echo "Infrastructure smoke test passed for PostgreSQL and authenticated S3 operations."