mirror of
https://github.com/offen/docker-volume-backup.git
synced 2025-12-05 17:18:02 +01:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9ebb9e14e | ||
|
|
6e1b8553e6 | ||
|
|
5ec2b2c3ff | ||
|
|
3bbeba5b83 | ||
|
|
9155b4d130 | ||
|
|
2a17e84ab6 | ||
|
|
00f2359461 | ||
|
|
0504a92a1f | ||
|
|
3ded77448c |
@@ -14,7 +14,7 @@ FROM alpine:3.15
|
|||||||
|
|
||||||
WORKDIR /root
|
WORKDIR /root
|
||||||
|
|
||||||
RUN apk add --update ca-certificates
|
RUN apk add --no-cache ca-certificates
|
||||||
|
|
||||||
COPY --from=builder /app/cmd/backup/backup /usr/bin/backup
|
COPY --from=builder /app/cmd/backup/backup /usr/bin/backup
|
||||||
|
|
||||||
|
|||||||
180
README.md
180
README.md
@@ -16,16 +16,19 @@ It handles __recurring or one-off backups of Docker volumes__ to a __local direc
|
|||||||
- [One-off backups using Docker CLI](#one-off-backups-using-docker-cli)
|
- [One-off backups using Docker CLI](#one-off-backups-using-docker-cli)
|
||||||
- [Configuration reference](#configuration-reference)
|
- [Configuration reference](#configuration-reference)
|
||||||
- [How to](#how-to)
|
- [How to](#how-to)
|
||||||
- [Stopping containers during backup](#stopping-containers-during-backup)
|
- [Stop containers during backup](#stop-containers-during-backup)
|
||||||
- [Automatically pruning old backups](#automatically-pruning-old-backups)
|
- [Automatically pruning old backups](#automatically-pruning-old-backups)
|
||||||
- [Send email notifications on failed backup runs](#send-email-notifications-on-failed-backup-runs)
|
- [Send email notifications on failed backup runs](#send-email-notifications-on-failed-backup-runs)
|
||||||
- [Customize notifications](#customize-notifications)
|
- [Customize notifications](#customize-notifications)
|
||||||
|
- [Run custom commands before / after backup](#run-custom-commands-before--after-backup)
|
||||||
- [Encrypting your backup using GPG](#encrypting-your-backup-using-gpg)
|
- [Encrypting your backup using GPG](#encrypting-your-backup-using-gpg)
|
||||||
- [Restoring a volume from a backup](#restoring-a-volume-from-a-backup)
|
- [Restoring a volume from a backup](#restoring-a-volume-from-a-backup)
|
||||||
- [Set the timezone the container runs in](#set-the-timezone-the-container-runs-in)
|
- [Set the timezone the container runs in](#set-the-timezone-the-container-runs-in)
|
||||||
- [Using with Docker Swarm](#using-with-docker-swarm)
|
- [Using with Docker Swarm](#using-with-docker-swarm)
|
||||||
- [Manually triggering a backup](#manually-triggering-a-backup)
|
- [Manually triggering a backup](#manually-triggering-a-backup)
|
||||||
- [Update deprecated email configuration](#update-deprecated-email-configuration)
|
- [Update deprecated email configuration](#update-deprecated-email-configuration)
|
||||||
|
- [Using a custom Docker host](#using-a-custom-docker-host)
|
||||||
|
- [Run multiple backup schedules in the same container](#run-multiple-backup-schedules-in-the-same-container)
|
||||||
- [Recipes](#recipes)
|
- [Recipes](#recipes)
|
||||||
- [Backing up to AWS S3](#backing-up-to-aws-s3)
|
- [Backing up to AWS S3](#backing-up-to-aws-s3)
|
||||||
- [Backing up to Filebase](#backing-up-to-filebase)
|
- [Backing up to Filebase](#backing-up-to-filebase)
|
||||||
@@ -36,6 +39,7 @@ It handles __recurring or one-off backups of Docker volumes__ to a __local direc
|
|||||||
- [Running on a custom cron schedule](#running-on-a-custom-cron-schedule)
|
- [Running on a custom cron schedule](#running-on-a-custom-cron-schedule)
|
||||||
- [Rotating away backups that are older than 7 days](#rotating-away-backups-that-are-older-than-7-days)
|
- [Rotating away backups that are older than 7 days](#rotating-away-backups-that-are-older-than-7-days)
|
||||||
- [Encrypting your backups using GPG](#encrypting-your-backups-using-gpg)
|
- [Encrypting your backups using GPG](#encrypting-your-backups-using-gpg)
|
||||||
|
- [Using mysqldump to prepare the backup](#using-mysqldump-to-prepare-the-backup)
|
||||||
- [Running multiple instances in the same setup](#running-multiple-instances-in-the-same-setup)
|
- [Running multiple instances in the same setup](#running-multiple-instances-in-the-same-setup)
|
||||||
- [Differences to `futurice/docker-volume-backup`](#differences-to-futuricedocker-volume-backup)
|
- [Differences to `futurice/docker-volume-backup`](#differences-to-futuricedocker-volume-backup)
|
||||||
|
|
||||||
@@ -278,6 +282,27 @@ You can populate below template according to your requirements and use it as you
|
|||||||
|
|
||||||
# BACKUP_STOP_CONTAINER_LABEL="service1"
|
# BACKUP_STOP_CONTAINER_LABEL="service1"
|
||||||
|
|
||||||
|
########### EXECUTING COMMANDS IN CONTAINERS PRE/POST BACKUP
|
||||||
|
|
||||||
|
# It is possible to define commands to be run in any container before and after
|
||||||
|
# a backup is conducted. The commands themselves are defined in labels like
|
||||||
|
# `docker-volume-backup.exec-pre=/bin/sh -c 'mysqldump [options] > dump.sql'.
|
||||||
|
# Several options exist for controlling this feature:
|
||||||
|
|
||||||
|
# By default, any output of such a command is suppressed. If this value
|
||||||
|
# is configured to be "true", command execution output will be forwarded to
|
||||||
|
# the backup container's stdout and stderr.
|
||||||
|
|
||||||
|
# EXEC_FORWARD_OUTPUT="true"
|
||||||
|
|
||||||
|
# Without any further configuration, all commands defined in labels will be
|
||||||
|
# run before and after a backup. If you need more fine grained control, you
|
||||||
|
# can use this option to set a label that will be used for narrowing down
|
||||||
|
# the set of eligible containers. When set, an eligible container will also need
|
||||||
|
# to be labeled as `docker-volume-backup.exec-label=database`.
|
||||||
|
|
||||||
|
# EXEC_LABEL="database"
|
||||||
|
|
||||||
########### NOTIFICATIONS
|
########### NOTIFICATIONS
|
||||||
|
|
||||||
# Notifications (email, Slack, etc.) can be sent out when a backup run finishes.
|
# Notifications (email, Slack, etc.) can be sent out when a backup run finishes.
|
||||||
@@ -298,6 +323,13 @@ You can populate below template according to your requirements and use it as you
|
|||||||
|
|
||||||
# NOTIFICATION_LEVEL="error"
|
# NOTIFICATION_LEVEL="error"
|
||||||
|
|
||||||
|
########### DOCKER HOST
|
||||||
|
|
||||||
|
# If you are interfacing with Docker via TCP you can set the Docker host here
|
||||||
|
# instead of mounting the Docker socket as a volume. This is unset by default.
|
||||||
|
|
||||||
|
# DOCKER_HOST="tcp://docker_socket_proxy:2375"
|
||||||
|
|
||||||
########### EMAIL NOTIFICATIONS
|
########### EMAIL NOTIFICATIONS
|
||||||
|
|
||||||
# ************************************************************************
|
# ************************************************************************
|
||||||
@@ -336,7 +368,7 @@ You can work around this by either updating `docker-compose` or unquoting your c
|
|||||||
|
|
||||||
## How to
|
## How to
|
||||||
|
|
||||||
### Stopping containers during backup
|
### Stop containers during backup
|
||||||
|
|
||||||
In many cases, it will be desirable to stop the services that are consuming the volume you want to backup in order to ensure data integrity.
|
In many cases, it will be desirable to stop the services that are consuming the volume you want to backup in order to ensure data integrity.
|
||||||
This image can automatically stop and restart containers and services (in case you are running Docker in Swarm mode).
|
This image can automatically stop and restart containers and services (in case you are running Docker in Swarm mode).
|
||||||
@@ -436,6 +468,63 @@ Overridable template names are: `title_success`, `body_success`, `title_failure`
|
|||||||
|
|
||||||
For a full list of available variables and functions, see [this page](https://github.com/offen/docker-volume-backup/blob/master/docs/NOTIFICATION-TEMPLATES.md).
|
For a full list of available variables and functions, see [this page](https://github.com/offen/docker-volume-backup/blob/master/docs/NOTIFICATION-TEMPLATES.md).
|
||||||
|
|
||||||
|
### Run custom commands before / after backup
|
||||||
|
|
||||||
|
In certain scenarios it can be required to run specific commands before and after a backup is taken (e.g. dumping a database).
|
||||||
|
When mounting the Docker socket into the `docker-volume-backup` container, you can define pre- and post-commands that will be run in the context of the target container.
|
||||||
|
Such commands are defined by specifying the command in a `docker-volume-backup.exec-[pre|post]` label.
|
||||||
|
|
||||||
|
Taking a database dump using `mysqldump` would look like this:
|
||||||
|
|
||||||
|
```yml
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# ... define other services using the `data` volume here
|
||||||
|
database:
|
||||||
|
image: mariadb
|
||||||
|
volumes:
|
||||||
|
- backup_data:/tmp/backups
|
||||||
|
labels:
|
||||||
|
- docker-volume-backup.exec-pre=/bin/sh -c 'mysqldump --all-databases > /backups/dump.sql'
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
backup_data:
|
||||||
|
```
|
||||||
|
|
||||||
|
Due to Docker limitations, you currently cannot use any kind of redirection in these commands unless you pass the command to `/bin/sh -c` or similar.
|
||||||
|
I.e. instead of using `echo "ok" > ok.txt` you will need to use `/bin/sh -c 'echo "ok" > ok.txt'`.
|
||||||
|
|
||||||
|
If you need fine grained control about which container's commands are run, you can use the `EXEC_LABEL` configuration on your `docker-volume-backup` container:
|
||||||
|
|
||||||
|
```yml
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
database:
|
||||||
|
image: mariadb
|
||||||
|
volumes:
|
||||||
|
- backup_data:/tmp/backups
|
||||||
|
labels:
|
||||||
|
- docker-volume-backup.exec-pre=/bin/sh -c 'mysqldump --all-databases > /tmp/volume/dump.sql'
|
||||||
|
- docker-volume-backup.exec-label=database
|
||||||
|
|
||||||
|
backup:
|
||||||
|
image: offen/docker-volume-backup:latest
|
||||||
|
environment:
|
||||||
|
EXEC_LABEL: database
|
||||||
|
volumes:
|
||||||
|
- data:/backup/dump:ro
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
backup_data:
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
The backup procedure is guaranteed to wait for all `pre` commands to finish.
|
||||||
|
However there are no guarantees about the order in which they are run, which could also happen concurrently.
|
||||||
|
|
||||||
### Encrypting your backup using GPG
|
### Encrypting your backup using GPG
|
||||||
|
|
||||||
The image supports encrypting backups using GPG out of the box.
|
The image supports encrypting backups using GPG out of the box.
|
||||||
@@ -467,6 +556,26 @@ In case you need to restore a volume from a backup, the most straight forward pr
|
|||||||
|
|
||||||
Depending on your setup and the application(s) you are running, this might involve other steps to be taken still.
|
Depending on your setup and the application(s) you are running, this might involve other steps to be taken still.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
If you want to rollback an entire volume to an earlier backup snapshot (recommended for database volumes):
|
||||||
|
|
||||||
|
- Trigger a manual backup if necessary (see `Manually triggering a backup`).
|
||||||
|
- Stop the container(s) that are using the volume.
|
||||||
|
- If volume was initially created using docker-compose, find out exact volume name using:
|
||||||
|
```console
|
||||||
|
docker volume ls
|
||||||
|
```
|
||||||
|
- Remove existing volume (the example assumes it's named `data`):
|
||||||
|
```console
|
||||||
|
docker volume rm data
|
||||||
|
```
|
||||||
|
- Create new volume with the same name and restore a snapshot:
|
||||||
|
```console
|
||||||
|
docker run --rm -it -v data:/backup/my-app-backup -v /path/to/local_backups:/archive:ro alpine tar -xvzf /archive/full_backup_filename.tar.gz
|
||||||
|
```
|
||||||
|
- Restart the container(s) that are using the volume.
|
||||||
|
|
||||||
### Set the timezone the container runs in
|
### Set the timezone the container runs in
|
||||||
|
|
||||||
By default a container based on this image will run in the UTC timezone.
|
By default a container based on this image will run in the UTC timezone.
|
||||||
@@ -536,6 +645,41 @@ After:
|
|||||||
NOTIFICATION_URLS=smtp://me:secret@posteo.de:587/?fromAddress=no-reply@example.com&toAddresses=you@example.com
|
NOTIFICATION_URLS=smtp://me:secret@posteo.de:587/?fromAddress=no-reply@example.com&toAddresses=you@example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Using a custom Docker host
|
||||||
|
|
||||||
|
If you are interfacing with Docker via TCP, set `DOCKER_HOST` to the correct URL.
|
||||||
|
```ini
|
||||||
|
DOCKER_HOST=tcp://docker_socket_proxy:2375
|
||||||
|
```
|
||||||
|
|
||||||
|
In case you are using a socket proxy, it must support `GET` and `POST` requests to the `/containers` endpoint. If you are using Docker Swarm, it must also support the `/services` endpoint. If you are using pre/post backup commands, it must also support the `/exec` endpoint.
|
||||||
|
|
||||||
|
### Run multiple backup schedules in the same container
|
||||||
|
|
||||||
|
Multiple backup schedules with different configuration can be configured by mounting an arbitrary number of configuration files (using the `.env` format) into `/etc/dockervolumebackup/conf.d`:
|
||||||
|
|
||||||
|
```
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# ... define other services using the `data` volume here
|
||||||
|
backup:
|
||||||
|
image: offen/docker-volume-backup:latest
|
||||||
|
volumes:
|
||||||
|
- data:/backup/my-app-backup:ro
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- ./configuration:/etc/dockervolumebackup/conf.d
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
data:
|
||||||
|
```
|
||||||
|
|
||||||
|
A separate cronjob will be created for each config file.
|
||||||
|
If a configuration value is set both in the global environment as well as in the config file, the config file will take precedence.
|
||||||
|
The `backup` command expects to run on an exclusive lock, so it is your responsibility to make sure the invocations do not overlap.
|
||||||
|
In case you need your schedules to overlap, you need to create a dedicated container for each schedule instead.
|
||||||
|
When changing the configuration, you currently need to manually restart the container for the changes to take effect.
|
||||||
|
|
||||||
## Recipes
|
## Recipes
|
||||||
|
|
||||||
This section lists configuration for some real-world use cases that you can mix and match according to your needs.
|
This section lists configuration for some real-world use cases that you can mix and match according to your needs.
|
||||||
@@ -739,6 +883,32 @@ volumes:
|
|||||||
data:
|
data:
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Using mysqldump to prepare the backup
|
||||||
|
|
||||||
|
```yml
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
database:
|
||||||
|
image: mariadb:latest
|
||||||
|
labels:
|
||||||
|
- docker-volume-backup.exec-pre=/bin/sh -c 'mysqldump -psecret --all-databases > /tmp/dumps/dump.sql'
|
||||||
|
volumes:
|
||||||
|
- app_data:/tmp/dumps
|
||||||
|
backup:
|
||||||
|
image: offen/docker-volume-backup:latest
|
||||||
|
environment:
|
||||||
|
BACKUP_FILENAME: db.tar.gz
|
||||||
|
BACKUP_CRON_EXPRESSION: "0 2 * * *"
|
||||||
|
volumes:
|
||||||
|
- ./local:/archive
|
||||||
|
- data:/backup/data:ro
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
data:
|
||||||
|
```
|
||||||
|
|
||||||
### Running multiple instances in the same setup
|
### Running multiple instances in the same setup
|
||||||
|
|
||||||
```yml
|
```yml
|
||||||
@@ -780,12 +950,12 @@ This image is heavily inspired by `futurice/docker-volume-backup`. We decided to
|
|||||||
|
|
||||||
- The original image is based on `ubuntu` and requires additional tools, making it heavy.
|
- The original image is based on `ubuntu` and requires additional tools, making it heavy.
|
||||||
This version is roughly 1/25 in compressed size (it's ~12MB).
|
This version is roughly 1/25 in compressed size (it's ~12MB).
|
||||||
- The original image uses a shell script, when this version is written in Go, which makes it easier to extend and maintain (more verbose too).
|
- The original image uses a shell script, when this version is written in Go.
|
||||||
- The original image proposed to handle backup rotation through AWS S3 lifecycle policies.
|
- The original image proposed to handle backup rotation through AWS S3 lifecycle policies.
|
||||||
This image adds the option to rotate away old backups through the same command so this functionality can also be offered for non-AWS storage backends like MinIO.
|
This image adds the option to rotate away old backups through the same command so this functionality can also be offered for non-AWS storage backends like MinIO.
|
||||||
Local copies of backups can also be pruned once they reach a certain age.
|
Local copies of backups can also be pruned once they reach a certain age.
|
||||||
- InfluxDB specific functionality from the original image was removed.
|
- InfluxDB specific functionality from the original image was removed.
|
||||||
- `arm64` and `arm/v7` architectures are supported.
|
- `arm64` and `arm/v7` architectures are supported.
|
||||||
- Docker in Swarm mode is supported.
|
- Docker in Swarm mode is supported.
|
||||||
- Notifications on failed backups are supported
|
- Notifications on finished backups are supported.
|
||||||
- IAM authentication through instance profiles is supported
|
- IAM authentication through instance profiles is supported.
|
||||||
|
|||||||
142
cmd/backup/archive.go
Normal file
142
cmd/backup/archive.go
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
// Copyright 2022 - Offen Authors <hioffen@posteo.de>
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
// Portions of this file are taken from package `targz`, Copyright (c) 2014 Fredrik Wallgren
|
||||||
|
// Licensed under the MIT License: https://github.com/walle/targz/blob/57fe4206da5abf7dd3901b4af3891ec2f08c7b08/LICENSE
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"compress/gzip"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func createArchive(inputFilePath, outputFilePath string) error {
|
||||||
|
inputFilePath = stripTrailingSlashes(inputFilePath)
|
||||||
|
inputFilePath, outputFilePath, err := makeAbsolute(inputFilePath, outputFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("createArchive: error transposing given file paths: %w", err)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(outputFilePath), 0755); err != nil {
|
||||||
|
return fmt.Errorf("createArchive: error creating output file path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := compress(inputFilePath, outputFilePath, filepath.Dir(inputFilePath)); err != nil {
|
||||||
|
return fmt.Errorf("createArchive: error creating archive: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func stripTrailingSlashes(path string) string {
|
||||||
|
if len(path) > 0 && path[len(path)-1] == '/' {
|
||||||
|
path = path[0 : len(path)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeAbsolute(inputFilePath, outputFilePath string) (string, string, error) {
|
||||||
|
inputFilePath, err := filepath.Abs(inputFilePath)
|
||||||
|
if err == nil {
|
||||||
|
outputFilePath, err = filepath.Abs(outputFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputFilePath, outputFilePath, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func compress(inPath, outFilePath, subPath string) error {
|
||||||
|
file, err := os.Create(outFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("compress: error creating out file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix := path.Dir(outFilePath)
|
||||||
|
gzipWriter := gzip.NewWriter(file)
|
||||||
|
tarWriter := tar.NewWriter(gzipWriter)
|
||||||
|
|
||||||
|
var paths []string
|
||||||
|
if err := filepath.WalkDir(inPath, func(path string, di fs.DirEntry, err error) error {
|
||||||
|
paths = append(paths, path)
|
||||||
|
return err
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("compress: error walking filesystem tree: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range paths {
|
||||||
|
if err := writeTarGz(p, tarWriter, prefix); err != nil {
|
||||||
|
return fmt.Errorf("compress error writing %s to archive: %w", p, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tarWriter.Close()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("compress: error closing tar writer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = gzipWriter.Close()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("compress: error closing gzip writer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = file.Close()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("compress: error closing file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeTarGz(path string, tarWriter *tar.Writer, prefix string) error {
|
||||||
|
fileInfo, err := os.Lstat(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("writeTarGz: error getting file infor for %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileInfo.Mode()&os.ModeSocket == os.ModeSocket {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var link string
|
||||||
|
if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink {
|
||||||
|
var err error
|
||||||
|
if link, err = os.Readlink(path); err != nil {
|
||||||
|
return fmt.Errorf("writeTarGz: error resolving symlink %s: %w", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
header, err := tar.FileInfoHeader(fileInfo, link)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("writeTarGz: error getting file info header: %w", err)
|
||||||
|
}
|
||||||
|
header.Name = strings.TrimPrefix(path, prefix)
|
||||||
|
|
||||||
|
err = tarWriter.WriteHeader(header)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("writeTarGz: error writing file info header: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fileInfo.Mode().IsRegular() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("writeTarGz: error opening %s: %w", path, err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(tarWriter, file)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("writeTarGz: error copying %s to tar writer: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -39,4 +39,6 @@ type Config struct {
|
|||||||
WebdavPath string `split_words:"true" default:"/"`
|
WebdavPath string `split_words:"true" default:"/"`
|
||||||
WebdavUsername string `split_words:"true"`
|
WebdavUsername string `split_words:"true"`
|
||||||
WebdavPassword string `split_words:"true"`
|
WebdavPassword string `split_words:"true"`
|
||||||
|
ExecLabel string `split_words:"true"`
|
||||||
|
ExecForwardOutput bool `split_words:"true"`
|
||||||
}
|
}
|
||||||
|
|||||||
122
cmd/backup/exec.go
Normal file
122
cmd/backup/exec.go
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
// Copyright 2022 - Offen Authors <hioffen@posteo.de>
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/cosiner/argv"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/docker/docker/pkg/stdcopy"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *script) exec(containerRef string, command string) ([]byte, []byte, error) {
|
||||||
|
args, _ := argv.Argv(command, nil, nil)
|
||||||
|
execID, err := s.cli.ContainerExecCreate(context.Background(), containerRef, types.ExecConfig{
|
||||||
|
Cmd: args[0],
|
||||||
|
AttachStdin: true,
|
||||||
|
AttachStderr: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("exec: error creating container exec: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := s.cli.ContainerExecAttach(context.Background(), execID.ID, types.ExecStartCheck{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("exec: error attaching container exec: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Close()
|
||||||
|
|
||||||
|
var outBuf, errBuf bytes.Buffer
|
||||||
|
outputDone := make(chan error)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
_, err := stdcopy.StdCopy(&outBuf, &errBuf, resp.Reader)
|
||||||
|
outputDone <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-outputDone:
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("exec: error demultiplexing output: %w", err)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout, err := ioutil.ReadAll(&outBuf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("exec: error reading stdout: %w", err)
|
||||||
|
}
|
||||||
|
stderr, err := ioutil.ReadAll(&errBuf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("exec: error reading stderr: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := s.cli.ContainerExecInspect(context.Background(), execID.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("exec: error inspecting container exec: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.ExitCode > 0 {
|
||||||
|
return stdout, stderr, fmt.Errorf("exec: running command exited %d", res.ExitCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return stdout, stderr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *script) runLabeledCommands(label string) error {
|
||||||
|
f := []filters.KeyValuePair{
|
||||||
|
{Key: "label", Value: label},
|
||||||
|
}
|
||||||
|
if s.c.ExecLabel != "" {
|
||||||
|
f = append(f, filters.KeyValuePair{
|
||||||
|
Key: "label",
|
||||||
|
Value: fmt.Sprintf("docker-volume-backup.exec-label=%s", s.c.ExecLabel),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
containersWithCommand, err := s.cli.ContainerList(context.Background(), types.ContainerListOptions{
|
||||||
|
Quiet: true,
|
||||||
|
Filters: filters.NewArgs(f...),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("runLabeledCommands: error querying for containers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(containersWithCommand) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
wg.Add(len(containersWithCommand))
|
||||||
|
|
||||||
|
var cmdErrors []error
|
||||||
|
for _, container := range containersWithCommand {
|
||||||
|
go func(c types.Container) {
|
||||||
|
cmd, _ := c.Labels[label]
|
||||||
|
s.logger.Infof("Running %s command %s for container %s", label, cmd, strings.TrimPrefix(c.Names[0], "/"))
|
||||||
|
stdout, stderr, err := s.exec(c.ID, cmd)
|
||||||
|
if err != nil {
|
||||||
|
cmdErrors = append(cmdErrors, err)
|
||||||
|
}
|
||||||
|
if s.c.ExecForwardOutput {
|
||||||
|
os.Stderr.Write(stderr)
|
||||||
|
os.Stdout.Write(stdout)
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}(container)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
if len(cmdErrors) != 0 {
|
||||||
|
return join(cmdErrors...)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -38,6 +38,13 @@ func main() {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
s.must(func() error {
|
s.must(func() error {
|
||||||
|
runPostCommands, err := s.runCommands()
|
||||||
|
defer func() {
|
||||||
|
s.must(runPostCommands())
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
restartContainers, err := s.stopContainers()
|
restartContainers, err := s.stopContainers()
|
||||||
// The mechanism for restarting containers is not using hooks as it
|
// The mechanism for restarting containers is not using hooks as it
|
||||||
// should happen as soon as possible (i.e. before uploading backups or
|
// should happen as soon as possible (i.e. before uploading backups or
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import (
|
|||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/kelseyhightower/envconfig"
|
"github.com/kelseyhightower/envconfig"
|
||||||
"github.com/leekchan/timeutil"
|
"github.com/leekchan/timeutil"
|
||||||
"github.com/m90/targz"
|
|
||||||
"github.com/minio/minio-go/v7"
|
"github.com/minio/minio-go/v7"
|
||||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||||
"github.com/otiai10/copy"
|
"github.com/otiai10/copy"
|
||||||
@@ -213,6 +212,22 @@ func newScript() (*script, error) {
|
|||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *script) runCommands() (func() error, error) {
|
||||||
|
if s.cli == nil {
|
||||||
|
return noop, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.runLabeledCommands("docker-volume-backup.exec-pre"); err != nil {
|
||||||
|
return noop, fmt.Errorf("runCommands: error running pre commands: %w", err)
|
||||||
|
}
|
||||||
|
return func() error {
|
||||||
|
if err := s.runLabeledCommands("docker-volume-backup.exec-post"); err != nil {
|
||||||
|
return fmt.Errorf("runCommands: error running post commands: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// stopContainers stops all Docker containers that are marked as to being
|
// stopContainers stops all Docker containers that are marked as to being
|
||||||
// stopped during the backup and returns a function that can be called to
|
// stopped during the backup and returns a function that can be called to
|
||||||
// restart everything that has been stopped.
|
// restart everything that has been stopped.
|
||||||
@@ -364,7 +379,7 @@ func (s *script) takeBackup() error {
|
|||||||
s.logger.Infof("Removed tar file `%s`.", tarFile)
|
s.logger.Infof("Removed tar file `%s`.", tarFile)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err := targz.Compress(backupSources, tarFile); err != nil {
|
if err := createArchive(backupSources, tarFile); err != nil {
|
||||||
return fmt.Errorf("takeBackup: error compressing backup folder: %w", err)
|
return fmt.Errorf("takeBackup: error compressing backup folder: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,21 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
if [ ! -d "/etc/dockervolumebackup/conf.d" ]; then
|
||||||
BACKUP_CRON_EXPRESSION="${BACKUP_CRON_EXPRESSION:-@daily}"
|
BACKUP_CRON_EXPRESSION="${BACKUP_CRON_EXPRESSION:-@daily}"
|
||||||
|
|
||||||
echo "Installing cron.d entry with expression $BACKUP_CRON_EXPRESSION."
|
echo "Installing cron.d entry with expression $BACKUP_CRON_EXPRESSION."
|
||||||
echo "$BACKUP_CRON_EXPRESSION backup 2>&1" | crontab -
|
echo "$BACKUP_CRON_EXPRESSION backup 2>&1" | crontab -
|
||||||
|
else
|
||||||
|
echo "/etc/dockervolumebackup/conf.d was found, using configuration files from this directory."
|
||||||
|
|
||||||
|
for file in /etc/dockervolumebackup/conf.d/*; do
|
||||||
|
source $file
|
||||||
|
BACKUP_CRON_EXPRESSION="${BACKUP_CRON_EXPRESSION:-@daily}"
|
||||||
|
echo "Appending cron.d entry with expression $BACKUP_CRON_EXPRESSION and configuration file $file"
|
||||||
|
(crontab -l; echo "$BACKUP_CRON_EXPRESSION /bin/sh -c 'set -a; source $file; set +a && backup' 2>&1") | crontab -
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Starting cron in foreground."
|
echo "Starting cron in foreground."
|
||||||
crond -f -l 8
|
crond -f -l 8
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -4,11 +4,11 @@ go 1.17
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/containrrr/shoutrrr v0.5.2
|
github.com/containrrr/shoutrrr v0.5.2
|
||||||
|
github.com/cosiner/argv v0.1.0
|
||||||
github.com/docker/docker v20.10.11+incompatible
|
github.com/docker/docker v20.10.11+incompatible
|
||||||
github.com/gofrs/flock v0.8.1
|
github.com/gofrs/flock v0.8.1
|
||||||
github.com/kelseyhightower/envconfig v1.4.0
|
github.com/kelseyhightower/envconfig v1.4.0
|
||||||
github.com/leekchan/timeutil v0.0.0-20150802142658-28917288c48d
|
github.com/leekchan/timeutil v0.0.0-20150802142658-28917288c48d
|
||||||
github.com/m90/targz v0.0.0-20220208141135-d3baeef59a97
|
|
||||||
github.com/minio/minio-go/v7 v7.0.16
|
github.com/minio/minio-go/v7 v7.0.16
|
||||||
github.com/otiai10/copy v1.7.0
|
github.com/otiai10/copy v1.7.0
|
||||||
github.com/sirupsen/logrus v1.8.1
|
github.com/sirupsen/logrus v1.8.1
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -208,6 +208,8 @@ github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+
|
|||||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||||
|
github.com/cosiner/argv v0.1.0 h1:BVDiEL32lwHukgJKP87btEPenzrrHUjajs/8yzaqcXg=
|
||||||
|
github.com/cosiner/argv v0.1.0/go.mod h1:EusR6TucWKX+zFgtdUsKT2Cvg45K5rtpCcWz4hK06d8=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||||
@@ -450,8 +452,6 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|||||||
github.com/leekchan/timeutil v0.0.0-20150802142658-28917288c48d h1:2puqoOQwi3Ai1oznMOsFIbifm6kIfJaLLyYzWD4IzTs=
|
github.com/leekchan/timeutil v0.0.0-20150802142658-28917288c48d h1:2puqoOQwi3Ai1oznMOsFIbifm6kIfJaLLyYzWD4IzTs=
|
||||||
github.com/leekchan/timeutil v0.0.0-20150802142658-28917288c48d/go.mod h1:hO90vCP2x3exaSH58BIAowSKvV+0OsY21TtzuFGHON4=
|
github.com/leekchan/timeutil v0.0.0-20150802142658-28917288c48d/go.mod h1:hO90vCP2x3exaSH58BIAowSKvV+0OsY21TtzuFGHON4=
|
||||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||||
github.com/m90/targz v0.0.0-20220208141135-d3baeef59a97 h1:Uc/WzUKI/zvhkqIzk5TyaPE6AY1SD1DWGc7RV7cky4s=
|
|
||||||
github.com/m90/targz v0.0.0-20220208141135-d3baeef59a97/go.mod h1:YZK3bSO/oVlk9G+v00BxgzxW2Us4p/R4ysHOBjk0fJI=
|
|
||||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ docker run --rm \
|
|||||||
--env BACKUP_FILENAME=test.tar.gz \
|
--env BACKUP_FILENAME=test.tar.gz \
|
||||||
--env "BACKUP_FROM_SNAPSHOT=true" \
|
--env "BACKUP_FROM_SNAPSHOT=true" \
|
||||||
--entrypoint backup \
|
--entrypoint backup \
|
||||||
offen/docker-volume-backup:$TEST_VERSION
|
offen/docker-volume-backup:${TEST_VERSION:-canary}
|
||||||
|
|
||||||
docker run --rm -it \
|
docker run --rm -it \
|
||||||
-v backup_data:/data alpine \
|
-v backup_data:/data alpine \
|
||||||
|
|||||||
1
test/commands/.gitignore
vendored
Normal file
1
test/commands/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
local
|
||||||
36
test/commands/docker-compose.yml
Normal file
36
test/commands/docker-compose.yml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
database:
|
||||||
|
image: mariadb:10.7
|
||||||
|
deploy:
|
||||||
|
restart_policy:
|
||||||
|
condition: on-failure
|
||||||
|
environment:
|
||||||
|
MARIADB_ROOT_PASSWORD: test
|
||||||
|
MARIADB_DATABASE: backup
|
||||||
|
labels:
|
||||||
|
- docker-volume-backup.exec-pre=/bin/sh -c 'mysqldump -ptest --all-databases > /tmp/volume/dump.sql'
|
||||||
|
- docker-volume-backup.exec-post=/bin/sh -c 'echo "post" > /tmp/volume/post.txt'
|
||||||
|
- docker-volume-backup.exec-label=test
|
||||||
|
volumes:
|
||||||
|
- app_data:/tmp/volume
|
||||||
|
|
||||||
|
backup:
|
||||||
|
image: offen/docker-volume-backup:${TEST_VERSION:-canary}
|
||||||
|
deploy:
|
||||||
|
restart_policy:
|
||||||
|
condition: on-failure
|
||||||
|
environment:
|
||||||
|
BACKUP_FILENAME: test.tar.gz
|
||||||
|
BACKUP_CRON_EXPRESSION: 0 0 5 31 2 ?
|
||||||
|
EXEC_LABEL: test
|
||||||
|
EXEC_FORWARD_OUTPUT: "true"
|
||||||
|
volumes:
|
||||||
|
- archive:/archive
|
||||||
|
- app_data:/backup/data:ro
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
app_data:
|
||||||
|
archive:
|
||||||
62
test/commands/run.sh
Normal file
62
test/commands/run.sh
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd $(dirname $0)
|
||||||
|
|
||||||
|
|
||||||
|
docker-compose up -d
|
||||||
|
sleep 30 # mariadb likes to take a bit before responding
|
||||||
|
|
||||||
|
docker-compose exec backup backup
|
||||||
|
sudo cp -r $(docker volume inspect --format='{{ .Mountpoint }}' commands_archive) ./local
|
||||||
|
|
||||||
|
tar -xvf ./local/test.tar.gz
|
||||||
|
if [ ! -f ./backup/data/dump.sql ]; then
|
||||||
|
echo "[TEST:FAIL] Could not find file written by pre command."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "[TEST:PASS] Found expected file."
|
||||||
|
|
||||||
|
if [ -f ./backup/data/post.txt ]; then
|
||||||
|
echo "[TEST:FAIL] File created in post command was present in backup."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "[TEST:PASS] Did not find unexpected file."
|
||||||
|
|
||||||
|
docker-compose down --volumes
|
||||||
|
sudo rm -rf ./local
|
||||||
|
|
||||||
|
|
||||||
|
echo "[TEST:INFO] Running commands test in swarm mode next."
|
||||||
|
|
||||||
|
docker swarm init
|
||||||
|
|
||||||
|
docker stack deploy --compose-file=docker-compose.yml test_stack
|
||||||
|
|
||||||
|
while [ -z $(docker ps -q -f name=backup) ]; do
|
||||||
|
echo "[TEST:INFO] Backup container not ready yet. Retrying."
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
sleep 20
|
||||||
|
|
||||||
|
docker exec $(docker ps -q -f name=backup) backup
|
||||||
|
|
||||||
|
sudo cp -r $(docker volume inspect --format='{{ .Mountpoint }}' test_stack_archive) ./local
|
||||||
|
|
||||||
|
tar -xvf ./local/test.tar.gz
|
||||||
|
if [ ! -f ./backup/data/dump.sql ]; then
|
||||||
|
echo "[TEST:FAIL] Could not find file written by pre command."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "[TEST:PASS] Found expected file."
|
||||||
|
|
||||||
|
if [ -f ./backup/data/post.txt ]; then
|
||||||
|
echo "[TEST:FAIL] File created in post command was present in backup."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "[TEST:PASS] Did not find unexpected file."
|
||||||
|
|
||||||
|
docker stack rm test_stack
|
||||||
|
docker swarm leave --force
|
||||||
@@ -21,8 +21,8 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- webdav_backup_data:/var/lib/dav
|
- webdav_backup_data:/var/lib/dav
|
||||||
|
|
||||||
backup: &default_backup_service
|
backup:
|
||||||
image: offen/docker-volume-backup:${TEST_VERSION}
|
image: offen/docker-volume-backup:${TEST_VERSION:-canary}
|
||||||
hostname: hostnametoken
|
hostname: hostnametoken
|
||||||
depends_on:
|
depends_on:
|
||||||
- minio
|
- minio
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ docker run --rm -it \
|
|||||||
-v compose_minio_backup_data:/minio_data \
|
-v compose_minio_backup_data:/minio_data \
|
||||||
-v compose_webdav_backup_data:/webdav_data alpine \
|
-v compose_webdav_backup_data:/webdav_data alpine \
|
||||||
ash -c 'apk add gnupg && \
|
ash -c 'apk add gnupg && \
|
||||||
echo 1234secret | gpg -d --pinentry-mode loopback --passphrase-fd 0 --yes /minio_data/backup/test-hostnametoken.tar.gz.gpg > /tmp/test-hostnametoken.tar.gz && tar -xf /tmp/test-hostnametoken.tar.gz -C /tmp && test -f /tmp/backup/app_data/offen.db && \
|
echo 1234secret | gpg -d --pinentry-mode loopback --passphrase-fd 0 --yes /minio_data/backup/test-hostnametoken.tar.gz.gpg > /tmp/test-hostnametoken.tar.gz && tar -xvf /tmp/test-hostnametoken.tar.gz -C /tmp && test -f /tmp/backup/app_data/offen.db && \
|
||||||
echo 1234secret | gpg -d --pinentry-mode loopback --passphrase-fd 0 --yes /webdav_data/data/my/new/path/test-hostnametoken.tar.gz.gpg > /tmp/test-hostnametoken.tar.gz && tar -xf /tmp/test-hostnametoken.tar.gz -C /tmp && test -f /tmp/backup/app_data/offen.db'
|
echo 1234secret | gpg -d --pinentry-mode loopback --passphrase-fd 0 --yes /webdav_data/data/my/new/path/test-hostnametoken.tar.gz.gpg > /tmp/test-hostnametoken.tar.gz && tar -xvf /tmp/test-hostnametoken.tar.gz -C /tmp && test -f /tmp/backup/app_data/offen.db'
|
||||||
|
|
||||||
echo "[TEST:PASS] Found relevant files in decrypted and untared remote backups."
|
echo "[TEST:PASS] Found relevant files in decrypted and untared remote backups."
|
||||||
|
|
||||||
|
|||||||
1
test/confd/.gitignore
vendored
Normal file
1
test/confd/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
local
|
||||||
2
test/confd/backup.env
Normal file
2
test/confd/backup.env
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
BACKUP_FILENAME="conf.tar.gz"
|
||||||
|
BACKUP_CRON_EXPRESSION="*/1 * * * *"
|
||||||
22
test/confd/docker-compose.yml
Normal file
22
test/confd/docker-compose.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
backup:
|
||||||
|
image: offen/docker-volume-backup:${TEST_VERSION:-canary}
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ./local:/archive
|
||||||
|
- app_data:/backup/app_data:ro
|
||||||
|
- ./backup.env:/etc/dockervolumebackup/conf.d/00backup.env
|
||||||
|
- ./never.env:/etc/dockervolumebackup/conf.d/10never.env
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
|
offen:
|
||||||
|
image: offen/offen:latest
|
||||||
|
labels:
|
||||||
|
- docker-volume-backup.stop-during-backup=true
|
||||||
|
volumes:
|
||||||
|
- app_data:/var/opt/offen
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
app_data:
|
||||||
2
test/confd/never.env
Normal file
2
test/confd/never.env
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
BACKUP_FILENAME="never.tar.gz"
|
||||||
|
BACKUP_CRON_EXPRESSION="0 0 5 31 2 ?"
|
||||||
26
test/confd/run.sh
Executable file
26
test/confd/run.sh
Executable file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd $(dirname $0)
|
||||||
|
|
||||||
|
mkdir -p local
|
||||||
|
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# sleep until a backup is guaranteed to have happened on the 1 minute schedule
|
||||||
|
sleep 100
|
||||||
|
|
||||||
|
docker-compose down --volumes
|
||||||
|
|
||||||
|
if [ ! -f ./local/conf.tar.gz ]; then
|
||||||
|
echo "[TEST:FAIL] Config from file was not used."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "[TEST:PASS] Config from file was used."
|
||||||
|
|
||||||
|
if [ -f ./local/never.tar.gz ]; then
|
||||||
|
echo "[TEST:FAIL] Unexpected file was found."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "[TEST:PASS] Unexpected cron did not run."
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
version: '3'
|
version: '3'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
backup: &default_backup_service
|
backup:
|
||||||
image: offen/docker-volume-backup:${TEST_VERSION}
|
image: offen/docker-volume-backup:${TEST_VERSION:-canary}
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
BACKUP_FILENAME: test.tar.gz
|
BACKUP_FILENAME: test.tar.gz
|
||||||
|
|||||||
1
test/ownership/.gitignore
vendored
Normal file
1
test/ownership/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
local
|
||||||
27
test/ownership/docker-compose.yml
Normal file
27
test/ownership/docker-compose.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:14-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- docker-volume-backup.stop-during-backup=true
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
- POSTGRES_PASSWORD=1FHJMSwt0yhIN1zS7I4DilGUhThBKq0x
|
||||||
|
- POSTGRES_USER=test
|
||||||
|
- POSTGRES_DB=test
|
||||||
|
|
||||||
|
backup:
|
||||||
|
image: offen/docker-volume-backup:${TEST_VERSION}
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
BACKUP_FILENAME: backup.tar.gz
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/backup/postgres:ro
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- ./local:/archive
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
28
test/ownership/run.sh
Normal file
28
test/ownership/run.sh
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# This test refers to https://github.com/offen/docker-volume-backup/issues/71
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd $(dirname $0)
|
||||||
|
|
||||||
|
mkdir -p local
|
||||||
|
|
||||||
|
docker-compose up -d
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
docker-compose exec backup backup
|
||||||
|
|
||||||
|
sudo tar --same-owner -xvf ./local/backup.tar.gz -C /tmp
|
||||||
|
|
||||||
|
sudo find /tmp/backup/postgres > /dev/null
|
||||||
|
echo "[TEST:PASS] Backup contains files at expected location"
|
||||||
|
|
||||||
|
for file in $(sudo find /tmp/backup/postgres); do
|
||||||
|
if [ "$(sudo stat -c '%u:%g' $file)" != "70:70" ]; then
|
||||||
|
echo "[TEST:FAIL] Unexpected file ownership for $file: $(sudo stat -c '%u:%g' $file)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "[TEST:PASS] All files and directories in backup preserved their ownership."
|
||||||
|
|
||||||
|
docker-compose down --volumes
|
||||||
@@ -18,8 +18,8 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- backup_data:/data
|
- backup_data:/data
|
||||||
|
|
||||||
backup: &default_backup_service
|
backup:
|
||||||
image: offen/docker-volume-backup:${TEST_VERSION}
|
image: offen/docker-volume-backup:${TEST_VERSION:-canary}
|
||||||
depends_on:
|
depends_on:
|
||||||
- minio
|
- minio
|
||||||
deploy:
|
deploy:
|
||||||
@@ -49,7 +49,7 @@ services:
|
|||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|
||||||
pg:
|
pg:
|
||||||
image: postgres:12.2-alpine
|
image: postgres:14-alpine
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_PASSWORD: example
|
POSTGRES_PASSWORD: example
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
Reference in New Issue
Block a user