diff --git a/postgres-dump-zstd-docker-sock.sh b/postgres-dump-zstd-docker-sock.sh index 58a37ed..d30fb8c 100755 --- a/postgres-dump-zstd-docker-sock.sh +++ b/postgres-dump-zstd-docker-sock.sh @@ -2,12 +2,23 @@ set -eu # PostgreSQL backup triggered through the Docker Engine unix socket. -# Example: ./postgres-dump-zstd-docker-sock.sh --socket=/var/run/docker.sock --api-version=v1.51 --backup-dir=/backups --container=postgres17 --backup-prefix=pgvector17_all_databases_zstd_ -# Remote example: curl -fsSL https://git.1-h.cc/Scripts/Linux/raw/branch/main/postgres-dump-zstd-docker-sock.sh | sh -s -- --socket=/var/run/docker.sock --api-version=v1.51 --backup-dir=/backups --container=postgres17 --backup-prefix=pgvector17_all_databases_zstd_ +# Example: ./postgres-dump-zstd-docker-sock.sh --container=postgres17 --backup-prefix=pgvector17_all_databases_zstd_ --socket=/var/run/docker.sock --api-version=v1.51 --backup-dir=/backups +# Remote example: curl -fsSL https://git.1-h.cc/Scripts/Linux/raw/branch/main/postgres-dump-zstd-docker-sock.sh | sh -s -- --container=postgres17 --backup-prefix=pgvector17_all_databases_zstd_ --socket=/var/run/docker.sock --api-version=v1.51 --backup-dir=/backups log() { printf '%s\n' "[cron] $(date -u '+%Y-%m-%dT%H:%M:%SZ') $*" } +log_stream() { + log_stream_prefix=$1 + log_stream_payload=$2 + + if [ -n "$log_stream_payload" ]; then + printf '%s\n' "$log_stream_payload" | while IFS= read -r log_stream_line; do + log "$log_stream_prefix: $log_stream_line" + done + fi +} + print_usage() { cat <&2 Usage: $0 [--socket=PATH] [--api-version=VERSION] --container=NAME [--backup-dir=DIR] --backup-prefix=PREFIX @@ -28,12 +39,75 @@ PG_CONTAINER_NAME="" BACKUP_DIR="/backups" BACKUP_PREFIX="" +require_command() { + if ! command -v "$1" >/dev/null 2>&1; then + printf '%s requires %s in PATH\n' "$0" "$1" >&2 + exit 1 + fi +} + missing_value() { printf 'Missing value for %s\n' "$1" >&2 print_usage exit 1 } +docker_exec_create() { + docker_exec_create_desc=$1 + docker_exec_create_cmd=$2 + docker_exec_create_payload=$(jq -n --arg cmd "$docker_exec_create_cmd" '{ + AttachStdin: false, + AttachStdout: true, + AttachStderr: true, + Tty: true, + Cmd: ["bash", "-lc", $cmd] + }') + + DOCKER_LAST_RESPONSE=$(curl --fail --silent --show-error --unix-socket "$DOCKER_SOCKET" \ + -X POST \ + -H "Content-Type: application/json" \ + -d "$docker_exec_create_payload" \ + "$exec_create_endpoint") + + docker_exec_create_id=$(printf '%s' "$DOCKER_LAST_RESPONSE" | jq -r '.Id // empty') + + if [ -z "$docker_exec_create_id" ]; then + log "failed to create $docker_exec_create_desc exec for $PG_CONTAINER_NAME" + log "docker response: $DOCKER_LAST_RESPONSE" + return 1 + fi + + printf '%s' "$docker_exec_create_id" +} + +docker_exec_start() { + docker_exec_start_id=$1 + docker_exec_start_desc=$2 + docker_exec_start_endpoint="${docker_api_base}/exec/${docker_exec_start_id}/start" + DOCKER_LAST_RESPONSE=$(curl --fail --show-error --silent --unix-socket "$DOCKER_SOCKET" \ + -X POST \ + -H "Content-Type: application/json" \ + -d '{"Detach": false, "Tty": true}' \ + "$docker_exec_start_endpoint") + + log_stream "$docker_exec_start_desc output" "$DOCKER_LAST_RESPONSE" + + printf '%s' "$DOCKER_LAST_RESPONSE" +} + +docker_exec_exit_code() { + docker_exec_exit_id=$1 + docker_exec_inspect_endpoint="${docker_api_base}/exec/${docker_exec_exit_id}/json" + DOCKER_LAST_RESPONSE=$(curl --fail --silent --show-error --unix-socket "$DOCKER_SOCKET" "$docker_exec_inspect_endpoint") + docker_exec_exit_code_value=$(printf '%s' "$DOCKER_LAST_RESPONSE" | jq -r '.ExitCode // empty') + + if [ -z "$docker_exec_exit_code_value" ]; then + return 1 + fi + + printf '%s' "$docker_exec_exit_code_value" +} + while [ "$#" -gt 0 ]; do case $1 in --socket=*) @@ -110,6 +184,9 @@ if [ "$#" -gt 0 ]; then exit 1 fi +require_command curl +require_command jq + if [ -z "$DOCKER_SOCKET" ] || [ -z "$DOCKER_API_VERSION" ] || [ -z "$PG_CONTAINER_NAME" ] || [ -z "$BACKUP_DIR" ] || [ -z "$BACKUP_PREFIX" ]; then print_usage exit 1 @@ -128,57 +205,26 @@ exec_create_endpoint="${docker_api_base}/containers/${PG_CONTAINER_NAME}/exec" log "preparing database backup at $backup_path via docker unix socket" cmd="set -o pipefail && pg_dumpall --username=\"\${POSTGRES_USER:-postgres}\" --clean | zstd > ${backup_path}" -create_payload=$(jq -n --arg cmd "$cmd" '{ - AttachStdin: false, - AttachStdout: true, - AttachStderr: true, - Tty: true, - Cmd: ["bash", "-lc", $cmd] -}') -create_response=$(curl --fail --silent --show-error --unix-socket "$DOCKER_SOCKET" \ - -X POST \ - -H "Content-Type: application/json" \ - -d "$create_payload" \ - "$exec_create_endpoint") - -exec_id=$(printf '%s' "$create_response" | jq -r '.Id // empty') - -if [ -z "$exec_id" ]; then - log "failed to create exec for $PG_CONTAINER_NAME" - log "docker response: $create_response" +if ! exec_id=$(docker_exec_create "backup" "$cmd"); then exit 1 fi log "starting exec $exec_id" -exec_start_endpoint="${docker_api_base}/exec/${exec_id}/start" -start_output=$(curl --fail --show-error --silent --unix-socket "$DOCKER_SOCKET" \ - -X POST \ - -H "Content-Type: application/json" \ - -d '{"Detach": false, "Tty": true}' \ - "$exec_start_endpoint") - -if [ -n "$start_output" ]; then - printf '%s\n' "$start_output" | while IFS= read -r line; do - log "exec output: $line" - done +if ! start_output=$(docker_exec_start "$exec_id" "exec"); then + exit 1 fi -exec_inspect_endpoint="${docker_api_base}/exec/${exec_id}/json" -inspect_response=$(curl --fail --silent --show-error --unix-socket "$DOCKER_SOCKET" "$exec_inspect_endpoint") - -exit_code=$(printf '%s' "$inspect_response" | jq -r '.ExitCode // empty') - -if [ -z "$exit_code" ]; then +if ! exit_code=$(docker_exec_exit_code "$exec_id"); then log "could not determine exec exit code" - log "docker inspect response: $inspect_response" + log "docker inspect response: $DOCKER_LAST_RESPONSE" exit 1 fi if [ "$exit_code" != "0" ]; then log "backup exec exited with status $exit_code" - log "docker inspect response: $inspect_response" + log "docker inspect response: $DOCKER_LAST_RESPONSE" exit "$exit_code" fi @@ -193,25 +239,8 @@ else log "backup file not found on host; verifying inside container $PG_CONTAINER_NAME" verify_cmd="set -eo pipefail && if [ -f \"${backup_path}\" ]; then du -h \"${backup_path}\" 2>/dev/null | awk 'NR==1{print \$1}' || printf 'exists'; else exit 44; fi" - verify_payload=$(jq -n --arg cmd "$verify_cmd" '{ - AttachStdin: false, - AttachStdout: true, - AttachStderr: true, - Tty: true, - Cmd: ["bash", "-lc", $cmd] - }') - verify_create_response=$(curl --fail --silent --show-error --unix-socket "$DOCKER_SOCKET" \ - -X POST \ - -H "Content-Type: application/json" \ - -d "$verify_payload" \ - "$exec_create_endpoint") - - verify_exec_id=$(printf '%s' "$verify_create_response" | jq -r '.Id // empty') - - if [ -z "$verify_exec_id" ]; then - log "failed to create verification exec for $PG_CONTAINER_NAME" - log "docker response: $verify_create_response" + if ! verify_exec_id=$(docker_exec_create "verification" "$verify_cmd"); then log "backup command succeeded, but file $backup_path not found" log "database backup finished" exit 0 @@ -219,27 +248,15 @@ else log "starting verification exec $verify_exec_id" - verify_start_endpoint="${docker_api_base}/exec/${verify_exec_id}/start" - verify_start_output=$(curl --fail --show-error --silent --unix-socket "$DOCKER_SOCKET" \ - -X POST \ - -H "Content-Type: application/json" \ - -d '{"Detach": false, "Tty": true}' \ - "$verify_start_endpoint") - - if [ -n "$verify_start_output" ]; then - printf '%s\n' "$verify_start_output" | while IFS= read -r line; do - log "verify output: $line" - done + if ! verify_start_output=$(docker_exec_start "$verify_exec_id" "verify"); then + log "backup command succeeded, but file $backup_path not confirmed" + log "database backup finished" + exit 0 fi - verify_inspect_endpoint="${docker_api_base}/exec/${verify_exec_id}/json" - verify_inspect_response=$(curl --fail --silent --show-error --unix-socket "$DOCKER_SOCKET" "$verify_inspect_endpoint") - - verify_exit_code=$(printf '%s' "$verify_inspect_response" | jq -r '.ExitCode // empty') - - if [ -z "$verify_exit_code" ]; then + if ! verify_exit_code=$(docker_exec_exit_code "$verify_exec_id"); then log "could not determine verification exec exit code" - log "docker inspect response: $verify_inspect_response" + log "docker inspect response: $DOCKER_LAST_RESPONSE" log "backup command succeeded, but file $backup_path not found" elif [ "$verify_exit_code" = "0" ]; then verify_size=$(printf '%s' "$verify_start_output" | awk 'NF {last=$0} END {print last}') @@ -253,7 +270,7 @@ else log "backup command succeeded, but file $backup_path not found inside container $PG_CONTAINER_NAME" else log "verification exec exited with status $verify_exit_code" - log "docker inspect response: $verify_inspect_response" + log "docker inspect response: $DOCKER_LAST_RESPONSE" log "backup command succeeded, but file $backup_path not confirmed" fi fi