commit cb9ec1325aa2656fb525cac4235edee51fcabcc2 Author: Knut Ahlers Date: Sun Jan 22 00:51:06 2023 +0100 Initial version diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e83849d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM bash:5 as prefetch + +ARG DUMB_INIT_VERSION=1.2.5 + +RUN set -ex \ + && apk --no-cache add \ + curl \ + && curl -sSfLo /dumb-init "https://github.com/Yelp/dumb-init/releases/download/v${DUMB_INIT_VERSION}/dumb-init_${DUMB_INIT_VERSION}_x86_64" \ + && chmod 0755 /dumb-init + + +FROM bash:5 + +RUN set -ex \ + && apk --no-cache add \ + coreutils \ + grep \ + openssh \ + rsync + +COPY --from=prefetch /dumb-init /usr/local/bin/ +COPY docker-entrypoint.sh /usr/local/bin/ + +ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..f107b8b --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,197 @@ +#!/usr/local/bin/dumb-init bash +set -euo pipefail + +: ${BASE_DIR:=.} # Where to create the backup dir +: ${EXIT_ON_ERROR:=false} # Exit on backup error (default keep running) +: ${HETZNER_WORKAROUND:=false} # Hetzner StorageBox needs sftp for symlinks +: ${INTERVAL:=3600} # When to backup (3600 = *:00, 1800 = *:00,30) +: ${KEEP_LAST_N:=0} # How many backups to keep +: ${LATEST_LINK:=latest} # How to name the latest link +: ${LOCAL_DIR:=/data} # Where to find the data to backup +: ${NAME_SCHEMA:=%Y-%m-%d_%H-%M-%S} # How to name backup dirs, make sure to make it sortable when using KEEP_LAST_N, do not use spaces +: ${ONESHOT:=false} # Run only once (backup only), set INTERVAL to 1 to execute directly on start +: ${REMOTE_HOST:=} # Where to send the backups +: ${SKIP_RESTORE_ON:=} # File to check, if exists restore will be skipped +: ${SSH_CONFIG_MOUNT:=~/.ssh-dist} # Where to search for ~/.ssh contents to copy into ~/.ssh (Secret mountPath) + +function cleanup_old_backups() { + info "Starting cleanup of backups..." + + for backup in $(ssh "${REMOTE_HOST}" -- ls "${BASE_DIR}" | grep -v "${LATEST_LINK}" | sort | head --lines=-${KEEP_LAST_N}); do + ssh "${REMOTE_HOST}" -- rm -rf "${BASE_DIR}/${backup}" || { + error "Failed to delete backup ${backup}" + return 1 + } + done +} + +function ensure_basedir() { + info "Ensuring base-dir..." + + ssh "${REMOTE_HOST}" -- mkdir -p "${BASE_DIR}" || { + error "Failed to create base-dir." + return 1 + } +} + +function error() { + log E "$@" +} + +function exit_error() { + if [[ $EXIT_ON_ERROR == true ]]; then + fatal "$@" + return 0 + fi + + error "$@" +} + +function fatal() { + log F "$@" + exit 1 +} + +function import_ssh_dir() { + info "Importing ~/.ssh from ${SSH_CONFIG_MOUNT}" + + mkdir -p ~/.ssh || { + error "Failed to create ~/.ssh dir." + return 1 + } + + rsync -a "${SSH_CONFIG_MOUNT}/" ~/.ssh/ + chown $(id -u) ~/.ssh/* + chmod 0600 ~/.ssh/* +} + +function info() { + log I "$@" +} + +function link_latest() { + info "Creating latest link..." + + local dest="$1" + local link="$2" + + if [[ $HETZNER_WORKAROUND == true ]]; then + echo -e "rm ${link}\nsymlink ${dest} ${link}" | sftp "${REMOTE_HOST}" && return 0 || { + error "Renewing latest-link (sftp)." + return 1 + } + fi + + ssh "${REMOTE_HOST}" -- ln -sf "${dest}" "${link}" || { + error "Renewing latest-link (ssh)." + return 1 + } +} + +function log() { + local level=$1 + shift + echo "[$(date +%H:%M:%S)][$level] $@" >&2 +} + +function main() { + [[ -n $REMOTE_HOST ]] || fatal "No REMOTE_HOST set" + + if [[ -d $SSH_CONFIG_MOUNT ]]; then + import_ssh_dir || fatal "Failed importing SSH config." + fi + + case "${1:-help}" in + backup) + while true; do + sleep $((INTERVAL - $(date +%s) % INTERVAL)) + run_backup || exit_error "Backup failed." + + if [ $KEEP_LAST_N -gt 0 ]; then + cleanup_old_backups + fi + + [[ $ONESHOT != true ]] || { + info "ONESHOT activated, exit now" + break + } + done + ;; + + restore) + run_restore || exit_error "Restore failed." + ;; + + *) + usage + fatal "Action ${1:-help} called" + ;; + esac +} + +function run_backup() { + local current="$(date +${NAME_SCHEMA})" + local dest="${BASE_DIR}/${current}" + local link="${BASE_DIR}/${LATEST_LINK}" + + info "Starting backup..." + + ensure_basedir || { + error "Failed to ensure base-dir." + return 1 + } + + info "Synchronizing backup..." + rsync -av --delete \ + "${LOCAL_DIR}/" \ + --link-dest "../${LATEST_LINK}/" \ + "${REMOTE_HOST}:${dest}/" || { + error "Failed to sync backup-dir." + return 1 + } + + link_latest "${current}" "${link}" || { + error "Failed to create latest link." + return 1 + } + + info "Backup finished." +} + +function run_restore() { + if [[ -n $SKIP_RESTORE_ON ]] && [[ -e $SKIP_RESTORE_ON ]]; then + info "Check-file ${SKIP_RESTORE_ON} exists, skipping restore." + return 0 + fi + + local link="${BASE_DIR}/${LATEST_LINK}" + + info "Starting restore..." + + mkdir -p "${LOCAL_DIR}" || { + error "Failed to ensure local dir..." + return 1 + } + + rsync -av --delete \ + "${REMOTE_HOST}:${link}/" \ + "${LOCAL_DIR}/" || { + error "Failed to sync remote data..." + return 1 + } + + info "Restore finished." +} + +function usage() { + cat >&2 < +EOF + +} + +main "$@"