Compare commits

...

4 commits

Author SHA1 Message Date
9fe2f05491
Add readme. 2022-08-30 22:44:38 +02:00
b9da9c74c4
Add possibility to create volumes implicitly. 2022-08-30 22:44:12 +02:00
17ace3fb33
Add security check. 2022-08-30 22:33:05 +02:00
bb060e5eec
Add restore command and test. 2022-08-30 22:26:52 +02:00
4 changed files with 141 additions and 24 deletions

39
README.md Normal file
View file

@ -0,0 +1,39 @@
# Docker Volume Backup
A simple script which creates tarballs from docker volumes and restores
them.
## Usage
### Backup a single volume
`./dvbackup.sh backup <volume_name> <path/to/tarball.tar>`
Creates a tarball from the contents of the volume.
### Restore a single volume
`./dvbackup.sh restore <path/to/tarball.tar> <volume_name>`
Restores the tarball into the volume. *This is destructive*, all old
contents will be removed from the volume. The volume must already exist.
### Backup all named volumes
`./dvbackup.sh backup_all`
Creates backups from all volumes which do not only contain the character
set `[0-9a-f]`.
This command will output a `<volume_name>.tar` file for every found
volume in the current working directory.
### Restore volumes
`./dvbackup.sh restore_all [volume.tar...]`
Restores all given tarballs into their respective volumes. The volumes
must already exist and the operation is *destructive* in the same way
as the `restore` operation. The default behaviour is to ask before
continuing, this can be overridden by setting `DVB_I_KNOW_WHAT_I_DO=y`.
Volumes can be created implicitly by setting `DVB_CREATE_VOLUME=y`.

View file

@ -1,50 +1,81 @@
#!/usr/bin/env bash #!/usr/bin/env sh
if [ -z "$DOCKER" ]; then
DOCKER=docker
fi
echo_and_run() {
echo "$@"
"$@"
return $?
}
backup() { backup() {
local volume
local target
local target_dir
local target_name
volume="$1" volume="$1"
target="$(realpath "$2")" target="$(realpath "$2")"
target_dir="$(dirname "$target")" target_dir="$(dirname "$target")"
target_name="$(basename "$target")" target_name="$(basename "$target")"
test -z "$(docker volume ls | grep "$volume")" && { echo "Error: No such volume, aborting" >&2; exit 1; } test -z "$("$DOCKER" volume ls | grep "$volume")" && { echo "Error: No such volume, aborting" >&2; exit 1; }
test -z "$target_dir" && { echo "Error: No base folder found for target=$target" >&2; exit 2; } test -z "$target_dir" && { echo "Error: No base folder found for target=$target" >&2; exit 2; }
test -z "$target_name" && { echo "Error: No target name found for target=$target" >&2; exit 3; } test -z "$target_name" && { echo "Error: No target name found for target=$target" >&2; exit 3; }
set -x echo_and_run "$DOCKER" run --rm \
docker run --rm --mount="type=volume,source=$volume,destination=/data,ro=true" --mount="type=bind,source=$target_dir,destination=/data2" busybox /bin/sh -c "tar cvf '/data2/$target_name' /data && chown $(id -u):$(id -g) /data2/$target_name" --mount="type=volume,source=$volume,destination=/data,ro=true" \
set +x --mount="type=bind,source=$target_dir,destination=/data2" \
busybox /bin/sh -c \
"cd /data/ && tar cf '/data2/$target_name' ./* && chown $(id -u):$(id -g) /data2/$target_name"
} }
restore() { restore() {
local volume
local target
local target_dir
local target_name
target="$(realpath "$1")" target="$(realpath "$1")"
target_dir="$(dirname "$target")" target_dir="$(dirname "$target")"
target_name="$(basename "$target")" target_name="$(basename "$target")"
volume="$2" volume="$2"
test -z "$(docker volume ls | grep "$volume")" && { echo "Error: No such volume, aborting" >&2; exit 1; } test -z "$("$DOCKER" volume ls | grep "$volume")" && { echo "Error: No such volume, aborting" >&2; exit 1; }
test -z "$target_dir" && { echo "Error: No base folder found for target=$target" >&2; exit 2; } test -z "$target_dir" && { echo "Error: No base folder found for target=$target" >&2; exit 2; }
test -z "$target_name" && { echo "Error: No target name found for target=$target" >&2; exit 3; } test -z "$target_name" && { echo "Error: No target name found for target=$target" >&2; exit 3; }
set -x echo_and_run "$DOCKER" run --rm \
docker run --rm --mount="type=volume,source=$volume,destination=/data" --mount="type=bind,source=$target_dir,destination=/data2" busybox /bin/sh -c "rm -rf /data/* && cd / && tar xvf '/data2/$target_name'" --mount="type=volume,source=$volume,destination=/data" \
set +x --mount="type=bind,source=$target_dir,destination=/data2" \
busybox /bin/sh -c \
"cd /data/ && rm -rf ./* && tar xf '/data2/$target_name'"
} }
main() { backup_all() {
local volumes "$DOCKER" volume ls \
volumes="$(docker volume ls | tail -n+2 | egrep -v '[a-z]+\s+[0-9a-f]+$' | egrep -o '\s.+$' | sed 's/\s//g')" | awk '!/^[a-z]+ +[0-9a-f]+$/ && (NR>1) {print $2}' \
while read -r line; do | while read -r volume_name; do
echo "$line -> $line.tar" echo "$volume_name -> $volume_name.tar"
backup "$line" "$line.tar" backup "$volume_name" "$volume_name.tar"
done <<< "$volumes" done
}
restore_all() {
if [ -z "$DVB_I_KNOW_WHAT_I_DO" ]; then
printf "The following operation will delete all data in the volumes to be restored, are you sure [y/N]? "
read -r DVB_I_KNOW_WHAT_I_DO
fi
if echo "$DVB_I_KNOW_WHAT_I_DO" | grep -Eviq 't|true|1|y|yes'; then
echo aborting
exit 1
fi
for tarball in "$@"; do
volume_name="${tarball%.tar}"
echo "$volume_name -> $volume"
if ! "$DOCKER" volume inspect "$volume_name" 1>&2 2>/dev/null; then
if echo "$DVB_CREATE_VOLUME" | grep -Eiq 't|true|1|y|yes'; then
"$DOCKER" volume create "$volume_name"
else
echo "Error: no such volume $volume_name" >&2
exit 4
fi
fi
restore "$tarball" "$volume_name"
done
} }
"$@" "$@"

