diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 55e57a9..06f81da 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -17,6 +17,8 @@ jobs: EXCLUDES: '*.jar,exclude_dir' RCON_PATH: /usr/bin/rcon-cli DEBUG: true + permissions: + contents: write services: registry: image: registry:2 @@ -26,11 +28,11 @@ jobs: - name: Checkout uses: actions/checkout@v3 -# Script needs to be cleaned up a lot for shellcheck'ing -# - name: ShellCheck -# uses: ludeeus/action-shellcheck@1.1.0 -# with: -# ignore: tests + # Script needs to be cleaned up a lot for shellcheck'ing + # - name: ShellCheck + # uses: ludeeus/action-shellcheck@1.1.0 + # with: + # ignore: tests - name: Hadolint Action uses: hadolint/hadolint-action@v3.1.0 @@ -66,3 +68,9 @@ jobs: INITIAL_DELAY: 0s RESTIC_PASSWORD: 1234 run: ./tests/test.simple.restic.sh + + - name: Test skip on startup + run: ./tests/skip-on-startup/test.skip-on-startup.sh + + - name: Test preserve manual backups + run: ./tests/preserve-manual-backups/test.preserve-manual-backups.sh diff --git a/README.md b/README.md index 3fcff17..8efccca 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Provides a side-car container to back up [itzg/minecraft-server](https://github. - `BACKUP_ON_STARTUP`=true : Set to false to skip first backup on startup. - `PAUSE_IF_NO_PLAYERS`=false - `PLAYERS_ONLINE_CHECK_INTERVAL`=5m +- `PRESERVE_MANUAL_BACKUPS`=false : Adds a "-preserve" suffix to [on-demand backups](#on-demand-backups) which will be ignored by prune steps - `PRUNE_BACKUPS_DAYS`=7 - `PRUNE_BACKUPS_COUNT`= -disabled unless set (only works with tar/rsync) - `PRUNE_RESTIC_RETENTION`=--keep-within 7d diff --git a/scripts/opt/backup-loop.sh b/scripts/opt/backup-loop.sh index 4d53cde..52314ed 100644 --- a/scripts/opt/backup-loop.sh +++ b/scripts/opt/backup-loop.sh @@ -22,6 +22,7 @@ fi : "${BACKUP_ON_STARTUP:=true}" : "${PAUSE_IF_NO_PLAYERS:=false}" : "${PLAYERS_ONLINE_CHECK_INTERVAL:=5m}" +: "${PRESERVE_MANUAL_BACKUPS:=false}" : "${BACKUP_METHOD:=tar}" # currently one of tar, restic, rsync : "${TAR_COMPRESS_METHOD:=gzip}" # bzip2 gzip zstd : "${ZSTD_PARAMETERS:=-3 --long=25 --single-thread}" @@ -77,6 +78,14 @@ is_one_shot() { fi } +PRESERVE_SUFFIX="-preserve" + +if is_one_shot && [[ "${PRESERVE_MANUAL_BACKUPS^^}" = TRUE ]]; then + suffix="-preserve" +else + suffix="" +fi + is_paused() { [[ -e "${SRC_DIR}/.paused" ]] } @@ -229,11 +238,11 @@ tar() { readarray -td, includes_patterns < <(printf '%s' "${INCLUDES:-.}") _find_old_backups() { - find "${DEST_DIR}" -maxdepth 1 -name "*.${backup_extension}" -mtime "+${PRUNE_BACKUPS_DAYS}" "${@}" + find "${DEST_DIR}" -maxdepth 1 -name "*.${backup_extension}" -not -name "*${PRESERVE_SUFFIX}*" -mtime "+${PRUNE_BACKUPS_DAYS}" "${@}" } _find_extra_backups() { - find "${DEST_DIR}" -maxdepth 1 -name "*.${backup_extension}" -exec ls -NtA {} \+ | \ + find "${DEST_DIR}" -maxdepth 1 -name "*.${backup_extension}" -not -name "*${PRESERVE_SUFFIX}*" -exec ls -NtA {} \+ | \ tail -n +$((PRUNE_BACKUPS_COUNT + 1)) } @@ -263,7 +272,7 @@ tar() { } backup() { ts=$(date +"%Y%m%d-%H%M%S") - outFile="${DEST_DIR}/${BACKUP_NAME}-${ts}.${backup_extension}" + outFile="${DEST_DIR}/${BACKUP_NAME}-${ts}${suffix}.${backup_extension}" log INFO "Backing up content in ${SRC_DIR} to ${outFile}" command tar "${excludes[@]}" "${tar_parameters[@]}" -cf "${outFile}" -C "${SRC_DIR}" "${includes_patterns[@]}" || exitCode=$? if [ ${exitCode:-0} -eq 0 ]; then @@ -275,7 +284,7 @@ tar() { exit 1 fi if [ "${LINK_LATEST^^}" == "TRUE" ]; then - ln -sf "${BACKUP_NAME}-${ts}.${backup_extension}" "${DEST_DIR}/latest.${backup_extension}" + ln -sf "${BACKUP_NAME}-${ts}${suffix}.${backup_extension}" "${DEST_DIR}/latest.${backup_extension}" fi } prune() { diff --git a/tests/preserve-manual-backups/docker-compose.override.yml b/tests/preserve-manual-backups/docker-compose.override.yml new file mode 100644 index 0000000..1e755ef --- /dev/null +++ b/tests/preserve-manual-backups/docker-compose.override.yml @@ -0,0 +1,4 @@ +services: + backup: + environment: + PRESERVE_MANUAL_BACKUPS: true diff --git a/tests/preserve-manual-backups/docker-compose.yml b/tests/preserve-manual-backups/docker-compose.yml new file mode 100644 index 0000000..a8fa3f1 --- /dev/null +++ b/tests/preserve-manual-backups/docker-compose.yml @@ -0,0 +1,35 @@ +services: + mc: + image: itzg/minecraft-server + restart: always + tty: true + stdin_open: true + ports: + - "25565:25565" + environment: + EULA: "TRUE" + ENABLE_AUTOPAUSE: "TRUE" + AUTOPAUSE_TIMEOUT_EST: 30 + volumes: + - "./data:/data" + + backup: + restart: "no" + build: ../../ + depends_on: + mc: + condition: service_healthy + environment: + BACKUP_INTERVAL: "1h" + BACKUP_ON_STARTUP: false # isolate to only ONE_SHOT backups + PRUNE_BACKUPS_DAYS: 1 # create a file 2 days old to get pruned in test + RCON_HOST: mc + INITIAL_DELAY: 0 + PAUSE_IF_NO_PLAYERS: true + deploy: + resources: + limits: + memory: 2G + volumes: + - "./data:/data:ro" + - "./backups:/backups" diff --git a/tests/preserve-manual-backups/test.preserve-manual-backups.sh b/tests/preserve-manual-backups/test.preserve-manual-backups.sh new file mode 100755 index 0000000..6d93f81 --- /dev/null +++ b/tests/preserve-manual-backups/test.preserve-manual-backups.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash + +set -x +WORKDIR="$(realpath "$(dirname "${0}")")" +cd "${WORKDIR}/" || exit + +mkdir -p ./backups +mkdir -p ./data + +cleanup_backups() { + rm -rf ./backups/* +} + +cleanup_all() { + docker compose down + cleanup_backups + rm -rf ./data/* +} + +## Clean up upon failure or reaching the end +trap cleanup_all EXIT + +setup() { + # Set inital exit state to PASS (0) + overall_status=0 + + # Build from current filesystem + echo "Building..." + docker compose build > /dev/null + + echo "Starting server..." + rm -rf ./data/* + docker compose up mc -d > /dev/null +} + +old_timestamp=$(TZ=UTC+96 date +%Y%m%d%H%M) # Two days old + +run_test1(){ + echo -e "\nTest 1: Ensure default behavior, no suffix" + cleanup_backups + + docker compose -f docker-compose.yml config + + docker compose -f docker-compose.yml run --rm --build backup now + preserved_backup_count=$(find backups/ -name "*.tgz" -name "*preserve*" | wc -l) + not_preserved_backup_count=$(find backups/ -name "*.tgz" -not -name "*preserve*" | wc -l) + + tree backups + + echo "Preserved backups: ${preserved_backup_count}" + echo "Not-Preserved backups: ${not_preserved_backup_count}" + + # Ensure typical backup does not have "-preserve" suffix + if [ 1 -eq "$not_preserved_backup_count" ]; then + echo "PASS" + else + echo "FAIL" + overall_status=1 + fi +} + +run_test2(){ + echo -e "\nTest 2: Ensure added suffix and preserved are not pruned" + cleanup_backups + + # Two old backups + touch -t "$old_timestamp" "./backups/fake-backup-preserve.tgz" + touch -t "$old_timestamp" "./backups/fake-backup.tgz" + + # Plus current preserved backup + docker compose -f docker-compose.yml -f docker-compose.override.yml run --rm --build backup now > /dev/null + + preserved_backup_count=$(find backups/ -name "*.tgz" -name "*preserve*" | wc -l) + not_preserved_backup_count=$(find backups/ -name "*.tgz" -not -name "*preserve*" | wc -l) + + tree backups + + echo "Preserved backups: ${preserved_backup_count}" + echo "Not-Preserved backups: ${not_preserved_backup_count}" + + # Ensure there's + # 2 preserved (1 old + 1 new) + # 0 not preserved (1 pruned) + if [[ 2 -eq "$preserved_backup_count" && 0 -eq "$not_preserved_backup_count" ]]; then + echo "PASS" + else + echo "FAIL" + overall_status=1 + fi +} + +setup +run_test1 +run_test2 + +exit $overall_status + diff --git a/tests/skip-on-startup/test.skip-on-startup.sh b/tests/skip-on-startup/test.skip-on-startup.sh index 96c46cd..9d39b20 100755 --- a/tests/skip-on-startup/test.skip-on-startup.sh +++ b/tests/skip-on-startup/test.skip-on-startup.sh @@ -1,10 +1,14 @@ #!/usr/bin/env bash +set -x +WORKDIR="$(realpath "$(dirname "${0}")")" +cd "${WORKDIR}/" || exit + mkdir -p ./backups mkdir -p ./data get_backup_count() { - backup_count=$(ls -1 backups | wc -l) + backup_count=$(find backups/* -prune | wc -l) echo "Output: ${backup_count} backups" } @@ -84,4 +88,3 @@ run_test2 run_test3 exit $overall_status - diff --git a/tests/test.simple.tar.sh b/tests/test.simple.tar.sh index 9fac1d9..28311cd 100755 --- a/tests/test.simple.tar.sh +++ b/tests/test.simple.tar.sh @@ -3,7 +3,7 @@ set -euo pipefail set -x -WORKDIR="$(readlink -m "$(dirname "${0}")")" +WORKDIR="$(realpath "$(dirname "${0}")")" cd "${WORKDIR}/.." @@ -34,7 +34,8 @@ export RCON_PATH export PRUNE_BACKUPS_DAYS mkdir "${EXTRACT_DIR}" -touch -d "$(( PRUNE_BACKUPS_DAYS + 2 )) days ago" "${LOCAL_DEST_DIR}/fake_backup_that_should_be_deleted.tgz" +old_timestamp=$(TZ=UTC+96 date +%Y%m%d%H%M) # Two days old +touch -t "$old_timestamp" "${LOCAL_DEST_DIR}/fake_backup_that_should_be_deleted.tgz" ls -al "${LOCAL_DEST_DIR}" timeout 50 docker run --rm \