mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-03 04:42:11 -04:00
211 lines
10 KiB
Bash
Executable File
211 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Isolated local-only smoke; never accepts an existing container, volume, or URL.
|
|
set -Eeuo pipefail
|
|
umask 077
|
|
|
|
# A whole-run bound also leaves time for the exit trap before an outer CI watchdog.
|
|
if [[ ${OPENNEST_SMOKE_WATCHDOG:-} != 1 ]]; then
|
|
command -v timeout >/dev/null || { printf 'Missing prerequisite: timeout\n' >&2; exit 2; }
|
|
exec timeout --signal=TERM --kill-after=30s 450s env OPENNEST_SMOKE_WATCHDOG=1 \
|
|
bash "${BASH_SOURCE[0]}" "$@"
|
|
fi
|
|
|
|
usage() {
|
|
printf 'Usage: %s --image <local-image> [--results <directory>]\n' "$0" >&2
|
|
}
|
|
image=''
|
|
results=''
|
|
while (($#)); do
|
|
case "$1" in
|
|
--image) [[ $# -ge 2 && -z "$image" ]] || { usage; exit 2; }; image=$2; shift 2 ;;
|
|
--results) [[ $# -ge 2 && -z "$results" ]] || { usage; exit 2; }; results=$2; shift 2 ;;
|
|
*) usage; exit 2 ;;
|
|
esac
|
|
done
|
|
[[ -n "$image" && "$image" != -* ]] || { usage; exit 2; }
|
|
for command in docker dotnet curl timeout mktemp; do
|
|
command -v "$command" >/dev/null || { printf 'Missing prerequisite: %s\n' "$command" >&2; exit 2; }
|
|
done
|
|
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
|
# Honor TMPDIR, including all dotnet build outputs and transient synthetic state.
|
|
tmp_root=${TMPDIR:-"$HOME/.cache"}
|
|
mkdir -p -- "$tmp_root"
|
|
results=${results:-"$root/.hermes/progress/server-container-smoke"}
|
|
mkdir -p -- "$results"
|
|
# Set up logs before allocating scratch, so results-path failures cannot leak it.
|
|
# Each invocation gets a private child directory; existing result files are never overwritten.
|
|
run_results=$(mktemp -d "$results/run.XXXXXXXXXX")
|
|
scratch=$(mktemp -d "$tmp_root/opennest-server-smoke.XXXXXXXXXX")
|
|
token=${scratch##*/}
|
|
owner_label='com.opennest.server-smoke.owner'
|
|
volume="$token-data"
|
|
container_names=("$token-first" "$token-replacement")
|
|
cidfiles=("$scratch/first.cid" "$scratch/replacement.cid")
|
|
volume_attempted=false
|
|
container_attempted=(false false)
|
|
|
|
# Explicit local default context, ignoring environment overrides for remote daemons.
|
|
docker_local() { timeout 20s env -u DOCKER_HOST -u DOCKER_CONTEXT docker --context default "$@"; }
|
|
owned_container() {
|
|
[[ $(docker_local inspect --format "{{index .Config.Labels \"$owner_label\"}}" "$1") == "$token" ]]
|
|
}
|
|
cleanup_absent() {
|
|
local kind=$1 target=$2 remaining filter
|
|
local -a inventory
|
|
if [[ "$kind" == container ]]; then
|
|
filter="id=$target"
|
|
if [[ "$target" == "${container_names[0]}" || "$target" == "${container_names[1]}" ]]; then
|
|
# Generated names contain only alphanumerics, hyphens, and dots.
|
|
filter="name=^/${target//./\\.}$"
|
|
fi
|
|
inventory=(container ls --all --quiet --no-trunc --filter "$filter")
|
|
else
|
|
inventory=(volume ls --quiet --filter "name=$target")
|
|
fi
|
|
# A failed inspect is not absence. Only a successful, empty inventory proves it.
|
|
if remaining=$(docker_local "${inventory[@]}" 2>> "$run_results/cleanup.log"); then
|
|
if [[ -z "$remaining" ]]; then
|
|
printf 'Confirmed absent: %s %s\n' "$kind" "$target" >> "$run_results/cleanup.log"
|
|
return 0
|
|
fi
|
|
printf 'Cleanup unknown/incomplete: %s %s remains in inventory: %s\n' \
|
|
"$kind" "$target" "$remaining" >> "$run_results/cleanup.log"
|
|
else
|
|
printf 'Cleanup unknown: %s %s inventory failed status=%s\n' \
|
|
"$kind" "$target" "$?" >> "$run_results/cleanup.log"
|
|
fi
|
|
return 1
|
|
}
|
|
cleanup_resource() {
|
|
local kind=$1 target=$2 index=${3:-} metadata resource owner
|
|
local -a inspect remove
|
|
if [[ "$kind" == container ]]; then
|
|
inspect=(inspect --format "{{.Id}} {{index .Config.Labels \"$owner_label\"}}")
|
|
remove=(rm -f)
|
|
else
|
|
inspect=(volume inspect --format "{{.Name}} {{index .Labels \"$owner_label\"}}")
|
|
remove=(volume rm)
|
|
fi
|
|
if metadata=$(docker_local "${inspect[@]}" "$target" 2>> "$run_results/cleanup.log"); then
|
|
read -r resource owner <<< "$metadata"
|
|
if [[ -z "$resource" || "$owner" != "$token" ]]; then
|
|
printf 'Cleanup unknown: %s %s owner could not be reconciled; refusing removal.\n' \
|
|
"$kind" "$target" >> "$run_results/cleanup.log"
|
|
return 1
|
|
fi
|
|
if [[ "$kind" == container ]]; then
|
|
# Name recovery resolves a full owned ID before removal, even without a cidfile.
|
|
if [[ ! "$resource" =~ ^[0-9a-f]{64}$ || ( "$target" != "${container_names[0]}" \
|
|
&& "$target" != "${container_names[1]}" && "$resource" != "$target" ) ]]; then
|
|
printf 'Cleanup unknown: container %s identity mismatch; refusing removal.\n' \
|
|
"$target" >> "$run_results/cleanup.log"
|
|
return 1
|
|
fi
|
|
docker_local logs "$resource" > "$run_results/container-$index.log" 2>&1
|
|
docker_local inspect --format 'id={{.Id}} running={{.State.Running}} exit={{.State.ExitCode}}' \
|
|
"$resource" > "$run_results/container-$index-state.log" 2>&1
|
|
elif [[ "$resource" != "$target" ]]; then
|
|
printf 'Cleanup unknown: volume %s identity mismatch; refusing removal.\n' \
|
|
"$target" >> "$run_results/cleanup.log"
|
|
return 1
|
|
fi
|
|
docker_local "${remove[@]}" "$resource" >> "$run_results/cleanup.log" 2>&1 || {
|
|
printf 'Cleanup incomplete: %s %s removal failed status=%s\n' \
|
|
"$kind" "$resource" "$?" >> "$run_results/cleanup.log"
|
|
return 1
|
|
}
|
|
cleanup_absent "$kind" "$resource"
|
|
else
|
|
printf 'Cleanup lookup failed: %s %s status=%s; checking absence.\n' \
|
|
"$kind" "$target" "$?" >> "$run_results/cleanup.log"
|
|
cleanup_absent "$kind" "$target"
|
|
fi
|
|
}
|
|
cleanup() {
|
|
local status=$? cleanup_failed=0 cid index
|
|
trap - EXIT INT TERM
|
|
set +e
|
|
for index in 0 1; do
|
|
[[ ${container_attempted[$index]} == true ]] || continue
|
|
cid=''
|
|
if [[ -s ${cidfiles[$index]} ]]; then
|
|
read -r cid < "${cidfiles[$index]}" || [[ -n "$cid" ]]
|
|
fi
|
|
cleanup_resource container "${cid:-${container_names[$index]}}" "$index" || cleanup_failed=1
|
|
done
|
|
# Docker volume create may return a preexisting name: delete only a matching owner label.
|
|
if [[ "$volume_attempted" == true ]]; then
|
|
cleanup_resource volume "$volume" || cleanup_failed=1
|
|
fi
|
|
rm -rf -- "$scratch"
|
|
if ((cleanup_failed != 0 && status == 0)); then status=1; fi
|
|
printf 'exit=%s cleanup_failed=%s\n' "$status" "$cleanup_failed" > "$run_results/outcome.log"
|
|
printf 'Smoke exit=%s; logs: %s\n' "$status" "$run_results"
|
|
exit "$status"
|
|
}
|
|
trap cleanup EXIT
|
|
trap 'exit 130' INT
|
|
trap 'exit 143' TERM
|
|
|
|
endpoint=$(docker_local context inspect default --format '{{.Endpoints.docker.Host}}')
|
|
[[ "$endpoint" == 'unix:///var/run/docker.sock' ]] || { printf 'Refusing a non-local default Docker endpoint.\n' >&2; exit 2; }
|
|
docker_local info --format 'daemon={{.Name}} os={{.OSType}}' > "$run_results/daemon.log"
|
|
docker_local image inspect --format 'image={{.Id}}' "$image" > "$run_results/image.log"
|
|
printf 'host_sdk=%s\n' "$(dotnet --version)" > "$run_results/toolchain.log"
|
|
printf 'owner=%s volume=%s\n' "$token" "$volume" > "$run_results/ownership.log"
|
|
timeout 180s dotnet build "$root/scripts/Server.Tests/Server.Tests.csproj" -c Release \
|
|
--artifacts-path "$scratch/build" -m:1 /nodeReuse:false > "$run_results/build.log" 2>&1
|
|
dll="$scratch/build/bin/Server.Tests/release/Server.Tests.dll"
|
|
[[ -f "$dll" ]] || { printf 'Smoke build did not produce the expected DLL.\n' >&2; exit 1; }
|
|
# Refuse collisions before creation; the post-create label is the authoritative ownership proof.
|
|
if docker_local volume inspect "$volume" >/dev/null 2>&1; then
|
|
printf 'Refusing a preexisting volume name.\n' >&2; exit 1
|
|
fi
|
|
volume_attempted=true
|
|
docker_local volume create --label "$owner_label=$token" "$volume" > "$run_results/volume.log"
|
|
[[ $(docker_local volume inspect --format "{{index .Labels \"$owner_label\"}}" "$volume") == "$token" ]] \
|
|
|| { printf 'Volume ownership could not be established.\n' >&2; exit 1; }
|
|
|
|
start_container() {
|
|
local index=$1 cid binding
|
|
if docker_local inspect "${container_names[$index]}" >/dev/null 2>&1; then
|
|
printf 'Refusing a preexisting container name.\n' >&2; return 1
|
|
fi
|
|
container_attempted[$index]=true
|
|
docker_local create --name "${container_names[$index]}" --cidfile "${cidfiles[$index]}" \
|
|
--label "$owner_label=$token" --publish 127.0.0.1::8090 \
|
|
--mount "type=volume,src=$volume,dst=/app/data" "$image" > "$run_results/create-$index.log"
|
|
read -r cid < "${cidfiles[$index]}" || [[ -n "$cid" ]]
|
|
owned_container "$cid" || { printf 'Container ownership could not be established.\n' >&2; return 1; }
|
|
docker_local start "$cid" > "$run_results/start-$index.log"
|
|
binding=$(docker_local port "$cid" 8090/tcp)
|
|
[[ "$binding" =~ ^127\.0\.0\.1:([0-9]+)$ ]] || { printf 'Unexpected port binding.\n' >&2; return 1; }
|
|
url="http://127.0.0.1:${BASH_REMATCH[1]}"
|
|
printf 'id=%s binding=%s\n' "$cid" "$binding" >> "$run_results/ownership.log"
|
|
local deadline=$((SECONDS + 45))
|
|
while ((SECONDS < deadline)); do
|
|
[[ $(docker_local inspect --format '{{.State.Running}}' "$cid") == true ]] \
|
|
|| { printf 'Container exited before readiness.\n' >&2; return 1; }
|
|
if curl --noproxy '*' --fail --silent --show-error --connect-timeout 1 --max-time 2 \
|
|
"$url/healthz" > "$scratch/health.json" 2>> "$run_results/readiness-$index.log"; then
|
|
[[ $(< "$scratch/health.json") == '{"status":"ok"}' ]] && return 0
|
|
fi
|
|
sleep 1
|
|
done
|
|
printf 'Container readiness timed out.\n' >&2; return 1
|
|
}
|
|
|
|
start_container 0
|
|
timeout 100s dotnet "$dll" seed --url "$url" --state "$scratch/state.json" > "$run_results/seed.log" 2>&1
|
|
timeout 100s dotnet "$dll" reject --url "$url" > "$run_results/reject.log" 2>&1
|
|
read -r first_cid < "${cidfiles[0]}" || [[ -n "$first_cid" ]]
|
|
docker_local logs "$first_cid" > "$run_results/container-0.log" 2>&1
|
|
docker_local inspect --format 'id={{.Id}} running={{.State.Running}} exit={{.State.ExitCode}}' \
|
|
"$first_cid" > "$run_results/container-0-state.log"
|
|
docker_local stop --time 10 "$first_cid" > "$run_results/stop-0.log"
|
|
docker_local rm "$first_cid" > "$run_results/remove-0.log"
|
|
# The replacement gets another Docker-allocated loopback port; only the volume is reused.
|
|
start_container 1
|
|
timeout 100s dotnet "$dll" verify --url "$url" --state "$scratch/state.json" > "$run_results/verify.log" 2>&1
|
|
printf 'PASS: real-client create/update/copy, rejections, and persistent recreation.\n'
|