138 lines
5.7 KiB
Bash
Executable File
138 lines
5.7 KiB
Bash
Executable File
#!/bin/sh
|
|
# Disposable production-Compose smoke test. All generated data stays beneath
|
|
# the ignored .test-runs/<run-id>/production-compose directory.
|
|
set -eu
|
|
|
|
repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
|
compose_file=$repo_root/deploy/production/compose.yaml
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
usage: scripts/test-production-compose run|clean --run-id ID [options]
|
|
|
|
run options:
|
|
--port PORT loopback HTTPS port (default: 18443)
|
|
--keep retain the exact Compose project for inspection
|
|
|
|
clean options:
|
|
--purge --yes also remove only .test-runs/<run-id>/production-compose
|
|
|
|
The run requires a locally available RVBox runtime image. By default it uses
|
|
rvbox-server:test; set RVBOX_PRODUCTION_SMOKE_IMAGE to override it. It creates a
|
|
test-only self-signed localhost certificate, verifies TLS /livez and /readyz,
|
|
and invokes rvc through the server's private control socket. It never writes to
|
|
/tmp or to deployment configuration files.
|
|
EOF
|
|
}
|
|
|
|
fail() { printf '%s\n' "test-production-compose: $*" >&2; exit 2; }
|
|
|
|
safe_id() {
|
|
case $1 in [a-z0-9]* ) ;; *) fail "run ID must start with lowercase alphanumeric" ;; esac
|
|
case $1 in ''|*[!a-z0-9-]*|????????????????????????????????????????????????????????????????*) fail "run ID must match [a-z0-9][a-z0-9-]{0,63}" ;; esac
|
|
}
|
|
|
|
action=${1-}
|
|
[ -n "$action" ] || { usage >&2; exit 2; }
|
|
shift
|
|
case $action in run|clean|--help|-h) ;; *) usage >&2; fail "unknown action $action" ;; esac
|
|
[ "$action" != --help ] && [ "$action" != -h ] || { usage; exit 0; }
|
|
|
|
run_id=
|
|
port=${RVBOX_PRODUCTION_SMOKE_PORT:-18443}
|
|
keep=no
|
|
purge=no
|
|
yes=no
|
|
while [ "$#" -gt 0 ]; do
|
|
case $1 in
|
|
--run-id) [ "$#" -ge 2 ] || fail "--run-id needs a value"; run_id=$2; shift 2 ;;
|
|
--port) [ "$#" -ge 2 ] || fail "--port needs a value"; port=$2; shift 2 ;;
|
|
--keep) keep=yes; shift ;;
|
|
--purge) purge=yes; shift ;;
|
|
--yes) yes=yes; shift ;;
|
|
--help|-h) usage; exit 0 ;;
|
|
*) fail "unknown argument $1" ;;
|
|
esac
|
|
done
|
|
[ -n "$run_id" ] || fail "$action requires --run-id"
|
|
safe_id "$run_id"
|
|
case $port in ''|*[!0-9]*) fail "--port must be an integer" ;; esac
|
|
[ "$port" -ge 1024 ] && [ "$port" -le 65535 ] || fail "--port must be 1024..65535"
|
|
[ "$purge" = no ] || [ "$action" = clean ] || fail "--purge is only valid with clean"
|
|
[ "$yes" = no ] || [ "$action" = clean ] || fail "--yes is only valid with clean"
|
|
[ "$purge" = no ] || [ "$yes" = yes ] || fail "--purge requires --yes"
|
|
|
|
image=${RVBOX_PRODUCTION_SMOKE_IMAGE:-rvbox-server:test}
|
|
project=rvbox-production-smoke-$run_id
|
|
run_root=$repo_root/.test-runs/$run_id/production-compose
|
|
config=$run_root/server.toml
|
|
cert=$run_root/server.pem
|
|
key=$run_root/server-key.pem
|
|
|
|
compose() {
|
|
RVBOX_SERVER_IMAGE=$image RVBOX_SERVER_CONFIG=$config RVBOX_TLS_CERT=$cert \
|
|
RVBOX_TLS_KEY=$key RVBOX_HTTPS_BIND=127.0.0.1 RVBOX_HTTPS_PORT=$port \
|
|
docker compose -p "$project" -f "$compose_file" "$@"
|
|
}
|
|
|
|
collect() {
|
|
[ -d "$run_root" ] || return 0
|
|
compose logs --no-color --tail=500 >"$run_root/compose.log" 2>&1 || true
|
|
}
|
|
|
|
down() {
|
|
compose down --volumes --remove-orphans || true
|
|
}
|
|
|
|
clean() {
|
|
collect
|
|
down
|
|
if [ "$purge" = yes ]; then
|
|
[ -L "$run_root" ] && fail "refusing symlink run root $run_root"
|
|
rm -rf "$run_root"
|
|
# Remove the per-run parent only when production-compose was its only
|
|
# child; another harness lane may legitimately share the same run ID.
|
|
rmdir "$repo_root/.test-runs/$run_id" 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
case $action in
|
|
clean)
|
|
clean
|
|
printf 'cleaned run_id=%s\n' "$run_id"
|
|
;;
|
|
run)
|
|
docker version >/dev/null 2>&1 || fail "Docker is unavailable"
|
|
docker compose version >/dev/null 2>&1 || fail "Docker Compose is unavailable"
|
|
command -v openssl >/dev/null 2>&1 || fail "openssl is required for the test-only certificate"
|
|
command -v curl >/dev/null 2>&1 || fail "curl is required for TLS probes"
|
|
docker image inspect "$image" >/dev/null 2>&1 || fail "runtime image $image is absent; build or set RVBOX_PRODUCTION_SMOKE_IMAGE"
|
|
[ ! -e "$run_root" ] || fail "refusing to overwrite existing run root $run_root; use clean --purge --yes"
|
|
umask 077
|
|
mkdir -p "$run_root"
|
|
cp "$repo_root/deploy/production/server.toml.example" "$config"
|
|
# Docker user-namespace remapping may prevent a container from reading
|
|
# a host file with mode 0600. This is a disposable test-only key under
|
|
# .test-runs, never a production certificate.
|
|
chmod 0644 "$config"
|
|
openssl req -x509 -newkey rsa:2048 -nodes -sha256 -days 1 -subj /CN=localhost \
|
|
-addext 'subjectAltName=DNS:localhost,IP:127.0.0.1' -keyout "$key" -out "$cert" >/dev/null 2>&1
|
|
chmod 0644 "$key" "$cert"
|
|
trap 'status=$?; collect; if [ "$keep" = no ]; then down; fi; exit "$status"' EXIT INT TERM
|
|
compose up -d
|
|
attempt=0
|
|
while [ "$attempt" -lt 40 ]; do
|
|
if curl --fail --silent --show-error --max-time 3 --cacert "$cert" "https://127.0.0.1:$port/livez" >/dev/null && \
|
|
curl --fail --silent --show-error --max-time 3 --cacert "$cert" "https://127.0.0.1:$port/readyz" >/dev/null; then
|
|
break
|
|
fi
|
|
attempt=$((attempt + 1))
|
|
sleep 1
|
|
done
|
|
curl --fail --silent --show-error --max-time 3 --cacert "$cert" "https://127.0.0.1:$port/livez" >/dev/null
|
|
curl --fail --silent --show-error --max-time 3 --cacert "$cert" "https://127.0.0.1:$port/readyz" >/dev/null
|
|
compose exec -T server /usr/local/bin/rvc --socket /run/rvbox/server.sock stat >"$run_root/rvc-stat.txt"
|
|
printf 'production Compose smoke passed: run_id=%s artifacts=%s\n' "$run_id" "$run_root"
|
|
;;
|
|
esac
|