Files
Magent/scripts/ci_container_smoke.sh
T

142 lines
5.3 KiB
Bash

#!/usr/bin/env bash
# No argument preserves the CI build-and-test entry point. Pass an image tag to
# test an already-built release without building, pulling, or publishing it.
set -euo pipefail
if [ "$#" -gt 1 ]; then
echo "Usage: $0 [existing-image]" >&2
exit 2
fi
script_directory="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
repository_directory="$(cd -- "$script_directory/.." && pwd)"
image="${1:-magent:ci}"
size_limit_mb="${MAGENT_IMAGE_MAX_MB:-350}"
managed_mode="${MAGENT_SMOKE_MANAGED:-false}"
if [[ "$managed_mode" != true && "$managed_mode" != false ]]; then
echo "MAGENT_SMOKE_MANAGED must be true or false." >&2
exit 2
fi
if ! [[ "$size_limit_mb" =~ ^[1-9][0-9]*$ ]]; then
echo "MAGENT_IMAGE_MAX_MB must be a positive integer (MiB)." >&2
exit 2
fi
container_name="magent-ci-${GITHUB_RUN_ID:-local}-$$-${RANDOM}"
volume_name="${container_name}-data"
network_name="${container_name}-isolated"
container_created=false
volume_created=false
network_created=false
cleanup() {
result=$?
trap - EXIT
if [ "$result" -ne 0 ] && [ "$container_created" = true ]; then
# Only synthetic credentials/data enter this test container.
docker logs --tail 100 "$container_name" >&2 || true
fi
if [ "$container_created" = true ]; then
docker rm -f "$container_name" >/dev/null 2>&1 || true
fi
if [ "$volume_created" = true ]; then
docker volume rm "$volume_name" >/dev/null 2>&1 || true
fi
if [ "$network_created" = true ]; then
docker network rm "$network_name" >/dev/null 2>&1 || true
fi
exit "$result"
}
trap cleanup EXIT
if [ "$#" -eq 0 ]; then
docker build --tag "$image" "$repository_directory"
fi
image_size="$(docker image inspect --format '{{.Size}}' "$image")"
image_id="$(docker image inspect --format '{{.Id}}' "$image")"
if [ "$image_size" -gt "$((size_limit_mb * 1024 * 1024))" ]; then
echo "Image exceeds ${size_limit_mb} MiB unpacked budget: ${image_size} bytes" >&2
exit 1
fi
echo "Image size: ${image_size} bytes (budget ${size_limit_mb} MiB unpacked)"
# Inspect the image's original filesystem before tmpfs or volume mounts could
# hide accidentally shipped build caches or private files.
docker run --rm --pull never --network none --read-only \
--cap-drop ALL --security-opt no-new-privileges:true \
--entrypoint python -i "$image_id" - packaging < "$script_directory/container_smoke.py"
# An internal network prevents accidental external integration calls. No host
# files, existing volumes, host credentials, or host ports are used.
docker network create --internal "$network_name" >/dev/null
network_created=true
docker volume create "$volume_name" >/dev/null
volume_created=true
start_container() {
local -a secret_environment
if [ "$managed_mode" = true ]; then
# Exercise the image defaults: no keys, origin or managed-mode variables.
secret_environment=()
else
secret_environment=(
--env JWT_SECRET=ci-only-secret-with-at-least-32-characters
--env SETTINGS_ENCRYPTION_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
--env SETUP_TOKEN=ci-only-setup-token-with-at-least-32-characters
--env AUTH_COOKIE_SECURE=true
--env MAGENT_APPLICATION_URL=https://magent-ci.example.test
)
fi
docker run --detach --name "$container_name" --pull never \
--network "$network_name" \
--read-only --cap-drop ALL --security-opt no-new-privileges:true \
--pids-limit 256 --memory 1g --cpus 2 \
--tmpfs /tmp:rw,noexec,nosuid,size=64m,uid=1000,gid=1000 \
--tmpfs /app/frontend/.next/cache:rw,noexec,nosuid,size=128m,uid=1000,gid=1000 \
--volume "$volume_name:/app/data" \
"${secret_environment[@]}" \
--env ADMIN_PASSWORD= \
--env AUTH_COOKIE_SAMESITE=strict \
--env BACKGROUND_TASKS_ENABLED=false \
--env MAGENT_METRICS_ENABLED=false \
"$image_id" >/dev/null
container_created=true
}
wait_for_health() {
local deadline=$((SECONDS + 150))
local status
while true; do
status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}missing{{end}}' "$container_name")"
if [ "$status" = healthy ]; then
return
fi
if [ "$status" = missing ] || [ "$SECONDS" -ge "$deadline" ]; then
echo "Container did not become healthy within 150 seconds (status: $status)" >&2
return 1
fi
if [ "$(docker inspect --format '{{.State.Running}}' "$container_name")" != true ]; then
echo "Container exited before becoming healthy" >&2
return 1
fi
sleep 2
done
}
# Deliberately no root/chown helper: the image must initialize a fresh named
# volume with correct ownership for its normal non-root runtime user.
start_container
wait_for_health
docker exec -i "$container_name" python - fresh < "$script_directory/container_smoke.py"
docker restart --time 15 "$container_name" >/dev/null
wait_for_health
docker exec -i "$container_name" python - persisted < "$script_directory/container_smoke.py"
# Recreation proves database/configuration are in the volume, not merely in the
# container's writable layer. Both test instances use the same immutable image.
docker rm -f "$container_name" >/dev/null
container_created=false
start_container
wait_for_health
docker exec -i "$container_name" python - persisted < "$script_directory/container_smoke.py"
echo "Container smoke passed: fresh install, security headers, assets, login, backup/restore, restart, recreation."