24
test/mock-docker.sh Executable file
View file

@ -0,0 +1,24 @@
#!/usr/bin/env sh
volume() {
if [ "$1" = 'ls' ]; then
echo "DRIVER VOLUME NAME"
echo "local 00c674e3f3c1587d88c2ebf2f91da5843b9dddb3e8df272898bdfd4e596aef79"
echo "local $DOCKER_MOCK_VOLUME"
return 0
elif [ "$2" = 'inspect' ]; then
if [ "$3" = "$DOCKER_MOCK_VOLUME" ]; then
return 0
else
return 1
fi
fi
}
if [ "$1" = "volume" ]; then
"$@"
else
docker "$@"
fi

23
test/test.sh Executable file
View file

@ -0,0 +1,23 @@
#!/usr/bin/env sh
cd "$(dirname "$0")" || exit 1
VOLUME_NAME="$(dd if=/dev/random bs=6 count=1 | base64)"
docker volume create "$VOLUME_NAME"
# shellcheck disable=SC2064
trap "docker volume rm $VOLUME_NAME && rm -f $VOLUME_NAME.tar" EXIT
docker run --rm --volume="$VOLUME_NAME:/data" alpine \
sh -c 'echo "test" > /data/a.txt'
DOCKER=./mock-docker.sh DOCKER_MOCK_VOLUME="$VOLUME_NAME" \
../dvbackup.sh backup_all
stat "$VOLUME_NAME.tar" || exit 1
docker run --rm --volume="$VOLUME_NAME:/data" alpine \
sh -c 'rm /data/a.txt' || exit 1
DOCKER=./mock-docker.sh DOCKER_MOCK_VOLUME="$VOLUME_NAME" \
../dvbackup.sh restore_all "$VOLUME_NAME.tar" || exit 1
docker run --rm --volume="$VOLUME_NAME:/data" alpine \
sh -c 'stat /data/a.txt' || exit 1
exit 0