447 lines
19 KiB
Bash
Executable File
447 lines
19 KiB
Bash
Executable File
#!/bin/sh
|
|
set -eu
|
|
|
|
cd "$(dirname "$0")/.."
|
|
|
|
mode=${1:-run}
|
|
case "$mode" in
|
|
run|--live-collision-test|--backup-restore) ;;
|
|
*) echo "usage: $0 [--live-collision-test|--backup-restore]" >&2; exit 2 ;;
|
|
esac
|
|
|
|
keep_resources=${KEEP_SMOKE_RESOURCES:-0}
|
|
if [ "${SMOKE_PROJECT+x}" = x ]; then
|
|
echo "SMOKE_PROJECT is not accepted; the smoke always generates an owned namespace" >&2
|
|
exit 2
|
|
fi
|
|
secret_dir=$(mktemp -d "${TMPDIR:-/tmp}/thothii-vector-smoke.XXXXXX")
|
|
suffix=$(basename "$secret_dir" | tr -cd 'a-z0-9')
|
|
smoke_project="thothii-vector-smoke-$(date +%s)-$$-$suffix"
|
|
smoke_owner="$smoke_project-owner"
|
|
marker="local-vector-$smoke_project"
|
|
restore_container="${smoke_project}-restore"
|
|
restore_volume="${smoke_project}-restore-data"
|
|
|
|
bootstrap_password="smoke-bootstrap-$smoke_project"
|
|
migrator_password="smoke-migrator-$smoke_project"
|
|
reader_password="smoke-reader-$smoke_project"
|
|
writer_password="smoke-writer-$smoke_project"
|
|
bundle="$secret_dir/thothii.secrets"
|
|
write_bundle() {
|
|
umask 077
|
|
{
|
|
printf 'THT_VECTOR_BOOTSTRAP_PASSWORD=%s\n' "$bootstrap_password"
|
|
printf 'THT_VECTOR_MIGRATOR_PASSWORD=%s\n' "$migrator_password"
|
|
printf 'THT_VECTOR_READER_PASSWORD=%s\n' "$reader_password"
|
|
printf 'THT_VECTOR_WRITER_PASSWORD=%s\n' "$writer_password"
|
|
} >"$bundle"
|
|
chmod 0600 "$bundle"
|
|
}
|
|
write_bundle
|
|
export THT_SECRETS_FILE="$bundle"
|
|
printf '%s\n' '{}' >"$secret_dir/pi-auth.json"
|
|
chmod 0600 "$secret_dir/pi-auth.json"
|
|
operator_env="$secret_dir/operator.env"
|
|
printf '%s\n' \
|
|
'THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git' \
|
|
"PI_AUTH_FILE=$secret_dir/pi-auth.json" \
|
|
"THT_SECRETS_FILE=$bundle" >"$operator_env"
|
|
# The rotation helper has an old/new file interface; these are test-only
|
|
# scratch files and are never mounted into a Compose service.
|
|
printf '%s' "$bootstrap_password" >"$secret_dir/bootstrap"
|
|
chmod 0600 "$secret_dir/bootstrap"
|
|
export THT_VECTOR_BOOTSTRAP_USER=thoth_bootstrap_smoke
|
|
export THOTH_SMOKE_OWNER="$smoke_owner"
|
|
|
|
compose() {
|
|
docker compose --env-file "$operator_env" -f compose.yaml -f deploy/compose.local-vector.yaml \
|
|
--project-name "$smoke_project" --profile local-vector "$@"
|
|
}
|
|
|
|
resource_ids() {
|
|
case "$1" in
|
|
container) docker ps -aq --filter "label=com.docker.compose.project=$smoke_project" ;;
|
|
volume) docker volume ls -q --filter "label=com.docker.compose.project=$smoke_project" ;;
|
|
network) docker network ls -q --filter "label=com.docker.compose.project=$smoke_project" ;;
|
|
esac
|
|
}
|
|
|
|
resource_owner() {
|
|
case "$1" in
|
|
container) docker inspect --format '{{ index .Config.Labels "io.thothii.smoke-owner" }}' "$2" ;;
|
|
volume) docker volume inspect --format '{{ index .Labels "io.thothii.smoke-owner" }}' "$2" ;;
|
|
network) docker network inspect --format '{{ index .Labels "io.thothii.smoke-owner" }}' "$2" ;;
|
|
esac
|
|
}
|
|
|
|
assert_no_collision() {
|
|
for kind in container volume network; do
|
|
ids=$(resource_ids "$kind")
|
|
if [ -n "$ids" ]; then
|
|
echo "refusing existing Compose project resources for generated namespace $smoke_project" >&2
|
|
return 1
|
|
fi
|
|
done
|
|
}
|
|
|
|
verify_owned_resources() {
|
|
for kind in container volume network; do
|
|
for id in $(resource_ids "$kind"); do
|
|
owner=$(resource_owner "$kind" "$id" 2>/dev/null || true)
|
|
if [ "$owner" != "$smoke_owner" ]; then
|
|
echo "refusing cleanup of resource not owned by this smoke: $kind $id" >&2
|
|
return 1
|
|
fi
|
|
done
|
|
done
|
|
}
|
|
|
|
cleanup() {
|
|
if [ "$keep_resources" = "1" ]; then
|
|
echo "Keeping smoke resources for project $smoke_project (KEEP_SMOKE_RESOURCES=1)." >&2
|
|
else
|
|
if verify_owned_resources; then
|
|
docker rm -f "$restore_container" >/dev/null 2>&1 || true
|
|
docker volume rm "$restore_volume" >/dev/null 2>&1 || true
|
|
compose down --volumes >/dev/null 2>&1 || true
|
|
fi
|
|
fi
|
|
rm -rf "$secret_dir"
|
|
}
|
|
trap cleanup EXIT HUP INT TERM
|
|
|
|
if [ "$mode" = "--live-collision-test" ]; then
|
|
collision_volume="${smoke_project}-collision"
|
|
docker volume create \
|
|
--label "com.docker.compose.project=$smoke_project" \
|
|
--label 'io.thothii.smoke-owner=foreign-owner' \
|
|
"$collision_volume" >/dev/null
|
|
if assert_no_collision 2>/dev/null; then
|
|
echo "live collision probe was not detected" >&2
|
|
docker volume rm "$collision_volume" >/dev/null
|
|
exit 1
|
|
fi
|
|
docker volume rm "$collision_volume" >/dev/null
|
|
echo "live local-vector project collision refusal passed."
|
|
exit 0
|
|
fi
|
|
|
|
probe_vector() {
|
|
compose exec -T core sh -ec '
|
|
. /app/docker/secret-policy.sh
|
|
tmp=$(mktemp -d); trap "rm -rf \"$tmp\"" EXIT
|
|
for role in READER WRITER; do
|
|
file="$tmp/$role"
|
|
read_bundle_secret /run/secrets/thothii.secrets "THT_VECTOR_${role}_PASSWORD" >"$file"
|
|
export "THT_VECTOR_${role}_PASSWORD_FILE=$file"
|
|
done
|
|
exec /opt/venv/bin/python - "$1" "$2"
|
|
' sh "$marker" "$1" <<'PY'
|
|
import hashlib
|
|
import os
|
|
import sys
|
|
|
|
from tht.adapters.vector.pgvector import PgVectorStore
|
|
from tht.config import DatabaseConfig
|
|
from tht.ports.vector import VectorWriteRecord
|
|
from tht.vectorstore.records import VectorRecord
|
|
|
|
marker = sys.argv[1]
|
|
mode = sys.argv[2]
|
|
database = "thoth"
|
|
host = "vector-db"
|
|
|
|
def credential(role: str) -> DatabaseConfig:
|
|
return DatabaseConfig(
|
|
host=host,
|
|
port=5432,
|
|
database=database,
|
|
schema="vectors",
|
|
user=f"thoth_vector_{role}",
|
|
password=open(os.environ[f"THT_VECTOR_{role.upper()}_PASSWORD_FILE"]).read(),
|
|
)
|
|
|
|
store = PgVectorStore(credential("reader"), credential("writer"), expected_dimension=768)
|
|
health = store.health()
|
|
assert health.ok, health
|
|
assert health.read_reachable is True and health.write_reachable is True, health
|
|
|
|
record = VectorRecord(
|
|
id=marker,
|
|
kind="memory",
|
|
ref=marker,
|
|
title="Local vector persistence smoke",
|
|
content=marker,
|
|
metadata={"smoke": True},
|
|
)
|
|
embedding = [1.0] + [0.0] * 767
|
|
if mode == "write":
|
|
store.upsert(
|
|
"memory",
|
|
[VectorWriteRecord(record, embedding, hashlib.sha256(marker.encode()).hexdigest())],
|
|
)
|
|
hits = store.search(["memory"], embedding, limit=1, kinds=["memory"])
|
|
assert hits and hits[0].id == marker, hits
|
|
print(f"role health and persisted search passed for {marker} ({mode})")
|
|
PY
|
|
}
|
|
|
|
assert_no_collision
|
|
compose config --quiet
|
|
services=$(compose config --services)
|
|
printf '%s\n' "$services" | grep -qx vector-db
|
|
printf '%s\n' "$services" | grep -qx vector-reconcile
|
|
printf '%s\n' "$services" | grep -qx vector-migrate
|
|
|
|
compose up --build --wait vector-reconcile vector-migrate core
|
|
core_id=$(compose ps -q core)
|
|
inspect_env=$(docker inspect --format '{{json .Config.Env}}' "$core_id")
|
|
if printf '%s' "$inspect_env" | grep -q "smoke-\(reader\|writer\)-${smoke_project}"; then
|
|
echo "docker inspect exposed a direct vector password" >&2
|
|
exit 1
|
|
fi
|
|
printf '%s' "$inspect_env" | grep -q 'THT_SECRETS_FILE=/run/secrets/thothii.secrets'
|
|
migration_status=$(compose run --rm --no-deps vector-migrate)
|
|
printf '%s\n' "$migration_status" | grep -q '"pending": \[\]'
|
|
migrator_flags=$(compose run --rm --no-deps --entrypoint sh vector-reconcile -ec '
|
|
. /opt/thoth/secret-policy.sh
|
|
export PGPASSWORD=$(read_bundle_secret /run/secrets/thothii.secrets THT_VECTOR_BOOTSTRAP_PASSWORD)
|
|
psql -At --host vector-db --username "$THT_VECTOR_BOOTSTRAP_USER" --dbname thoth \
|
|
--command "SELECT (NOT rolcreaterole) AND (NOT rolcreatedb) AND (NOT rolsuper) FROM pg_roles WHERE rolname = '\''thoth_vector_migrator'\''"
|
|
')
|
|
test "$migrator_flags" = t
|
|
probe_vector write
|
|
|
|
old_reader_password="$reader_password"
|
|
migrator_password="rotated-migrator-$smoke_project"
|
|
reader_password="rotated-reader-$smoke_project"
|
|
writer_password="rotated-writer-$smoke_project"
|
|
write_bundle
|
|
|
|
compose run --rm vector-reconcile
|
|
rotation_status=$(compose run --rm --no-deps vector-migrate)
|
|
printf '%s\n' "$rotation_status" | grep -q '"pending": \[\]'
|
|
if compose run --rm --no-deps --entrypoint psql \
|
|
-e PGPASSWORD="$old_reader_password" vector-reconcile \
|
|
--host vector-db --username thoth_vector_reader --dbname thoth --command 'SELECT 1' \
|
|
>/dev/null 2>&1; then
|
|
echo "old reader credential still works after rotation" >&2
|
|
exit 1
|
|
fi
|
|
|
|
compose up --force-recreate --no-deps --wait core
|
|
probe_vector read
|
|
|
|
old_bootstrap_password="$bootstrap_password"
|
|
printf '%s' "wrong-bootstrap-${smoke_project}" >"$secret_dir/bootstrap-wrong"
|
|
printf '%s' "next-bootstrap-'quoted-${smoke_project}" >"$secret_dir/bootstrap-next"
|
|
cp "$secret_dir/bootstrap" "$secret_dir/bootstrap-before-negative"
|
|
printf 'invalid bootstrap password\n' >"$secret_dir/bootstrap-whitespace"
|
|
chmod 0600 "$secret_dir/bootstrap-wrong" "$secret_dir/bootstrap-next" \
|
|
"$secret_dir/bootstrap-before-negative" "$secret_dir/bootstrap-whitespace"
|
|
if COMPOSE_PROJECT_NAME="$smoke_project" \
|
|
./scripts/vector-rotate-bootstrap-password.sh \
|
|
--env-file "$operator_env" \
|
|
"$secret_dir/bootstrap" "$secret_dir/bootstrap-whitespace" \
|
|
>/dev/null 2>&1; then
|
|
echo "bootstrap rotation accepted whitespace in a secret" >&2
|
|
exit 1
|
|
fi
|
|
cmp "$secret_dir/bootstrap" "$secret_dir/bootstrap-before-negative"
|
|
compose run --rm --no-deps --entrypoint psql \
|
|
-e PGPASSWORD="$old_bootstrap_password" vector-reconcile \
|
|
--host vector-db --username "$THT_VECTOR_BOOTSTRAP_USER" --dbname thoth \
|
|
--command 'SELECT 1' >/dev/null
|
|
|
|
if COMPOSE_PROJECT_NAME="$smoke_project" \
|
|
./scripts/vector-rotate-bootstrap-password.sh \
|
|
--env-file "$operator_env" \
|
|
"$secret_dir/bootstrap-wrong" "$secret_dir/bootstrap-next" \
|
|
>/dev/null 2>&1; then
|
|
echo "bootstrap rotation accepted the wrong old secret" >&2
|
|
exit 1
|
|
fi
|
|
cmp "$secret_dir/bootstrap" "$secret_dir/bootstrap-before-negative"
|
|
|
|
COMPOSE_PROJECT_NAME="$smoke_project" \
|
|
./scripts/vector-rotate-bootstrap-password.sh \
|
|
--env-file "$operator_env" \
|
|
"$secret_dir/bootstrap" "$secret_dir/bootstrap-next"
|
|
new_bootstrap_password=$(cat "$secret_dir/bootstrap")
|
|
bootstrap_password="$new_bootstrap_password"
|
|
write_bundle
|
|
test "$new_bootstrap_password" != "$old_bootstrap_password"
|
|
if compose run --rm --no-deps --entrypoint psql \
|
|
-e PGPASSWORD="$old_bootstrap_password" vector-reconcile \
|
|
--host vector-db --username "$THT_VECTOR_BOOTSTRAP_USER" --dbname thoth --command 'SELECT 1' \
|
|
>/dev/null 2>&1; then
|
|
echo "old bootstrap credential still works after rotation" >&2
|
|
exit 1
|
|
fi
|
|
compose run --rm --no-deps --entrypoint psql \
|
|
-e PGPASSWORD="$new_bootstrap_password" vector-reconcile \
|
|
--host vector-db --username "$THT_VECTOR_BOOTSTRAP_USER" --dbname thoth --command 'SELECT 1' \
|
|
>/dev/null
|
|
compose run --rm vector-reconcile
|
|
bootstrap_rotation_status=$(compose run --rm --no-deps vector-migrate)
|
|
printf '%s\n' "$bootstrap_rotation_status" | grep -q '"pending": \[\]'
|
|
compose up --force-recreate --no-deps --wait core
|
|
probe_vector read
|
|
|
|
compose restart vector-db core
|
|
compose up --wait vector-db core
|
|
probe_vector read
|
|
|
|
if [ "$mode" = "--backup-restore" ]; then
|
|
image=$(compose images -q vector-db)
|
|
network="${smoke_project}_default"
|
|
docker volume create \
|
|
--label "com.docker.compose.project=$smoke_project" \
|
|
--label "io.thothii.smoke-owner=$smoke_owner" "$restore_volume" >/dev/null
|
|
docker run -d --name "$restore_container" \
|
|
--label "com.docker.compose.project=$smoke_project" \
|
|
--label "io.thothii.smoke-owner=$smoke_owner" \
|
|
--network "$network" --network-alias vector-db-restore \
|
|
--mount "type=volume,source=$restore_volume,target=/var/lib/postgresql/data" \
|
|
--mount "type=bind,source=$bundle,target=/run/secrets/thothii.secrets,readonly" \
|
|
--mount "type=bind,source=$(pwd)/deploy/vector/vector-db-entrypoint.sh,target=/opt/thoth/vector-db-entrypoint.sh,readonly" \
|
|
--mount "type=bind,source=$(pwd)/deploy/vector/secret-policy.sh,target=/opt/thoth/secret-policy.sh,readonly" \
|
|
-e POSTGRES_DB=thoth -e POSTGRES_USER="$THT_VECTOR_BOOTSTRAP_USER" \
|
|
-e THT_SECRETS_FILE=/run/secrets/thothii.secrets \
|
|
--entrypoint /opt/thoth/vector-db-entrypoint.sh "$image" >/dev/null
|
|
attempts=0
|
|
until docker exec "$restore_container" pg_isready \
|
|
-U "$THT_VECTOR_BOOTSTRAP_USER" -d thoth >/dev/null 2>&1; do
|
|
attempts=$((attempts + 1))
|
|
[ "$attempts" -lt 30 ] || { echo "restore database did not become ready" >&2; exit 1; }
|
|
sleep 1
|
|
done
|
|
docker exec -e PGPASSWORD="$new_bootstrap_password" "$restore_container" psql -X \
|
|
-U "$THT_VECTOR_BOOTSTRAP_USER" -d thoth -v ON_ERROR_STOP=1 --command \
|
|
"CREATE SCHEMA vectors; CREATE EXTENSION vector WITH SCHEMA vectors;
|
|
CREATE TABLE vectors.memory (
|
|
id bigserial PRIMARY KEY, record_key text UNIQUE NOT NULL, kind text NOT NULL,
|
|
content_hash text NOT NULL, metadata jsonb NOT NULL,
|
|
embedding vectors.vector(768) NOT NULL, indexed_at timestamptz NOT NULL DEFAULT now());
|
|
INSERT INTO vectors.memory (record_key, kind, content_hash, metadata, embedding)
|
|
VALUES ('restore-sentinel', 'memory', 'sentinel-original', '{}',
|
|
('[' || '1,' || repeat('0,', 766) || '0]')::vectors.vector);" >/dev/null
|
|
|
|
docker run --rm --network "$network" \
|
|
--mount "type=bind,source=$(pwd),target=/repo,readonly" \
|
|
--mount "type=bind,source=$secret_dir,target=/scratch" "$image" \
|
|
/repo/scripts/vector-backup.sh --host vector-db --database thoth \
|
|
--user "$THT_VECTOR_BOOTSTRAP_USER" --password-file /scratch/bootstrap \
|
|
--output /scratch/vector.dump
|
|
|
|
compose exec -T vector-db sh -ec '
|
|
. /opt/thoth/secret-policy.sh
|
|
export PGPASSWORD=$(read_bundle_secret /run/secrets/thothii.secrets THT_VECTOR_BOOTSTRAP_PASSWORD)
|
|
psql -X -U "$POSTGRES_USER" -d thoth -v ON_ERROR_STOP=1 --command \
|
|
"UPDATE vectors.memory SET content_hash = '\''mutated-after-backup'\'' WHERE record_key = '\''$1'\''"' \
|
|
sh "$marker" >/dev/null
|
|
|
|
if docker run --rm --network "$network" \
|
|
--mount "type=bind,source=$(pwd),target=/repo,readonly" \
|
|
--mount "type=bind,source=$secret_dir,target=/scratch" "$image" \
|
|
/repo/scripts/vector-restore.sh \
|
|
--active-host vector-db --active-database thoth --active-user "$THT_VECTOR_BOOTSTRAP_USER" \
|
|
--active-password-file /scratch/bootstrap \
|
|
--target-host vector-db-restore --target-database thoth \
|
|
--target-user "$THT_VECTOR_BOOTSTRAP_USER" --target-password-file /scratch/bootstrap \
|
|
--input /scratch/vector.dump --force-nonempty >/dev/null 2>&1; then
|
|
echo "forced restore unexpectedly succeeded without archived ACL roles" >&2
|
|
exit 1
|
|
fi
|
|
sentinel=$(docker exec -e PGPASSWORD="$new_bootstrap_password" "$restore_container" psql \
|
|
-XAt -U "$THT_VECTOR_BOOTSTRAP_USER" -d thoth --command \
|
|
"SELECT content_hash FROM vectors.memory WHERE record_key='restore-sentinel'")
|
|
test "$sentinel" = sentinel-original
|
|
docker exec -e PGPASSWORD="$new_bootstrap_password" "$restore_container" psql -X \
|
|
-U "$THT_VECTOR_BOOTSTRAP_USER" -d thoth -v ON_ERROR_STOP=1 --command \
|
|
"DROP TABLE vectors.memory; CREATE ROLE vector_reader NOLOGIN; CREATE ROLE vector_writer NOLOGIN;" \
|
|
>/dev/null
|
|
|
|
docker run --rm --network "$network" \
|
|
--mount "type=bind,source=$(pwd),target=/repo,readonly" \
|
|
--mount "type=bind,source=$secret_dir,target=/scratch" "$image" \
|
|
/repo/scripts/vector-restore.sh \
|
|
--active-host vector-db --active-database thoth --active-user "$THT_VECTOR_BOOTSTRAP_USER" \
|
|
--active-password-file /scratch/bootstrap \
|
|
--target-host vector-db-restore --target-database thoth \
|
|
--target-user "$THT_VECTOR_BOOTSTRAP_USER" --target-password-file /scratch/bootstrap \
|
|
--input /scratch/vector.dump
|
|
|
|
docker run --rm --network "$network" \
|
|
--mount "type=bind,source=$(pwd)/deploy/vector/reconcile-roles.sh,target=/opt/thoth/reconcile-roles.sh,readonly" \
|
|
--mount "type=bind,source=$(pwd)/deploy/vector/secret-policy.sh,target=/opt/thoth/secret-policy.sh,readonly" \
|
|
--mount "type=bind,source=$bundle,target=/run/secrets/thothii.secrets,readonly" \
|
|
-e PGHOST=vector-db-restore -e PGDATABASE=thoth \
|
|
-e PGUSER="$THT_VECTOR_BOOTSTRAP_USER" \
|
|
-e THT_SECRETS_FILE=/run/secrets/thothii.secrets \
|
|
-e THT_VECTOR_MIGRATOR_USER=thoth_vector_migrator \
|
|
-e THT_VECTOR_READER_USER=thoth_vector_reader \
|
|
-e THT_VECTOR_WRITER_USER=thoth_vector_writer \
|
|
--entrypoint /opt/thoth/reconcile-roles.sh "$image" >/dev/null
|
|
|
|
compose exec -T core sh -ec '
|
|
. /app/docker/secret-policy.sh
|
|
tmp=$(mktemp -d); trap "rm -rf \"$tmp\"" EXIT
|
|
for role in READER WRITER; do
|
|
file="$tmp/$role"
|
|
read_bundle_secret /run/secrets/thothii.secrets "THT_VECTOR_${role}_PASSWORD" >"$file"
|
|
export "THT_VECTOR_${role}_PASSWORD_FILE=$file"
|
|
done
|
|
exec /opt/venv/bin/python - "$1"
|
|
' sh "$marker" <<'PY'
|
|
import hashlib
|
|
import os
|
|
import sys
|
|
|
|
from tht.adapters.vector.pgvector import PgVectorStore
|
|
from tht.config import DatabaseConfig
|
|
from tht.ports.vector import VectorWriteRecord
|
|
from tht.vectorstore.records import VectorRecord
|
|
|
|
def config(role):
|
|
return DatabaseConfig(
|
|
host="vector-db-restore", port=5432, database="thoth", schema="vectors",
|
|
user=f"thoth_vector_{role}",
|
|
password=open(os.environ[f"THT_VECTOR_{role.upper()}_PASSWORD_FILE"]).read(),
|
|
)
|
|
|
|
store = PgVectorStore(config("reader"), config("writer"), expected_dimension=768)
|
|
assert store.health().ok, store.health()
|
|
embedding = [1.0] + [0.0] * 767
|
|
marker = sys.argv[1]
|
|
assert store.search(["memory"], embedding, limit=1, kinds=["memory"])[0].id == marker
|
|
write_id = marker + "-restore-write"
|
|
record = VectorRecord(
|
|
id=write_id, kind="memory", ref=write_id, title="restore writer",
|
|
content=write_id, metadata={},
|
|
)
|
|
store.upsert("memory", [VectorWriteRecord(record, embedding, hashlib.sha256(write_id.encode()).hexdigest())])
|
|
assert store.existing_hashes("memory", ["memory"])[write_id]
|
|
PY
|
|
|
|
restored=$(docker exec -e PGPASSWORD="$new_bootstrap_password" "$restore_container" psql \
|
|
-XAt -U "$THT_VECTOR_BOOTSTRAP_USER" -d thoth --command \
|
|
"SELECT content_hash <> 'mutated-after-backup' FROM vectors.memory WHERE record_key = '$marker'")
|
|
test "$restored" = t
|
|
expected_migrations=$(find harness/tht/migrations/vector -type f -name '[0-9][0-9][0-9]_*.sql' \
|
|
-exec basename {} \; | sed 's/_.*//' | sort | paste -sd, -)
|
|
applied_migrations=$(docker exec -e PGPASSWORD="$new_bootstrap_password" "$restore_container" psql \
|
|
-XAt -U "$THT_VECTOR_BOOTSTRAP_USER" -d thoth --command \
|
|
"SELECT string_agg(version, ',' ORDER BY version) FROM public.tht_vector_migrations")
|
|
test "$applied_migrations" = "$expected_migrations"
|
|
dimensions=$(docker exec -e PGPASSWORD="$new_bootstrap_password" "$restore_container" psql \
|
|
-XAt -U "$THT_VECTOR_BOOTSTRAP_USER" -d thoth --command \
|
|
"SELECT count(*) = 3 FROM pg_attribute a JOIN pg_class c ON c.oid=a.attrelid
|
|
JOIN pg_namespace n ON n.oid=c.relnamespace
|
|
WHERE n.nspname='vectors' AND a.attname='embedding' AND format_type(a.atttypid,a.atttypmod)='vectors.vector(768)'")
|
|
test "$dimensions" = t
|
|
echo "Transactional rollback and disposable-volume restore adapter parity passed."
|
|
fi
|
|
|
|
echo "Local pgvector runtime/bootstrap rotation, least-privilege roles, and persistence passed."
|