feat: backup cron
This commit is contained in:
Executable
+230
@@ -0,0 +1,230 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
umask 077
|
||||
|
||||
confirmation_phrase="replace-bowli-production-database"
|
||||
|
||||
if [ "${RESTORE_CONFIRM:-}" != "$confirmation_phrase" ]; then
|
||||
echo "Database restoration was not confirmed." >&2
|
||||
echo "Run it with RESTORE_CONFIRM=$confirmation_phrase" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
require_variable() {
|
||||
variable_name="$1"
|
||||
eval "variable_value=\${$variable_name:-}"
|
||||
|
||||
if [ -z "$variable_value" ]; then
|
||||
echo "$variable_name is required." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
aws_cli() {
|
||||
endpoint="${BACKUP_S3_ENDPOINT:-${AWS_ENDPOINT:-}}"
|
||||
|
||||
if [ -n "$endpoint" ]; then
|
||||
aws --endpoint-url "$endpoint" "$@"
|
||||
else
|
||||
aws "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
for variable in DB_DATABASE DB_HOST DB_PASSWORD DB_USERNAME
|
||||
do
|
||||
require_variable "$variable"
|
||||
done
|
||||
|
||||
case "$DB_DATABASE" in
|
||||
*[!A-Za-z0-9_-]*)
|
||||
echo "DB_DATABASE may only contain letters, numbers, underscores and hyphens for restoration." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
source_name="${1:-latest-s3}"
|
||||
backup_directory="${BACKUP_DIRECTORY:-/backups}"
|
||||
backup_prefix="${BACKUP_S3_PREFIX:-database-backups/postgres}"
|
||||
backup_prefix="${backup_prefix#/}"
|
||||
backup_prefix="${backup_prefix%/}"
|
||||
temporary_directory="$(mktemp -d)"
|
||||
restore_path=""
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$temporary_directory"
|
||||
}
|
||||
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
download_s3_backup() {
|
||||
s3_uri="$1"
|
||||
file_name="${s3_uri##*/}"
|
||||
restore_path="$temporary_directory/$file_name"
|
||||
|
||||
aws_cli s3 cp "$s3_uri" "$restore_path" --only-show-errors
|
||||
aws_cli s3 cp "$s3_uri.sha256" "$restore_path.sha256" --only-show-errors
|
||||
}
|
||||
|
||||
case "$source_name" in
|
||||
latest-local)
|
||||
restore_path="$(find "$backup_directory" -maxdepth 1 -type f -name "*.dump" | sort | tail -n 1)"
|
||||
;;
|
||||
latest-s3)
|
||||
require_variable AWS_ACCESS_KEY_ID
|
||||
require_variable AWS_DEFAULT_REGION
|
||||
require_variable AWS_SECRET_ACCESS_KEY
|
||||
require_variable BACKUP_S3_BUCKET
|
||||
|
||||
s3_directory="s3://$BACKUP_S3_BUCKET"
|
||||
if [ -n "$backup_prefix" ]; then
|
||||
s3_directory="$s3_directory/$backup_prefix"
|
||||
fi
|
||||
|
||||
latest_key="$(aws_cli s3 ls "$s3_directory/" --recursive \
|
||||
| awk '$4 ~ /\.dump$/ { print $4 }' \
|
||||
| sort \
|
||||
| tail -n 1)"
|
||||
|
||||
if [ -n "$latest_key" ]; then
|
||||
download_s3_backup "s3://$BACKUP_S3_BUCKET/$latest_key"
|
||||
fi
|
||||
;;
|
||||
s3://*)
|
||||
require_variable AWS_ACCESS_KEY_ID
|
||||
require_variable AWS_DEFAULT_REGION
|
||||
require_variable AWS_SECRET_ACCESS_KEY
|
||||
download_s3_backup "$source_name"
|
||||
;;
|
||||
/*)
|
||||
restore_path="$source_name"
|
||||
;;
|
||||
*)
|
||||
restore_path="$backup_directory/$source_name"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -z "$restore_path" ] || [ ! -f "$restore_path" ]; then
|
||||
echo "Backup not found: $source_name" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$restore_path.sha256" ]; then
|
||||
echo "Checksum not found for: $restore_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
(
|
||||
cd "$(dirname "$restore_path")"
|
||||
sha256sum -c "$(basename "$restore_path").sha256"
|
||||
)
|
||||
pg_restore --list "$restore_path" >/dev/null
|
||||
|
||||
if [ "$(dirname "$restore_path")" != "$temporary_directory" ]; then
|
||||
cp "$restore_path" "$temporary_directory/"
|
||||
cp "$restore_path.sha256" "$temporary_directory/"
|
||||
restore_path="$temporary_directory/$(basename "$restore_path")"
|
||||
fi
|
||||
|
||||
export PGPASSWORD="$DB_PASSWORD"
|
||||
export PGCONNECT_TIMEOUT="${PGCONNECT_TIMEOUT:-10}"
|
||||
|
||||
if [ "${RESTORE_SKIP_SAFETY_BACKUP:-false}" != "true" ]; then
|
||||
echo "Creating a safety backup before restoration."
|
||||
/usr/local/bin/backup-database pre-restore
|
||||
fi
|
||||
|
||||
timestamp="$(date -u +"%Y%m%dT%H%M%SZ")"
|
||||
database_prefix="$(printf "%s" "$DB_DATABASE" | cut -c1-28)"
|
||||
restore_database="${database_prefix}_restore_${timestamp}_$$"
|
||||
previous_database="${database_prefix}_before_${timestamp}_$$"
|
||||
|
||||
createdb \
|
||||
--host="$DB_HOST" \
|
||||
--port="${DB_PORT:-5432}" \
|
||||
--username="$DB_USERNAME" \
|
||||
--owner="$DB_USERNAME" \
|
||||
"$restore_database"
|
||||
|
||||
restore_failed=true
|
||||
current_database_renamed=false
|
||||
|
||||
cleanup_restore_database() {
|
||||
if [ "$restore_failed" = "true" ]; then
|
||||
if [ "$current_database_renamed" = "true" ]; then
|
||||
psql \
|
||||
--host="$DB_HOST" \
|
||||
--port="${DB_PORT:-5432}" \
|
||||
--username="$DB_USERNAME" \
|
||||
--dbname=postgres \
|
||||
--command="ALTER DATABASE \"$previous_database\" RENAME TO \"$DB_DATABASE\";" \
|
||||
>/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
dropdb \
|
||||
--host="$DB_HOST" \
|
||||
--port="${DB_PORT:-5432}" \
|
||||
--username="$DB_USERNAME" \
|
||||
--force \
|
||||
--if-exists \
|
||||
"$restore_database" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
cleanup
|
||||
}
|
||||
|
||||
trap cleanup_restore_database EXIT INT TERM
|
||||
|
||||
echo "Restoring and validating temporary database $restore_database."
|
||||
pg_restore \
|
||||
--host="$DB_HOST" \
|
||||
--port="${DB_PORT:-5432}" \
|
||||
--username="$DB_USERNAME" \
|
||||
--dbname="$restore_database" \
|
||||
--exit-on-error \
|
||||
--no-owner \
|
||||
--no-acl \
|
||||
"$restore_path"
|
||||
vacuumdb \
|
||||
--host="$DB_HOST" \
|
||||
--port="${DB_PORT:-5432}" \
|
||||
--username="$DB_USERNAME" \
|
||||
--dbname="$restore_database" \
|
||||
--analyze-in-stages
|
||||
|
||||
database_exists="$(psql \
|
||||
--host="$DB_HOST" \
|
||||
--port="${DB_PORT:-5432}" \
|
||||
--username="$DB_USERNAME" \
|
||||
--dbname=postgres \
|
||||
--tuples-only \
|
||||
--no-align \
|
||||
--command="SELECT 1 FROM pg_database WHERE datname = '$DB_DATABASE';")"
|
||||
|
||||
if [ "$database_exists" = "1" ]; then
|
||||
psql \
|
||||
--host="$DB_HOST" \
|
||||
--port="${DB_PORT:-5432}" \
|
||||
--username="$DB_USERNAME" \
|
||||
--dbname=postgres \
|
||||
--set=ON_ERROR_STOP=1 \
|
||||
--command="SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '$DB_DATABASE' AND pid <> pg_backend_pid();" \
|
||||
--command="ALTER DATABASE \"$DB_DATABASE\" RENAME TO \"$previous_database\";"
|
||||
current_database_renamed=true
|
||||
fi
|
||||
|
||||
psql \
|
||||
--host="$DB_HOST" \
|
||||
--port="${DB_PORT:-5432}" \
|
||||
--username="$DB_USERNAME" \
|
||||
--dbname=postgres \
|
||||
--set=ON_ERROR_STOP=1 \
|
||||
--command="ALTER DATABASE \"$restore_database\" RENAME TO \"$DB_DATABASE\";"
|
||||
|
||||
restore_failed=false
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
echo "Database restoration completed successfully."
|
||||
if [ "$database_exists" = "1" ]; then
|
||||
echo "Previous database kept for rollback as: $previous_database"
|
||||
fi
|
||||
Reference in New Issue
Block a user