fix: retire active pgvector artifacts

This commit is contained in:
2026-08-08 19:27:05 +02:00
parent 4e3fecbe8e
commit a6dbe1d023
16 changed files with 630 additions and 292 deletions
+76 -61
View File
@@ -1,80 +1,95 @@
#!/bin/sh
set -eu
root=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)
. "$root/scripts/secret-file-utils.sh"
usage() {
echo "usage: $0 --active-host HOST --active-database DB --active-user USER --active-password-file FILE --target-host HOST --target-database DB --target-user USER --target-password-file FILE --input FILE [--active-port PORT] [--target-port PORT] [--force-nonempty]" >&2
echo "usage: $0 --project-name NAME --input FILE --confirm-project NAME" >&2
exit 2
}
active_host= active_database= active_user= active_password_file= active_port=5432
target_host= target_database= target_user= target_password_file= target_port=5432
input= force=0
project_name=
input=
confirm_project=
while [ "$#" -gt 0 ]; do
case "$1" in
--active-host) active_host=${2-}; shift 2 ;;
--active-port) active_port=${2-}; shift 2 ;;
--active-database) active_database=${2-}; shift 2 ;;
--active-user) active_user=${2-}; shift 2 ;;
--active-password-file) active_password_file=${2-}; shift 2 ;;
--target-host) target_host=${2-}; shift 2 ;;
--target-port) target_port=${2-}; shift 2 ;;
--target-database) target_database=${2-}; shift 2 ;;
--target-user) target_user=${2-}; shift 2 ;;
--target-password-file) target_password_file=${2-}; shift 2 ;;
--project-name) project_name=${2-}; shift 2 ;;
--input) input=${2-}; shift 2 ;;
--force-nonempty) force=1; shift ;;
--confirm-project) confirm_project=${2-}; shift 2 ;;
*) usage ;;
esac
done
for value in "$active_host" "$active_database" "$active_user" "$active_password_file" \
"$target_host" "$target_database" "$target_user" "$target_password_file" "$input"; do
[ -n "$value" ] || usage
done
[ -n "$project_name" ] && [ -n "$input" ] && [ -n "$confirm_project" ] || usage
[ "$confirm_project" = "$project_name" ] || {
echo "restore confirmation must match --project-name exactly" >&2
exit 2
}
[ -r "$input" ] || { echo "backup input is not readable" >&2; exit 2; }
validate_secret_file "$active_password_file" "active source password file"
validate_secret_file "$target_password_file" "target password file"
umask 077
active_pass=$(mktemp "${TMPDIR:-/tmp}/thoth-vector-active-pgpass.XXXXXX")
target_pass=$(mktemp "${TMPDIR:-/tmp}/thoth-vector-target-pgpass.XXXXXX")
cleanup() { rm -f "$active_pass" "$target_pass"; }
resolve_volume() {
names=$(docker volume ls \
--filter "label=com.docker.compose.project=$project_name" \
--filter "label=com.docker.compose.volume=qdrant-data" \
--format '{{.Name}}')
count=$(printf '%s\n' "$names" | sed '/^$/d' | wc -l | tr -d ' ')
[ "$count" -eq 1 ] || {
echo "expected exactly one qdrant-data volume for compose project $project_name" >&2
exit 2
}
printf '%s\n' "$names" | sed -n '/./{p;q;}'
}
resolve_mountpoint() {
mountpoint=$(docker volume inspect --format '{{ .Mountpoint }}' "$1")
[ -n "$mountpoint" ] || { echo "docker did not return a qdrant-data mountpoint" >&2; exit 2; }
case "$mountpoint" in
/*) ;;
*) echo "qdrant-data mountpoint is not absolute: $mountpoint" >&2; exit 2 ;;
esac
[ -d "$mountpoint" ] || { echo "qdrant-data mountpoint is not a directory: $mountpoint" >&2; exit 2; }
printf '%s\n' "$mountpoint"
}
volume_name=$(resolve_volume)
mountpoint=$(resolve_mountpoint "$volume_name")
running_container=$(docker compose --project-name "$project_name" ps --status running -q qdrant)
restart_qdrant=0
staging_dir=
extract_dir=
cleanup() {
status=$?
if [ "$status" -ne 0 ] && [ -n "${staging_dir:-}" ] && [ -d "${staging_dir:-}" ]; then
find "$mountpoint" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +
find "$staging_dir" -mindepth 1 -maxdepth 1 -exec mv {} "$mountpoint"/ \;
fi
if [ -n "${staging_dir:-}" ] && [ -d "${staging_dir:-}" ]; then
rm -rf "$staging_dir"
fi
if [ -n "${extract_dir:-}" ] && [ -d "${extract_dir:-}" ]; then
rm -rf "$extract_dir"
fi
if [ "$restart_qdrant" -eq 1 ]; then
docker compose --project-name "$project_name" start qdrant >/dev/null
fi
exit "$status"
}
trap cleanup EXIT HUP INT TERM
make_passfile() {
secret=$(read_secret_file "$5" "database password file")
escaped=$(printf '%s' "$secret" | sed 's/\\/\\\\/g; s/:/\\:/g')
printf '%s:%s:%s:%s:%s\n' "$1" "$2" "$3" "$4" "$escaped" >"$6"
chmod 0600 "$6"
}
make_passfile "$active_host" "$active_port" "$active_database" "$active_user" \
"$active_password_file" "$active_pass"
make_passfile "$target_host" "$target_port" "$target_database" "$target_user" \
"$target_password_file" "$target_pass"
identity_sql="SELECT system_identifier::text FROM pg_control_system()"
active_identity=$(PGPASSFILE=$active_pass psql -XAt --host="$active_host" --port="$active_port" \
--username="$active_user" --dbname="$active_database" --command="$identity_sql")
target_identity=$(PGPASSFILE=$target_pass psql -XAt --host="$target_host" --port="$target_port" \
--username="$target_user" --dbname="$target_database" --command="$identity_sql")
[ "$active_identity" != "$target_identity" ] || {
echo "refusing restore: active source and target are on the same PostgreSQL cluster" >&2
exit 2
}
object_count=$(PGPASSFILE=$target_pass psql -XAt --host="$target_host" --port="$target_port" \
--username="$target_user" --dbname="$target_database" --command="
SELECT count(*) FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace
WHERE (n.nspname='vectors' OR (n.nspname='public' AND c.relname='tht_vector_migrations'))
AND c.relkind IN ('r','p','S','v','m');")
if [ "$object_count" != 0 ] && [ "$force" != 1 ]; then
echo "refusing restore into non-empty target; use --force-nonempty explicitly" >&2
exit 2
if [ -n "$running_container" ]; then
docker compose --project-name "$project_name" stop qdrant >/dev/null
restart_qdrant=1
fi
PGPASSFILE=$target_pass pg_restore --exit-on-error --single-transaction \
--clean --if-exists --no-owner \
--host="$target_host" --port="$target_port" --username="$target_user" \
--dbname="$target_database" "$input"
echo "Vector restore completed into explicit target $target_host:$target_port/$target_database"
parent_dir=$(dirname "$mountpoint")
staging_dir=$(mktemp -d "$parent_dir/.qdrant-restore-staging.XXXXXX")
extract_dir=$(mktemp -d "${TMPDIR:-/tmp}/qdrant-restore.XXXXXX")
tar -C "$extract_dir" -xf "$input"
find "$mountpoint" -mindepth 1 -maxdepth 1 -exec mv {} "$staging_dir"/ \;
find "$extract_dir" -mindepth 1 -maxdepth 1 -exec mv {} "$mountpoint"/ \;
rm -rf "$staging_dir"
staging_dir=
rm -rf "$extract_dir"
extract_dir=
echo "Qdrant restore completed into $volume_name for compose project $project_name"