build: make embedded pi images reproducible

This commit is contained in:
2026-08-04 16:19:04 +02:00
parent a248fb46d0
commit d42fdf4b71
10 changed files with 131 additions and 116 deletions
+13
View File
@@ -0,0 +1,13 @@
$ErrorActionPreference = "Continue"
$repositoryRoot = Split-Path -Parent $PSScriptRoot
Set-Location $repositoryRoot
& docker compose -f compose.yaml -f deploy/compose.local.yaml build --pull
$exitCode = $LASTEXITCODE
if ($exitCode -eq 0) {
Write-Output "Next: docker compose -f compose.yaml -f deploy/compose.local.yaml up -d"
}
exit $exitCode
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -u
cd "$(dirname "$0")/.."
docker compose -f compose.yaml -f deploy/compose.local.yaml build --pull
status=$?
if [[ "$status" -eq 0 ]]; then
printf '%s\n' 'Next: docker compose -f compose.yaml -f deploy/compose.local.yaml up -d'
fi
exit "$status"
+42 -102
View File
@@ -1,112 +1,52 @@
#!/bin/sh
set -eu
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT HUP INT TERM
project="thothii-task5-$(date +%s)-$$"
expected_pi_version=$(sed -n 's/^ARG PI_VERSION=//p' docker/core.Dockerfile)
trap 'docker compose --project-name "$project" -f compose.yaml -f deploy/compose.local.yaml down --volumes --remove-orphans >/dev/null 2>&1 || true; rm -rf "$tmp"' EXIT HUP INT TERM
bundle="$tmp/thothii.secrets"
cat >"$bundle" <<'EOF'
# disposable deployment-contract bundle
THT_MODEL_API_KEY=test-model
THT_VECTOR_BOOTSTRAP_PASSWORD=contract-bootstrap
THT_VECTOR_MIGRATOR_PASSWORD=contract-migrator
THT_VECTOR_READER_PASSWORD=contract-reader
THT_VECTOR_WRITER_PASSWORD=contract-writer
EOF
chmod 0600 "$bundle"
export THT_SECRETS_FILE="$bundle"
test -n "$expected_pi_version"
printf '{}\n' >"$tmp/pi-auth.json"
chmod 0600 "$tmp/pi-auth.json"
docker compose config >"$tmp/base.yaml"
grep -q '^ core:' "$tmp/base.yaml"
grep -q '^ frontend:' "$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"
grep -q 'THT_SECRETS_FILE: /run/secrets/thothii.secrets' "$tmp/base.yaml"
grep -q 'target: /home/thoth/.pi/agent/models.json' "$tmp/base.yaml"
grep -q 'source: .*/deploy/pi/models.json' "$tmp/base.yaml"
grep -q 'target: /home/thoth/.pi/agent/settings.json' "$tmp/base.yaml"
if grep -q 'THT_[A-Z0-9_]*_SECRET_FILE:' "$tmp/base.yaml"; then
echo "base Compose must not require legacy secret-file variables" >&2
export PI_AUTH_FILE="$tmp/pi-auth.json"
export THT_WORKSPACE_GIT_REMOTE="https://git.example.invalid/thothii/workspaces.git"
# Let Docker assign loopback ports so this isolated contract test never collides with an operator stack.
export THOTH_CORE_HTTP_PORT=0
export THOTH_HTTP_PORT=0
docker compose --project-name "$project" -f compose.yaml -f deploy/compose.local.yaml build --pull
core_label=$(docker image inspect --format '{{ index .Config.Labels "io.thothii.pi.version" }}' thothii-core:local)
test "$core_label" = "$expected_pi_version"
test "$(docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.title" }}' thothii-core:local)" = "thothii-core"
test "$(docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.title" }}' thothii-frontend:local)" = "thothii-frontend"
docker compose --project-name "$project" -f compose.yaml -f deploy/compose.local.yaml up --detach --wait --wait-timeout 90
docker compose --project-name "$project" -f compose.yaml -f deploy/compose.local.yaml exec -T core sh -ceu '
test "$(id -u)" = 10001
test "$(pi --version)" = "$PI_VERSION"
command -v pi >/dev/null
test ! -e /var/run/docker.sock
touch /data/.task5-writable
rm /data/.task5-writable
if find /app /home /data -xdev \( -iname "*chirone*" -o -iname "*omics*portal*" \) -print -quit | grep -q .; then
echo "portal or Chirone path found in core image" >&2
exit 1
fi
'
if docker compose --project-name "$project" -f compose.yaml -f deploy/compose.local.yaml config | grep -Eqi 'docker\.sock|/var/run/docker|docker[-_]?daemon'; then
echo "Compose must not mount a Docker socket or daemon" >&2
exit 1
fi
docker compose -f compose.yaml -f deploy/compose.local-vector.yaml \
--profile local-vector config >"$tmp/local-vector.yaml"
grep -q 'target: thothii.secrets' "$tmp/local-vector.yaml"
if grep -Eq 'vector_(bootstrap|migrator|reader|writer)_password|THT_[A-Z0-9_]+_SECRET_FILE' "$tmp/local-vector.yaml"; then
echo "rendered local-vector config contains legacy per-secret references" >&2
exit 1
fi
if grep -q 'contract-' "$tmp/local-vector.yaml"; then
echo "rendered local-vector config leaked a bundle secret value" >&2
exit 1
fi
frontend_address=$(docker compose --project-name "$project" -f compose.yaml -f deploy/compose.local.yaml port frontend 8080 | head -n 1)
curl --fail --silent --show-error "http://$frontend_address/" >/dev/null
curl --fail --silent --show-error "http://$frontend_address/api/health" >/dev/null
docker compose -f compose.yaml -f deploy/compose.local.yaml \
config >"$tmp/local.yaml"
if grep -q 'env_file:' "$tmp/local.yaml"; then
echo "local Compose must use the root .env interpolation file" >&2
exit 1
fi
printf '%s\n' 'THT_MODEL_API_KEY=test-model' >"$tmp/thothii.secrets"
chmod 0600 "$tmp/thothii.secrets"
THT_SECRETS_FILE="$tmp/thothii.secrets" \
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 \
config >"$tmp/production.yaml"
grep -q 'AUTH_MODE: upstream' "$tmp/production.yaml"
grep -q 'THOTH_PUBLIC_EXPOSURE: "true"' "$tmp/production.yaml"
grep -q 'THT_SECRETS_FILE: /run/secrets/thothii.secrets' "$tmp/production.yaml"
grep -q 'target: thothii.secrets' "$tmp/production.yaml"
if grep -q 'test-model' "$tmp/production.yaml"; then
echo "rendered production config leaked the model API key" >&2
exit 1
fi
if PI_PROVIDER_API_KEY='must-not-leak' ./docker/core-entrypoint.sh doctor 2>"$tmp/legacy-model.err"; then
echo "legacy generic model credential was accepted" >&2
exit 1
fi
grep -q 'PI_PROVIDER_API_KEY is unsupported' "$tmp/legacy-model.err"
if grep -q 'must-not-leak' "$tmp/legacy-model.err"; then
echo "legacy model credential leaked through entrypoint diagnostics" >&2
exit 1
fi
printf 'THT_VECTOR_READER_PASSWORD=one\nTHT_VECTOR_READER_PASSWORD=two\n' >"$tmp/invalid-bundle"
chmod 0600 "$tmp/invalid-bundle"
if THT_SECRETS_FILE="$tmp/invalid-bundle" ./docker/core-entrypoint.sh doctor \
>"$tmp/invalid-bundle.out" 2>"$tmp/invalid-bundle.err"; then
echo "entrypoint accepted an invalid secret bundle" >&2
exit 1
fi
grep -q 'THT_SECRETS_FILE points to an invalid secret bundle' "$tmp/invalid-bundle.err"
if grep -q 'THT_VECTOR_READER_PASSWORD' "$tmp/invalid-bundle.err"; then
echo "invalid bundle diagnostics leaked key material" >&2
exit 1
fi
before_tmp=$(find "${TMPDIR:-/tmp}" -maxdepth 1 -type d -name 'thothii-secrets.*' -print | sort)
THT_SECRETS_FILE="$bundle" ./docker/core-entrypoint.sh doctor >/dev/null 2>&1 || true
after_tmp=$(find "${TMPDIR:-/tmp}" -maxdepth 1 -type d -name 'thothii-secrets.*' -print | sort)
test "$before_tmp" = "$after_tmp"
if grep -Eq 'THT_VECTOR_(BOOTSTRAP|MIGRATOR|READER|WRITER)_PASSWORD_FILE|target: vector_(bootstrap|migrator|reader|writer)_password|dwh_api_key|model_api_key|THT_[A-Z0-9_]+_SECRET_FILE' "$tmp/production.yaml"; then
echo "production external config contains local direct vector secrets" >&2
exit 1
fi
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
grep -qx 'deploy/\*' .dockerignore
grep -qx '!deploy/vector/' .dockerignore
grep -qx 'deploy/vector/\*' .dockerignore
grep -qx '!deploy/vector/secret-policy.sh' .dockerignore
echo "container deployment security contract passed."
echo "Task 5 container deployment contract passed."
+6
View File
@@ -16,6 +16,12 @@ docker buildx build --platform "$platform" --load \
docker buildx build --platform "$platform" --load \
-f docker/frontend.Dockerfile -t "$frontend_image" .
expected_pi_version=$(sed -n 's/^ARG PI_VERSION=//p' docker/core.Dockerfile)
test -n "$expected_pi_version"
test "$(docker image inspect --format '{{ index .Config.Labels "io.thothii.pi.version" }}' "$core_image")" = "$expected_pi_version"
test "$(docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.title" }}' "$core_image")" = "thothii-core"
test "$(docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.title" }}' "$frontend_image")" = "thothii-frontend"
docker run --rm --platform "$platform" --entrypoint /app/docker/smoke/core-smoke.sh \
"$core_image"
./scripts/test-vector-migration-image.sh "$core_image" "$platform"