Files
ThothII/scripts/workspace-registry-smoke.sh
T

345 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
# Exercises the registry through an isolated Compose project. An optional WORKSPACE_GIT_REMOTE
# is contacted read-only as a connectivity preflight; all pull/fallback mutations target a fresh
# temporary bare repository so this smoke test can never alter an operator's shared registry.
set -euo pipefail
root="$(cd "$(dirname "$0")/.." && pwd -P)"
tmp="$(mktemp -d "${TMPDIR:-/tmp}/thoth-workspace-registry-smoke.XXXXXX")"
tmp="$(cd "$tmp" && pwd -P)"
tmp_slug="$(basename "$tmp" | tr '[:upper:]._' '[:lower:]--' | tr -cd 'a-z0-9-')"
project="thoth-workspace-registry-smoke-${tmp_slug}-$$"
image="thothii-workspace-registry-smoke:${project}"
remote="$tmp/remote.git"
seed="$tmp/seed"
branch="workspace-registry-smoke"
core_remote="/fixtures/remote.git"
cleanup_smoke_image() {
local inspect_status listed list_status
docker image inspect --format '{{.Id}}' "$image" >/dev/null 2>&1
inspect_status=$?
if [[ "$inspect_status" -eq 0 ]]; then
docker image rm -f "$image" >/dev/null 2>&1
return $?
fi
listed="$(docker image ls --quiet --no-trunc "$image" 2>/dev/null)"
list_status=$?
[[ "$list_status" -eq 0 && -z "$listed" ]]
}
workspace_registry_smoke_leftovers() {
local output status=0
if output="$(docker ps -a --filter "label=com.docker.compose.project=$project" -q)"; then
printf '%s\n' "$output"
else
status=1
fi
if output="$(docker volume ls --filter "label=com.docker.compose.project=$project" -q)"; then
printf '%s\n' "$output"
else
status=1
fi
if output="$(docker network ls --filter "label=com.docker.compose.project=$project" -q)"; then
printf '%s\n' "$output"
else
status=1
fi
if output="$(docker image inspect --format '{{.Id}}' "$image" 2>/dev/null)"; then
printf '%s\n' "$output"
elif output="$(docker image ls --quiet --no-trunc "$image" 2>/dev/null)"; then
printf '%s\n' "$output"
else
status=1
fi
return "$status"
}
cleanup() {
local body_status=$?
local down_status image_status enumeration_status rm_status=0 cleanup_incomplete=0 final_status
local leftovers
trap - EXIT HUP INT TERM
set +e
compose down --volumes --remove-orphans >/dev/null 2>&1
down_status=$?
cleanup_smoke_image
image_status=$?
leftovers="$(workspace_registry_smoke_leftovers)"
enumeration_status=$?
leftovers="$(printf '%s\n' "$leftovers" | sed '/^$/d')"
if [[ "$down_status" -ne 0 || "$image_status" -ne 0 || "$enumeration_status" -ne 0 || -n "$leftovers" ]]; then
cleanup_incomplete=1
else
rm -rf "$tmp"
rm_status=$?
[[ "$rm_status" -eq 0 ]] || cleanup_incomplete=1
fi
if [[ "$cleanup_incomplete" -ne 0 ]]; then
echo "workspace registry cleanup incomplete: compose down status=$down_status, image cleanup status=$image_status, enumeration status=$enumeration_status, temp cleanup status=$rm_status." >&2
if [[ -n "$leftovers" ]]; then
echo "workspace registry cleanup left owned Docker resources:" >&2
printf '%s\n' "$leftovers" >&2
fi
echo "workspace registry smoke recovery path retained: $tmp" >&2
if [[ "$body_status" -ne 0 ]]; then
final_status=$body_status
else
final_status=1
fi
else
echo "workspace registry cleanup proof: no compose containers, volumes, networks, image, or temporary path remain for $project."
final_status=$body_status
fi
exit "$final_status"
}
compose() {
SMOKE_ROOT="$root" \
SMOKE_IMAGE="$image" \
SMOKE_REMOTE="$remote" \
SMOKE_BRANCH="$branch" \
SMOKE_CORE_REMOTE="$core_remote" \
docker compose --project-name "$project" -f - "$@" <<'COMPOSE_YAML'
services:
core:
build:
context: "${SMOKE_ROOT:?}"
dockerfile: docker/core.Dockerfile
image: "${SMOKE_IMAGE:?}"
environment:
HOST: 0.0.0.0
PORT: "8787"
AUTH_MODE: none
THT_HARNESS_DIR: /app/harness
THT_BIN: /opt/venv/bin/tht
SETTINGS_FILE: /tmp/settings.json
THT_WORKSPACE_REGISTRY_ROOT: /data/workspace-registry
THT_WORKSPACE_GIT_REMOTE: "${SMOKE_CORE_REMOTE:?}"
THT_WORKSPACE_GIT_BRANCH: "${SMOKE_BRANCH:?}"
THT_WORKSPACE_INSTALLATION_ID: smoke
THT_WORKSPACE_SECRET_ROOTS: /run/secrets
volumes:
- type: volume
source: workspace-registry
target: /data/workspace-registry
- type: bind
source: "${SMOKE_REMOTE:?}"
target: /fixtures/remote.git
read_only: true
volumes:
workspace-registry: {}
COMPOSE_YAML
}
workspace_registry_smoke_self_test_image_cleanup_identity() {
local calls exact_image foreign_project foreign_tag leftovers removed=0
calls="$(mktemp "${TMPDIR:-/tmp}/thoth-workspace-registry-smoke-image-contract.XXXXXX")"
project="thoth-workspace-registry-smoke-selftest-123"
exact_image="thothii-workspace-registry-smoke:${project}"
foreign_project="thothii-workspace-registry-smoke:thoth-workspace-registry-smoke-foreign-456"
foreign_tag="thothii-workspace-registry-smoke:local"
image="$exact_image"
docker() {
printf '%s\n' "docker $*" >>"$calls"
case "$1 $2" in
"image inspect")
[[ "$3" == "--format" && "$5" == "$exact_image" ]] || return 43
[[ "$removed" -eq 0 ]]
;;
"image rm")
[[ "$3" == "-f" && "$4" == "$exact_image" ]] || return 42
removed=1
;;
"image ls")
[[ "$3" == "--quiet" && "$4" == "--no-trunc" && "$5" == "$exact_image" ]] || return 47
;;
"ps -a"|"volume ls"|"network ls")
[[ "$*" == *"label=com.docker.compose.project=$project"* ]] || return 45
;;
*)
return 46
;;
esac
}
cleanup_smoke_image
leftovers="$(workspace_registry_smoke_leftovers)"
[[ -z "$(printf '%s\n' "$leftovers" | sed '/^$/d')" ]] || {
echo "self-test observed leftovers for the per-run image" >&2
printf '%s\n' "$leftovers" >&2
return 1
}
grep -Fq "docker image rm -f $exact_image" "$calls" \
|| { echo "self-test did not remove the exact per-run image reference" >&2; return 1; }
if grep -Fq "$foreign_project" "$calls" || grep -Fq "$foreign_tag" "$calls"; then
echo "self-test cleanup touched a foreign workspace-registry smoke image reference" >&2
return 1
fi
rm -f "$calls"
echo "workspace registry smoke image cleanup identity self-test passed"
}
workspace_registry_smoke_self_test_compose_config() {
local special_root rendered
special_root="$tmp/compose config path # colon: fixture"
root="$special_root/root context"
remote="$special_root/remote repo # fixture.git"
branch="workspace registry # branch"
core_remote="/fixtures/remote repo # fixture.git"
image="thothii-workspace-registry-smoke:compose-config-selftest"
project="thoth-workspace-registry-smoke-compose-config-selftest-$$"
rendered="$special_root/rendered.yaml"
mkdir -p "$root" "$remote"
compose config --quiet
compose config --format json >"$rendered"
grep -Fq "$root" "$rendered"
grep -Fq "$remote" "$rendered"
grep -Fq "$branch" "$rendered"
grep -Fq "$core_remote" "$rendered"
grep -Fq '"read_only": true' "$rendered"
rm -rf "$tmp"
echo "workspace registry smoke Compose config special-path self-test passed"
}
workspace_registry_smoke_self_test_cleanup_failure_path() {
local fixture_root retained calls output status
fixture_root="$tmp/cleanup failure # fixture"
retained="$fixture_root/retained smoke path"
calls="$fixture_root/calls.log"
mkdir -p "$retained"
set +e
output="$(
(
tmp="$retained"
project="thoth-workspace-registry-smoke-cleanup-failure-selftest"
image="thothii-workspace-registry-smoke:cleanup-failure-selftest"
compose() { printf '%s\n' "compose $*" >>"$calls"; return 71; }
cleanup_smoke_image() { printf '%s\n' 'image cleanup' >>"$calls"; return 72; }
workspace_registry_smoke_leftovers() { printf '%s\n' 'owned-resource-selftest'; printf '%s\n' 'enumerate leftovers' >>"$calls"; return 73; }
trap cleanup EXIT
exit 37
) 2>&1
)"
status=$?
set -e
[[ "$status" -eq 37 ]] || { echo "cleanup self-test did not preserve body status 37 (got $status)" >&2; return 1; }
grep -Fq 'compose down --volumes --remove-orphans' "$calls"
grep -Fq 'image cleanup' "$calls"
grep -Fq 'enumerate leftovers' "$calls"
grep -Fq 'cleanup incomplete: compose down status=71, image cleanup status=72, enumeration status=73' <<<"$output"
grep -Fq 'owned-resource-selftest' <<<"$output"
grep -Fq "recovery path retained: $retained" <<<"$output"
[[ -d "$retained" ]] || { echo "cleanup self-test did not retain its recovery path" >&2; return 1; }
rm -rf "$tmp"
echo "workspace registry smoke cleanup failure-path self-test passed"
}
case "${WORKSPACE_REGISTRY_SMOKE_SELF_TEST:-}" in
"") ;;
image-cleanup-identity)
workspace_registry_smoke_self_test_image_cleanup_identity
rm -rf "$tmp"
exit 0
;;
compose-config-contract)
workspace_registry_smoke_self_test_compose_config
exit 0
;;
cleanup-failure-path)
workspace_registry_smoke_self_test_cleanup_failure_path
exit 0
;;
*)
echo "unknown workspace registry smoke self-test: ${WORKSPACE_REGISTRY_SMOKE_SELF_TEST}" >&2
rm -rf "$tmp"
exit 2
;;
esac
trap cleanup EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
wait_for_core() {
local attempt
for attempt in $(seq 1 30); do
if compose exec -T core curl -fsS http://127.0.0.1:8787/health >/dev/null 2>&1; then
return 0
fi
sleep 1
done
compose logs core >&2 || true
return 1
}
if [[ -n "${WORKSPACE_GIT_REMOTE:-}" ]]; then
echo "== Read-only Git remote preflight =="
git ls-remote --heads "$WORKSPACE_GIT_REMOTE" >/dev/null
fi
echo "== Seed isolated workspace registry =="
git init --bare --initial-branch=main "$remote" >/dev/null
git clone "$remote" "$seed" >/dev/null
git -C "$seed" checkout -b "$branch" >/dev/null
mkdir -p "$seed/workspaces"
cp "$root/scripts/fixtures/workspace-registry-smoke.yaml" "$seed/workspaces/local.yaml"
git -C "$seed" add workspaces/local.yaml
git -C "$seed" -c user.name='Workspace Registry Smoke' -c user.email='workspace-registry-smoke@example.invalid' \
commit -m 'Seed workspace registry smoke' >/dev/null
git -C "$seed" push origin "HEAD:$branch" >/dev/null
echo "== Build and start isolated Compose core =="
compose up -d --build
wait_for_core
initial_status="$(compose exec -T core curl -fsS http://127.0.0.1:8787/workspace-registry/status)"
printf '%s' "$initial_status" | grep -Eq '"head":"[0-9a-f]{40}"'
compose exec -T core test -f /data/workspace-registry/state/active.json
echo "== Recreate offline and prove registry-volume fallback =="
core_remote="/fixtures/offline.git"
compose up -d --force-recreate
wait_for_core
recreated_status="$(compose exec -T core curl -fsS http://127.0.0.1:8787/workspace-registry/status)"
initial_head="$(printf '%s' "$initial_status" | sed -n 's/.*"head":"\([0-9a-f]*\)".*/\1/p')"
recreated_head="$(printf '%s' "$recreated_status" | sed -n 's/.*"head":"\([0-9a-f]*\)".*/\1/p')"
test -n "$initial_head" && test "$initial_head" = "$recreated_head"
printf '%s' "$recreated_status" | grep -Fq '"degraded":true'
compose exec -T core curl -fsS http://127.0.0.1:8787/workspaces | grep -Fq 'Local'
echo "== Pull a valid remote update =="
sed -i.bak 's/name: Local/name: Local Updated/' "$seed/workspaces/local.yaml"
rm "$seed/workspaces/local.yaml.bak"
git -C "$seed" add workspaces/local.yaml
git -C "$seed" -c user.name='Workspace Registry Smoke' -c user.email='workspace-registry-smoke@example.invalid' \
commit -m 'Update workspace registry smoke' >/dev/null
git -C "$seed" push origin "HEAD:$branch" >/dev/null
core_remote="/fixtures/remote.git"
compose up -d --force-recreate
wait_for_core
compose exec -T core curl -fsS -X POST http://127.0.0.1:8787/workspace-registry/pull | grep -Eq '"head":"[0-9a-f]{40}"'
compose exec -T core curl -fsS http://127.0.0.1:8787/workspaces | grep -Fq 'Local Updated'
echo "== Reject invalid remote content and retain the last valid snapshot =="
printf '%s\n' 'workspace: invalid' >"$seed/workspaces/local.yaml"
git -C "$seed" add workspaces/local.yaml
git -C "$seed" -c user.name='Workspace Registry Smoke' -c user.email='workspace-registry-smoke@example.invalid' \
commit -m 'Invalid workspace registry smoke fixture' >/dev/null
git -C "$seed" push origin "HEAD:$branch" >/dev/null
if compose exec -T core curl -fsS -X POST http://127.0.0.1:8787/workspace-registry/pull >/dev/null; then
echo "registry accepted invalid remote workspace content" >&2
exit 1
fi
compose exec -T core curl -fsS http://127.0.0.1:8787/workspaces | grep -Fq 'Local Updated'
echo "workspace registry smoke passed"