Compare commits

...

4 Commits

Author SHA1 Message Date
MaxJa4
336c5bed71 Replace Gzip with PGzip (#266)
* Replace Gzip with optimized PGzip. Add concurrency option.

* Add shortened timeout for 'dc down' too.

* Add NaturalNumberZero to allow zero.

* Add test for concurrency=0

* Rename to GZIP_PARALLELISM

* Fix block size. Fix compression level. Fix CI.

* Refactor compression writer fetching. Renamed WholeNumber
2023-09-03 16:49:52 +02:00
Frederik Ring
1e39ac41f4 Run tests Docker in Docker (#261)
* Try running tests in Docker

* Spawn new container for each test

* Store test artifacts outside of mount

* When requested, build up to date image in test script

* sudo is unneccessary in containerized test env

* Skip azure test

* Backdate fixture file in JSON database

* Pin versions for azure tools

* Mount temp volume for /var/lib/docker to prevent dangling ones created by VOLUME instruction

* Fail backdating tests with message

* Add some documentation on test setup

* Cache images

* Run compose stacks with shortened default timeout
2023-09-02 15:17:46 +02:00
MaxJa4
43c4961116 Support _FILE based configuration values (#264)
* Replace envconfig with env

* Adjust config options and processing

* Added _FILE variant for all password vars.

* Try pathenvconfig

* Revert everything so far

* Use our fork of envconfig with custom lookup

* Use our fork of envconfig with custom lookup

* Test compose timeout option

* Remove secret resolving and specific _FILE config

* Fix timing issue in swarm tests

* Revert "Test compose timeout option"

This reverts commit ab50b21748, reversing
changes made to 0282514b2b.

Revert "Test compose timeout option"

This reverts commit 0282514b2b.

* Use offen/envconfig v1.5.0

* Add info about _FILE in README

* Value > File. Panic on file error. Panic on duplicate presence.

* Test panic on duplicate vars and panic on file error.
2023-08-30 20:14:24 +02:00
Frederik Ring
24a6ec9480 Dropbox was missing from notification docs 2023-08-28 20:42:45 +02:00
53 changed files with 459 additions and 242 deletions

View File

@@ -15,16 +15,7 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build Docker Image
env:
DOCKER_BUILDKIT: '1'
run: docker build . -t offen/docker-volume-backup:test
- name: Run Tests
working-directory: ./test
run: |
# Stop the buildx container so the tests can make assertions
# about the number of running containers
docker rm -f $(docker ps -aq)
export GPG_TTY=$(tty)
./test.sh test
BUILD_IMAGE=1 ./test.sh

View File

@@ -139,6 +139,9 @@ Documentation references Docker Hub, but all examples will work using ghcr.io ju
## Configuration reference
Backup targets, schedule and retention are configured in environment variables.
Note: You can use any environment variable from below also with a `_FILE` suffix to be able to load the value from a file. This is usually useful when using [Docker Secrets](https://docs.docker.com/engine/swarm/secrets/) or similar.
You can populate below template according to your requirements and use it as your `env_file`:
```ini
@@ -156,6 +159,13 @@ You can populate below template according to your requirements and use it as you
# BACKUP_COMPRESSION="gz"
# Parallelism level for "gz" (Gzip) compression.
# Defines how many blocks of data are concurrently processed.
# Higher values result in faster compression. No effect on decompression
# Default = 1. Setting this to 0 will use all available threads.
# GZIP_PARALLELISM=1
# The name of the backup file including the extension.
# Format verbs will be replaced as in `strftime`. Omitting them
# will result in the same filename for every backup run, which means previous
@@ -233,14 +243,6 @@ You can populate below template according to your requirements and use it as you
# AWS_ACCESS_KEY_ID="<xxx>"
# AWS_SECRET_ACCESS_KEY="<xxx>"
# It is possible to provide the keys in files, allowing to hide the sensitive data.
# These values have a higher priority than the ones above, meaning if both are set
# the values from the files will be used.
# This option is most useful with Docker [secrets](https://docs.docker.com/engine/swarm/secrets/).
# AWS_ACCESS_KEY_ID_FILE="/path/to/file"
# AWS_SECRET_ACCESS_KEY_FILE="/path/to/file"
# Instead of providing static credentials, you can also use IAM instance profiles
# or similar to provide authentication. Some possible configuration options on AWS:
# - EC2: http://169.254.169.254

View File

@@ -8,18 +8,20 @@ package main
import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"github.com/klauspost/pgzip"
"github.com/klauspost/compress/zstd"
)
func createArchive(files []string, inputFilePath, outputFilePath string, compression string) error {
func createArchive(files []string, inputFilePath, outputFilePath string, compression string, compressionConcurrency int) error {
inputFilePath = stripTrailingSlashes(inputFilePath)
inputFilePath, outputFilePath, err := makeAbsolute(inputFilePath, outputFilePath)
if err != nil {
@@ -29,7 +31,7 @@ func createArchive(files []string, inputFilePath, outputFilePath string, compres
return fmt.Errorf("createArchive: error creating output file path: %w", err)
}
if err := compress(files, outputFilePath, filepath.Dir(inputFilePath), compression); err != nil {
if err := compress(files, outputFilePath, filepath.Dir(inputFilePath), compression, compressionConcurrency); err != nil {
return fmt.Errorf("createArchive: error creating archive: %w", err)
}
@@ -53,26 +55,17 @@ func makeAbsolute(inputFilePath, outputFilePath string) (string, string, error)
return inputFilePath, outputFilePath, err
}
func compress(paths []string, outFilePath, subPath string, algo string) error {
func compress(paths []string, outFilePath, subPath string, algo string, concurrency int) error {
file, err := os.Create(outFilePath)
var compressWriter io.WriteCloser
if err != nil {
return fmt.Errorf("compress: error creating out file: %w", err)
}
prefix := path.Dir(outFilePath)
switch algo {
case "gz":
compressWriter = gzip.NewWriter(file)
case "zst":
compressWriter, err = zstd.NewWriter(file)
if err != nil {
return fmt.Errorf("compress: zstd error: %w", err)
}
default:
return fmt.Errorf("compress: unsupported compression algorithm: %s", algo)
compressWriter, err := getCompressionWriter(file, algo, concurrency)
if err != nil {
return fmt.Errorf("compress: error getting compression writer: %w", err)
}
tarWriter := tar.NewWriter(compressWriter)
for _, p := range paths {
@@ -99,6 +92,34 @@ func compress(paths []string, outFilePath, subPath string, algo string) error {
return nil
}
func getCompressionWriter(file *os.File, algo string, concurrency int) (io.WriteCloser, error) {
switch algo {
case "gz":
w, err := pgzip.NewWriterLevel(file, 5)
if err != nil {
return nil, fmt.Errorf("getCompressionWriter: gzip error: %w", err)
}
if concurrency == 0 {
concurrency = runtime.GOMAXPROCS(0)
}
if err := w.SetConcurrency(1<<20, concurrency); err != nil {
return nil, fmt.Errorf("getCompressionWriter: error setting concurrency: %w", err)
}
return w, nil
case "zst":
compressWriter, err := zstd.NewWriter(file)
if err != nil {
return nil, fmt.Errorf("getCompressionWriter: zstd error: %w", err)
}
return compressWriter, nil
default:
return nil, fmt.Errorf("getCompressionWriter: unsupported compression algorithm: %s", algo)
}
}
func writeTarball(path string, tarWriter *tar.Writer, prefix string) error {
fileInfo, err := os.Lstat(path)
if err != nil {

View File

@@ -24,12 +24,11 @@ type Config struct {
AwsEndpointCACert CertDecoder `envconfig:"AWS_ENDPOINT_CA_CERT"`
AwsStorageClass string `split_words:"true"`
AwsAccessKeyID string `envconfig:"AWS_ACCESS_KEY_ID"`
AwsAccessKeyIDFile string `envconfig:"AWS_ACCESS_KEY_ID_FILE"`
AwsSecretAccessKey string `split_words:"true"`
AwsSecretAccessKeyFile string `split_words:"true"`
AwsIamRoleEndpoint string `split_words:"true"`
AwsPartSize int64 `split_words:"true"`
BackupCompression CompressionType `split_words:"true" default:"gz"`
GzipParallelism WholeNumber `split_words:"true" default:"1"`
BackupSources string `split_words:"true" default:"/backup"`
BackupFilename string `split_words:"true" default:"backup-%Y-%m-%dT%H-%M-%S.{{ .Extension }}"`
BackupFilenameExpand bool `split_words:"true"`
@@ -80,17 +79,6 @@ type Config struct {
DropboxConcurrencyLevel NaturalNumber `split_words:"true" default:"6"`
}
func (c *Config) resolveSecret(envVar string, secretPath string) (string, error) {
if secretPath == "" {
return envVar, nil
}
data, err := os.ReadFile(secretPath)
if err != nil {
return "", fmt.Errorf("resolveSecret: error reading secret path: %w", err)
}
return string(data), nil
}
type CompressionType string
func (c *CompressionType) Decode(v string) error {
@@ -144,6 +132,7 @@ func (r *RegexpDecoder) Decode(v string) error {
return nil
}
// NaturalNumber is a type that can be used to decode a positive, non-zero natural number
type NaturalNumber int
func (n *NaturalNumber) Decode(v string) error {
@@ -161,3 +150,22 @@ func (n *NaturalNumber) Decode(v string) error {
func (n *NaturalNumber) Int() int {
return int(*n)
}
// WholeNumber is a type that can be used to decode a positive whole number, including zero
type WholeNumber int
func (n *WholeNumber) Decode(v string) error {
asInt, err := strconv.Atoi(v)
if err != nil {
return fmt.Errorf("config: error converting %s to int", v)
}
if asInt < 0 {
return fmt.Errorf("config: expected a whole, positive number, including zero. Got %d", asInt)
}
*n = WholeNumber(asInt)
return nil
}
func (n *WholeNumber) Int() int {
return int(*n)
}

View File

@@ -35,8 +35,8 @@ import (
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
"github.com/kelseyhightower/envconfig"
"github.com/leekchan/timeutil"
"github.com/offen/envconfig"
"github.com/otiai10/copy"
"golang.org/x/sync/errgroup"
)
@@ -89,6 +89,28 @@ func newScript() (*script, error) {
return nil
})
envconfig.Lookup = func(key string) (string, bool) {
value, okValue := os.LookupEnv(key)
location, okFile := os.LookupEnv(key + "_FILE")
switch {
case okValue && !okFile: // only value
return value, true
case !okValue && okFile: // only file
contents, err := os.ReadFile(location)
if err != nil {
s.must(fmt.Errorf("newScript: failed to read %s! Error: %s", location, err))
return "", false
}
return string(contents), true
case okValue && okFile: // both
s.must(fmt.Errorf("newScript: both %s and %s are set!", key, key+"_FILE"))
return "", false
default: // neither, ignore
return "", false
}
}
if err := envconfig.Process("", s.c); err != nil {
return nil, fmt.Errorf("newScript: failed to process configuration values: %w", err)
}
@@ -137,18 +159,10 @@ func newScript() (*script, error) {
}
if s.c.AwsS3BucketName != "" {
accessKeyID, err := s.c.resolveSecret(s.c.AwsAccessKeyID, s.c.AwsAccessKeyIDFile)
if err != nil {
return nil, fmt.Errorf("newScript: error resolving AwsAccessKeyID: %w", err)
}
secretAccessKey, err := s.c.resolveSecret(s.c.AwsSecretAccessKey, s.c.AwsSecretAccessKeyFile)
if err != nil {
return nil, fmt.Errorf("newScript: error resolving AwsSecretAccessKey: %w", err)
}
s3Config := s3.Config{
Endpoint: s.c.AwsEndpoint,
AccessKeyID: accessKeyID,
SecretAccessKey: secretAccessKey,
AccessKeyID: s.c.AwsAccessKeyID,
SecretAccessKey: s.c.AwsSecretAccessKey,
IamRoleEndpoint: s.c.AwsIamRoleEndpoint,
EndpointProto: s.c.AwsEndpointProto,
EndpointInsecure: s.c.AwsEndpointInsecure,
@@ -489,7 +503,7 @@ func (s *script) createArchive() error {
return fmt.Errorf("createArchive: error walking filesystem tree: %w", err)
}
if err := createArchive(filesEligibleForBackup, backupSources, tarFile, s.c.BackupCompression.String()); err != nil {
if err := createArchive(filesEligibleForBackup, backupSources, tarFile, s.c.BackupCompression.String(), s.c.GzipParallelism.Int()); err != nil {
return fmt.Errorf("createArchive: error compressing backup folder: %w", err)
}

View File

@@ -25,7 +25,7 @@ Here is a list of all data passed to the template:
* `FullPath`: full path of the backup file (e.g. `/archive/backup-2022-02-11T01-00-00.tar.gz`)
* `Size`: size in bytes of the backup file
* `Storages`: object that holds stats about each storage
* `Local`, `S3`, `WebDAV`, `Azure` or `SSH`:
* `Local`, `S3`, `WebDAV`, `Azure`, `Dropbox` or `SSH`:
* `Total`: total number of backup files
* `Pruned`: number of backup files that were deleted due to pruning rule
* `PruneErrors`: number of backup files that were unable to be pruned

3
go.mod
View File

@@ -9,10 +9,10 @@ require (
github.com/cosiner/argv v0.1.0
github.com/docker/docker v24.0.5+incompatible
github.com/gofrs/flock v0.8.1
github.com/kelseyhightower/envconfig v1.4.0
github.com/klauspost/compress v1.16.7
github.com/leekchan/timeutil v0.0.0-20150802142658-28917288c48d
github.com/minio/minio-go/v7 v7.0.62
github.com/offen/envconfig v1.5.0
github.com/otiai10/copy v1.11.0
github.com/pkg/sftp v1.13.6
github.com/studio-b12/gowebdav v0.9.0
@@ -45,6 +45,7 @@ require (
github.com/google/uuid v1.3.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/klauspost/pgzip v1.2.6
github.com/kr/fs v0.1.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect

6
go.sum
View File

@@ -451,8 +451,6 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
@@ -460,6 +458,8 @@ github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQs
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
@@ -527,6 +527,8 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/offen/envconfig v1.5.0 h1:LHL4wYIDVeoGxSDI40MShmWfss3gYUlCdstfSiSq4Fk=
github.com/offen/envconfig v1.5.0/go.mod h1:L7ny7R+4JWH3VVnZ+ARHvZysWUiZ2eQcm3L0imU9ACY=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=

13
test/Dockerfile Normal file
View File

@@ -0,0 +1,13 @@
FROM docker:24-dind
RUN apk add \
coreutils \
curl \
gpg \
jq \
moreutils \
tar \
zstd \
--no-cache
WORKDIR /code/test

70
test/README.md Normal file
View File

@@ -0,0 +1,70 @@
# Integration Tests
## Running tests
The main entry point for running tests is the `./test.sh` script.
It can be used to run the entire test suite, or just a single test case.
### Run all tests
```sh
./test.sh
```
### Run a single test case
```sh
./test.sh <directory-name>
```
### Configuring a test run
In addition to the match pattern, which can be given as the first positional argument, certain behavior can be changed by setting environment variables:
#### `BUILD_IMAGE`
When set, the test script will build an up-to-date `docker-volume-backup` image from the current state of your source tree, and run the tests against it.
```sh
BUILD_IMAGE=1 ./test.sh
```
The default behavior is not to build an image, and instead look for a version on your host system.
#### `IMAGE_TAG`
Setting this value lets you run tests against different existing images, so you can compare behavior:
```sh
IMAGE_TAG=v2.30.0 ./test.sh
```
#### `NO_IMAGE_CACHE`
When set, images from remote registries will not be cached and shared between sandbox containers.
```sh
NO_IMAGE_CACHE=1 ./test.sh
```
By default, two local images are created that persist the image data and provide it to containers at runtime.
## Understanding the test setup
The test setup runs each test case in an isolated Docker container, which itself is running an otherwise unused Docker daemon.
This means, tests can rely on noone else using that daemon, making expectations about the number of running containers and so forth.
As the sandbox container is also expected to be torn down post test, the scripts do not need to do any clean up or similar.
## Anatomy of a test case
The `test.sh` script looks for an exectuable file called `run.sh` in each directory.
When found, it is executed and signals success by returning a 0 exit code.
Any other exit code is considered a failure and will halt execution of further tests.
There is an `util.sh` file containing a few commonly used helpers which can be used by putting the following prelude to a new test case:
```sh
cd "$(dirname "$0")"
. ../util.sh
current_test=$(basename $(pwd))
```

View File

@@ -2,19 +2,19 @@ version: '3'
services:
storage:
image: mcr.microsoft.com/azure-storage/azurite
image: mcr.microsoft.com/azure-storage/azurite:3.26.0
volumes:
- azurite_backup_data:/data
- ${DATA_DIR:-./data}:/data
command: azurite-blob --blobHost 0.0.0.0 --blobPort 10000 --location /data
healthcheck:
test: nc 127.0.0.1 10000 -z
interval: 1s
retries: 30
test: nc 127.0.0.1 10000 -z
interval: 1s
retries: 30
az_cli:
image: mcr.microsoft.com/azure-cli
image: mcr.microsoft.com/azure-cli:2.51.0
volumes:
- ./local:/dump
- ${LOCAL_DIR:-./local}:/dump
command:
- /bin/sh
- -c
@@ -53,6 +53,4 @@ services:
- app_data:/var/opt/offen
volumes:
azurite_backup_data:
name: azurite_backup_data
app_data:

40
test/azure/run.sh Normal file → Executable file
View File

@@ -6,12 +6,17 @@ cd "$(dirname "$0")"
. ../util.sh
current_test=$(basename $(pwd))
export LOCAL_DIR=$(mktemp -d)
export TMP_DIR=$(mktemp -d)
export DATA_DIR=$(mktemp -d)
download_az () {
docker compose run --rm az_cli \
az storage blob download -f /dump/$1.tar.gz -c test-container -n path/to/backup/$1.tar.gz
}
docker compose up -d --quiet-pull
sleep 5
docker compose exec backup backup
@@ -21,9 +26,15 @@ sleep 5
expect_running_containers "3"
download_az "test"
tar -xvf ./local/test.tar.gz -C /tmp && test -f /tmp/backup/app_data/offen.db
tar -xvf "$LOCAL_DIR/test.tar.gz" -C $TMP_DIR
if [ ! -f "$TMP_DIR/backup/app_data/offen.db" ]; then
fail "Could not find expeced file in untared backup"
fi
pass "Found relevant files in untared remote backups."
rm "$LOCAL_DIR/test.tar.gz"
# The second part of this test checks if backups get deleted when the retention
# is set to 0 days (which it should not as it would mean all backups get deleted)
@@ -33,8 +44,9 @@ sleep 5
docker compose exec backup backup
download_az "test"
test -f ./local/test.tar.gz
if [ ! -f "$LOCAL_DIR/test.tar.gz" ]; then
fail "Remote backup was deleted"
fi
pass "Remote backups have not been deleted."
# The third part of this test checks if old backups get deleted when the retention
@@ -46,21 +58,29 @@ sleep 5
info "Create first backup with no prune"
docker compose exec backup backup
sudo date --set="14 days ago"
docker compose run --rm az_cli \
az storage blob upload -f /dump/test.tar.gz -c test-container -n path/to/backup/test-old.tar.gz
sudo date --set="14 days"
docker compose down
rm "$LOCAL_DIR/test.tar.gz"
back_date="$(date "+%Y-%m-%dT%H:%M:%S%z" -d "14 days ago" | rev | cut -c 3- | rev):00"
jq --arg back_date "$back_date" '(.collections[] | select(.name=="$BLOBS_COLLECTION$") | .data[] | select(.name=="path/to/backup/test-old.tar.gz") | .properties.creationTime = $back_date)' "$DATA_DIR/__azurite_db_blob__.json" | sponge "$DATA_DIR/__azurite_db_blob__.json"
docker compose up -d
sleep 5
info "Create second backup and prune"
docker compose exec backup backup
info "Download first backup which should be pruned"
download_az "test-old" || true
test ! -f ./local/test-old.tar.gz
test -f ./local/test.tar.gz
if [ -f "$LOCAL_DIR/test-old.tar.gz" ]; then
fail "Backdated file was not deleted"
fi
download_az "test" || true
if [ ! -f "$LOCAL_DIR/test.tar.gz" ]; then
fail "Recent file was not found"
fi
pass "Old remote backup has been pruned, new one is still present."
docker compose down --volumes

View File

@@ -12,8 +12,8 @@ services:
entrypoint: /bin/ash -c 'mkdir -p /data/backup && minio server --certs-dir "/certs" --address ":443" /data'
volumes:
- minio_backup_data:/data
- ./minio.crt:/certs/public.crt
- ./minio.key:/certs/private.key
- ${CERT_DIR:-.}/minio.crt:/certs/public.crt
- ${CERT_DIR:-.}/minio.key:/certs/private.key
backup:
image: offen/docker-volume-backup:${TEST_VERSION:-canary}
@@ -33,7 +33,7 @@ services:
volumes:
- app_data:/backup/app_data:ro
- /var/run/docker.sock:/var/run/docker.sock
- ./rootCA.crt:/root/minio-rootCA.crt
- ${CERT_DIR:-.}/rootCA.crt:/root/minio-rootCA.crt
offen:
image: offen/offen:latest

22
test/certs/run.sh Normal file → Executable file
View File

@@ -6,23 +6,25 @@ cd "$(dirname "$0")"
. ../util.sh
current_test=$(basename $(pwd))
openssl genrsa -des3 -passout pass:test -out rootCA.key 4096
export CERT_DIR=$(mktemp -d)
openssl genrsa -des3 -passout pass:test -out "$CERT_DIR/rootCA.key" 4096
openssl req -passin pass:test \
-subj "/C=DE/ST=BE/O=IntegrationTest, Inc." \
-x509 -new -key rootCA.key -sha256 -days 1 -out rootCA.crt
-x509 -new -key "$CERT_DIR/rootCA.key" -sha256 -days 1 -out "$CERT_DIR/rootCA.crt"
openssl genrsa -out minio.key 4096
openssl req -new -sha256 -key minio.key \
openssl genrsa -out "$CERT_DIR/minio.key" 4096
openssl req -new -sha256 -key "$CERT_DIR/minio.key" \
-subj "/C=DE/ST=BE/O=IntegrationTest, Inc./CN=minio" \
-out minio.csr
-out "$CERT_DIR/minio.csr"
openssl x509 -req -passin pass:test \
-in minio.csr \
-CA rootCA.crt -CAkey rootCA.key -CAcreateserial \
-in "$CERT_DIR/minio.csr" \
-CA "$CERT_DIR/rootCA.crt" -CAkey "$CERT_DIR/rootCA.key" -CAcreateserial \
-extfile san.cnf \
-out minio.crt -days 1 -sha256
-out "$CERT_DIR/minio.crt" -days 1 -sha256
openssl x509 -in minio.crt -noout -text
openssl x509 -in "$CERT_DIR/minio.crt" -noout -text
docker compose up -d --quiet-pull
sleep 5
@@ -39,5 +41,3 @@ docker run --rm \
ash -c 'tar -xvf /minio_data/backup/test.tar.gz -C /tmp && test -f /tmp/backup/app_data/offen.db'
pass "Found relevant files in untared remote backups."
docker compose down --volumes

View File

@@ -1 +0,0 @@
local

View File

@@ -42,7 +42,7 @@ services:
EXEC_LABEL: test
EXEC_FORWARD_OUTPUT: "true"
volumes:
- ./local:/archive
- ${LOCAL_DIR:-./local}:/archive
- app_data:/backup/data:ro
- /var/run/docker.sock:/var/run/docker.sock

26
test/commands/run.sh Normal file → Executable file
View File

@@ -6,36 +6,37 @@ cd $(dirname $0)
. ../util.sh
current_test=$(basename $(pwd))
mkdir -p ./local
export LOCAL_DIR=$(mktemp -d)
export TMP_DIR=$(mktemp -d)
docker compose up -d --quiet-pull
sleep 30 # mariadb likes to take a bit before responding
docker compose exec backup backup
tar -xvf ./local/test.tar.gz
if [ ! -f ./backup/data/dump.sql ]; then
tar -xvf "$LOCAL_DIR/test.tar.gz" -C $TMP_DIR
if [ ! -f "$TMP_DIR/backup/data/dump.sql" ]; then
fail "Could not find file written by pre command."
fi
pass "Found expected file."
if [ -f ./backup/data/not-relevant.txt ]; then
if [ -f "$TMP_DIR/backup/data/not-relevant.txt" ]; then
fail "Command ran for container with other label."
fi
pass "Command did not run for container with other label."
if [ -f ./backup/data/post.txt ]; then
if [ -f "$TMP_DIR/backup/data/post.txt" ]; then
fail "File created in post command was present in backup."
fi
pass "Did not find unexpected file."
docker compose down --volumes
sudo rm -rf ./local
info "Running commands test in swarm mode next."
mkdir -p ./local
export LOCAL_DIR=$(mktemp -d)
export TMP_DIR=$(mktemp -d)
docker swarm init
docker stack deploy --compose-file=docker-compose.yml test_stack
@@ -49,16 +50,13 @@ sleep 20
docker exec $(docker ps -q -f name=backup) backup
tar -xvf ./local/test.tar.gz
if [ ! -f ./backup/data/dump.sql ]; then
tar -xvf "$LOCAL_DIR/test.tar.gz" -C $TMP_DIR
if [ ! -f "$TMP_DIR/backup/data/dump.sql" ]; then
fail "Could not find file written by pre command."
fi
pass "Found expected file."
if [ -f ./backup/data/post.txt ]; then
if [ -f "$TMP_DIR/backup/data/post.txt" ]; then
fail "File created in post command was present in backup."
fi
pass "Did not find unexpected file."
docker stack rm test_stack
docker swarm leave --force

View File

@@ -1 +0,0 @@
local

View File

@@ -5,7 +5,7 @@ services:
image: offen/docker-volume-backup:${TEST_VERSION:-canary}
restart: always
volumes:
- ./local:/archive
- ${LOCAL_DIR:-./local}:/archive
- app_data:/backup/app_data:ro
- ./01backup.env:/etc/dockervolumebackup/conf.d/01backup.env
- ./02backup.env:/etc/dockervolumebackup/conf.d/02backup.env

View File

@@ -6,26 +6,24 @@ cd $(dirname $0)
. ../util.sh
current_test=$(basename $(pwd))
mkdir -p local
export LOCAL_DIR=$(mktemp -d)
docker compose up -d --quiet-pull
# 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
if [ ! -f "$LOCAL_DIR/conf.tar.gz" ]; then
fail "Config from file was not used."
fi
pass "Config from file was used."
if [ ! -f ./local/other.tar.gz ]; then
if [ ! -f "$LOCAL_DIR/other.tar.gz" ]; then
fail "Run on same schedule did not succeed."
fi
pass "Run on same schedule succeeded."
if [ -f ./local/never.tar.gz ]; then
if [ -f "$LOCAL_DIR/never.tar.gz" ]; then
fail "Unexpected file was found."
fi
pass "Unexpected cron did not run."

View File

@@ -9,7 +9,7 @@ services:
ports:
- 8080:8080
volumes:
- ./user_v2_ready.yaml:/etc/openapi/user_v2.yaml
- ${SPEC_FILE:-./user_v2.yaml}:/etc/openapi/user_v2.yaml
oauth2_mock:
image: ghcr.io/navikt/mock-oauth2-server:1.0.0

12
test/dropbox/run.sh Normal file → Executable file
View File

@@ -6,9 +6,10 @@ cd "$(dirname "$0")"
. ../util.sh
current_test=$(basename $(pwd))
cp user_v2.yaml user_v2_ready.yaml
sudo sed -i 's/SERVER_MODIFIED_1/'"$(date "+%Y-%m-%dT%H:%M:%SZ")/g" user_v2_ready.yaml
sudo sed -i 's/SERVER_MODIFIED_2/'"$(date "+%Y-%m-%dT%H:%M:%SZ" -d "14 days ago")/g" user_v2_ready.yaml
export SPEC_FILE=$(mktemp -d)/user_v2.yaml
cp user_v2.yaml $SPEC_FILE
sed -i 's/SERVER_MODIFIED_1/'"$(date "+%Y-%m-%dT%H:%M:%SZ")/g" $SPEC_FILE
sed -i 's/SERVER_MODIFIED_2/'"$(date "+%Y-%m-%dT%H:%M:%SZ" -d "14 days ago")/g" $SPEC_FILE
docker compose up -d --quiet-pull
sleep 5
@@ -42,7 +43,6 @@ fi
# The third part of this test checks if old backups get deleted when the retention
# is set to 7 days (which it should)
BACKUP_RETENTION_DAYS="7" docker compose up -d
sleep 5
@@ -59,7 +59,3 @@ elif echo "$logs" | grep -q "None of 1 existing backups were pruned"; then
else
fail "Pruning failed, unknown result: $logs"
fi
docker compose down --volumes
rm user_v2_ready.yaml

View File

@@ -11,7 +11,7 @@ services:
BACKUP_CRON_EXPRESSION: 0 0 5 31 2 ?
EXEC_FORWARD_OUTPUT: "true"
volumes:
- ./local:/local
- ${LOCAL_DIR:-local}:/local
- app_data:/backup/app_data:ro
- /var/run/docker.sock:/var/run/docker.sock

6
test/extend/run.sh Normal file → Executable file
View File

@@ -6,7 +6,7 @@ cd "$(dirname "$0")"
. ../util.sh
current_test=$(basename $(pwd))
mkdir -p local
export LOCAL_DIR=$(mktemp -d)
export BASE_VERSION="${TEST_VERSION:-canary}"
export TEST_VERSION="${TEST_VERSION:-canary}-with-rsync"
@@ -22,8 +22,6 @@ sleep 5
expect_running_containers "2"
if [ ! -f "./local/app_data/offen.db" ]; then
if [ ! -f "$LOCAL_DIR/app_data/offen.db" ]; then
fail "Could not find expected file in untared archive."
fi
docker compose down --volumes

1
test/gpg/.gitignore vendored
View File

@@ -1 +0,0 @@
local

View File

@@ -11,7 +11,7 @@ services:
BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-7}
GPG_PASSPHRASE: 1234#$$ecret
volumes:
- ./local:/archive
- ${LOCAL_DIR:-./local}:/archive
- app_data:/backup/app_data:ro
- /var/run/docker.sock:/var/run/docker.sock

View File

@@ -6,7 +6,7 @@ cd "$(dirname "$0")"
. ../util.sh
current_test=$(basename $(pwd))
mkdir -p local
export LOCAL_DIR=$(mktemp -d)
docker compose up -d --quiet-pull
sleep 5
@@ -15,19 +15,18 @@ docker compose exec backup backup
expect_running_containers "2"
tmp_dir=$(mktemp -d)
TMP_DIR=$(mktemp -d)
echo "1234#\$ecret" | gpg -d --pinentry-mode loopback --yes --passphrase-fd 0 ./local/test.tar.gz.gpg > ./local/decrypted.tar.gz
tar -xf ./local/decrypted.tar.gz -C $tmp_dir
if [ ! -f $tmp_dir/backup/app_data/offen.db ]; then
echo "1234#\$ecret" | gpg -d --pinentry-mode loopback --yes --passphrase-fd 0 "$LOCAL_DIR/test.tar.gz.gpg" > "$LOCAL_DIR/decrypted.tar.gz"
tar -xf "$LOCAL_DIR/decrypted.tar.gz" -C $TMP_DIR
if [ ! -f $TMP_DIR/backup/app_data/offen.db ]; then
fail "Could not find expected file in untared archive."
fi
rm ./local/decrypted.tar.gz
rm "$LOCAL_DIR/decrypted.tar.gz"
pass "Found relevant files in decrypted and untared local backup."
if [ ! -L ./local/test-latest.tar.gz.gpg ]; then
if [ ! -L "$LOCAL_DIR/test-latest.tar.gz.gpg" ]; then
fail "Could not find local symlink to latest encrypted backup."
fi
docker compose down --volumes

View File

@@ -1 +0,0 @@
local

View File

@@ -11,5 +11,5 @@ services:
BACKUP_CRON_EXPRESSION: 0 0 5 31 2 ?
BACKUP_EXCLUDE_REGEXP: '\.(me|you)$$'
volumes:
- ./local:/archive
- ${LOCAL_DIR:-./local}:/archive
- ./sources:/backup/data:ro

12
test/ignore/run.sh Normal file → Executable file
View File

@@ -6,23 +6,21 @@ cd $(dirname $0)
. ../util.sh
current_test=$(basename $(pwd))
mkdir -p local
export LOCAL_DIR=$(mktemp -d)
docker compose up -d --quiet-pull
sleep 5
docker compose exec backup backup
docker compose down --volumes
TMP_DIR=$(mktemp -d)
tar --same-owner -xvf "$LOCAL_DIR/test.tar.gz" -C "$TMP_DIR"
out=$(mktemp -d)
sudo tar --same-owner -xvf ./local/test.tar.gz -C "$out"
if [ ! -f "$out/backup/data/me.txt" ]; then
if [ ! -f "$TMP_DIR/backup/data/me.txt" ]; then
fail "Expected file was not found."
fi
pass "Expected file was found."
if [ -f "$out/backup/data/skip.me" ]; then
if [ -f "$TMP_DIR/backup/data/skip.me" ]; then
fail "Ignored file was found."
fi
pass "Ignored file was not found."

View File

@@ -1 +0,0 @@
local

View File

@@ -16,7 +16,7 @@ services:
volumes:
- app_data:/backup/app_data:ro
- /var/run/docker.sock:/var/run/docker.sock
- ./local:/archive
- ${LOCAL_DIR:-./local}:/archive
offen:
image: offen/offen:latest

View File

@@ -6,7 +6,7 @@ cd "$(dirname "$0")"
. ../util.sh
current_test=$(basename $(pwd))
mkdir -p local
export LOCAL_DIR=$(mktemp -d)
docker compose up -d --quiet-pull
sleep 5
@@ -21,11 +21,11 @@ sleep 5
expect_running_containers "2"
tmp_dir=$(mktemp -d)
tar -xvf ./local/test-hostnametoken.tar.gz -C $tmp_dir
tar -xvf "$LOCAL_DIR/test-hostnametoken.tar.gz" -C $tmp_dir
if [ ! -f "$tmp_dir/backup/app_data/offen.db" ]; then
fail "Could not find expected file in untared archive."
fi
rm -f ./local/test-hostnametoken.tar.gz
rm -f "$LOCAL_DIR/test-hostnametoken.tar.gz"
if [ ! -L "$tmp_dir/backup/app_data/db.link" ]; then
fail "Could not find expected symlink in untared archive."
@@ -33,7 +33,7 @@ fi
pass "Found relevant files in decrypted and untared local backup."
if [ ! -L ./local/test-hostnametoken.latest.tar.gz.gpg ]; then
if [ ! -L "$LOCAL_DIR/test-hostnametoken.latest.tar.gz.gpg" ]; then
fail "Could not find symlink to latest version."
fi
@@ -46,8 +46,8 @@ sleep 5
docker compose exec backup backup
if [ "$(find ./local -type f | wc -l)" != "1" ]; then
fail "Backups should not have been deleted, instead seen: "$(find ./local -type f)""
if [ "$(find "$LOCAL_DIR" -type f | wc -l)" != "1" ]; then
fail "Backups should not have been deleted, instead seen: "$(find "$local_dir" -type f)""
fi
pass "Local backups have not been deleted."
@@ -60,14 +60,17 @@ sleep 5
info "Create first backup with no prune"
docker compose exec backup backup
touch -r ./local/test-hostnametoken.tar.gz -d "14 days ago" ./local/test-hostnametoken-old.tar.gz
touch -r "$LOCAL_DIR/test-hostnametoken.tar.gz" -d "14 days ago" "$LOCAL_DIR/test-hostnametoken-old.tar.gz"
info "Create second backup and prune"
docker compose exec backup backup
test ! -f ./local/test-hostnametoken-old.tar.gz
test -f ./local/test-hostnametoken.tar.gz
if [ -f "$LOCAL_DIR/test-hostnametoken-old.tar.gz" ]; then
fail "Backdated file has not been deleted."
fi
if [ ! -f "$LOCAL_DIR/test-hostnametoken.tar.gz" ]; then
fail "Recent file has been deleted."
fi
pass "Old remote backup has been pruned, new one is still present."
docker compose down --volumes

View File

@@ -1 +0,0 @@
local

View File

@@ -12,7 +12,7 @@ services:
NOTIFICATION_URLS: ${NOTIFICATION_URLS}
EXTRA_VALUE: extra-value
volumes:
- ./local:/archive
- ${LOCAL_DIR:-./local}:/archive
- app_data:/backup/app_data:ro
- ./notifications.tmpl:/etc/dockervolumebackup/notifications.d/notifications.tmpl

View File

@@ -6,7 +6,7 @@ cd $(dirname $0)
. ../util.sh
current_test=$(basename $(pwd))
mkdir -p local
export LOCAL_DIR=$(mktemp -d)
docker compose up -d --quiet-pull
sleep 5
@@ -46,5 +46,3 @@ if [ "$MESSAGE_BODY" != "Backing up /tmp/test.tar.gz succeeded." ]; then
fail "Unexpected notification body $MESSAGE_BODY"
fi
pass "Custom notification body was used."
docker compose down --volumes

View File

@@ -1 +0,0 @@
local

View File

@@ -9,9 +9,9 @@ services:
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=1FHJMSwt0yhIN1zS7I4DilGUhThBKq0x
- POSTGRES_USER=test
- POSTGRES_DB=test
POSTGRES_PASSWORD: 1FHJMSwt0yhIN1zS7I4DilGUhThBKq0x
POSTGRES_USER: test
POSTGRES_DB: test
backup:
image: offen/docker-volume-backup:${TEST_VERSION}
@@ -21,7 +21,7 @@ services:
volumes:
- postgres_data:/backup/postgres:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./local:/archive
- ${LOCAL_DIR:-./local}:/archive
volumes:
postgres_data:

16
test/ownership/run.sh Normal file → Executable file
View File

@@ -7,24 +7,22 @@ cd $(dirname $0)
. ../util.sh
current_test=$(basename $(pwd))
mkdir -p local
export LOCAL_DIR=$(mktemp -d)
docker compose up -d --quiet-pull
sleep 5
docker compose exec backup backup
tmp_dir=$(mktemp -d)
sudo tar --same-owner -xvf ./local/backup.tar.gz -C $tmp_dir
TMP_DIR=$(mktemp -d)
tar --same-owner -xvf "$LOCAL_DIR/backup.tar.gz" -C $TMP_DIR
sudo find $tmp_dir/backup/postgres > /dev/null
find $TMP_DIR/backup/postgres > /dev/null
pass "Backup contains files at expected location"
for file in $(sudo find $tmp_dir/backup/postgres); do
if [ "$(sudo stat -c '%u:%g' $file)" != "70:70" ]; then
fail "Unexpected file ownership for $file: $(sudo stat -c '%u:%g' $file)"
for file in $(find $TMP_DIR/backup/postgres); do
if [ "$(stat -c '%u:%g' $file)" != "70:70" ]; then
fail "Unexpected file ownership for $file: $(stat -c '%u:%g' $file)"
fi
done
pass "All files and directories in backup preserved their ownership."
docker compose down --volumes

42
test/pgzip/run.sh Executable file
View File

@@ -0,0 +1,42 @@
#!/bin/sh
set -e
cd $(dirname $0)
. ../util.sh
current_test=$(basename $(pwd))
docker network create test_network
docker volume create app_data
LOCAL_DIR=$(mktemp -d)
docker run -d -q \
--name offen \
--network test_network \
-v app_data:/var/opt/offen/ \
offen/offen:latest
sleep 5
docker run --rm -q \
--network test_network \
-v app_data:/backup/app_data \
-v $LOCAL_DIR:/archive \
-v /var/run/docker.sock:/var/run/docker.sock \
--env BACKUP_COMPRESSION=gz \
--env GZIP_PARALLELISM=0 \
--env BACKUP_FILENAME='test.{{ .Extension }}' \
--entrypoint backup \
offen/docker-volume-backup:${TEST_VERSION:-canary}
tmp_dir=$(mktemp -d)
tar -xvf "$LOCAL_DIR/test.tar.gz" -C $tmp_dir
if [ ! -f "$tmp_dir/backup/app_data/offen.db" ]; then
fail "Could not find expected file in untared archive."
fi
pass "Found relevant files in untared local backup."
# This test does not stop containers during backup. This is happening on
# purpose in order to cover this setup as well.
expect_running_containers "1"

6
test/pruning/run.sh Normal file → Executable file
View File

@@ -57,7 +57,9 @@ info "Create backup with no prune for both backends"
docker compose exec -e BACKUP_SKIP_BACKENDS_FROM_PRUNE="s3,local" backup backup
info "Check if old backup has NOT been pruned (local)"
test -f ./local/test-hostnametoken-old.tar.gz
if [ ! -f ./local/test-hostnametoken-old.tar.gz ]; then
fail "Backdated file has not been deleted"
fi
info "Check if old backup has NOT been pruned (s3)"
docker run --rm \
@@ -66,5 +68,3 @@ docker run --rm \
ash -c 'test -f /minio_data/backup/test-hostnametoken-old.tar.gz'
pass "Skipped all backends while pruning."
docker compose down --volumes

2
test/s3/run.sh Normal file → Executable file
View File

@@ -59,5 +59,3 @@ docker run --rm \
ash -c 'test ! -f /minio_data/backup/test-hostnametoken-old.tar.gz && test -f /minio_data/backup/test-hostnametoken.tar.gz'
pass "Old remote backup has been pruned, new one is still present."
docker compose down --volumes

View File

@@ -31,14 +31,12 @@ pass "Found relevant files in untared backup."
sleep 5
expect_running_containers "5"
docker stack rm test_stack
docker exec -e AWS_ACCESS_KEY_ID=test $(docker ps -q -f name=backup) backup \
&& fail "Backup should have failed due to duplicate env variables."
docker secret rm minio_root_password
docker secret rm minio_root_user
pass "Backup failed due to duplicate env variables."
docker swarm leave --force
docker exec -e AWS_ACCESS_KEY_ID_FILE=/tmp/nonexistant $(docker ps -q -f name=backup) backup \
&& fail "Backup should have failed due to non existing file env variable."
sleep 10
docker volume rm backup_data
docker volume rm pg_data
pass "Backup failed due to non existing file env variable."

View File

@@ -8,7 +8,7 @@ services:
- PGID=1000
- USER_NAME=test
volumes:
- ./id_rsa.pub:/config/.ssh/authorized_keys
- ${KEY_DIR:-.}/id_rsa.pub:/config/.ssh/authorized_keys
- ssh_backup_data:/tmp
backup:
@@ -30,7 +30,7 @@ services:
SSH_REMOTE_PATH: /tmp
SSH_IDENTITY_PASSPHRASE: test1234
volumes:
- ./id_rsa:/root/.ssh/id_rsa
- ${KEY_DIR:-.}/id_rsa:/root/.ssh/id_rsa
- app_data:/backup/app_data:ro
- /var/run/docker.sock:/var/run/docker.sock

7
test/ssh/run.sh Normal file → Executable file
View File

@@ -6,7 +6,9 @@ cd "$(dirname "$0")"
. ../util.sh
current_test=$(basename $(pwd))
ssh-keygen -t rsa -m pem -b 4096 -N "test1234" -f id_rsa -C "docker-volume-backup@local"
export KEY_DIR=$(mktemp -d)
ssh-keygen -t rsa -m pem -b 4096 -N "test1234" -f "$KEY_DIR/id_rsa" -C "docker-volume-backup@local"
docker compose up -d --quiet-pull
sleep 5
@@ -63,6 +65,3 @@ docker run --rm \
ash -c 'test ! -f /ssh_data/test-hostnametoken-old.tar.gz && test -f /ssh_data/test-hostnametoken.tar.gz'
pass "Old remote backup has been pruned, new one is still present."
docker compose down --volumes
rm -f id_rsa id_rsa.pub

View File

@@ -27,11 +27,3 @@ pass "Found relevant files in untared backup."
sleep 5
expect_running_containers "5"
docker stack rm test_stack
docker swarm leave --force
sleep 10
docker volume rm backup_data
docker volume rm pg_data

View File

@@ -2,15 +2,69 @@
set -e
TEST_VERSION=${1:-canary}
MATCH_PATTERN=$1
IMAGE_TAG=${IMAGE_TAG:-canary}
for dir in $(ls -d -- */); do
test="${dir}run.sh"
sandbox="docker_volume_backup_test_sandbox"
tarball="$(mktemp -d)/image.tar.gz"
trap finish EXIT INT TERM
finish () {
rm -rf $(dirname $tarball)
if [ ! -z $(docker ps -aq --filter=name=$sandbox) ]; then
docker rm -f $(docker stop $sandbox)
fi
if [ ! -z $(docker volume ls -q --filter=name="^${sandbox}\$") ]; then
docker volume rm $sandbox
fi
}
docker build -t offen/docker-volume-backup:test-sandbox .
if [ ! -z "$BUILD_IMAGE" ]; then
docker build -t offen/docker-volume-backup:$IMAGE_TAG $(dirname $(pwd))
fi
docker save offen/docker-volume-backup:$IMAGE_TAG -o $tarball
find_args="-mindepth 1 -maxdepth 1 -type d"
if [ ! -z "$MATCH_PATTERN" ]; then
find_args="$find_args -name $MATCH_PATTERN"
fi
for dir in $(find $find_args | sort); do
dir=$(echo $dir | cut -c 3-)
echo "################################################"
echo "Now running $test"
echo "Now running ${dir}"
echo "################################################"
echo ""
TEST_VERSION=$TEST_VERSION /bin/sh $test
test="${dir}/run.sh"
docker_run_args="--name "$sandbox" --detach \
--privileged \
-v $(dirname $(pwd)):/code \
-v $tarball:/cache/image.tar.gz \
-v $sandbox:/var/lib/docker"
if [ -z "$NO_IMAGE_CACHE" ]; then
docker_run_args="$docker_run_args \
-v "${sandbox}_image":/var/lib/docker/image \
-v "${sandbox}_overlay2":/var/lib/docker/overlay2"
fi
docker run $docker_run_args offen/docker-volume-backup:test-sandbox
until docker exec $sandbox /bin/sh -c 'docker info' > /dev/null 2>&1; do
sleep 0.5
done
sleep 0.5
docker exec $sandbox /bin/sh -c "docker load -i /cache/image.tar.gz"
docker exec -e TEST_VERSION=$IMAGE_TAG $sandbox /bin/sh -c "/code/test/$test"
docker rm $(docker stop $sandbox)
docker volume rm $sandbox
echo ""
echo "$test passed"
echo ""

View File

@@ -1,2 +0,0 @@
local
backup

View File

@@ -10,7 +10,6 @@ services:
- docker-volume-backup.archive-pre.user=testuser
- docker-volume-backup.archive-pre=/bin/sh -c 'whoami > /tmp/whoami.txt'
backup:
image: offen/docker-volume-backup:${TEST_VERSION:-canary}
deploy:
@@ -21,10 +20,10 @@ services:
BACKUP_CRON_EXPRESSION: 0 0 5 31 2 ?
EXEC_FORWARD_OUTPUT: "true"
volumes:
- ./local:/archive
- ${LOCAL_DIR:-./local}:/archive
- app_data:/backup/data:ro
- /var/run/docker.sock:/var/run/docker.sock
volumes:
app_data:
archive:
archive:

18
test/user/run.sh Normal file → Executable file
View File

@@ -6,25 +6,25 @@ cd $(dirname $0)
. ../util.sh
current_test=$(basename $(pwd))
docker compose up -d --quiet-pull
export LOCAL_DIR=$(mktemp -d)
export TMP_DIR=$(mktemp -d)
echo "LOCAL_DIR $LOCAL_DIR"
echo "TMP_DIR $TMP_DIR"
docker compose up -d --quiet-pull
user_name=testuser
docker exec user-alpine-1 adduser --disabled-password "$user_name"
docker compose exec backup backup
tar -xvf ./local/test.tar.gz
if [ ! -f ./backup/data/whoami.txt ]; then
tar -xvf "$LOCAL_DIR/test.tar.gz" -C "$TMP_DIR"
if [ ! -f "$TMP_DIR/backup/data/whoami.txt" ]; then
fail "Could not find file written by pre command."
fi
pass "Found expected file."
tar -xvf ./local/test.tar.gz
if [ "$(cat ./backup/data/whoami.txt)" != "$user_name" ]; then
if [ "$(cat $TMP_DIR/backup/data/whoami.txt)" != "$user_name" ]; then
fail "Could not find expected user name."
fi
pass "Found expected user."
docker compose down --volumes
sudo rm -rf ./local

View File

@@ -15,9 +15,34 @@ fail () {
exit 1
}
skip () {
echo "[test:${current_test:-none}:skip] "$1""
exit 0
}
expect_running_containers () {
if [ "$(docker ps -q | wc -l)" != "$1" ]; then
fail "Expected $1 containers to be running, instead seen: "$(docker ps -a | wc -l)""
fi
pass "$1 containers running."
}
docker() {
case $1 in
compose)
shift
case $1 in
up)
shift
command docker compose up --timeout 3 "$@";;
down)
shift
command docker compose down --timeout 3 "$@";;
*)
command docker compose "$@";;
esac
;;
*)
command docker "$@";;
esac
}

2
test/webdav/run.sh Normal file → Executable file
View File

@@ -61,5 +61,3 @@ docker run --rm \
ash -c 'test ! -f /webdav_data/data/my/new/path/test-hostnametoken-old.tar.gz && test -f /webdav_data/data/my/new/path/test-hostnametoken.tar.gz'
pass "Old remote backup has been pruned, new one is still present."
docker compose down --volumes

View File

@@ -9,7 +9,7 @@ current_test=$(basename $(pwd))
docker network create test_network
docker volume create app_data
mkdir -p local
LOCAL_DIR=$(mktemp -d)
docker run -d -q \
--name offen \
@@ -22,7 +22,7 @@ sleep 10
docker run --rm -q \
--network test_network \
-v app_data:/backup/app_data \
-v ./local:/archive \
-v $LOCAL_DIR:/archive \
-v /var/run/docker.sock:/var/run/docker.sock \
--env BACKUP_COMPRESSION=zst \
--env BACKUP_FILENAME='test.{{ .Extension }}' \
@@ -30,7 +30,7 @@ docker run --rm -q \
offen/docker-volume-backup:${TEST_VERSION:-canary}
tmp_dir=$(mktemp -d)
tar -xvf ./local/test.tar.zst --zstd -C $tmp_dir
tar -xvf "$LOCAL_DIR/test.tar.zst" --zstd -C $tmp_dir
if [ ! -f "$tmp_dir/backup/app_data/offen.db" ]; then
fail "Could not find expected file in untared archive."
fi
@@ -39,8 +39,3 @@ pass "Found relevant files in untared local backup."
# This test does not stop containers during backup. This is happening on
# purpose in order to cover this setup as well.
expect_running_containers "1"
docker rm $(docker stop offen)
docker volume rm app_data
docker network rm test_network