mirror of
https://github.com/offen/docker-volume-backup.git
synced 2025-12-05 09:08:02 +01:00
Compare commits
1 Commits
v2.29.0
...
serve-metr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4138fd733 |
@@ -1,7 +1,7 @@
|
|||||||
# Copyright 2021 - Offen Authors <hioffen@posteo.de>
|
# Copyright 2021 - Offen Authors <hioffen@posteo.de>
|
||||||
# SPDX-License-Identifier: MPL-2.0
|
# SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
FROM golang:1.21-alpine as builder
|
FROM golang:1.20-alpine as builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . .
|
COPY . .
|
||||||
@@ -13,7 +13,7 @@ FROM alpine:3.18
|
|||||||
|
|
||||||
WORKDIR /root
|
WORKDIR /root
|
||||||
|
|
||||||
RUN apk add --no-cache ca-certificates
|
RUN apk add --no-cache ca-certificates busybox-extras
|
||||||
|
|
||||||
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 --chmod=755 ./entrypoint.sh /root/
|
||||||
|
|||||||
17
README.md
17
README.md
@@ -148,22 +148,13 @@ You can populate below template according to your requirements and use it as you
|
|||||||
|
|
||||||
# BACKUP_CRON_EXPRESSION="0 2 * * *"
|
# BACKUP_CRON_EXPRESSION="0 2 * * *"
|
||||||
|
|
||||||
# The compression algorithm used in conjunction with tar.
|
# The name of the backup file including the `.tar.gz` extension.
|
||||||
# Valid options are: "gz" (Gzip) and "zst" (Zstd).
|
|
||||||
# Note that the selection affects the file extension.
|
|
||||||
|
|
||||||
# BACKUP_COMPRESSION="gz"
|
|
||||||
|
|
||||||
# The name of the backup file including the extension.
|
|
||||||
# Format verbs will be replaced as in `strftime`. Omitting them
|
# Format verbs will be replaced as in `strftime`. Omitting them
|
||||||
# will result in the same filename for every backup run, which means previous
|
# will result in the same filename for every backup run, which means previous
|
||||||
# versions will be overwritten on subsequent runs.
|
# versions will be overwritten on subsequent runs. The default results
|
||||||
# Extension can be defined literally or via "{{ .Extension }}" template,
|
# in filenames like `backup-2021-08-29T04-00-00.tar.gz`.
|
||||||
# in which case it will become either "tar.gz" or "tar.zst" (depending
|
|
||||||
# on your BACKUP_COMPRESSION setting).
|
|
||||||
# The default results in filenames like: `backup-2021-08-29T04-00-00.tar.gz`.
|
|
||||||
|
|
||||||
# BACKUP_FILENAME="backup-%Y-%m-%dT%H-%M-%S.{{ .Extension }}"
|
# BACKUP_FILENAME="backup-%Y-%m-%dT%H-%M-%S.tar.gz"
|
||||||
|
|
||||||
# Setting BACKUP_FILENAME_EXPAND to true allows for environment variable
|
# Setting BACKUP_FILENAME_EXPAND to true allows for environment variable
|
||||||
# placeholders in BACKUP_FILENAME, BACKUP_LATEST_SYMLINK and in
|
# placeholders in BACKUP_FILENAME, BACKUP_LATEST_SYMLINK and in
|
||||||
|
|||||||
@@ -15,11 +15,9 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/klauspost/compress/zstd"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func createArchive(files []string, inputFilePath, outputFilePath string, compression string) error {
|
func createArchive(files []string, inputFilePath, outputFilePath string) error {
|
||||||
inputFilePath = stripTrailingSlashes(inputFilePath)
|
inputFilePath = stripTrailingSlashes(inputFilePath)
|
||||||
inputFilePath, outputFilePath, err := makeAbsolute(inputFilePath, outputFilePath)
|
inputFilePath, outputFilePath, err := makeAbsolute(inputFilePath, outputFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -29,7 +27,7 @@ func createArchive(files []string, inputFilePath, outputFilePath string, compres
|
|||||||
return fmt.Errorf("createArchive: error creating output file path: %w", err)
|
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)); err != nil {
|
||||||
return fmt.Errorf("createArchive: error creating archive: %w", err)
|
return fmt.Errorf("createArchive: error creating archive: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,30 +51,18 @@ func makeAbsolute(inputFilePath, outputFilePath string) (string, string, error)
|
|||||||
return inputFilePath, outputFilePath, err
|
return inputFilePath, outputFilePath, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func compress(paths []string, outFilePath, subPath string, algo string) error {
|
func compress(paths []string, outFilePath, subPath string) error {
|
||||||
file, err := os.Create(outFilePath)
|
file, err := os.Create(outFilePath)
|
||||||
var compressWriter io.WriteCloser
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("compress: error creating out file: %w", err)
|
return fmt.Errorf("compress: error creating out file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
prefix := path.Dir(outFilePath)
|
prefix := path.Dir(outFilePath)
|
||||||
switch algo {
|
gzipWriter := gzip.NewWriter(file)
|
||||||
case "gz":
|
tarWriter := tar.NewWriter(gzipWriter)
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
tarWriter := tar.NewWriter(compressWriter)
|
|
||||||
|
|
||||||
for _, p := range paths {
|
for _, p := range paths {
|
||||||
if err := writeTarball(p, tarWriter, prefix); err != nil {
|
if err := writeTarGz(p, tarWriter, prefix); err != nil {
|
||||||
return fmt.Errorf("compress: error writing %s to archive: %w", p, err)
|
return fmt.Errorf("compress: error writing %s to archive: %w", p, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,9 +72,9 @@ func compress(paths []string, outFilePath, subPath string, algo string) error {
|
|||||||
return fmt.Errorf("compress: error closing tar writer: %w", err)
|
return fmt.Errorf("compress: error closing tar writer: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = compressWriter.Close()
|
err = gzipWriter.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("compress: error closing compression writer: %w", err)
|
return fmt.Errorf("compress: error closing gzip writer: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = file.Close()
|
err = file.Close()
|
||||||
@@ -99,10 +85,10 @@ func compress(paths []string, outFilePath, subPath string, algo string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeTarball(path string, tarWriter *tar.Writer, prefix string) error {
|
func writeTarGz(path string, tarWriter *tar.Writer, prefix string) error {
|
||||||
fileInfo, err := os.Lstat(path)
|
fileInfo, err := os.Lstat(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("writeTarball: error getting file infor for %s: %w", path, err)
|
return fmt.Errorf("writeTarGz: error getting file infor for %s: %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if fileInfo.Mode()&os.ModeSocket == os.ModeSocket {
|
if fileInfo.Mode()&os.ModeSocket == os.ModeSocket {
|
||||||
@@ -113,19 +99,19 @@ func writeTarball(path string, tarWriter *tar.Writer, prefix string) error {
|
|||||||
if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink {
|
if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink {
|
||||||
var err error
|
var err error
|
||||||
if link, err = os.Readlink(path); err != nil {
|
if link, err = os.Readlink(path); err != nil {
|
||||||
return fmt.Errorf("writeTarball: error resolving symlink %s: %w", path, err)
|
return fmt.Errorf("writeTarGz: error resolving symlink %s: %w", path, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
header, err := tar.FileInfoHeader(fileInfo, link)
|
header, err := tar.FileInfoHeader(fileInfo, link)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("writeTarball: error getting file info header: %w", err)
|
return fmt.Errorf("writeTarGz: error getting file info header: %w", err)
|
||||||
}
|
}
|
||||||
header.Name = strings.TrimPrefix(path, prefix)
|
header.Name = strings.TrimPrefix(path, prefix)
|
||||||
|
|
||||||
err = tarWriter.WriteHeader(header)
|
err = tarWriter.WriteHeader(header)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("writeTarball: error writing file info header: %w", err)
|
return fmt.Errorf("writeTarGz: error writing file info header: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !fileInfo.Mode().IsRegular() {
|
if !fileInfo.Mode().IsRegular() {
|
||||||
@@ -134,13 +120,13 @@ func writeTarball(path string, tarWriter *tar.Writer, prefix string) error {
|
|||||||
|
|
||||||
file, err := os.Open(path)
|
file, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("writeTarball: error opening %s: %w", path, err)
|
return fmt.Errorf("writeTarGz: error opening %s: %w", path, err)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
_, err = io.Copy(tarWriter, file)
|
_, err = io.Copy(tarWriter, file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("writeTarball: error copying %s to tar writer: %w", path, err)
|
return fmt.Errorf("writeTarGz: error copying %s to tar writer: %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -16,60 +16,59 @@ import (
|
|||||||
// Config holds all configuration values that are expected to be set
|
// Config holds all configuration values that are expected to be set
|
||||||
// by users.
|
// by users.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
AwsS3BucketName string `split_words:"true"`
|
AwsS3BucketName string `split_words:"true"`
|
||||||
AwsS3Path string `split_words:"true"`
|
AwsS3Path string `split_words:"true"`
|
||||||
AwsEndpoint string `split_words:"true" default:"s3.amazonaws.com"`
|
AwsEndpoint string `split_words:"true" default:"s3.amazonaws.com"`
|
||||||
AwsEndpointProto string `split_words:"true" default:"https"`
|
AwsEndpointProto string `split_words:"true" default:"https"`
|
||||||
AwsEndpointInsecure bool `split_words:"true"`
|
AwsEndpointInsecure bool `split_words:"true"`
|
||||||
AwsEndpointCACert CertDecoder `envconfig:"AWS_ENDPOINT_CA_CERT"`
|
AwsEndpointCACert CertDecoder `envconfig:"AWS_ENDPOINT_CA_CERT"`
|
||||||
AwsStorageClass string `split_words:"true"`
|
AwsStorageClass string `split_words:"true"`
|
||||||
AwsAccessKeyID string `envconfig:"AWS_ACCESS_KEY_ID"`
|
AwsAccessKeyID string `envconfig:"AWS_ACCESS_KEY_ID"`
|
||||||
AwsAccessKeyIDFile string `envconfig:"AWS_ACCESS_KEY_ID_FILE"`
|
AwsAccessKeyIDFile string `envconfig:"AWS_ACCESS_KEY_ID_FILE"`
|
||||||
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"`
|
AwsPartSize int64 `split_words:"true"`
|
||||||
BackupCompression CompressionType `split_words:"true" default:"gz"`
|
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.{{ .Extension }}"`
|
BackupFilenameExpand bool `split_words:"true"`
|
||||||
BackupFilenameExpand bool `split_words:"true"`
|
BackupLatestSymlink string `split_words:"true"`
|
||||||
BackupLatestSymlink string `split_words:"true"`
|
BackupArchive string `split_words:"true" default:"/archive"`
|
||||||
BackupArchive string `split_words:"true" default:"/archive"`
|
BackupRetentionDays int32 `split_words:"true" default:"-1"`
|
||||||
BackupRetentionDays int32 `split_words:"true" default:"-1"`
|
BackupPruningLeeway time.Duration `split_words:"true" default:"1m"`
|
||||||
BackupPruningLeeway time.Duration `split_words:"true" default:"1m"`
|
BackupPruningPrefix string `split_words:"true"`
|
||||||
BackupPruningPrefix string `split_words:"true"`
|
BackupStopContainerLabel string `split_words:"true" default:"true"`
|
||||||
BackupStopContainerLabel string `split_words:"true" default:"true"`
|
BackupFromSnapshot bool `split_words:"true"`
|
||||||
BackupFromSnapshot bool `split_words:"true"`
|
BackupExcludeRegexp RegexpDecoder `split_words:"true"`
|
||||||
BackupExcludeRegexp RegexpDecoder `split_words:"true"`
|
GpgPassphrase string `split_words:"true"`
|
||||||
GpgPassphrase string `split_words:"true"`
|
NotificationURLs []string `envconfig:"NOTIFICATION_URLS"`
|
||||||
NotificationURLs []string `envconfig:"NOTIFICATION_URLS"`
|
NotificationLevel string `split_words:"true" default:"error"`
|
||||||
NotificationLevel string `split_words:"true" default:"error"`
|
EmailNotificationRecipient string `split_words:"true"`
|
||||||
EmailNotificationRecipient string `split_words:"true"`
|
EmailNotificationSender string `split_words:"true" default:"noreply@nohost"`
|
||||||
EmailNotificationSender string `split_words:"true" default:"noreply@nohost"`
|
EmailSMTPHost string `envconfig:"EMAIL_SMTP_HOST"`
|
||||||
EmailSMTPHost string `envconfig:"EMAIL_SMTP_HOST"`
|
EmailSMTPPort int `envconfig:"EMAIL_SMTP_PORT" default:"587"`
|
||||||
EmailSMTPPort int `envconfig:"EMAIL_SMTP_PORT" default:"587"`
|
EmailSMTPUsername string `envconfig:"EMAIL_SMTP_USERNAME"`
|
||||||
EmailSMTPUsername string `envconfig:"EMAIL_SMTP_USERNAME"`
|
EmailSMTPPassword string `envconfig:"EMAIL_SMTP_PASSWORD"`
|
||||||
EmailSMTPPassword string `envconfig:"EMAIL_SMTP_PASSWORD"`
|
WebdavUrl string `split_words:"true"`
|
||||||
WebdavUrl string `split_words:"true"`
|
WebdavUrlInsecure bool `split_words:"true"`
|
||||||
WebdavUrlInsecure bool `split_words:"true"`
|
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"`
|
||||||
SSHHostName string `split_words:"true"`
|
SSHPort string `split_words:"true" default:"22"`
|
||||||
SSHPort string `split_words:"true" default:"22"`
|
SSHUser string `split_words:"true"`
|
||||||
SSHUser string `split_words:"true"`
|
SSHPassword string `split_words:"true"`
|
||||||
SSHPassword string `split_words:"true"`
|
SSHIdentityFile string `split_words:"true" default:"/root/.ssh/id_rsa"`
|
||||||
SSHIdentityFile string `split_words:"true" default:"/root/.ssh/id_rsa"`
|
SSHIdentityPassphrase string `split_words:"true"`
|
||||||
SSHIdentityPassphrase string `split_words:"true"`
|
SSHRemotePath 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"`
|
AzureStorageAccountName string `split_words:"true"`
|
||||||
AzureStorageAccountName string `split_words:"true"`
|
AzureStoragePrimaryAccountKey string `split_words:"true"`
|
||||||
AzureStoragePrimaryAccountKey string `split_words:"true"`
|
AzureStorageContainerName string `split_words:"true"`
|
||||||
AzureStorageContainerName string `split_words:"true"`
|
AzureStoragePath string `split_words:"true"`
|
||||||
AzureStoragePath string `split_words:"true"`
|
AzureStorageEndpoint string `split_words:"true" default:"https://{{ .AccountName }}.blob.core.windows.net/"`
|
||||||
AzureStorageEndpoint string `split_words:"true" default:"https://{{ .AccountName }}.blob.core.windows.net/"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) resolveSecret(envVar string, secretPath string) (string, error) {
|
func (c *Config) resolveSecret(envVar string, secretPath string) (string, error) {
|
||||||
@@ -83,22 +82,6 @@ func (c *Config) resolveSecret(envVar string, secretPath string) (string, error)
|
|||||||
return string(data), nil
|
return string(data), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type CompressionType string
|
|
||||||
|
|
||||||
func (c *CompressionType) Decode(v string) error {
|
|
||||||
switch v {
|
|
||||||
case "gz", "zst":
|
|
||||||
*c = CompressionType(v)
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("config: error decoding compression type %s", v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CompressionType) String() string {
|
|
||||||
return string(*c)
|
|
||||||
}
|
|
||||||
|
|
||||||
type CertDecoder struct {
|
type CertDecoder struct {
|
||||||
Cert *x509.Certificate
|
Cert *x509.Certificate
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ func (s *script) runLabeledCommands(label string) error {
|
|||||||
userLabelName := fmt.Sprintf("%s.user", label)
|
userLabelName := fmt.Sprintf("%s.user", label)
|
||||||
user := c.Labels[userLabelName]
|
user := c.Labels[userLabelName]
|
||||||
|
|
||||||
s.logger.Info(fmt.Sprintf("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, user)
|
||||||
if s.c.ExecForwardOutput {
|
if s.c.ExecForwardOutput {
|
||||||
os.Stderr.Write(stderr)
|
os.Stderr.Write(stderr)
|
||||||
|
|||||||
@@ -41,11 +41,9 @@ func (s *script) lock(lockfile string) (func() error, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !s.encounteredLock {
|
if !s.encounteredLock {
|
||||||
s.logger.Info(
|
s.logger.Infof(
|
||||||
fmt.Sprintf(
|
"Exclusive lock was not available on first attempt. Will retry until it becomes available or the timeout of %s is exceeded.",
|
||||||
"Exclusive lock was not available on first attempt. Will retry until it becomes available or the timeout of %s is exceeded.",
|
s.c.LockTimeout,
|
||||||
s.c.LockTimeout,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
s.encounteredLock = true
|
s.encounteredLock = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,9 +21,7 @@ func main() {
|
|||||||
if pArg := recover(); pArg != nil {
|
if pArg := recover(); pArg != nil {
|
||||||
if err, ok := pArg.(error); ok {
|
if err, ok := pArg.(error); ok {
|
||||||
if hookErr := s.runHooks(err); hookErr != nil {
|
if hookErr := s.runHooks(err); hookErr != nil {
|
||||||
s.logger.Error(
|
s.logger.Errorf("An error occurred calling the registered hooks: %s", hookErr)
|
||||||
fmt.Sprintf("An error occurred calling the registered hooks: %s", hookErr),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@@ -32,12 +29,9 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := s.runHooks(nil); err != nil {
|
if err := s.runHooks(nil); err != nil {
|
||||||
s.logger.Error(
|
s.logger.Errorf(
|
||||||
fmt.Sprintf(
|
"Backup procedure ran successfully, but an error ocurred calling the registered hooks: %v",
|
||||||
|
err,
|
||||||
"Backup procedure ran successfully, but an error ocurred calling the registered hooks: %v",
|
|
||||||
err,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log/slog"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -34,6 +32,7 @@ import (
|
|||||||
"github.com/kelseyhightower/envconfig"
|
"github.com/kelseyhightower/envconfig"
|
||||||
"github.com/leekchan/timeutil"
|
"github.com/leekchan/timeutil"
|
||||||
"github.com/otiai10/copy"
|
"github.com/otiai10/copy"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/crypto/openpgp"
|
"golang.org/x/crypto/openpgp"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
@@ -43,7 +42,7 @@ import (
|
|||||||
type script struct {
|
type script struct {
|
||||||
cli *client.Client
|
cli *client.Client
|
||||||
storages []storage.Backend
|
storages []storage.Backend
|
||||||
logger *slog.Logger
|
logger *logrus.Logger
|
||||||
sender *router.ServiceRouter
|
sender *router.ServiceRouter
|
||||||
template *template.Template
|
template *template.Template
|
||||||
hooks []hook
|
hooks []hook
|
||||||
@@ -64,8 +63,13 @@ type script struct {
|
|||||||
func newScript() (*script, error) {
|
func newScript() (*script, error) {
|
||||||
stdOut, logBuffer := buffer(os.Stdout)
|
stdOut, logBuffer := buffer(os.Stdout)
|
||||||
s := &script{
|
s := &script{
|
||||||
c: &Config{},
|
c: &Config{},
|
||||||
logger: slog.New(slog.NewTextHandler(stdOut, nil)),
|
logger: &logrus.Logger{
|
||||||
|
Out: stdOut,
|
||||||
|
Formatter: new(logrus.TextFormatter),
|
||||||
|
Hooks: make(logrus.LevelHooks),
|
||||||
|
Level: logrus.InfoLevel,
|
||||||
|
},
|
||||||
stats: &Stats{
|
stats: &Stats{
|
||||||
StartTime: time.Now(),
|
StartTime: time.Now(),
|
||||||
LogOutput: logBuffer,
|
LogOutput: logBuffer,
|
||||||
@@ -90,20 +94,6 @@ func newScript() (*script, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s.file = path.Join("/tmp", s.c.BackupFilename)
|
s.file = path.Join("/tmp", s.c.BackupFilename)
|
||||||
|
|
||||||
tmplFileName, tErr := template.New("extension").Parse(s.file)
|
|
||||||
if tErr != nil {
|
|
||||||
return nil, fmt.Errorf("newScript: unable to parse backup file extension template: %w", tErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
var bf bytes.Buffer
|
|
||||||
if tErr := tmplFileName.Execute(&bf, map[string]string{
|
|
||||||
"Extension": fmt.Sprintf("tar.%s", s.c.BackupCompression),
|
|
||||||
}); tErr != nil {
|
|
||||||
return nil, fmt.Errorf("newScript: error executing backup file extension template: %w", tErr)
|
|
||||||
}
|
|
||||||
s.file = bf.String()
|
|
||||||
|
|
||||||
if s.c.BackupFilenameExpand {
|
if s.c.BackupFilenameExpand {
|
||||||
s.file = os.ExpandEnv(s.file)
|
s.file = os.ExpandEnv(s.file)
|
||||||
s.c.BackupLatestSymlink = os.ExpandEnv(s.c.BackupLatestSymlink)
|
s.c.BackupLatestSymlink = os.ExpandEnv(s.c.BackupLatestSymlink)
|
||||||
@@ -124,12 +114,12 @@ func newScript() (*script, error) {
|
|||||||
logFunc := func(logType storage.LogLevel, context string, msg string, params ...any) {
|
logFunc := func(logType storage.LogLevel, context string, msg string, params ...any) {
|
||||||
switch logType {
|
switch logType {
|
||||||
case storage.LogLevelWarning:
|
case storage.LogLevelWarning:
|
||||||
s.logger.Warn(fmt.Sprintf("["+context+"] "+msg, params...))
|
s.logger.Warnf("["+context+"] "+msg, params...)
|
||||||
case storage.LogLevelError:
|
case storage.LogLevelError:
|
||||||
s.logger.Error(fmt.Sprintf("["+context+"] "+msg, params...))
|
s.logger.Errorf("["+context+"] "+msg, params...)
|
||||||
case storage.LogLevelInfo:
|
case storage.LogLevelInfo:
|
||||||
default:
|
default:
|
||||||
s.logger.Info(fmt.Sprintf("["+context+"] "+msg, params...))
|
s.logger.Infof("["+context+"] "+msg, params...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,13 +306,11 @@ func (s *script) stopContainers() (func() error, error) {
|
|||||||
return noop, nil
|
return noop, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
s.logger.Info(
|
s.logger.Infof(
|
||||||
fmt.Sprintf(
|
"Stopping %d container(s) labeled `%s` out of %d running container(s).",
|
||||||
"Stopping %d container(s) labeled `%s` out of %d running container(s).",
|
len(containersToStop),
|
||||||
len(containersToStop),
|
containerLabel,
|
||||||
containerLabel,
|
len(allContainers),
|
||||||
len(allContainers),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var stoppedContainers []types.Container
|
var stoppedContainers []types.Container
|
||||||
@@ -394,11 +382,9 @@ func (s *script) stopContainers() (func() error, error) {
|
|||||||
errors.Join(restartErrors...),
|
errors.Join(restartErrors...),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
s.logger.Info(
|
s.logger.Infof(
|
||||||
fmt.Sprintf(
|
"Restarted %d container(s) and the matching service(s).",
|
||||||
"Restarted %d container(s) and the matching service(s).",
|
len(stoppedContainers),
|
||||||
len(stoppedContainers),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
return nil
|
return nil
|
||||||
}, stopError
|
}, stopError
|
||||||
@@ -422,9 +408,7 @@ func (s *script) createArchive() error {
|
|||||||
if err := remove(backupSources); err != nil {
|
if err := remove(backupSources); err != nil {
|
||||||
return fmt.Errorf("createArchive: error removing snapshot: %w", err)
|
return fmt.Errorf("createArchive: error removing snapshot: %w", err)
|
||||||
}
|
}
|
||||||
s.logger.Info(
|
s.logger.Infof("Removed snapshot `%s`.", backupSources)
|
||||||
fmt.Sprintf("Removed snapshot `%s`.", backupSources),
|
|
||||||
)
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err := copy.Copy(s.c.BackupSources, backupSources, copy.Options{
|
if err := copy.Copy(s.c.BackupSources, backupSources, copy.Options{
|
||||||
@@ -433,9 +417,7 @@ func (s *script) createArchive() error {
|
|||||||
}); err != nil {
|
}); err != nil {
|
||||||
return fmt.Errorf("createArchive: error creating snapshot: %w", err)
|
return fmt.Errorf("createArchive: error creating snapshot: %w", err)
|
||||||
}
|
}
|
||||||
s.logger.Info(
|
s.logger.Infof("Created snapshot of `%s` at `%s`.", s.c.BackupSources, backupSources)
|
||||||
fmt.Sprintf("Created snapshot of `%s` at `%s`.", s.c.BackupSources, backupSources),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tarFile := s.file
|
tarFile := s.file
|
||||||
@@ -443,9 +425,7 @@ func (s *script) createArchive() error {
|
|||||||
if err := remove(tarFile); err != nil {
|
if err := remove(tarFile); err != nil {
|
||||||
return fmt.Errorf("createArchive: error removing tar file: %w", err)
|
return fmt.Errorf("createArchive: error removing tar file: %w", err)
|
||||||
}
|
}
|
||||||
s.logger.Info(
|
s.logger.Infof("Removed tar file `%s`.", tarFile)
|
||||||
fmt.Sprintf("Removed tar file `%s`.", tarFile),
|
|
||||||
)
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -469,13 +449,11 @@ func (s *script) createArchive() error {
|
|||||||
return fmt.Errorf("createArchive: error walking filesystem tree: %w", err)
|
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); err != nil {
|
||||||
return fmt.Errorf("createArchive: error compressing backup folder: %w", err)
|
return fmt.Errorf("createArchive: error compressing backup folder: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.logger.Info(
|
s.logger.Infof("Created backup of `%s` at `%s`.", backupSources, tarFile)
|
||||||
fmt.Sprintf("Created backup of `%s` at `%s`.", backupSources, tarFile),
|
|
||||||
)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,9 +470,7 @@ func (s *script) encryptArchive() error {
|
|||||||
if err := remove(gpgFile); err != nil {
|
if err := remove(gpgFile); err != nil {
|
||||||
return fmt.Errorf("encryptArchive: error removing gpg file: %w", err)
|
return fmt.Errorf("encryptArchive: error removing gpg file: %w", err)
|
||||||
}
|
}
|
||||||
s.logger.Info(
|
s.logger.Infof("Removed GPG file `%s`.", gpgFile)
|
||||||
fmt.Sprintf("Removed GPG file `%s`.", gpgFile),
|
|
||||||
)
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -524,9 +500,7 @@ func (s *script) encryptArchive() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s.file = gpgFile
|
s.file = gpgFile
|
||||||
s.logger.Info(
|
s.logger.Infof("Encrypted backup using given passphrase, saving as `%s`.", s.file)
|
||||||
fmt.Sprintf("Encrypted backup using given passphrase, saving as `%s`.", s.file),
|
|
||||||
)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -598,9 +572,7 @@ func (s *script) pruneBackups() error {
|
|||||||
// is non-nil.
|
// is non-nil.
|
||||||
func (s *script) must(err error) {
|
func (s *script) must(err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error(
|
s.logger.Errorf("Fatal error running backup: %s", err)
|
||||||
fmt.Sprintf("Fatal error running backup: %s", err),
|
|
||||||
)
|
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,5 +22,12 @@ 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
|
||||||
|
|||||||
8
go.mod
8
go.mod
@@ -1,6 +1,6 @@
|
|||||||
module github.com/offen/docker-volume-backup
|
module github.com/offen/docker-volume-backup
|
||||||
|
|
||||||
go 1.21
|
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.3.0
|
||||||
@@ -10,11 +10,11 @@ require (
|
|||||||
github.com/docker/docker v24.0.5+incompatible
|
github.com/docker/docker v24.0.5+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/klauspost/compress v1.16.7
|
|
||||||
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.61
|
||||||
github.com/otiai10/copy v1.11.0
|
github.com/otiai10/copy v1.11.0
|
||||||
github.com/pkg/sftp v1.13.6
|
github.com/pkg/sftp v1.13.5
|
||||||
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/studio-b12/gowebdav v0.9.0
|
github.com/studio-b12/gowebdav v0.9.0
|
||||||
golang.org/x/crypto v0.11.0
|
golang.org/x/crypto v0.11.0
|
||||||
golang.org/x/sync v0.3.0
|
golang.org/x/sync v0.3.0
|
||||||
@@ -34,6 +34,7 @@ require (
|
|||||||
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
github.com/google/uuid v1.3.0 // 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/cpuid/v2 v2.2.5 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||||
github.com/kr/fs v0.1.0 // indirect
|
github.com/kr/fs v0.1.0 // indirect
|
||||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||||
@@ -51,7 +52,6 @@ require (
|
|||||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
|
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // 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.5.0 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
|
||||||
golang.org/x/net v0.12.0 // indirect
|
golang.org/x/net v0.12.0 // indirect
|
||||||
golang.org/x/sys v0.10.0 // indirect
|
golang.org/x/sys v0.10.0 // indirect
|
||||||
golang.org/x/text v0.11.0 // indirect
|
golang.org/x/text v0.11.0 // indirect
|
||||||
|
|||||||
9
go.sum
9
go.sum
@@ -188,7 +188,6 @@ github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9Orh
|
|||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY=
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.2.0 h1:Ma67P/GGprNwsslzEH6+Kb8nybI8jpDTm4Wmzu2ReK8=
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.2.0 h1:Ma67P/GGprNwsslzEH6+Kb8nybI8jpDTm4Wmzu2ReK8=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.2.0/go.mod h1:c+Lifp3EDEamAkPVzMooRNOK6CZjNSdEnf1A7jsI9u4=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.1.0 h1:nVocQV40OQne5613EeLayJiRAJuKlBGy+m22qWG+WRg=
|
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.1.0 h1:nVocQV40OQne5613EeLayJiRAJuKlBGy+m22qWG+WRg=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.1.0/go.mod h1:7QJP7dr2wznCMeqIrhMgWGf7XpAQnVrJqDm9nvV3Cu4=
|
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.1.0/go.mod h1:7QJP7dr2wznCMeqIrhMgWGf7XpAQnVrJqDm9nvV3Cu4=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
|
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
|
||||||
@@ -248,7 +247,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
|||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
|
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
|
||||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
|
||||||
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
|
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
|
||||||
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||||
github.com/docker/docker v24.0.5+incompatible h1:WmgcE4fxyI6EEXxBRxsHnZXrO1pQ3smi0k/jho4HLeY=
|
github.com/docker/docker v24.0.5+incompatible h1:WmgcE4fxyI6EEXxBRxsHnZXrO1pQ3smi0k/jho4HLeY=
|
||||||
@@ -545,7 +543,6 @@ github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799/go.mod
|
|||||||
github.com/otiai10/copy v1.11.0 h1:OKBD80J/mLBrwnzXqGtFCzprFSGioo30JcmR4APsNwc=
|
github.com/otiai10/copy v1.11.0 h1:OKBD80J/mLBrwnzXqGtFCzprFSGioo30JcmR4APsNwc=
|
||||||
github.com/otiai10/copy v1.11.0/go.mod h1:rSaLseMUsZFFbsFGc7wCJnnkTAvdc5L6VWxPE4308Ww=
|
github.com/otiai10/copy v1.11.0/go.mod h1:rSaLseMUsZFFbsFGc7wCJnnkTAvdc5L6VWxPE4308Ww=
|
||||||
github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
|
github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
|
||||||
github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
|
|
||||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||||
@@ -557,8 +554,8 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
|||||||
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.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||||
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
|
github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go=
|
||||||
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
|
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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||||
@@ -660,6 +657,7 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
|||||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||||
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
||||||
@@ -905,7 +903,6 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn
|
|||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
|
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
|
||||||
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
cd $(dirname $0)
|
|
||||||
. ../util.sh
|
|
||||||
current_test=$(basename $(pwd))
|
|
||||||
|
|
||||||
docker network create test_network
|
|
||||||
docker volume create backup_data
|
|
||||||
docker volume create app_data
|
|
||||||
# This volume is created to test whether empty directories are handled
|
|
||||||
# correctly. It is not supposed to hold any data.
|
|
||||||
docker volume create empty_data
|
|
||||||
|
|
||||||
docker run -d \
|
|
||||||
--name minio \
|
|
||||||
--network test_network \
|
|
||||||
--env MINIO_ROOT_USER=test \
|
|
||||||
--env MINIO_ROOT_PASSWORD=test \
|
|
||||||
--env MINIO_ACCESS_KEY=test \
|
|
||||||
--env MINIO_SECRET_KEY=GMusLtUmILge2by+z890kQ \
|
|
||||||
-v backup_data:/data \
|
|
||||||
minio/minio:RELEASE.2020-08-04T23-10-51Z server /data
|
|
||||||
|
|
||||||
docker exec minio mkdir -p /data/backup
|
|
||||||
|
|
||||||
docker run -d \
|
|
||||||
--name offen \
|
|
||||||
--network test_network \
|
|
||||||
-v app_data:/var/opt/offen/ \
|
|
||||||
offen/offen:latest
|
|
||||||
|
|
||||||
sleep 10
|
|
||||||
|
|
||||||
docker run --rm \
|
|
||||||
--network test_network \
|
|
||||||
-v app_data:/backup/app_data \
|
|
||||||
-v empty_data:/backup/empty_data \
|
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
|
||||||
--env AWS_ACCESS_KEY_ID=test \
|
|
||||||
--env AWS_SECRET_ACCESS_KEY=GMusLtUmILge2by+z890kQ \
|
|
||||||
--env AWS_ENDPOINT=minio:9000 \
|
|
||||||
--env AWS_ENDPOINT_PROTO=http \
|
|
||||||
--env AWS_S3_BUCKET_NAME=backup \
|
|
||||||
--env BACKUP_COMPRESSION=zst \
|
|
||||||
--env BACKUP_FILENAME='test.{{ .Extension }}' \
|
|
||||||
--env "BACKUP_FROM_SNAPSHOT=true" \
|
|
||||||
--entrypoint backup \
|
|
||||||
offen/docker-volume-backup:${TEST_VERSION:-canary}
|
|
||||||
|
|
||||||
# Have to install tar and zstd on Alpine because the plain image comes with very
|
|
||||||
# basic tar from busybox and it does not seem to support zstd
|
|
||||||
docker run --rm \
|
|
||||||
-v backup_data:/data alpine \
|
|
||||||
ash -c 'apk add --no-cache zstd tar && tar -xvf /data/backup/test.tar.zst --zstd && test -f /backup/app_data/offen.db && test -d /backup/empty_data'
|
|
||||||
|
|
||||||
pass "Found relevant files in untared remote 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 "2"
|
|
||||||
|
|
||||||
docker rm $(docker stop minio offen)
|
|
||||||
docker volume rm backup_data app_data
|
|
||||||
docker network rm test_network
|
|
||||||
Reference in New Issue
Block a user