fix(deploy): close container final review

This commit is contained in:
2026-07-12 00:44:39 +02:00
parent 0ca6346f75
commit a3a266fd81
30 changed files with 526 additions and 37 deletions
+19 -2
View File
@@ -43,7 +43,24 @@ compose up --build --wait core frontend
published=$(compose port frontend 8080)
http_port=${published##*:}
curl --fail --silent --show-error "http://127.0.0.1:$http_port/health" >/dev/null
assert_health() {
health_headers=$(mktemp)
health_body=$(mktemp)
if ! curl --fail --silent --show-error --dump-header "$health_headers" \
"http://127.0.0.1:$http_port/health" >"$health_body"; then
rm -f "$health_headers" "$health_body"
return 1
fi
if ! grep -qi '^content-type: application/json' "$health_headers" ||
! jq -e 'type == "object" and keys == ["status"] and .status == "ok"' \
"$health_body" >/dev/null; then
rm -f "$health_headers" "$health_body"
return 1
fi
rm -f "$health_headers" "$health_body"
}
assert_health
# The nginx proxy must preserve streaming semantics for the backend SSE endpoint.
headers=$(mktemp)
@@ -68,5 +85,5 @@ persisted=$(compose exec -T core sh -c 'cat /data/.compose-smoke-marker')
[ "$persisted" = "$marker" ]
compose exec -T core rm -f /data/.compose-smoke-marker
curl --fail --silent --show-error "http://127.0.0.1:$http_port/health" >/dev/null
assert_health
echo "Compose health, SSE proxy, and restart persistence checks passed."
+42
View File
@@ -0,0 +1,42 @@
#!/bin/sh
set -eu
cd "$(dirname "$0")/.."
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT HUP INT TERM
docker compose --profile external config >"$tmp/base.yaml"
grep -q 'host_ip: 127.0.0.1' "$tmp/base.yaml"
grep -q 'AUTH_MODE: none' "$tmp/base.yaml"
grep -q 'THOTH_PUBLIC_EXPOSURE: "false"' "$tmp/base.yaml"
if grep -q 'env_file:' "$tmp/base.yaml"; then
echo "base/production-neutral Compose must not load the local env file" >&2
exit 1
fi
docker compose -f compose.yaml -f deploy/compose.local.yaml \
--profile external config >"$tmp/local.yaml"
grep -q 'env_file:' deploy/compose.local.yaml
for secret in dwh reader writer ca; do printf '%s\n' "test-$secret" >"$tmp/$secret"; done
THT_DWH_API_KEY_SECRET_FILE="$tmp/dwh" \
THT_VEC_API_KEY_SECRET_FILE="$tmp/reader" \
THT_VEC_WRITE_API_KEY_SECRET_FILE="$tmp/writer" \
THT_CA_SECRET_FILE="$tmp/ca" \
THT_DB_NAME=test THT_DWH_REST_URL=https://dwh.example.test \
THT_VEC_REST_URL=https://vector.example.test THT_OLLAMA_URL=https://embed.example.test \
docker compose -f compose.yaml -f deploy/compose.production.yaml \
--profile external config >"$tmp/production.yaml"
grep -q 'AUTH_MODE: upstream' "$tmp/production.yaml"
grep -q 'THOTH_PUBLIC_EXPOSURE: "true"' "$tmp/production.yaml"
grep -q 'target: thoth_ca.pem' "$tmp/production.yaml"
grep -q 'THT_DWH_API_KEY_FILE: /run/secrets/dwh_api_key' "$tmp/production.yaml"
if awk '/^FROM / && $2 !~ /@sha256:/ { found=1 } END { exit !found }' \
docker/core.Dockerfile docker/frontend.Dockerfile; then
echo "every Dockerfile base must include an immutable digest" >&2
exit 1
fi
echo "container deployment security contract passed."
+75 -8
View File
@@ -3,17 +3,84 @@ set -eu
cd "$(dirname "$0")/.."
script=scripts/docker-smoke.sh
sh -n "$script"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT HUP INT TERM
log="$tmp/docker.log"
marker_file="$tmp/marker"
grep -q 'SMOKE_PROJECT' "$script"
grep -q -- '--project-name' "$script"
grep -q 'KEEP_SMOKE_RESOURCES' "$script"
grep -q 'down --volumes' "$script"
mkdir -p "$tmp/bin"
cat >"$tmp/bin/docker" <<'EOF'
#!/bin/sh
set -eu
printf '%s\n' "$*" >>"$FAKE_DOCKER_LOG"
if grep -q -- 'down --remove-orphans' "$script"; then
case " $* " in
*" port frontend 8080 "*) printf '%s\n' '127.0.0.1:49152' ;;
*" exec -T core sh -c "*"printf"*)
for last do :; done
printf '%s\n' "$last" >"$FAKE_MARKER_FILE"
;;
*" exec -T core sh -c "*"cat /data/.compose-smoke-marker"*)
cat "$FAKE_MARKER_FILE"
;;
esac
EOF
cat >"$tmp/bin/curl" <<'EOF'
#!/bin/sh
set -eu
header_file=""
for arg do
if [ "${previous:-}" = "--dump-header" ]; then header_file=$arg; fi
previous=$arg
done
if [ -n "$header_file" ]; then
case "$*" in
*"/events"*) printf 'HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nCache-Control: no-cache\r\n\r\n' >"$header_file" ;;
*) printf 'HTTP/1.1 200 OK\r\nContent-Type: application/json; charset=utf-8\r\n\r\n' >"$header_file" ;;
esac
fi
case "$*" in
*"/health"*) printf '%s\n' '{"status":"ok"}' ;;
esac
EOF
chmod +x "$tmp/bin/docker" "$tmp/bin/curl"
run_smoke() {
PATH="$tmp/bin:$PATH" \
FAKE_DOCKER_LOG="$log" \
FAKE_MARKER_FILE="$marker_file" \
SMOKE_PROJECT="$1" \
KEEP_SMOKE_RESOURCES="${2:-0}" \
./scripts/docker-smoke.sh
}
run_smoke thothii-smoke-dynamic
while IFS= read -r invocation; do
case "$invocation" in
"compose --project-name thothii-smoke-dynamic --profile external "*) ;;
*) echo "Compose invocation escaped the smoke project/profile: $invocation" >&2; exit 1 ;;
esac
done <"$log"
grep -q ' down --volumes$' "$log"
if grep -q -- '--remove-orphans' "$log"; then
echo "smoke cleanup must not remove operator orphans" >&2
exit 1
fi
echo "docker-smoke isolation contract passed."
: >"$log"
run_smoke thothii-smoke-kept 1
if grep -q ' down ' "$log"; then
echo "KEEP_SMOKE_RESOURCES=1 unexpectedly cleaned the project" >&2
exit 1
fi
: >"$log"
if PATH="$tmp/bin:$PATH" FAKE_DOCKER_LOG="$log" FAKE_MARKER_FILE="$marker_file" \
SMOKE_PROJECT=thothii ./scripts/docker-smoke.sh >/dev/null 2>&1; then
echo "reserved operator project was accepted" >&2
exit 1
fi
test ! -s "$log"
echo "docker-smoke dynamic isolation contract passed."
+50
View File
@@ -0,0 +1,50 @@
#!/bin/sh
set -eu
cd "$(dirname "$0")/.."
platform=${PLATFORM:-linux/arm64}
slug=$(printf '%s' "$platform" | tr '/:' '--')
core_image="thothii-core:verify-$slug"
frontend_image="thothii-frontend:verify-$slug"
inventory_dir=${CONTAINER_INVENTORY_DIR:-.artifacts/container-images/$slug}
mkdir -p "$inventory_dir"
docker buildx build --platform "$platform" --load \
-f docker/core.Dockerfile -t "$core_image" .
docker buildx build --platform "$platform" --load \
-f docker/frontend.Dockerfile -t "$frontend_image" .
docker run --rm --platform "$platform" --entrypoint /app/docker/smoke/core-smoke.sh \
"$core_image"
docker run --rm --platform "$platform" -e BACKEND_BASE_URL=/api \
"$frontend_image" frontend-config-smoke
docker run --rm --platform "$platform" -e BACKEND_BASE_URL= \
"$frontend_image" frontend-config-smoke
if docker run --rm --platform "$platform" -e BACKEND_BASE_URL=/backend \
"$frontend_image" frontend-config-smoke >/dev/null 2>&1; then
echo "frontend accepted an unsupported BACKEND_BASE_URL" >&2
exit 1
fi
if docker run --rm --platform "$platform" -e THOTH_PUBLIC_EXPOSURE=true -e AUTH_MODE=none \
"$core_image" server >/dev/null 2>&1; then
echo "core accepted public exposure without upstream authentication" >&2
exit 1
fi
docker image inspect "$core_image" >"$inventory_dir/core-image-inspect.json"
docker image inspect "$frontend_image" >"$inventory_dir/frontend-image-inspect.json"
docker run --rm --platform "$platform" --entrypoint sh "$core_image" -c \
'dpkg-query -W; /opt/venv/bin/pip freeze; /opt/venv/bin/python -c '"'"'import glob,json; rows=set();
for path in glob.glob("/app/backend/node_modules/**/package.json", recursive=True):
try:
package=json.load(open(path)); rows.add((package.get("name","?"), package.get("version","?")))
except (OSError, ValueError): pass
print("\n".join(f"{name}=={version}" for name,version in sorted(rows)))'"'"'' \
>"$inventory_dir/core-packages.txt"
docker run --rm --platform "$platform" --entrypoint sh "$frontend_image" -c 'apk info -vv' \
>"$inventory_dir/frontend-packages.txt"
echo "container verification and inventory complete for $platform"