mirror of
https://github.com/offen/docker-volume-backup.git
synced 2025-12-06 09:38:01 +01:00
Compare commits
2 Commits
serve-metr
...
test-post-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24796928e9 | ||
|
|
81e36289c1 |
75
.circleci/config.yml
Normal file
75
.circleci/config.yml
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
version: 2.1
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
canary:
|
||||||
|
machine:
|
||||||
|
image: ubuntu-2004:202201-02
|
||||||
|
working_directory: ~/docker-volume-backup
|
||||||
|
resource_class: large
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- run:
|
||||||
|
name: Build
|
||||||
|
command: |
|
||||||
|
docker build . -t offen/docker-volume-backup:canary
|
||||||
|
- run:
|
||||||
|
name: Install gnupg
|
||||||
|
command: |
|
||||||
|
sudo apt-get install -y gnupg
|
||||||
|
- run:
|
||||||
|
name: Run tests
|
||||||
|
working_directory: ~/docker-volume-backup/test
|
||||||
|
command: |
|
||||||
|
export GPG_TTY=$(tty)
|
||||||
|
./test.sh canary
|
||||||
|
|
||||||
|
build:
|
||||||
|
docker:
|
||||||
|
- image: cimg/base:2020.06
|
||||||
|
environment:
|
||||||
|
DOCKER_BUILDKIT: '1'
|
||||||
|
DOCKER_CLI_EXPERIMENTAL: enabled
|
||||||
|
working_directory: ~/docker-volume-backup
|
||||||
|
resource_class: large
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- setup_remote_docker:
|
||||||
|
version: 20.10.6
|
||||||
|
- docker/install-docker-credential-helper:
|
||||||
|
release-tag: v0.6.4
|
||||||
|
- docker/configure-docker-credentials-store
|
||||||
|
- run:
|
||||||
|
name: Push to Docker Hub
|
||||||
|
command: |
|
||||||
|
echo "$DOCKER_ACCESSTOKEN" | docker login --username offen --password-stdin
|
||||||
|
# This is required for building ARM: https://gitlab.alpinelinux.org/alpine/aports/-/issues/12406
|
||||||
|
docker run --rm --privileged linuxkit/binfmt:v0.8
|
||||||
|
docker context create docker-volume-backup
|
||||||
|
docker buildx create docker-volume-backup --name docker-volume-backup --use
|
||||||
|
docker buildx inspect --bootstrap
|
||||||
|
tag_args="-t offen/docker-volume-backup:$CIRCLE_TAG"
|
||||||
|
if [[ "$CIRCLE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
# prerelease tags like `v2.0.0-alpha.1` should not be released as `latest`
|
||||||
|
tag_args="$tag_args -t offen/docker-volume-backup:latest"
|
||||||
|
tag_args="$tag_args -t offen/docker-volume-backup:$(echo "$CIRCLE_TAG" | cut -d. -f1)"
|
||||||
|
fi
|
||||||
|
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 \
|
||||||
|
$tag_args . --push
|
||||||
|
|
||||||
|
workflows:
|
||||||
|
version: 2
|
||||||
|
docker_image:
|
||||||
|
jobs:
|
||||||
|
- canary:
|
||||||
|
filters:
|
||||||
|
tags:
|
||||||
|
ignore: /^v.*/
|
||||||
|
- build:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
ignore: /.*/
|
||||||
|
tags:
|
||||||
|
only: /^v.*/
|
||||||
|
|
||||||
|
orbs:
|
||||||
|
docker: circleci/docker@2.1.4
|
||||||
10
.github/dependabot.yml
vendored
10
.github/dependabot.yml
vendored
@@ -1,10 +0,0 @@
|
|||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: docker
|
|
||||||
directory: /
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
- package-ecosystem: gomod
|
|
||||||
directory: /
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
59
.github/workflows/release.yml
vendored
59
.github/workflows/release.yml
vendored
@@ -1,59 +0,0 @@
|
|||||||
name: Release Docker Image
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags: v**
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
push_to_registries:
|
|
||||||
name: Push Docker image to multiple registries
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
packages: write
|
|
||||||
contents: read
|
|
||||||
steps:
|
|
||||||
- name: Check out the repo
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v2
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Log in to GHCR
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Extract Docker tags
|
|
||||||
id: meta
|
|
||||||
run: |
|
|
||||||
version_tag="${{github.ref_name}}"
|
|
||||||
tags=($version_tag)
|
|
||||||
if [[ "$version_tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
||||||
# prerelease tags like `v2.0.0-alpha.1` should not be released as `latest` nor `v2`
|
|
||||||
tags+=("latest")
|
|
||||||
tags+=($(echo "$version_tag" | cut -d. -f1))
|
|
||||||
fi
|
|
||||||
releases=""
|
|
||||||
for tag in "${tags[@]}"; do
|
|
||||||
releases="${releases:+$releases,}offen/docker-volume-backup:$tag,ghcr.io/offen/docker-volume-backup:$tag"
|
|
||||||
done
|
|
||||||
echo "releases=$releases" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Build and push Docker images
|
|
||||||
uses: docker/build-push-action@v4
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
|
||||||
tags: ${{ steps.meta.outputs.releases }}
|
|
||||||
30
.github/workflows/test.yml
vendored
30
.github/workflows/test.yml
vendored
@@ -1,30 +0,0 @@
|
|||||||
name: Run Integration Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- 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
|
|
||||||
@@ -9,13 +9,15 @@ RUN go mod download
|
|||||||
WORKDIR /app/cmd/backup
|
WORKDIR /app/cmd/backup
|
||||||
RUN go build -o backup .
|
RUN go build -o backup .
|
||||||
|
|
||||||
FROM alpine:3.18
|
FROM alpine:3.17
|
||||||
|
|
||||||
WORKDIR /root
|
WORKDIR /root
|
||||||
|
|
||||||
RUN apk add --no-cache ca-certificates busybox-extras
|
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
|
||||||
COPY --chmod=755 ./entrypoint.sh /root/
|
|
||||||
|
COPY ./entrypoint.sh /root/
|
||||||
|
RUN chmod +x entrypoint.sh
|
||||||
|
|
||||||
ENTRYPOINT ["/root/entrypoint.sh"]
|
ENTRYPOINT ["/root/entrypoint.sh"]
|
||||||
|
|||||||
66
README.md
66
README.md
@@ -14,7 +14,6 @@ It handles __recurring or one-off backups of Docker volumes__ to a __local direc
|
|||||||
- [Quickstart](#quickstart)
|
- [Quickstart](#quickstart)
|
||||||
- [Recurring backups in a compose setup](#recurring-backups-in-a-compose-setup)
|
- [Recurring backups in a compose setup](#recurring-backups-in-a-compose-setup)
|
||||||
- [One-off backups using Docker CLI](#one-off-backups-using-docker-cli)
|
- [One-off backups using Docker CLI](#one-off-backups-using-docker-cli)
|
||||||
- [Available image registries](#available-image-registries)
|
|
||||||
- [Configuration reference](#configuration-reference)
|
- [Configuration reference](#configuration-reference)
|
||||||
- [How to](#how-to)
|
- [How to](#how-to)
|
||||||
- [Stop containers during backup](#stop-containers-during-backup)
|
- [Stop containers during backup](#stop-containers-during-backup)
|
||||||
@@ -31,7 +30,6 @@ It handles __recurring or one-off backups of Docker volumes__ to a __local direc
|
|||||||
- [Replace deprecated `BACKUP_FROM_SNAPSHOT` usage](#replace-deprecated-backup_from_snapshot-usage)
|
- [Replace deprecated `BACKUP_FROM_SNAPSHOT` usage](#replace-deprecated-backup_from_snapshot-usage)
|
||||||
- [Replace deprecated `exec-pre` and `exec-post` labels](#replace-deprecated-exec-pre-and-exec-post-labels)
|
- [Replace deprecated `exec-pre` and `exec-post` labels](#replace-deprecated-exec-pre-and-exec-post-labels)
|
||||||
- [Using a custom Docker host](#using-a-custom-docker-host)
|
- [Using a custom Docker host](#using-a-custom-docker-host)
|
||||||
- [Use with rootless Docker](#use-with-rootless-docker)
|
|
||||||
- [Run multiple backup schedules in the same container](#run-multiple-backup-schedules-in-the-same-container)
|
- [Run multiple backup schedules in the same container](#run-multiple-backup-schedules-in-the-same-container)
|
||||||
- [Define different retention schedules](#define-different-retention-schedules)
|
- [Define different retention schedules](#define-different-retention-schedules)
|
||||||
- [Use special characters in notification URLs](#use-special-characters-in-notification-urls)
|
- [Use special characters in notification URLs](#use-special-characters-in-notification-urls)
|
||||||
@@ -122,18 +120,6 @@ docker run --rm \
|
|||||||
|
|
||||||
Alternatively, pass a `--env-file` in order to use a full config as described below.
|
Alternatively, pass a `--env-file` in order to use a full config as described below.
|
||||||
|
|
||||||
### Available image registries
|
|
||||||
|
|
||||||
This Docker image is published to both Docker Hub and the GitHub container registry.
|
|
||||||
Depending on your preferences and needs, you can reference both `offen/docker-volume-backup` as well as `ghcr.io/offen/docker-volume-backup`:
|
|
||||||
|
|
||||||
```
|
|
||||||
docker pull offen/docker-volume-backup:v2
|
|
||||||
docker pull ghcr.io/offen/docker-volume-backup:v2
|
|
||||||
```
|
|
||||||
|
|
||||||
Documentation references Docker Hub, but all examples will work using ghcr.io just as well.
|
|
||||||
|
|
||||||
## Configuration reference
|
## Configuration reference
|
||||||
|
|
||||||
Backup targets, schedule and retention are configured in environment variables.
|
Backup targets, schedule and retention are configured in environment variables.
|
||||||
@@ -260,15 +246,6 @@ You can populate below template according to your requirements and use it as you
|
|||||||
|
|
||||||
# AWS_STORAGE_CLASS="GLACIER"
|
# AWS_STORAGE_CLASS="GLACIER"
|
||||||
|
|
||||||
# Setting this variable will change the S3 default part size for the copy step.
|
|
||||||
# This value is useful when you want to upload large files.
|
|
||||||
# NB : While using Scaleway as S3 provider, be aware that the parts counter is set to 1.000.
|
|
||||||
# While Minio uses a hard coded value to 10.000. As a workaround, try to set a higher value.
|
|
||||||
# Defaults to "16" (MB) if unset (from minio), you can set this value according to your needs.
|
|
||||||
# The unit is in MB and an integer.
|
|
||||||
|
|
||||||
# AWS_PART_SIZE=16
|
|
||||||
|
|
||||||
# You can also backup files to any WebDAV server:
|
# You can also backup files to any WebDAV server:
|
||||||
|
|
||||||
# The URL of the remote WebDAV server
|
# The URL of the remote WebDAV server
|
||||||
@@ -433,7 +410,7 @@ You can populate below template according to your requirements and use it as you
|
|||||||
|
|
||||||
# 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.
|
||||||
# Configuration is provided as a comma-separated list of URLs as consumed
|
# Configuration is provided as a comma-separated list of URLs as consumed
|
||||||
# by `shoutrrr`: https://containrrr.dev/shoutrrr/0.7/services/overview/
|
# by `shoutrrr`: https://containrrr.dev/shoutrrr/v0.5/services/overview/
|
||||||
# The content of such notifications can be customized. Dedicated documentation
|
# The content of such notifications can be customized. Dedicated documentation
|
||||||
# on how to do this can be found in the README. When providing multiple URLs or
|
# on how to do this can be found in the README. When providing multiple URLs or
|
||||||
# an URL that contains a comma, the values can be URL encoded to avoid ambiguities.
|
# an URL that contains a comma, the values can be URL encoded to avoid ambiguities.
|
||||||
@@ -577,7 +554,7 @@ services:
|
|||||||
Notification backends other than email are also supported.
|
Notification backends other than email are also supported.
|
||||||
Refer to the documentation of [shoutrrr][shoutrrr-docs] to find out about options and configuration.
|
Refer to the documentation of [shoutrrr][shoutrrr-docs] to find out about options and configuration.
|
||||||
|
|
||||||
[shoutrrr-docs]: https://containrrr.dev/shoutrrr/0.7/services/overview/
|
[shoutrrr-docs]: https://containrrr.dev/shoutrrr/v0.5/services/overview/
|
||||||
|
|
||||||
### Customize notifications
|
### Customize notifications
|
||||||
|
|
||||||
@@ -666,24 +643,6 @@ volumes:
|
|||||||
The backup procedure is guaranteed to wait for all `pre` or `post` commands to finish before proceeding.
|
The backup procedure is guaranteed to wait for all `pre` or `post` commands to finish before proceeding.
|
||||||
However there are no guarantees about the order in which they are run, which could also happen concurrently.
|
However there are no guarantees about the order in which they are run, which could also happen concurrently.
|
||||||
|
|
||||||
By default the backup command is executed by the user provided by the container's image.
|
|
||||||
It is possible to specify a custom user that is used to run commands in dedicated labels with the format `docker-volume-backup.[step]-[pre|post].user`:
|
|
||||||
|
|
||||||
```yml
|
|
||||||
version: '3'
|
|
||||||
|
|
||||||
services:
|
|
||||||
gitea:
|
|
||||||
image: gitea/gitea
|
|
||||||
volumes:
|
|
||||||
- backup_data:/tmp
|
|
||||||
labels:
|
|
||||||
- docker-volume-backup.archive-pre.user=git
|
|
||||||
- docker-volume-backup.archive-pre=/bin/bash -c 'cd /tmp; /usr/local/bin/gitea dump -c /data/gitea/conf/app.ini -R -f dump.zip'
|
|
||||||
```
|
|
||||||
|
|
||||||
Make sure the user exists and is present in `passwd` inside the target container.
|
|
||||||
|
|
||||||
### 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.
|
||||||
@@ -823,7 +782,7 @@ services:
|
|||||||
- docker-volume-backup.archive-post=rm -rf /tmp/backup/my-app
|
- docker-volume-backup.archive-post=rm -rf /tmp/backup/my-app
|
||||||
|
|
||||||
backup:
|
backup:
|
||||||
image: offen/docker-volume-backup:v2
|
image: offen/docker-volume-backup:latest
|
||||||
environment:
|
environment:
|
||||||
BACKUP_SOURCES: /tmp/backup
|
BACKUP_SOURCES: /tmp/backup
|
||||||
volumes:
|
volumes:
|
||||||
@@ -861,23 +820,6 @@ 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.
|
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.
|
||||||
|
|
||||||
### Use with rootless Docker
|
|
||||||
|
|
||||||
It's also possible to use this image with a [rootless Docker installation][rootless-docker].
|
|
||||||
Instead of mounting `/var/run/docker.sock`, mount the user-specific socket into the container:
|
|
||||||
|
|
||||||
```yml
|
|
||||||
services:
|
|
||||||
backup:
|
|
||||||
image: offen/docker-volume-backup:v2
|
|
||||||
# ... configuration omitted
|
|
||||||
volumes:
|
|
||||||
- backup:/backup:ro
|
|
||||||
- /run/user/1000/docker.sock:/var/run/docker.sock:ro
|
|
||||||
```
|
|
||||||
|
|
||||||
[rootless-docker]: https://docs.docker.com/engine/security/rootless/
|
|
||||||
|
|
||||||
### Run multiple backup schedules in the same container
|
### 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`:
|
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`:
|
||||||
@@ -928,7 +870,7 @@ BACKUP_SOURCES=/backup/app2_data
|
|||||||
|
|
||||||
If you want to manage backup retention on different schedules, the most straight forward approach is to define a dedicated configuration for retention rule using a different prefix in the `BACKUP_FILENAME` parameter and then run them on different cron schedules.
|
If you want to manage backup retention on different schedules, the most straight forward approach is to define a dedicated configuration for retention rule using a different prefix in the `BACKUP_FILENAME` parameter and then run them on different cron schedules.
|
||||||
|
|
||||||
For example, if you wanted to keep daily backups for 7 days, weekly backups for a month, and retain monthly backups forever, you could create three configuration files and mount them into `/etc/dockervolumebackup/conf.d`:
|
For example, if you wanted to keep daily backups for 7 days, weekly backups for a month, and retain monthly backups forever, you could create three configuration files and mount them into `/etc/dockervolumebackup.d`:
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
# 01daily.conf
|
# 01daily.conf
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ type Config struct {
|
|||||||
AwsSecretAccessKey string `split_words:"true"`
|
AwsSecretAccessKey string `split_words:"true"`
|
||||||
AwsSecretAccessKeyFile string `split_words:"true"`
|
AwsSecretAccessKeyFile string `split_words:"true"`
|
||||||
AwsIamRoleEndpoint string `split_words:"true"`
|
AwsIamRoleEndpoint string `split_words:"true"`
|
||||||
AwsPartSize int64 `split_words:"true"`
|
|
||||||
BackupSources string `split_words:"true" default:"/backup"`
|
BackupSources string `split_words:"true" default:"/backup"`
|
||||||
BackupFilename string `split_words:"true" default:"backup-%Y-%m-%dT%H-%M-%S.tar.gz"`
|
BackupFilename string `split_words:"true" default:"backup-%Y-%m-%dT%H-%M-%S.tar.gz"`
|
||||||
BackupFilenameExpand bool `split_words:"true"`
|
BackupFilenameExpand bool `split_words:"true"`
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import (
|
|||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *script) exec(containerRef string, command string, user string) ([]byte, []byte, error) {
|
func (s *script) exec(containerRef string, command string) ([]byte, []byte, error) {
|
||||||
args, _ := argv.Argv(command, nil, nil)
|
args, _ := argv.Argv(command, nil, nil)
|
||||||
commandEnv := []string{
|
commandEnv := []string{
|
||||||
fmt.Sprintf("COMMAND_RUNTIME_ARCHIVE_FILEPATH=%s", s.file),
|
fmt.Sprintf("COMMAND_RUNTIME_ARCHIVE_FILEPATH=%s", s.file),
|
||||||
@@ -31,7 +31,6 @@ func (s *script) exec(containerRef string, command string, user string) ([]byte,
|
|||||||
AttachStdin: true,
|
AttachStdin: true,
|
||||||
AttachStderr: true,
|
AttachStderr: true,
|
||||||
Env: commandEnv,
|
Env: commandEnv,
|
||||||
User: user,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("exec: error creating container exec: %w", err)
|
return nil, nil, fmt.Errorf("exec: error creating container exec: %w", err)
|
||||||
@@ -91,6 +90,7 @@ func (s *script) runLabeledCommands(label string) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
containersWithCommand, err := s.cli.ContainerList(context.Background(), types.ContainerListOptions{
|
containersWithCommand, err := s.cli.ContainerList(context.Background(), types.ContainerListOptions{
|
||||||
|
Quiet: true,
|
||||||
Filters: filters.NewArgs(f...),
|
Filters: filters.NewArgs(f...),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -104,6 +104,7 @@ func (s *script) runLabeledCommands(label string) error {
|
|||||||
Value: "docker-volume-backup.exec-pre",
|
Value: "docker-volume-backup.exec-pre",
|
||||||
}
|
}
|
||||||
deprecatedContainers, err := s.cli.ContainerList(context.Background(), types.ContainerListOptions{
|
deprecatedContainers, err := s.cli.ContainerList(context.Background(), types.ContainerListOptions{
|
||||||
|
Quiet: true,
|
||||||
Filters: filters.NewArgs(f...),
|
Filters: filters.NewArgs(f...),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -121,6 +122,7 @@ func (s *script) runLabeledCommands(label string) error {
|
|||||||
Value: "docker-volume-backup.exec-post",
|
Value: "docker-volume-backup.exec-post",
|
||||||
}
|
}
|
||||||
deprecatedContainers, err := s.cli.ContainerList(context.Background(), types.ContainerListOptions{
|
deprecatedContainers, err := s.cli.ContainerList(context.Background(), types.ContainerListOptions{
|
||||||
|
Quiet: true,
|
||||||
Filters: filters.NewArgs(f...),
|
Filters: filters.NewArgs(f...),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -157,11 +159,8 @@ func (s *script) runLabeledCommands(label string) error {
|
|||||||
cmd, _ = c.Labels["docker-volume-backup.exec-post"]
|
cmd, _ = c.Labels["docker-volume-backup.exec-post"]
|
||||||
}
|
}
|
||||||
|
|
||||||
userLabelName := fmt.Sprintf("%s.user", label)
|
|
||||||
user := c.Labels[userLabelName]
|
|
||||||
|
|
||||||
s.logger.Infof("Running %s command %s for container %s", label, cmd, strings.TrimPrefix(c.Names[0], "/"))
|
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, user)
|
stdout, stderr, err := s.exec(c.ID, cmd)
|
||||||
if s.c.ExecForwardOutput {
|
if s.c.ExecForwardOutput {
|
||||||
os.Stderr.Write(stderr)
|
os.Stderr.Write(stderr)
|
||||||
os.Stdout.Write(stdout)
|
os.Stdout.Write(stdout)
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import (
|
|||||||
"github.com/containrrr/shoutrrr"
|
"github.com/containrrr/shoutrrr"
|
||||||
"github.com/containrrr/shoutrrr/pkg/router"
|
"github.com/containrrr/shoutrrr/pkg/router"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
ctr "github.com/docker/docker/api/types/container"
|
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
"github.com/docker/docker/api/types/swarm"
|
"github.com/docker/docker/api/types/swarm"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
@@ -111,7 +110,7 @@ func newScript() (*script, error) {
|
|||||||
s.cli = cli
|
s.cli = cli
|
||||||
}
|
}
|
||||||
|
|
||||||
logFunc := func(logType storage.LogLevel, context string, msg string, params ...any) {
|
logFunc := func(logType storage.LogLevel, context string, msg string, params ...interface{}) {
|
||||||
switch logType {
|
switch logType {
|
||||||
case storage.LogLevelWarning:
|
case storage.LogLevelWarning:
|
||||||
s.logger.Warnf("["+context+"] "+msg, params...)
|
s.logger.Warnf("["+context+"] "+msg, params...)
|
||||||
@@ -143,7 +142,6 @@ func newScript() (*script, error) {
|
|||||||
BucketName: s.c.AwsS3BucketName,
|
BucketName: s.c.AwsS3BucketName,
|
||||||
StorageClass: s.c.AwsStorageClass,
|
StorageClass: s.c.AwsStorageClass,
|
||||||
CACert: s.c.AwsEndpointCACert.Cert,
|
CACert: s.c.AwsEndpointCACert.Cert,
|
||||||
PartSize: s.c.AwsPartSize,
|
|
||||||
}
|
}
|
||||||
if s3Backend, err := s3.NewStorageBackend(s3Config, logFunc); err != nil {
|
if s3Backend, err := s3.NewStorageBackend(s3Config, logFunc); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -282,7 +280,9 @@ func (s *script) stopContainers() (func() error, error) {
|
|||||||
return noop, nil
|
return noop, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
allContainers, err := s.cli.ContainerList(context.Background(), types.ContainerListOptions{})
|
allContainers, err := s.cli.ContainerList(context.Background(), types.ContainerListOptions{
|
||||||
|
Quiet: true,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return noop, fmt.Errorf("stopContainers: error querying for containers: %w", err)
|
return noop, fmt.Errorf("stopContainers: error querying for containers: %w", err)
|
||||||
}
|
}
|
||||||
@@ -292,6 +292,7 @@ func (s *script) stopContainers() (func() error, error) {
|
|||||||
s.c.BackupStopContainerLabel,
|
s.c.BackupStopContainerLabel,
|
||||||
)
|
)
|
||||||
containersToStop, err := s.cli.ContainerList(context.Background(), types.ContainerListOptions{
|
containersToStop, err := s.cli.ContainerList(context.Background(), types.ContainerListOptions{
|
||||||
|
Quiet: true,
|
||||||
Filters: filters.NewArgs(filters.KeyValuePair{
|
Filters: filters.NewArgs(filters.KeyValuePair{
|
||||||
Key: "label",
|
Key: "label",
|
||||||
Value: containerLabel,
|
Value: containerLabel,
|
||||||
@@ -316,7 +317,7 @@ func (s *script) stopContainers() (func() error, error) {
|
|||||||
var stoppedContainers []types.Container
|
var stoppedContainers []types.Container
|
||||||
var stopErrors []error
|
var stopErrors []error
|
||||||
for _, container := range containersToStop {
|
for _, container := range containersToStop {
|
||||||
if err := s.cli.ContainerStop(context.Background(), container.ID, ctr.StopOptions{}); err != nil {
|
if err := s.cli.ContainerStop(context.Background(), container.ID, nil); err != nil {
|
||||||
stopErrors = append(stopErrors, err)
|
stopErrors = append(stopErrors, err)
|
||||||
} else {
|
} else {
|
||||||
stoppedContainers = append(stoppedContainers, container)
|
stoppedContainers = append(stoppedContainers, container)
|
||||||
@@ -365,7 +366,7 @@ func (s *script) stopContainers() (func() error, error) {
|
|||||||
if serviceMatch.ID == "" {
|
if serviceMatch.ID == "" {
|
||||||
return fmt.Errorf("stopContainers: couldn't find service with name %s", serviceName)
|
return fmt.Errorf("stopContainers: couldn't find service with name %s", serviceName)
|
||||||
}
|
}
|
||||||
serviceMatch.Spec.TaskTemplate.ForceUpdate += 1
|
serviceMatch.Spec.TaskTemplate.ForceUpdate = 1
|
||||||
if _, err := s.cli.ServiceUpdate(
|
if _, err := s.cli.ServiceUpdate(
|
||||||
context.Background(), serviceMatch.ID,
|
context.Background(), serviceMatch.ID,
|
||||||
serviceMatch.Version, serviceMatch.Spec, types.ServiceUpdateOptions{},
|
serviceMatch.Version, serviceMatch.Spec, types.ServiceUpdateOptions{},
|
||||||
|
|||||||
@@ -22,12 +22,5 @@ else
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -z "$SERVE_METRICS_PATH" ]; then
|
|
||||||
mkdir -p /var/www/html${SERVE_METRICS_PATH}
|
|
||||||
echo "ok" > /var/www/html${SERVE_METRICS_PATH}/metrics.txt
|
|
||||||
httpd -h /var/www/html -p "${SERVE_METRICS_PORT:-80}"
|
|
||||||
echo "Serving metrics on port ${SERVE_METRICS_PORT:-80}."
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Starting cron in foreground."
|
echo "Starting cron in foreground."
|
||||||
crond -f -d 8
|
crond -f -d 8
|
||||||
|
|||||||
69
go.mod
69
go.mod
@@ -3,59 +3,72 @@ module github.com/offen/docker-volume-backup
|
|||||||
go 1.19
|
go 1.19
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.1.0
|
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.6.1
|
||||||
github.com/containrrr/shoutrrr v0.7.1
|
github.com/containrrr/shoutrrr v0.5.2
|
||||||
github.com/cosiner/argv v0.1.0
|
github.com/cosiner/argv v0.1.0
|
||||||
github.com/docker/docker v24.0.5+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/minio/minio-go/v7 v7.0.61
|
github.com/minio/minio-go/v7 v7.0.44
|
||||||
github.com/otiai10/copy v1.11.0
|
github.com/otiai10/copy v1.7.0
|
||||||
github.com/pkg/sftp v1.13.5
|
github.com/pkg/sftp v1.13.5
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.0
|
||||||
github.com/studio-b12/gowebdav v0.9.0
|
github.com/studio-b12/gowebdav v0.0.0-20220128162035-c7b1ff8a5e62
|
||||||
golang.org/x/crypto v0.11.0
|
golang.org/x/crypto v0.3.0
|
||||||
golang.org/x/sync v0.3.0
|
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.4 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.1 // indirect
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 // indirect
|
github.com/AzureAD/microsoft-authentication-library-for-go v0.7.0 // indirect
|
||||||
github.com/Microsoft/go-winio v0.5.2 // indirect
|
github.com/Microsoft/go-winio v0.5.2 // indirect
|
||||||
github.com/docker/distribution v2.8.2+incompatible // indirect
|
github.com/containerd/containerd v1.6.6 // indirect
|
||||||
|
github.com/docker/distribution v2.7.1+incompatible // indirect
|
||||||
github.com/docker/go-connections v0.4.0 // indirect
|
github.com/docker/go-connections v0.4.0 // indirect
|
||||||
github.com/docker/go-units v0.4.0 // indirect
|
github.com/docker/go-units v0.4.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||||
github.com/fatih/color v1.13.0 // indirect
|
github.com/fatih/color v1.10.0 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
|
||||||
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
|
github.com/gorilla/mux v1.7.3 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/compress v1.16.7 // indirect
|
github.com/klauspost/compress v1.15.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.1 // indirect
|
||||||
github.com/kr/fs v0.1.0 // indirect
|
github.com/kr/fs v0.1.0 // indirect
|
||||||
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
github.com/mattn/go-isatty v0.0.12 // indirect
|
||||||
github.com/minio/md5-simd v1.1.2 // indirect
|
github.com/minio/md5-simd v1.1.2 // indirect
|
||||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
github.com/minio/sha256-simd v1.0.0 // indirect
|
||||||
github.com/moby/term v0.0.0-20200312100748-672ec06f55cd // indirect
|
github.com/moby/term v0.0.0-20200312100748-672ec06f55cd // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/morikuni/aec v1.0.0 // indirect
|
github.com/morikuni/aec v1.0.0 // indirect
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||||
|
github.com/nxadm/tail v1.4.6 // indirect
|
||||||
|
github.com/onsi/ginkgo v1.14.2 // indirect
|
||||||
|
github.com/onsi/gomega v1.10.3 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect
|
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect
|
||||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
|
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/rs/xid v1.5.0 // indirect
|
github.com/rs/xid v1.4.0 // indirect
|
||||||
golang.org/x/net v0.12.0 // indirect
|
golang.org/x/net v0.2.0 // indirect
|
||||||
golang.org/x/sys v0.10.0 // indirect
|
golang.org/x/sys v0.2.0 // indirect
|
||||||
golang.org/x/text v0.11.0 // indirect
|
golang.org/x/text v0.4.0 // indirect
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||||
|
google.golang.org/genproto v0.0.0-20220602131408-e326c6e8e9c8 // indirect
|
||||||
|
google.golang.org/grpc v1.47.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.28.0 // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gotest.tools/v3 v3.0.3 // indirect
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
@@ -23,7 +22,6 @@ type s3Storage struct {
|
|||||||
client *minio.Client
|
client *minio.Client
|
||||||
bucket string
|
bucket string
|
||||||
storageClass string
|
storageClass string
|
||||||
partSize int64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config contains values that define the configuration of a S3 backend.
|
// Config contains values that define the configuration of a S3 backend.
|
||||||
@@ -37,7 +35,6 @@ type Config struct {
|
|||||||
RemotePath string
|
RemotePath string
|
||||||
BucketName string
|
BucketName string
|
||||||
StorageClass string
|
StorageClass string
|
||||||
PartSize int64
|
|
||||||
CACert *x509.Certificate
|
CACert *x509.Certificate
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +89,6 @@ func NewStorageBackend(opts Config, logFunc storage.Log) (storage.Backend, error
|
|||||||
client: mc,
|
client: mc,
|
||||||
bucket: opts.BucketName,
|
bucket: opts.BucketName,
|
||||||
storageClass: opts.StorageClass,
|
storageClass: opts.StorageClass,
|
||||||
partSize: opts.PartSize,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,32 +100,16 @@ func (v *s3Storage) Name() string {
|
|||||||
// Copy copies the given file to the S3/Minio storage backend.
|
// Copy copies the given file to the S3/Minio storage backend.
|
||||||
func (b *s3Storage) Copy(file string) error {
|
func (b *s3Storage) Copy(file string) error {
|
||||||
_, name := path.Split(file)
|
_, name := path.Split(file)
|
||||||
putObjectOptions := minio.PutObjectOptions{
|
|
||||||
|
if _, err := b.client.FPutObject(context.Background(), b.bucket, filepath.Join(b.DestinationPath, name), file, minio.PutObjectOptions{
|
||||||
ContentType: "application/tar+gzip",
|
ContentType: "application/tar+gzip",
|
||||||
StorageClass: b.storageClass,
|
StorageClass: b.storageClass,
|
||||||
}
|
}); err != nil {
|
||||||
|
|
||||||
if b.partSize > 0 {
|
|
||||||
srcFileInfo, err := os.Stat(file)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("(*s3Storage).Copy: error reading the local file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, partSize, _, err := minio.OptimalPartInfo(srcFileInfo.Size(), uint64(b.partSize*1024*1024))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("(*s3Storage).Copy: error computing the optimal s3 part size: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
putObjectOptions.PartSize = uint64(partSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := b.client.FPutObject(context.Background(), b.bucket, filepath.Join(b.DestinationPath, name), file, putObjectOptions); err != nil {
|
|
||||||
if errResp := minio.ToErrorResponse(err); errResp.Message != "" {
|
if errResp := minio.ToErrorResponse(err); errResp.Message != "" {
|
||||||
return fmt.Errorf("(*s3Storage).Copy: error uploading backup to remote storage: [Message]: '%s', [Code]: %s, [StatusCode]: %d", errResp.Message, errResp.Code, errResp.StatusCode)
|
return fmt.Errorf("(*s3Storage).Copy: error uploading backup to remote storage: [Message]: '%s', [Code]: %s, [StatusCode]: %d", errResp.Message, errResp.Code, errResp.StatusCode)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("(*s3Storage).Copy: error uploading backup to remote storage: %w", err)
|
return fmt.Errorf("(*s3Storage).Copy: error uploading backup to remote storage: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
b.Log(storage.LogLevelInfo, b.Name(), "Uploaded a copy of backup `%s` to bucket `%s`.", file, b.bucket)
|
b.Log(storage.LogLevelInfo, b.Name(), "Uploaded a copy of backup `%s` to bucket `%s`.", file, b.bucket)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const (
|
|||||||
LogLevelError
|
LogLevelError
|
||||||
)
|
)
|
||||||
|
|
||||||
type Log func(logType LogLevel, context string, msg string, params ...any)
|
type Log func(logType LogLevel, context string, msg string, params ...interface{})
|
||||||
|
|
||||||
// PruneStats is a wrapper struct for returning stats after pruning
|
// PruneStats is a wrapper struct for returning stats after pruning
|
||||||
type PruneStats struct {
|
type PruneStats struct {
|
||||||
|
|||||||
@@ -6,18 +6,18 @@ cd "$(dirname "$0")"
|
|||||||
. ../util.sh
|
. ../util.sh
|
||||||
current_test=$(basename $(pwd))
|
current_test=$(basename $(pwd))
|
||||||
|
|
||||||
docker compose up -d
|
docker-compose up -d
|
||||||
sleep 5
|
sleep 5
|
||||||
|
|
||||||
# A symlink for a known file in the volume is created so the test can check
|
# A symlink for a known file in the volume is created so the test can check
|
||||||
# whether symlinks are preserved on backup.
|
# whether symlinks are preserved on backup.
|
||||||
docker compose exec backup backup
|
docker-compose exec backup backup
|
||||||
|
|
||||||
sleep 5
|
sleep 5
|
||||||
|
|
||||||
expect_running_containers "3"
|
expect_running_containers "3"
|
||||||
|
|
||||||
docker compose run --rm az_cli \
|
docker-compose run --rm az_cli \
|
||||||
az storage blob download -f /dump/test.tar.gz -c test-container -n path/to/backup/test.tar.gz
|
az storage blob download -f /dump/test.tar.gz -c test-container -n path/to/backup/test.tar.gz
|
||||||
tar -xvf ./local/test.tar.gz -C /tmp && test -f /tmp/backup/app_data/offen.db
|
tar -xvf ./local/test.tar.gz -C /tmp && test -f /tmp/backup/app_data/offen.db
|
||||||
|
|
||||||
@@ -26,15 +26,15 @@ pass "Found relevant files in untared remote backups."
|
|||||||
# The second part of this test checks if backups get deleted when the retention
|
# 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)
|
# is set to 0 days (which it should not as it would mean all backups get deleted)
|
||||||
# TODO: find out if we can test actual deletion without having to wait for a day
|
# TODO: find out if we can test actual deletion without having to wait for a day
|
||||||
BACKUP_RETENTION_DAYS="0" docker compose up -d
|
BACKUP_RETENTION_DAYS="0" docker-compose up -d
|
||||||
sleep 5
|
sleep 5
|
||||||
|
|
||||||
docker compose exec backup backup
|
docker-compose exec backup backup
|
||||||
|
|
||||||
docker compose run --rm az_cli \
|
docker-compose run --rm az_cli \
|
||||||
az storage blob download -f /dump/test.tar.gz -c test-container -n path/to/backup/test.tar.gz
|
az storage blob download -f /dump/test.tar.gz -c test-container -n path/to/backup/test.tar.gz
|
||||||
test -f ./local/test.tar.gz
|
test -f ./local/test.tar.gz
|
||||||
|
|
||||||
pass "Remote backups have not been deleted."
|
pass "Remote backups have not been deleted."
|
||||||
|
|
||||||
docker compose down --volumes
|
docker-compose down --volumes
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ sleep 5
|
|||||||
|
|
||||||
expect_running_containers "3"
|
expect_running_containers "3"
|
||||||
|
|
||||||
docker run --rm \
|
docker run --rm -it \
|
||||||
-v minio_backup_data:/minio_data \
|
-v minio_backup_data:/minio_data \
|
||||||
alpine \
|
alpine \
|
||||||
ash -c 'tar -xvf /minio_data/backup/test.tar.gz -C /tmp && test -f /tmp/backup/app_data/offen.db'
|
ash -c 'tar -xvf /minio_data/backup/test.tar.gz -C /tmp && test -f /tmp/backup/app_data/offen.db'
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ docker run --rm \
|
|||||||
--entrypoint backup \
|
--entrypoint backup \
|
||||||
offen/docker-volume-backup:${TEST_VERSION:-canary}
|
offen/docker-volume-backup:${TEST_VERSION:-canary}
|
||||||
|
|
||||||
docker run --rm \
|
docker run --rm -it \
|
||||||
-v backup_data:/data alpine \
|
-v backup_data:/data alpine \
|
||||||
ash -c 'tar -xvf /data/backup/test.tar.gz && test -f /backup/app_data/offen.db && test -d /backup/empty_data'
|
ash -c 'tar -xvf /data/backup/test.tar.gz && test -f /backup/app_data/offen.db && test -d /backup/empty_data'
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ services:
|
|||||||
MARIADB_DATABASE: backup
|
MARIADB_DATABASE: backup
|
||||||
labels:
|
labels:
|
||||||
# this is testing the deprecated label on purpose
|
# this is testing the deprecated label on purpose
|
||||||
- docker-volume-backup.exec-pre=/bin/sh -c 'mysqldump -ptest --all-databases > /tmp/volume/dump.sql'
|
- docker-volume-backup.archive-pre=/bin/sh -c 'mysqldump -ptest --all-databases > /tmp/volume/dump.sql'
|
||||||
|
- docker-volume-backup.archive-post=/bin/sh -c 'rm /tmp/volume/dump.sql'
|
||||||
- docker-volume-backup.copy-post=/bin/sh -c 'echo "post" > /tmp/volume/post.txt'
|
- docker-volume-backup.copy-post=/bin/sh -c 'echo "post" > /tmp/volume/post.txt'
|
||||||
- docker-volume-backup.exec-label=test
|
- docker-volume-backup.exec-label=test
|
||||||
volumes:
|
volumes:
|
||||||
@@ -42,9 +43,10 @@ services:
|
|||||||
EXEC_LABEL: test
|
EXEC_LABEL: test
|
||||||
EXEC_FORWARD_OUTPUT: "true"
|
EXEC_FORWARD_OUTPUT: "true"
|
||||||
volumes:
|
volumes:
|
||||||
- ./local:/archive
|
- archive:/archive
|
||||||
- app_data:/backup/data:ro
|
- app_data:/backup/data:ro
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
app_data:
|
app_data:
|
||||||
|
archive:
|
||||||
|
|||||||
@@ -6,12 +6,11 @@ cd $(dirname $0)
|
|||||||
. ../util.sh
|
. ../util.sh
|
||||||
current_test=$(basename $(pwd))
|
current_test=$(basename $(pwd))
|
||||||
|
|
||||||
mkdir -p ./local
|
|
||||||
|
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
sleep 30 # mariadb likes to take a bit before responding
|
sleep 30 # mariadb likes to take a bit before responding
|
||||||
|
|
||||||
docker compose exec backup backup
|
docker compose exec backup backup
|
||||||
|
sudo cp -r $(docker volume inspect --format='{{ .Mountpoint }}' commands_archive) ./local
|
||||||
|
|
||||||
tar -xvf ./local/test.tar.gz
|
tar -xvf ./local/test.tar.gz
|
||||||
if [ ! -f ./backup/data/dump.sql ]; then
|
if [ ! -f ./backup/data/dump.sql ]; then
|
||||||
@@ -35,7 +34,6 @@ sudo rm -rf ./local
|
|||||||
|
|
||||||
info "Running commands test in swarm mode next."
|
info "Running commands test in swarm mode next."
|
||||||
|
|
||||||
mkdir -p ./local
|
|
||||||
docker swarm init
|
docker swarm init
|
||||||
|
|
||||||
docker stack deploy --compose-file=docker-compose.yml test_stack
|
docker stack deploy --compose-file=docker-compose.yml test_stack
|
||||||
@@ -49,6 +47,8 @@ sleep 20
|
|||||||
|
|
||||||
docker exec $(docker ps -q -f name=backup) backup
|
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
|
tar -xvf ./local/test.tar.gz
|
||||||
if [ ! -f ./backup/data/dump.sql ]; then
|
if [ ! -f ./backup/data/dump.sql ]; then
|
||||||
fail "Could not find file written by pre command."
|
fail "Could not find file written by pre command."
|
||||||
|
|||||||
@@ -8,10 +8,9 @@ current_test=$(basename $(pwd))
|
|||||||
|
|
||||||
mkdir -p local
|
mkdir -p local
|
||||||
|
|
||||||
export BASE_VERSION="${TEST_VERSION:-canary}"
|
|
||||||
export TEST_VERSION="${TEST_VERSION:-canary}-with-rsync"
|
export TEST_VERSION="${TEST_VERSION:-canary}-with-rsync"
|
||||||
|
|
||||||
docker build . -t offen/docker-volume-backup:$TEST_VERSION --build-arg version=$BASE_VERSION
|
docker build . -t offen/docker-volume-backup:$TEST_VERSION
|
||||||
|
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
sleep 5
|
sleep 5
|
||||||
|
|||||||
28
test/order/docker-compose.yml
Normal file
28
test/order/docker-compose.yml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
version: "3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
rsync:
|
||||||
|
image: eeacms/rsync
|
||||||
|
tty: true
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- docker-volume-backup.exec-label=order
|
||||||
|
- docker-volume-backup.archive-pre=sh -c "rsync -aAX --ignore-missing-args --delete-missing-args /data/ /bu/"
|
||||||
|
- docker-volume-backup.archive-post=sh -c "rm -rf /bu/*"
|
||||||
|
volumes:
|
||||||
|
- ./fixture:/data:ro
|
||||||
|
- bu:/bu
|
||||||
|
|
||||||
|
backup:
|
||||||
|
image: offen/docker-volume-backup:${TEST_VERSION:-canary}
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
BACKUP_FILENAME: backup.tar.gz
|
||||||
|
BACKUP_EXEC_LABEL: order
|
||||||
|
volumes:
|
||||||
|
- bu:/backup/order:ro
|
||||||
|
- ./local:/archive
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
bu:
|
||||||
1
test/order/fixture/test.txt
Normal file
1
test/order/fixture/test.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ok
|
||||||
26
test/order/run.sh
Normal file
26
test/order/run.sh
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd $(dirname $0)
|
||||||
|
. ../util.sh
|
||||||
|
current_test=$(basename $(pwd))
|
||||||
|
|
||||||
|
mkdir -p local
|
||||||
|
|
||||||
|
docker compose up -d
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
docker compose exec backup backup
|
||||||
|
|
||||||
|
if [ ! -f "./local/backup.tar.gz" ]; then
|
||||||
|
fail "Could not find expected backup file."
|
||||||
|
fi
|
||||||
|
|
||||||
|
tmp_dir=$(mktemp -d)
|
||||||
|
tar -xvf ./local/backup.tar.gz -C $tmp_dir
|
||||||
|
if [ ! -f "$tmp_dir/backup/order/test.txt" ]; then
|
||||||
|
fail "Could not find expected file in untared archive."
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker compose down --volumes
|
||||||
@@ -17,7 +17,7 @@ sleep 5
|
|||||||
|
|
||||||
expect_running_containers "3"
|
expect_running_containers "3"
|
||||||
|
|
||||||
docker run --rm \
|
docker run --rm -it \
|
||||||
-v minio_backup_data:/minio_data \
|
-v minio_backup_data:/minio_data \
|
||||||
alpine \
|
alpine \
|
||||||
ash -c 'tar -xvf /minio_data/backup/test-hostnametoken.tar.gz -C /tmp && test -f /tmp/backup/app_data/offen.db'
|
ash -c 'tar -xvf /minio_data/backup/test-hostnametoken.tar.gz -C /tmp && test -f /tmp/backup/app_data/offen.db'
|
||||||
@@ -32,7 +32,7 @@ sleep 5
|
|||||||
|
|
||||||
docker compose exec backup backup
|
docker compose exec backup backup
|
||||||
|
|
||||||
docker run --rm \
|
docker run --rm -it \
|
||||||
-v minio_backup_data:/minio_data \
|
-v minio_backup_data:/minio_data \
|
||||||
alpine \
|
alpine \
|
||||||
ash -c '[ $(find /minio_data/backup/ -type f | wc -l) = "1" ]'
|
ash -c '[ $(find /minio_data/backup/ -type f | wc -l) = "1" ]'
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ sleep 20
|
|||||||
|
|
||||||
docker exec $(docker ps -q -f name=backup) backup
|
docker exec $(docker ps -q -f name=backup) backup
|
||||||
|
|
||||||
docker run --rm \
|
docker run --rm -it \
|
||||||
-v backup_data:/data alpine \
|
-v backup_data:/data alpine \
|
||||||
ash -c 'tar -xf /data/backup/test.tar.gz && test -f /backup/pg_data/PG_VERSION'
|
ash -c 'tar -xf /data/backup/test.tar.gz && test -f /backup/pg_data/PG_VERSION'
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ sleep 5
|
|||||||
|
|
||||||
expect_running_containers 3
|
expect_running_containers 3
|
||||||
|
|
||||||
docker run --rm \
|
docker run --rm -it \
|
||||||
-v ssh_backup_data:/ssh_data \
|
-v ssh_backup_data:/ssh_data \
|
||||||
alpine \
|
alpine \
|
||||||
ash -c 'tar -xvf /ssh_data/test-hostnametoken.tar.gz -C /tmp && test -f /tmp/backup/app_data/offen.db'
|
ash -c 'tar -xvf /ssh_data/test-hostnametoken.tar.gz -C /tmp && test -f /tmp/backup/app_data/offen.db'
|
||||||
@@ -32,7 +32,7 @@ sleep 5
|
|||||||
|
|
||||||
docker compose exec backup backup
|
docker compose exec backup backup
|
||||||
|
|
||||||
docker run --rm \
|
docker run --rm -it \
|
||||||
-v ssh_backup_data:/ssh_data \
|
-v ssh_backup_data:/ssh_data \
|
||||||
alpine \
|
alpine \
|
||||||
ash -c '[ $(find /ssh_data/ -type f | wc -l) = "1" ]'
|
ash -c '[ $(find /ssh_data/ -type f | wc -l) = "1" ]'
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ sleep 20
|
|||||||
|
|
||||||
docker exec $(docker ps -q -f name=backup) backup
|
docker exec $(docker ps -q -f name=backup) backup
|
||||||
|
|
||||||
docker run --rm \
|
docker run --rm -it \
|
||||||
-v backup_data:/data alpine \
|
-v backup_data:/data alpine \
|
||||||
ash -c 'tar -xf /data/backup/test.tar.gz && test -f /backup/pg_data/PG_VERSION'
|
ash -c 'tar -xf /data/backup/test.tar.gz && test -f /backup/pg_data/PG_VERSION'
|
||||||
|
|
||||||
|
|||||||
2
test/user/.gitignore
vendored
2
test/user/.gitignore
vendored
@@ -1,2 +0,0 @@
|
|||||||
local
|
|
||||||
backup
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
version: '2.4'
|
|
||||||
|
|
||||||
services:
|
|
||||||
alpine:
|
|
||||||
image: alpine:3.17.3
|
|
||||||
tty: true
|
|
||||||
volumes:
|
|
||||||
- app_data:/tmp
|
|
||||||
labels:
|
|
||||||
- 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:
|
|
||||||
restart_policy:
|
|
||||||
condition: on-failure
|
|
||||||
environment:
|
|
||||||
BACKUP_FILENAME: test.tar.gz
|
|
||||||
BACKUP_CRON_EXPRESSION: 0 0 5 31 2 ?
|
|
||||||
EXEC_FORWARD_OUTPUT: "true"
|
|
||||||
volumes:
|
|
||||||
- ./local:/archive
|
|
||||||
- app_data:/backup/data:ro
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
app_data:
|
|
||||||
archive:
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
cd $(dirname $0)
|
|
||||||
. ../util.sh
|
|
||||||
current_test=$(basename $(pwd))
|
|
||||||
|
|
||||||
docker compose up -d
|
|
||||||
|
|
||||||
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
|
|
||||||
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
|
|
||||||
fail "Could not find expected user name."
|
|
||||||
fi
|
|
||||||
pass "Found expected user."
|
|
||||||
|
|
||||||
docker compose down --volumes
|
|
||||||
sudo rm -rf ./local
|
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ sleep 5
|
|||||||
|
|
||||||
expect_running_containers "3"
|
expect_running_containers "3"
|
||||||
|
|
||||||
docker run --rm \
|
docker run --rm -it \
|
||||||
-v webdav_backup_data:/webdav_data \
|
-v webdav_backup_data:/webdav_data \
|
||||||
alpine \
|
alpine \
|
||||||
ash -c 'tar -xvf /webdav_data/data/my/new/path/test-hostnametoken.tar.gz -C /tmp && test -f /tmp/backup/app_data/offen.db'
|
ash -c 'tar -xvf /webdav_data/data/my/new/path/test-hostnametoken.tar.gz -C /tmp && test -f /tmp/backup/app_data/offen.db'
|
||||||
@@ -30,7 +30,7 @@ sleep 5
|
|||||||
|
|
||||||
docker compose exec backup backup
|
docker compose exec backup backup
|
||||||
|
|
||||||
docker run --rm \
|
docker run --rm -it \
|
||||||
-v webdav_backup_data:/webdav_data \
|
-v webdav_backup_data:/webdav_data \
|
||||||
alpine \
|
alpine \
|
||||||
ash -c '[ $(find /webdav_data/data/my/new/path/ -type f | wc -l) = "1" ]'
|
ash -c '[ $(find /webdav_data/data/my/new/path/ -type f | wc -l) = "1" ]'
|
||||||
|
|||||||
Reference in New Issue
Block a user