fix: harden qdrant restore safety
This commit is contained in:
+52
-14
@@ -1,6 +1,10 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
helper_image='qdrant/qdrant:v1.18.2@sha256:75eab8c4ba42096724fdcfde8b4de0b5713d529dde32f285a1f86fdcb2c9e50c'
|
||||
archive_format='thothii-qdrant-backup-v1'
|
||||
volume_role='qdrant-data'
|
||||
|
||||
usage() {
|
||||
echo "usage: $0 --project-name NAME --output FILE" >&2
|
||||
exit 2
|
||||
@@ -23,37 +27,43 @@ output_dir=$(dirname "$output")
|
||||
output_name=$(basename "$output")
|
||||
[ -d "$output_dir" ] || { echo "backup destination directory does not exist" >&2; exit 2; }
|
||||
|
||||
expected_volume_name="${project_name}_${volume_role}"
|
||||
|
||||
resolve_volume() {
|
||||
names=$(docker volume ls \
|
||||
--filter "label=com.docker.compose.project=$project_name" \
|
||||
--filter "label=com.docker.compose.volume=qdrant-data" \
|
||||
--filter "label=com.docker.compose.volume=$volume_role" \
|
||||
--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;}'
|
||||
resolved=$(printf '%s\n' "$names" | sed -n '/./{p;q;}')
|
||||
[ "$resolved" = "$expected_volume_name" ] || {
|
||||
echo "unexpected qdrant-data volume name: $resolved" >&2
|
||||
exit 2
|
||||
}
|
||||
printf '%s\n' "$resolved"
|
||||
}
|
||||
|
||||
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"
|
||||
validate_volume_metadata() {
|
||||
metadata=$(docker volume inspect --format '{{ .Name }} {{ index .Labels "com.docker.compose.project" }} {{ index .Labels "com.docker.compose.volume" }}' "$1")
|
||||
set -- $metadata
|
||||
[ "${1-}" = "$expected_volume_name" ] || { echo "volume metadata name mismatch" >&2; exit 2; }
|
||||
[ "${2-}" = "$project_name" ] || { echo "volume metadata project label mismatch" >&2; exit 2; }
|
||||
[ "${3-}" = "$volume_role" ] || { echo "volume metadata role label mismatch" >&2; exit 2; }
|
||||
}
|
||||
|
||||
volume_name=$(resolve_volume)
|
||||
mountpoint=$(resolve_mountpoint "$volume_name")
|
||||
validate_volume_metadata "$volume_name"
|
||||
|
||||
running_container=$(docker compose --project-name "$project_name" ps --status running -q qdrant)
|
||||
restart_qdrant=0
|
||||
temporary_output=
|
||||
cleanup() {
|
||||
status=$?
|
||||
if [ "${temporary_output:-}" ] && [ -e "${temporary_output:-}" ]; then
|
||||
if [ -n "${temporary_output:-}" ] && [ -e "${temporary_output:-}" ]; then
|
||||
rm -f "$temporary_output"
|
||||
fi
|
||||
if [ "$restart_qdrant" -eq 1 ]; then
|
||||
@@ -70,7 +80,35 @@ fi
|
||||
|
||||
umask 077
|
||||
temporary_output=$(mktemp "$output_dir/.${output_name}.tmp.XXXXXX")
|
||||
tar -C "$mountpoint" -cf "$temporary_output" .
|
||||
created_utc=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
docker run --rm \
|
||||
--mount "type=volume,src=$volume_name,dst=/qdrant-data,readonly" \
|
||||
--mount "type=bind,src=$output_dir,dst=/backup" \
|
||||
"$helper_image" \
|
||||
/bin/sh -eu -c '
|
||||
archive_format=$1
|
||||
project_name=$2
|
||||
volume_name=$3
|
||||
volume_role=$4
|
||||
helper_image=$5
|
||||
created_utc=$6
|
||||
output_name=$7
|
||||
staging=$(mktemp -d /tmp/qdrant-backup.XXXXXX)
|
||||
trap "rm -rf \"$staging\"" EXIT HUP INT TERM
|
||||
mkdir -p "$staging/payload"
|
||||
cat >"$staging/manifest.env" <<EOF
|
||||
format=$archive_format
|
||||
project_name=$project_name
|
||||
volume_name=$volume_name
|
||||
volume_role=$volume_role
|
||||
helper_image=$helper_image
|
||||
created_utc=$created_utc
|
||||
EOF
|
||||
cp -R /qdrant-data/. "$staging/payload"/
|
||||
tar -C "$staging" -cf "/backup/$output_name" manifest.env payload
|
||||
' sh "$archive_format" "$project_name" "$volume_name" "$volume_role" "$helper_image" "$created_utc" "$(basename "$temporary_output")"
|
||||
|
||||
if ! ln "$temporary_output" "$output"; then
|
||||
echo "refusing to replace backup destination created concurrently: $output" >&2
|
||||
exit 2
|
||||
|
||||
Reference in New Issue
Block a user