mirror of
https://github.com/offen/docker-volume-backup.git
synced 2024-11-25 14:40:28 +01:00
SSH Backup Storage Support (#107)
* SSH Client implemented * Private key auth implemented Code refactoring * Refactoring * Passphrase renamed to IdentityPassphrase Default private key location changed to .ssh/id
This commit is contained in:
parent
8c8a2fa088
commit
0b205fe6dc
59
README.md
59
README.md
@ -7,7 +7,7 @@
|
|||||||
Backup Docker volumes locally or to any S3 compatible storage.
|
Backup Docker volumes locally or to any S3 compatible storage.
|
||||||
|
|
||||||
The [offen/docker-volume-backup](https://hub.docker.com/r/offen/docker-volume-backup) Docker image can be used as a lightweight (below 15MB) sidecar container to an existing Docker setup.
|
The [offen/docker-volume-backup](https://hub.docker.com/r/offen/docker-volume-backup) Docker image can be used as a lightweight (below 15MB) sidecar container to an existing Docker setup.
|
||||||
It handles __recurring or one-off backups of Docker volumes__ to a __local directory__, __any S3 or WebDAV compatible storage (or any combination) and rotates away old backups__ if configured. It also supports __encrypting your backups using GPG__ and __sending notifications for failed backup runs__.
|
It handles __recurring or one-off backups of Docker volumes__ to a __local directory__, __any S3, WebDAV or SSH compatible storage (or any combination) and rotates away old backups__ if configured. It also supports __encrypting your backups using GPG__ and __sending notifications for failed backup runs__.
|
||||||
|
|
||||||
<!-- MarkdownTOC -->
|
<!-- MarkdownTOC -->
|
||||||
|
|
||||||
@ -36,6 +36,7 @@ It handles __recurring or one-off backups of Docker volumes__ to a __local direc
|
|||||||
- [Backing up to Filebase](#backing-up-to-filebase)
|
- [Backing up to Filebase](#backing-up-to-filebase)
|
||||||
- [Backing up to MinIO](#backing-up-to-minio)
|
- [Backing up to MinIO](#backing-up-to-minio)
|
||||||
- [Backing up to WebDAV](#backing-up-to-webdav)
|
- [Backing up to WebDAV](#backing-up-to-webdav)
|
||||||
|
- [Backing up to SSH](#backing-up-to-ssh)
|
||||||
- [Backing up locally](#backing-up-locally)
|
- [Backing up locally](#backing-up-locally)
|
||||||
- [Backing up to AWS S3 as well as locally](#backing-up-to-aws-s3-as-well-as-locally)
|
- [Backing up to AWS S3 as well as locally](#backing-up-to-aws-s3-as-well-as-locally)
|
||||||
- [Running on a custom cron schedule](#running-on-a-custom-cron-schedule)
|
- [Running on a custom cron schedule](#running-on-a-custom-cron-schedule)
|
||||||
@ -245,6 +246,39 @@ You can populate below template according to your requirements and use it as you
|
|||||||
|
|
||||||
# WEBDAV_URL_INSECURE="true"
|
# WEBDAV_URL_INSECURE="true"
|
||||||
|
|
||||||
|
# You can also backup files to any SSH server:
|
||||||
|
|
||||||
|
# The URL of the remote SSH server
|
||||||
|
|
||||||
|
# SSH_HOST_NAME="server.local"
|
||||||
|
|
||||||
|
# The port of the remote SSH server
|
||||||
|
# Optional variable default value is `22`
|
||||||
|
|
||||||
|
# SSH_PORT=2222
|
||||||
|
|
||||||
|
# The Directory to place the backups to on the SSH server.
|
||||||
|
|
||||||
|
# SSH_REMOTE_PATH="/my/directory/"
|
||||||
|
|
||||||
|
# The username for the SSH server
|
||||||
|
|
||||||
|
# SSH_USER="user"
|
||||||
|
|
||||||
|
# The password for the SSH server
|
||||||
|
|
||||||
|
# SSH_PASSWORD="password"
|
||||||
|
|
||||||
|
# The private key path in container for SSH server
|
||||||
|
# Default value: /root/.ssh/id
|
||||||
|
# If file is mounted to /root/.ssh/id path it will be used.
|
||||||
|
|
||||||
|
# SSH_IDENTITY_FILE="/root/.ssh/id"
|
||||||
|
|
||||||
|
# The passphrase for the identity file
|
||||||
|
|
||||||
|
# SSH_IDENTITY_PASSPHRASE="pass"
|
||||||
|
|
||||||
# In addition to storing backups remotely, you can also keep local copies.
|
# In addition to storing backups remotely, you can also keep local copies.
|
||||||
# Pass a container-local path to store your backups if needed. You also need to
|
# Pass a container-local path to store your backups if needed. You also need to
|
||||||
# mount a local folder or Docker volume into that location (`/archive`
|
# mount a local folder or Docker volume into that location (`/archive`
|
||||||
@ -870,6 +904,29 @@ volumes:
|
|||||||
data:
|
data:
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Backing up to SSH
|
||||||
|
|
||||||
|
```yml
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# ... define other services using the `data` volume here
|
||||||
|
backup:
|
||||||
|
image: offen/docker-volume-backup:v2
|
||||||
|
environment:
|
||||||
|
SSH_HOST_NAME: server.local
|
||||||
|
SSH_PORT: 2222
|
||||||
|
SSH_USER: user
|
||||||
|
SSH_REMOTE_PATH: /data
|
||||||
|
volumes:
|
||||||
|
- data:/backup/my-app-backup:ro
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- /path/to/private_key:/root/.ssh/id
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
data:
|
||||||
|
```
|
||||||
|
|
||||||
### Backing up locally
|
### Backing up locally
|
||||||
|
|
||||||
```yml
|
```yml
|
||||||
|
@ -45,6 +45,13 @@ type Config struct {
|
|||||||
WebdavPath string `split_words:"true" default:"/"`
|
WebdavPath string `split_words:"true" default:"/"`
|
||||||
WebdavUsername string `split_words:"true"`
|
WebdavUsername string `split_words:"true"`
|
||||||
WebdavPassword string `split_words:"true"`
|
WebdavPassword string `split_words:"true"`
|
||||||
|
SSHHostName string `split_words:"true"`
|
||||||
|
SSHPort string `split_words:"true" default:"22"`
|
||||||
|
SSHUser string `split_words:"true"`
|
||||||
|
SSHPassword string `split_words:"true"`
|
||||||
|
SSHIdentityFile string `split_words:"true" default:"/root/.ssh/id"`
|
||||||
|
SSHIdentityPassphrase string `split_words:"true"`
|
||||||
|
SSHRemotePath string `split_words:"true"`
|
||||||
ExecLabel string `split_words:"true"`
|
ExecLabel string `split_words:"true"`
|
||||||
ExecForwardOutput bool `split_words:"true"`
|
ExecForwardOutput bool `split_words:"true"`
|
||||||
LockTimeout time.Duration `split_words:"true" default:"60m"`
|
LockTimeout time.Duration `split_words:"true" default:"60m"`
|
||||||
|
@ -7,8 +7,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/pkg/sftp"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@ -39,6 +42,8 @@ type script struct {
|
|||||||
cli *client.Client
|
cli *client.Client
|
||||||
minioClient *minio.Client
|
minioClient *minio.Client
|
||||||
webdavClient *gowebdav.Client
|
webdavClient *gowebdav.Client
|
||||||
|
sshClient *ssh.Client
|
||||||
|
sftpClient *sftp.Client
|
||||||
logger *logrus.Logger
|
logger *logrus.Logger
|
||||||
sender *router.ServiceRouter
|
sender *router.ServiceRouter
|
||||||
template *template.Template
|
template *template.Template
|
||||||
@ -159,6 +164,57 @@ func newScript() (*script, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.c.SSHHostName != "" {
|
||||||
|
var authMethods []ssh.AuthMethod
|
||||||
|
|
||||||
|
if s.c.SSHPassword != "" {
|
||||||
|
authMethods = append(authMethods, ssh.Password(s.c.SSHPassword))
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(s.c.SSHIdentityFile); err == nil {
|
||||||
|
key, err := ioutil.ReadFile(s.c.SSHIdentityFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("newScript: error reading the private key")
|
||||||
|
}
|
||||||
|
|
||||||
|
var signer ssh.Signer
|
||||||
|
if s.c.SSHIdentityPassphrase != "" {
|
||||||
|
signer, err = ssh.ParsePrivateKeyWithPassphrase(key, []byte(s.c.SSHIdentityPassphrase))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("newScript: error parsing the encrypted private key")
|
||||||
|
}
|
||||||
|
authMethods = append(authMethods, ssh.PublicKeys(signer))
|
||||||
|
} else {
|
||||||
|
signer, err = ssh.ParsePrivateKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("newScript: error parsing the private key")
|
||||||
|
}
|
||||||
|
authMethods = append(authMethods, ssh.PublicKeys(signer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sshClientConfig := &ssh.ClientConfig{
|
||||||
|
User: s.c.SSHUser,
|
||||||
|
Auth: authMethods,
|
||||||
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||||
|
}
|
||||||
|
sshClient, err := ssh.Dial("tcp", fmt.Sprintf("%s:%s", s.c.SSHHostName, s.c.SSHPort), sshClientConfig)
|
||||||
|
s.sshClient = sshClient
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("newScript: error creating ssh client: %w", err)
|
||||||
|
}
|
||||||
|
_, _, err = s.sshClient.SendRequest("keepalive", false, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sftpClient, err := sftp.NewClient(sshClient)
|
||||||
|
s.sftpClient = sftpClient
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("newScript: error creating sftp client: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if s.c.EmailNotificationRecipient != "" {
|
if s.c.EmailNotificationRecipient != "" {
|
||||||
emailURL := fmt.Sprintf(
|
emailURL := fmt.Sprintf(
|
||||||
"smtp://%s:%s@%s:%d/?from=%s&to=%s",
|
"smtp://%s:%s@%s:%d/?from=%s&to=%s",
|
||||||
@ -512,6 +568,52 @@ func (s *script) copyBackup() error {
|
|||||||
s.logger.Infof("Uploaded a copy of backup `%s` to WebDAV-URL '%s' at path '%s'.", s.file, s.c.WebdavUrl, s.c.WebdavPath)
|
s.logger.Infof("Uploaded a copy of backup `%s` to WebDAV-URL '%s' at path '%s'.", s.file, s.c.WebdavUrl, s.c.WebdavPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.sshClient != nil {
|
||||||
|
source, err := os.Open(s.file)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("copyBackup: error reading the file to be uploaded: %w", err)
|
||||||
|
}
|
||||||
|
defer source.Close()
|
||||||
|
|
||||||
|
destination, err := s.sftpClient.Create(filepath.Join(s.c.SSHRemotePath, name))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("copyBackup: error creating file on SSH storage: %w", err)
|
||||||
|
}
|
||||||
|
defer destination.Close()
|
||||||
|
|
||||||
|
chunk := make([]byte, 1000000)
|
||||||
|
for {
|
||||||
|
num, err := source.Read(chunk)
|
||||||
|
if err == io.EOF {
|
||||||
|
tot, err := destination.Write(chunk[:num])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("copyBackup: error uploading the file to SSH storage: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tot != len(chunk[:num]) {
|
||||||
|
return fmt.Errorf("sshClient: failed to write stream")
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("copyBackup: error uploading the file to SSH storage: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tot, err := destination.Write(chunk[:num])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("copyBackup: error uploading the file to SSH storage: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tot != len(chunk[:num]) {
|
||||||
|
return fmt.Errorf("sshClient: failed to write stream")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Infof("Uploaded a copy of backup `%s` to SSH storage '%s' at path '%s'.", s.file, s.c.SSHHostName, s.c.SSHRemotePath)
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := os.Stat(s.c.BackupArchive); !os.IsNotExist(err) {
|
if _, err := os.Stat(s.c.BackupArchive); !os.IsNotExist(err) {
|
||||||
if err := copyFile(s.file, path.Join(s.c.BackupArchive, name)); err != nil {
|
if err := copyFile(s.file, path.Join(s.c.BackupArchive, name)); err != nil {
|
||||||
return fmt.Errorf("copyBackup: error copying file to local archive: %w", err)
|
return fmt.Errorf("copyBackup: error copying file to local archive: %w", err)
|
||||||
@ -645,6 +747,37 @@ func (s *script) pruneBackups() error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.sshClient != nil {
|
||||||
|
candidates, err := s.sftpClient.ReadDir(s.c.SSHRemotePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("pruneBackups: error reading directory from SSH storage: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var matches []string
|
||||||
|
for _, candidate := range candidates {
|
||||||
|
if !strings.HasPrefix(candidate.Name(), s.c.BackupPruningPrefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if candidate.ModTime().Before(deadline) {
|
||||||
|
matches = append(matches, candidate.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.stats.Storages.SSH = StorageStats{
|
||||||
|
Total: uint(len(candidates)),
|
||||||
|
Pruned: uint(len(matches)),
|
||||||
|
}
|
||||||
|
|
||||||
|
doPrune(len(matches), len(candidates), "SSH backup(s)", func() error {
|
||||||
|
for _, match := range matches {
|
||||||
|
if err := s.sftpClient.Remove(filepath.Join(s.c.SSHRemotePath, match)); err != nil {
|
||||||
|
return fmt.Errorf("pruneBackups: error removing file from SSH storage: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := os.Stat(s.c.BackupArchive); !os.IsNotExist(err) {
|
if _, err := os.Stat(s.c.BackupArchive); !os.IsNotExist(err) {
|
||||||
globPattern := path.Join(
|
globPattern := path.Join(
|
||||||
s.c.BackupArchive,
|
s.c.BackupArchive,
|
||||||
|
@ -30,10 +30,11 @@ type StorageStats struct {
|
|||||||
PruneErrors uint
|
PruneErrors uint
|
||||||
}
|
}
|
||||||
|
|
||||||
// StoragesStats stats about each possible archival location (Local, WebDAV, S3)
|
// StoragesStats stats about each possible archival location (Local, WebDAV, SSH, S3)
|
||||||
type StoragesStats struct {
|
type StoragesStats struct {
|
||||||
Local StorageStats
|
Local StorageStats
|
||||||
WebDAV StorageStats
|
WebDAV StorageStats
|
||||||
|
SSH StorageStats
|
||||||
S3 StorageStats
|
S3 StorageStats
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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`)
|
* `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
|
* `Size`: size in bytes of the backup file
|
||||||
* `Storages`: object that holds stats about each storage
|
* `Storages`: object that holds stats about each storage
|
||||||
* `Local`, `S3` or `WebDAV`:
|
* `Local`, `S3`, `WebDAV` or `SSH`:
|
||||||
* `Total`: total number of backup files
|
* `Total`: total number of backup files
|
||||||
* `Pruned`: number of backup files that were deleted due to pruning rule
|
* `Pruned`: number of backup files that were deleted due to pruning rule
|
||||||
* `PruneErrors`: number of backup files that were unable to be pruned
|
* `PruneErrors`: number of backup files that were unable to be pruned
|
||||||
|
4
go.mod
4
go.mod
@ -11,9 +11,10 @@ require (
|
|||||||
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.16
|
github.com/minio/minio-go/v7 v7.0.16
|
||||||
github.com/otiai10/copy v1.7.0
|
github.com/otiai10/copy v1.7.0
|
||||||
|
github.com/pkg/sftp v1.13.5
|
||||||
github.com/sirupsen/logrus v1.8.1
|
github.com/sirupsen/logrus v1.8.1
|
||||||
github.com/studio-b12/gowebdav v0.0.0-20220128162035-c7b1ff8a5e62
|
github.com/studio-b12/gowebdav v0.0.0-20220128162035-c7b1ff8a5e62
|
||||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
|
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
|
||||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f
|
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -33,6 +34,7 @@ require (
|
|||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/compress v1.15.6 // indirect
|
github.com/klauspost/compress v1.15.6 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
|
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
|
||||||
|
github.com/kr/fs v0.1.0 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.8 // indirect
|
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.12 // indirect
|
github.com/mattn/go-isatty v0.0.12 // indirect
|
||||||
|
14
go.sum
14
go.sum
@ -170,6 +170,8 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02
|
|||||||
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ=
|
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ=
|
||||||
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.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||||
|
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
@ -253,6 +255,8 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
|||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go=
|
||||||
|
github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
@ -326,8 +330,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
|||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
|
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M=
|
||||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
@ -354,6 +358,7 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R
|
|||||||
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d h1:4SFsTMi4UahlKoloni7L4eYzhFRifURQLw+yv0QDCx8=
|
golang.org/x/net v0.0.0-20220607020251-c690dde0001d h1:4SFsTMi4UahlKoloni7L4eYzhFRifURQLw+yv0QDCx8=
|
||||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
@ -396,15 +401,20 @@ golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
@ -21,12 +21,24 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- webdav_backup_data:/var/lib/dav
|
- webdav_backup_data:/var/lib/dav
|
||||||
|
|
||||||
|
ssh:
|
||||||
|
image: linuxserver/openssh-server:version-8.6_p1-r3
|
||||||
|
environment:
|
||||||
|
- PUID=1000
|
||||||
|
- PGID=1000
|
||||||
|
- USER_NAME=test
|
||||||
|
volumes:
|
||||||
|
- ./id_rsa.pub:/config/.ssh/authorized_keys
|
||||||
|
- ssh_backup_data:/tmp
|
||||||
|
- ssh_config:/config
|
||||||
|
|
||||||
backup:
|
backup:
|
||||||
image: offen/docker-volume-backup:${TEST_VERSION:-canary}
|
image: offen/docker-volume-backup:${TEST_VERSION:-canary}
|
||||||
hostname: hostnametoken
|
hostname: hostnametoken
|
||||||
depends_on:
|
depends_on:
|
||||||
- minio
|
- minio
|
||||||
- webdav
|
- webdav
|
||||||
|
- ssh
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
AWS_ACCESS_KEY_ID: test
|
AWS_ACCESS_KEY_ID: test
|
||||||
@ -47,8 +59,14 @@ services:
|
|||||||
WEBDAV_PATH: /my/new/path/
|
WEBDAV_PATH: /my/new/path/
|
||||||
WEBDAV_USERNAME: test
|
WEBDAV_USERNAME: test
|
||||||
WEBDAV_PASSWORD: test
|
WEBDAV_PASSWORD: test
|
||||||
|
SSH_HOST_NAME: ssh
|
||||||
|
SSH_PORT: 2222
|
||||||
|
SSH_USER: test
|
||||||
|
SSH_REMOTE_PATH: /tmp
|
||||||
|
SSH_IDENTITY_PASSPHRASE: test1234
|
||||||
volumes:
|
volumes:
|
||||||
- ./local:/archive
|
- ./local:/archive
|
||||||
|
- ./id_rsa:/root/.ssh/id
|
||||||
- app_data:/backup/app_data:ro
|
- app_data:/backup/app_data:ro
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
@ -62,4 +80,6 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
minio_backup_data:
|
minio_backup_data:
|
||||||
webdav_backup_data:
|
webdav_backup_data:
|
||||||
|
ssh_backup_data:
|
||||||
|
ssh_config:
|
||||||
app_data:
|
app_data:
|
||||||
|
@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
cd $(dirname $0)
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
mkdir -p local
|
mkdir -p local
|
||||||
|
ssh-keygen -t rsa -m pem -b 4096 -N "test1234" -f id_rsa -C "docker-volume-backup@local"
|
||||||
|
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
sleep 5
|
sleep 5
|
||||||
@ -15,7 +16,7 @@ docker-compose exec offen ln -s /var/opt/offen/offen.db /var/opt/offen/db.link
|
|||||||
docker-compose exec backup backup
|
docker-compose exec backup backup
|
||||||
|
|
||||||
sleep 5
|
sleep 5
|
||||||
if [ "$(docker-compose ps -q | wc -l)" != "4" ]; then
|
if [ "$(docker-compose ps -q | wc -l)" != "5" ]; then
|
||||||
echo "[TEST:FAIL] Expected all containers to be running post backup, instead seen:"
|
echo "[TEST:FAIL] Expected all containers to be running post backup, instead seen:"
|
||||||
docker-compose ps
|
docker-compose ps
|
||||||
exit 1
|
exit 1
|
||||||
@ -25,10 +26,12 @@ echo "[TEST:PASS] All containers running post backup."
|
|||||||
|
|
||||||
docker run --rm -it \
|
docker run --rm -it \
|
||||||
-v compose_minio_backup_data:/minio_data \
|
-v compose_minio_backup_data:/minio_data \
|
||||||
-v compose_webdav_backup_data:/webdav_data alpine \
|
-v compose_webdav_backup_data:/webdav_data \
|
||||||
|
-v compose_ssh_backup_data:/ssh_data alpine \
|
||||||
ash -c 'apk add gnupg && \
|
ash -c 'apk add gnupg && \
|
||||||
echo 1234secret | gpg -d --pinentry-mode loopback --passphrase-fd 0 --yes /minio_data/backup/test-hostnametoken.tar.gz.gpg > /tmp/test-hostnametoken.tar.gz && tar -xvf /tmp/test-hostnametoken.tar.gz -C /tmp && test -f /tmp/backup/app_data/offen.db && \
|
echo 1234secret | gpg -d --pinentry-mode loopback --passphrase-fd 0 --yes /minio_data/backup/test-hostnametoken.tar.gz.gpg > /tmp/test-hostnametoken.tar.gz && tar -xvf /tmp/test-hostnametoken.tar.gz -C /tmp && test -f /tmp/backup/app_data/offen.db && \
|
||||||
echo 1234secret | gpg -d --pinentry-mode loopback --passphrase-fd 0 --yes /webdav_data/data/my/new/path/test-hostnametoken.tar.gz.gpg > /tmp/test-hostnametoken.tar.gz && tar -xvf /tmp/test-hostnametoken.tar.gz -C /tmp && test -f /tmp/backup/app_data/offen.db'
|
echo 1234secret | gpg -d --pinentry-mode loopback --passphrase-fd 0 --yes /webdav_data/data/my/new/path/test-hostnametoken.tar.gz.gpg > /tmp/test-hostnametoken.tar.gz && tar -xvf /tmp/test-hostnametoken.tar.gz -C /tmp && test -f /tmp/backup/app_data/offen.db && \
|
||||||
|
echo 1234secret | gpg -d --pinentry-mode loopback --passphrase-fd 0 --yes /ssh_data/test-hostnametoken.tar.gz.gpg > /tmp/test-hostnametoken.tar.gz && tar -xvf /tmp/test-hostnametoken.tar.gz -C /tmp && test -f /tmp/backup/app_data/offen.db'
|
||||||
|
|
||||||
echo "[TEST:PASS] Found relevant files in decrypted and untared remote backups."
|
echo "[TEST:PASS] Found relevant files in decrypted and untared remote backups."
|
||||||
|
|
||||||
@ -52,9 +55,11 @@ docker-compose exec backup backup
|
|||||||
|
|
||||||
docker run --rm -it \
|
docker run --rm -it \
|
||||||
-v compose_minio_backup_data:/minio_data \
|
-v compose_minio_backup_data:/minio_data \
|
||||||
-v compose_webdav_backup_data:/webdav_data alpine \
|
-v compose_webdav_backup_data:/webdav_data \
|
||||||
|
-v compose_ssh_backup_data:/ssh_data alpine \
|
||||||
ash -c '[ $(find /minio_data/backup/ -type f | wc -l) = "1" ] && \
|
ash -c '[ $(find /minio_data/backup/ -type f | wc -l) = "1" ] && \
|
||||||
[ $(find /webdav_data/data/my/new/path/ -type f | wc -l) = "1" ]'
|
[ $(find /webdav_data/data/my/new/path/ -type f | wc -l) = "1" ] && \
|
||||||
|
[ $(find /ssh_data/ -type f | wc -l) = "1" ]'
|
||||||
|
|
||||||
echo "[TEST:PASS] Remote backups have not been deleted."
|
echo "[TEST:PASS] Remote backups have not been deleted."
|
||||||
|
|
||||||
@ -66,3 +71,4 @@ fi
|
|||||||
echo "[TEST:PASS] Local backups have not been deleted."
|
echo "[TEST:PASS] Local backups have not been deleted."
|
||||||
|
|
||||||
docker-compose down --volumes
|
docker-compose down --volumes
|
||||||
|
rm id_rsa id_rsa.pub
|
||||||
|
Loading…
Reference in New Issue
Block a user