2021-08-22 18:07:32 +02:00
// Copyright 2021 - Offen Authors <hioffen@posteo.de>
// SPDX-License-Identifier: MPL-2.0
2021-08-21 19:05:49 +02:00
package main
import (
2021-09-09 07:24:18 +02:00
"bytes"
2021-08-21 19:05:49 +02:00
"context"
2021-08-29 19:39:51 +02:00
"errors"
2021-08-21 19:05:49 +02:00
"fmt"
2021-08-21 21:26:27 +02:00
"io"
2022-01-22 13:29:21 +01:00
"io/fs"
2021-08-21 19:05:49 +02:00
"os"
2021-08-21 21:26:27 +02:00
"path"
2021-08-22 14:00:21 +02:00
"path/filepath"
2021-12-17 20:54:25 +01:00
"sort"
2021-08-29 19:39:51 +02:00
"strings"
2021-08-22 11:02:10 +02:00
"time"
2021-08-21 19:05:49 +02:00
2021-12-17 08:31:28 +01:00
"github.com/containrrr/shoutrrr"
2021-12-18 09:56:05 +01:00
"github.com/containrrr/shoutrrr/pkg/router"
2021-12-17 08:31:28 +01:00
sTypes "github.com/containrrr/shoutrrr/pkg/types"
2021-08-21 19:05:49 +02:00
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
2021-08-23 18:46:49 +02:00
"github.com/gofrs/flock"
2021-08-24 09:01:44 +02:00
"github.com/kelseyhightower/envconfig"
2021-08-22 21:06:51 +02:00
"github.com/leekchan/timeutil"
2021-09-03 19:06:42 +02:00
"github.com/m90/targz"
2021-08-24 11:39:27 +02:00
"github.com/minio/minio-go/v7"
2021-08-21 21:26:27 +02:00
"github.com/minio/minio-go/v7/pkg/credentials"
2021-11-08 08:39:18 +01:00
"github.com/otiai10/copy"
2021-08-22 16:41:06 +02:00
"github.com/sirupsen/logrus"
2022-01-22 13:29:21 +01:00
"github.com/studio-b12/gowebdav"
2022-01-22 13:35:13 +01:00
"golang.org/x/crypto/openpgp"
2021-08-21 19:05:49 +02:00
)
func main ( ) {
2021-08-24 09:01:44 +02:00
unlock := lock ( "/var/lock/dockervolumebackup.lock" )
2021-08-22 15:52:47 +02:00
defer unlock ( )
2021-08-21 19:26:42 +02:00
2021-08-24 11:39:27 +02:00
s , err := newScript ( )
if err != nil {
panic ( err )
}
2021-09-12 08:54:33 +02:00
defer func ( ) {
2021-11-08 19:10:10 +01:00
if pArg := recover ( ) ; pArg != nil {
if err , ok := pArg . ( error ) ; ok {
2021-12-18 09:56:05 +01:00
if hookErr := s . runHooks ( err ) ; hookErr != nil {
2021-11-08 19:10:10 +01:00
s . logger . Errorf ( "An error occurred calling the registered hooks: %s" , hookErr )
}
2021-09-12 08:54:33 +02:00
os . Exit ( 1 )
}
2021-11-08 19:10:10 +01:00
panic ( pArg )
}
2021-12-18 09:56:05 +01:00
if err := s . runHooks ( nil ) ; err != nil {
2021-11-08 19:10:10 +01:00
s . logger . Errorf (
"Backup procedure ran successfully, but an error ocurred calling the registered hooks: %v" ,
err ,
)
os . Exit ( 1 )
2021-09-12 08:54:33 +02:00
}
2021-11-08 19:10:10 +01:00
s . logger . Info ( "Finished running backup tasks." )
2021-09-12 08:54:33 +02:00
} ( )
2021-08-24 11:39:27 +02:00
s . must ( func ( ) error {
restartContainers , err := s . stopContainers ( )
2021-11-08 19:10:10 +01:00
// The mechanism for restarting containers is not using hooks as it
// should happen as soon as possible (i.e. before uploading backups or
// similar).
2021-08-26 16:22:24 +02:00
defer func ( ) {
s . must ( restartContainers ( ) )
} ( )
2021-08-24 11:39:27 +02:00
if err != nil {
return err
}
return s . takeBackup ( )
} ( ) )
2021-11-08 19:10:10 +01:00
s . must ( s . encryptBackup ( ) )
s . must ( s . copyBackup ( ) )
2021-08-22 19:37:48 +02:00
s . must ( s . pruneOldBackups ( ) )
2021-08-21 19:26:42 +02:00
}
2021-08-23 08:19:22 +02:00
// script holds all the stateful information required to orchestrate a
// single backup run.
2021-08-21 19:26:42 +02:00
type script struct {
2022-01-22 13:35:13 +01:00
cli * client . Client
minioClient * minio . Client
webdavClient * gowebdav . Client
logger * logrus . Logger
sender * router . ServiceRouter
hooks [ ] hook
hookLevel hookLevel
2021-08-22 22:02:19 +02:00
2021-09-09 07:24:18 +02:00
start time . Time
file string
output * bytes . Buffer
2021-08-22 22:02:19 +02:00
2021-08-24 09:01:44 +02:00
c * config
}
type config struct {
2021-09-09 08:58:03 +02:00
BackupSources string ` split_words:"true" default:"/backup" `
BackupFilename string ` split_words:"true" default:"backup-%Y-%m-%dT%H-%M-%S.tar.gz" `
2021-12-22 14:39:46 +01:00
BackupFilenameExpand bool ` split_words:"true" `
2021-10-01 10:07:46 +02:00
BackupLatestSymlink string ` split_words:"true" `
2021-09-09 08:58:03 +02:00
BackupArchive string ` split_words:"true" default:"/archive" `
BackupRetentionDays int32 ` split_words:"true" default:"-1" `
BackupPruningLeeway time . Duration ` split_words:"true" default:"1m" `
BackupPruningPrefix string ` split_words:"true" `
BackupStopContainerLabel string ` split_words:"true" default:"true" `
2021-11-08 08:39:18 +01:00
BackupFromSnapshot bool ` split_words:"true" `
2021-09-09 08:58:03 +02:00
AwsS3BucketName string ` split_words:"true" `
2022-01-25 21:16:16 +01:00
AwsS3Path string ` split_words:"true" `
2021-09-09 08:58:03 +02:00
AwsEndpoint string ` split_words:"true" default:"s3.amazonaws.com" `
AwsEndpointProto string ` split_words:"true" default:"https" `
AwsEndpointInsecure bool ` split_words:"true" `
AwsAccessKeyID string ` envconfig:"AWS_ACCESS_KEY_ID" `
AwsSecretAccessKey string ` split_words:"true" `
2021-09-30 19:24:43 +02:00
AwsIamRoleEndpoint string ` split_words:"true" `
2021-09-09 08:58:03 +02:00
GpgPassphrase string ` split_words:"true" `
2021-12-17 08:31:28 +01:00
NotificationURLs [ ] string ` envconfig:"NOTIFICATION_URLS" `
2021-12-17 20:45:15 +01:00
NotificationLevel string ` split_words:"true" default:"error" `
2021-09-09 08:58:03 +02:00
EmailNotificationRecipient string ` split_words:"true" `
EmailNotificationSender string ` split_words:"true" default:"noreply@nohost" `
EmailSMTPHost string ` envconfig:"EMAIL_SMTP_HOST" `
EmailSMTPPort int ` envconfig:"EMAIL_SMTP_PORT" default:"587" `
EmailSMTPUsername string ` envconfig:"EMAIL_SMTP_USERNAME" `
EmailSMTPPassword string ` envconfig:"EMAIL_SMTP_PASSWORD" `
2022-01-22 13:29:21 +01:00
WebdavUrl string ` split_words:"true" `
WebdavPath string ` split_words:"true" default:"/" `
WebdavUsername string ` split_words:"true" `
WebdavPassword string ` split_words:"true" `
2021-08-21 19:26:42 +02:00
}
2021-09-12 08:54:33 +02:00
var msgBackupFailed = "backup run failed"
2021-08-24 11:39:27 +02:00
// newScript creates all resources needed for the script to perform actions against
2021-08-23 08:19:22 +02:00
// remote resources like the Docker engine or remote storage locations. All
// reading from env vars or other configuration sources is expected to happen
// in this method.
2021-08-24 11:39:27 +02:00
func newScript ( ) ( * script , error ) {
2021-09-09 07:24:18 +02:00
stdOut , logBuffer := buffer ( os . Stdout )
2021-08-24 11:39:27 +02:00
s := & script {
2021-08-29 18:26:40 +02:00
c : & config { } ,
2021-08-24 11:39:27 +02:00
logger : & logrus . Logger {
2021-09-09 07:24:18 +02:00
Out : stdOut ,
2021-08-24 11:39:27 +02:00
Formatter : new ( logrus . TextFormatter ) ,
Hooks : make ( logrus . LevelHooks ) ,
Level : logrus . InfoLevel ,
} ,
2021-09-09 07:24:18 +02:00
start : time . Now ( ) ,
output : logBuffer ,
2021-08-24 11:39:27 +02:00
}
2021-08-21 19:26:42 +02:00
2021-08-24 09:01:44 +02:00
if err := envconfig . Process ( "" , s . c ) ; err != nil {
2021-08-24 11:39:27 +02:00
return nil , fmt . Errorf ( "newScript: failed to process configuration values: %w" , err )
2021-08-21 19:26:42 +02:00
}
2021-08-24 11:39:27 +02:00
s . file = path . Join ( "/tmp" , s . c . BackupFilename )
2021-12-22 14:39:46 +01:00
if s . c . BackupFilenameExpand {
s . file = os . ExpandEnv ( s . file )
s . c . BackupLatestSymlink = os . ExpandEnv ( s . c . BackupLatestSymlink )
2021-12-23 09:22:56 +01:00
s . c . BackupPruningPrefix = os . ExpandEnv ( s . c . BackupPruningPrefix )
2021-12-22 14:39:46 +01:00
}
s . file = timeutil . Strftime ( & s . start , s . file )
2021-08-24 11:39:27 +02:00
2021-08-22 15:52:47 +02:00
_ , err := os . Stat ( "/var/run/docker.sock" )
if ! os . IsNotExist ( err ) {
2021-08-21 19:26:42 +02:00
cli , err := client . NewClientWithOpts ( client . FromEnv , client . WithAPIVersionNegotiation ( ) )
2021-08-21 19:05:49 +02:00
if err != nil {
2021-08-24 11:39:27 +02:00
return nil , fmt . Errorf ( "newScript: failed to create docker client" )
2021-08-21 19:05:49 +02:00
}
2021-08-21 19:26:42 +02:00
s . cli = cli
}
2021-08-21 21:26:27 +02:00
2021-08-24 09:01:44 +02:00
if s . c . AwsS3BucketName != "" {
2021-09-30 19:24:43 +02:00
var creds * credentials . Credentials
if s . c . AwsAccessKeyID != "" && s . c . AwsSecretAccessKey != "" {
creds = credentials . NewStaticV4 (
2021-08-24 09:01:44 +02:00
s . c . AwsAccessKeyID ,
s . c . AwsSecretAccessKey ,
2021-08-21 21:26:27 +02:00
"" ,
2021-09-30 19:24:43 +02:00
)
} else if s . c . AwsIamRoleEndpoint != "" {
creds = credentials . NewIAM ( s . c . AwsIamRoleEndpoint )
} else {
return nil , errors . New ( "newScript: AWS_S3_BUCKET_NAME is defined, but no credentials were provided" )
}
2021-10-28 19:55:39 +02:00
2021-10-28 19:51:35 +02:00
options := minio . Options {
2021-09-30 19:24:43 +02:00
Creds : creds ,
2021-10-28 19:51:35 +02:00
Secure : s . c . AwsEndpointProto == "https" ,
}
2021-10-28 19:55:39 +02:00
2021-10-28 19:51:35 +02:00
if s . c . AwsEndpointInsecure {
2021-10-28 19:55:39 +02:00
if ! options . Secure {
2021-10-28 19:51:35 +02:00
return nil , errors . New ( "newScript: AWS_ENDPOINT_INSECURE = true is only meaningful for https" )
}
2021-10-28 19:55:39 +02:00
transport , err := minio . DefaultTransport ( true )
if err != nil {
return nil , fmt . Errorf ( "newScript: failed to create default minio transport" )
}
transport . TLSClientConfig . InsecureSkipVerify = true
options . Transport = transport
2021-10-28 19:51:35 +02:00
}
mc , err := minio . New ( s . c . AwsEndpoint , & options )
2021-08-21 21:26:27 +02:00
if err != nil {
2021-08-24 11:39:27 +02:00
return nil , fmt . Errorf ( "newScript: error setting up minio client: %w" , err )
2021-08-21 21:26:27 +02:00
}
2022-01-22 13:35:13 +01:00
s . minioClient = mc
2021-08-21 21:26:27 +02:00
}
2021-08-22 16:41:06 +02:00
2022-01-22 13:29:21 +01:00
if s . c . WebdavUrl != "" {
if s . c . WebdavUsername == "" || s . c . WebdavPassword == "" {
return nil , errors . New ( "newScript: WEBDAV_URL is defined, but no credentials were provided" )
} else {
webdavClient := gowebdav . NewClient ( s . c . WebdavUrl , s . c . WebdavUsername , s . c . WebdavPassword )
s . webdavClient = webdavClient
}
}
2021-09-09 08:58:03 +02:00
if s . c . EmailNotificationRecipient != "" {
2021-12-17 08:31:28 +01:00
emailURL := fmt . Sprintf (
"smtp://%s:%s@%s:%d/?from=%s&to=%s" ,
s . c . EmailSMTPUsername ,
s . c . EmailSMTPPassword ,
s . c . EmailSMTPHost ,
s . c . EmailSMTPPort ,
s . c . EmailNotificationSender ,
s . c . EmailNotificationRecipient ,
)
s . c . NotificationURLs = append ( s . c . NotificationURLs , emailURL )
2021-12-17 17:44:22 +01:00
s . logger . Warn (
"Using EMAIL_* keys for providing notification configuration has been deprecated and will be removed in the next major version." ,
)
s . logger . Warn (
"Please use NOTIFICATION_URLS instead. Refer to the README for an upgrade guide." ,
)
2021-12-17 08:31:28 +01:00
}
2021-09-09 08:58:03 +02:00
2021-12-18 09:56:05 +01:00
hookLevel , ok := hookLevels [ s . c . NotificationLevel ]
2021-12-17 20:45:15 +01:00
if ! ok {
return nil , fmt . Errorf ( "newScript: unknown NOTIFICATION_LEVEL %s" , s . c . NotificationLevel )
2021-09-09 08:58:03 +02:00
}
2021-12-18 09:56:05 +01:00
s . hookLevel = hookLevel
if len ( s . c . NotificationURLs ) > 0 {
sender , senderErr := shoutrrr . CreateSender ( s . c . NotificationURLs ... )
if senderErr != nil {
return nil , fmt . Errorf ( "newScript: error creating sender: %w" , senderErr )
}
s . sender = sender
// To prevent duplicate notifications, ensure the regsistered callbacks
// run mutually exclusive.
s . registerHook ( hookLevelError , func ( err error ) error {
if err == nil {
return nil
}
return s . notifyFailure ( err )
} )
s . registerHook ( hookLevelInfo , func ( err error ) error {
if err != nil {
return nil
}
return s . notifySuccess ( )
} )
}
2021-09-09 08:58:03 +02:00
2021-08-24 11:39:27 +02:00
return s , nil
2021-08-21 19:26:42 +02:00
}
2021-08-24 11:39:27 +02:00
var noop = func ( ) error { return nil }
2021-11-08 19:10:10 +01:00
// registerHook adds the given action at the given level.
2021-12-17 17:44:22 +01:00
func ( s * script ) registerHook ( level hookLevel , action func ( err error ) error ) {
2021-11-08 19:10:10 +01:00
s . hooks = append ( s . hooks , hook { level , action } )
}
2021-12-18 09:56:05 +01:00
// notifyFailure sends a notification about a failed backup run
func ( s * script ) notifyFailure ( err error ) error {
body := fmt . Sprintf (
"Running docker-volume-backup failed with error: %s\n\nLog output of the failed run was:\n\n%s\n" , err , s . output . String ( ) ,
)
title := fmt . Sprintf ( "Failure running docker-volume-backup at %s" , s . start . Format ( time . RFC3339 ) )
if err := s . sendNotification ( title , body ) ; err != nil {
return fmt . Errorf ( "notifyFailure: error notifying: %w" , err )
2021-12-17 17:44:22 +01:00
}
2021-12-18 09:56:05 +01:00
return nil
}
2021-12-17 17:44:22 +01:00
2021-12-18 09:56:05 +01:00
// notifyFailure sends a notification about a successful backup run
func ( s * script ) notifySuccess ( ) error {
title := fmt . Sprintf ( "Success running docker-volume-backup at %s" , s . start . Format ( time . RFC3339 ) )
body := fmt . Sprintf (
"Running docker-volume-backup succeeded.\n\nLog output was:\n\n%s\n" , s . output . String ( ) ,
)
if err := s . sendNotification ( title , body ) ; err != nil {
return fmt . Errorf ( "notifySuccess: error notifying: %w" , err )
2021-12-17 17:44:22 +01:00
}
2021-12-18 09:56:05 +01:00
return nil
}
2021-12-17 17:44:22 +01:00
2021-12-18 09:56:05 +01:00
// sendNotification sends a notification to all configured third party services
func ( s * script ) sendNotification ( title , body string ) error {
2021-12-17 17:44:22 +01:00
var errs [ ] error
2021-12-18 09:56:05 +01:00
for _ , result := range s . sender . Send ( body , & sTypes . Params { "title" : title } ) {
2021-12-17 17:44:22 +01:00
if result != nil {
errs = append ( errs , result )
}
}
if len ( errs ) != 0 {
2021-12-18 09:56:05 +01:00
return fmt . Errorf ( "sendNotification: error sending message: %w" , join ( errs ... ) )
2021-12-17 17:44:22 +01:00
}
return nil
}
2021-08-24 11:39:27 +02:00
// stopContainers stops all Docker containers that are marked as to being
// stopped during the backup and returns a function that can be called to
// restart everything that has been stopped.
func ( s * script ) stopContainers ( ) ( func ( ) error , error ) {
2021-08-21 19:26:42 +02:00
if s . cli == nil {
2021-08-24 11:39:27 +02:00
return noop , nil
2021-08-21 19:26:42 +02:00
}
2021-08-22 22:44:36 +02:00
2021-08-29 18:26:40 +02:00
allContainers , err := s . cli . ContainerList ( context . Background ( ) , types . ContainerListOptions {
2021-08-22 11:02:10 +02:00
Quiet : true ,
} )
if err != nil {
2021-08-24 11:39:27 +02:00
return noop , fmt . Errorf ( "stopContainersAndRun: error querying for containers: %w" , err )
2021-08-22 11:02:10 +02:00
}
2021-08-22 19:37:48 +02:00
containerLabel := fmt . Sprintf (
"docker-volume-backup.stop-during-backup=%s" ,
2021-08-24 09:01:44 +02:00
s . c . BackupStopContainerLabel ,
2021-08-22 19:37:48 +02:00
)
2021-08-29 18:26:40 +02:00
containersToStop , err := s . cli . ContainerList ( context . Background ( ) , types . ContainerListOptions {
2021-08-21 19:26:42 +02:00
Quiet : true ,
Filters : filters . NewArgs ( filters . KeyValuePair {
2021-08-22 19:37:48 +02:00
Key : "label" ,
Value : containerLabel ,
2021-08-21 19:26:42 +02:00
} ) ,
} )
if err != nil {
2021-08-24 11:39:27 +02:00
return noop , fmt . Errorf ( "stopContainersAndRun: error querying for containers to stop: %w" , err )
2021-08-21 19:05:49 +02:00
}
2021-08-23 08:19:22 +02:00
if len ( containersToStop ) == 0 {
2021-08-24 11:39:27 +02:00
return noop , nil
2021-08-23 08:19:22 +02:00
}
2021-08-22 19:37:48 +02:00
s . logger . Infof (
2021-08-23 08:19:22 +02:00
"Stopping %d container(s) labeled `%s` out of %d running container(s)." ,
2021-08-22 19:37:48 +02:00
len ( containersToStop ) ,
containerLabel ,
len ( allContainers ) ,
)
2021-08-21 19:05:49 +02:00
2021-08-22 15:52:47 +02:00
var stoppedContainers [ ] types . Container
2021-08-23 08:19:22 +02:00
var stopErrors [ ] error
for _ , container := range containersToStop {
2021-08-29 18:26:40 +02:00
if err := s . cli . ContainerStop ( context . Background ( ) , container . ID , nil ) ; err != nil {
2021-08-23 08:19:22 +02:00
stopErrors = append ( stopErrors , err )
} else {
stoppedContainers = append ( stoppedContainers , container )
2021-08-22 15:52:47 +02:00
}
}
2021-08-29 18:51:05 +02:00
var stopError error
2021-08-24 11:39:27 +02:00
if len ( stopErrors ) != 0 {
2021-08-29 18:51:05 +02:00
stopError = fmt . Errorf (
2021-08-24 11:39:27 +02:00
"stopContainersAndRun: %d error(s) stopping containers: %w" ,
len ( stopErrors ) ,
2021-08-29 19:39:51 +02:00
join ( stopErrors ... ) ,
2021-08-24 11:39:27 +02:00
)
}
return func ( ) error {
2021-08-22 15:52:47 +02:00
servicesRequiringUpdate := map [ string ] struct { } { }
var restartErrors [ ] error
for _ , container := range stoppedContainers {
if swarmServiceName , ok := container . Labels [ "com.docker.swarm.service.name" ] ; ok {
servicesRequiringUpdate [ swarmServiceName ] = struct { } { }
continue
}
2021-08-29 18:26:40 +02:00
if err := s . cli . ContainerStart ( context . Background ( ) , container . ID , types . ContainerStartOptions { } ) ; err != nil {
2021-08-22 15:52:47 +02:00
restartErrors = append ( restartErrors , err )
}
}
if len ( servicesRequiringUpdate ) != 0 {
2021-08-29 18:26:40 +02:00
services , _ := s . cli . ServiceList ( context . Background ( ) , types . ServiceListOptions { } )
2021-08-22 15:52:47 +02:00
for serviceName := range servicesRequiringUpdate {
var serviceMatch swarm . Service
for _ , service := range services {
if service . Spec . Name == serviceName {
serviceMatch = service
break
}
}
if serviceMatch . ID == "" {
2021-08-27 15:05:12 +02:00
return fmt . Errorf ( "stopContainersAndRun: couldn't find service with name %s" , serviceName )
2021-08-22 15:52:47 +02:00
}
serviceMatch . Spec . TaskTemplate . ForceUpdate = 1
2021-11-28 20:06:24 +01:00
if _ , err := s . cli . ServiceUpdate (
2021-08-29 18:26:40 +02:00
context . Background ( ) , serviceMatch . ID ,
2021-08-22 15:52:47 +02:00
serviceMatch . Version , serviceMatch . Spec , types . ServiceUpdateOptions { } ,
2021-11-28 20:06:24 +01:00
) ; err != nil {
2021-08-22 15:52:47 +02:00
restartErrors = append ( restartErrors , err )
}
2021-08-21 19:05:49 +02:00
}
}
2021-08-22 15:52:47 +02:00
if len ( restartErrors ) != 0 {
return fmt . Errorf (
"stopContainersAndRun: %d error(s) restarting containers and services: %w" ,
len ( restartErrors ) ,
2021-08-29 19:39:51 +02:00
join ( restartErrors ... ) ,
2021-08-22 15:52:47 +02:00
)
}
2021-08-23 18:46:49 +02:00
s . logger . Infof (
"Restarted %d container(s) and the matching service(s)." ,
len ( stoppedContainers ) ,
)
2021-08-22 15:52:47 +02:00
return nil
2021-08-29 18:51:05 +02:00
} , stopError
2021-08-21 19:26:42 +02:00
}
2021-08-21 19:05:49 +02:00
2021-08-22 15:52:47 +02:00
// takeBackup creates a tar archive of the configured backup location and
// saves it to disk.
2021-08-21 19:26:42 +02:00
func ( s * script ) takeBackup ( ) error {
2021-11-08 08:39:18 +01:00
backupSources := s . c . BackupSources
2021-11-08 19:10:10 +01:00
2021-11-08 08:39:18 +01:00
if s . c . BackupFromSnapshot {
2021-11-08 19:10:10 +01:00
backupSources = filepath . Join ( "/tmp" , s . c . BackupSources )
2021-11-08 08:39:18 +01:00
// copy before compressing guard against a situation where backup folder's content are still growing.
2021-12-17 20:45:15 +01:00
s . registerHook ( hookLevelPlumbing , func ( error ) error {
2021-11-08 19:10:10 +01:00
if err := remove ( backupSources ) ; err != nil {
return fmt . Errorf ( "takeBackup: error removing snapshot: %w" , err )
}
s . logger . Infof ( "Removed snapshot `%s`." , backupSources )
return nil
} )
2021-11-29 08:40:55 +01:00
if err := copy . Copy ( s . c . BackupSources , backupSources , copy . Options {
PreserveTimes : true ,
PreserveOwner : true ,
} ) ; err != nil {
2021-11-08 19:10:10 +01:00
return fmt . Errorf ( "takeBackup: error creating snapshot: %w" , err )
2021-11-08 08:39:18 +01:00
}
2021-11-08 19:10:10 +01:00
s . logger . Infof ( "Created snapshot of `%s` at `%s`." , s . c . BackupSources , backupSources )
2021-11-08 08:39:18 +01:00
}
2021-11-08 19:10:10 +01:00
tarFile := s . file
2021-12-17 20:45:15 +01:00
s . registerHook ( hookLevelPlumbing , func ( error ) error {
2021-11-08 19:10:10 +01:00
if err := remove ( tarFile ) ; err != nil {
return fmt . Errorf ( "takeBackup: error removing tar file: %w" , err )
}
s . logger . Infof ( "Removed tar file `%s`." , tarFile )
return nil
} )
if err := targz . Compress ( backupSources , tarFile ) ; err != nil {
2021-08-21 21:26:27 +02:00
return fmt . Errorf ( "takeBackup: error compressing backup folder: %w" , err )
}
2021-11-08 19:10:10 +01:00
s . logger . Infof ( "Created backup of `%s` at `%s`." , backupSources , tarFile )
2021-08-21 21:26:27 +02:00
return nil
2021-08-21 19:26:42 +02:00
}
2021-08-21 19:05:49 +02:00
2021-08-22 15:52:47 +02:00
// encryptBackup encrypts the backup file using PGP and the configured passphrase.
// In case no passphrase is given it returns early, leaving the backup file
2021-08-29 10:23:25 +02:00
// untouched.
2021-08-21 19:26:42 +02:00
func ( s * script ) encryptBackup ( ) error {
2021-08-24 09:01:44 +02:00
if s . c . GpgPassphrase == "" {
2021-08-21 19:26:42 +02:00
return nil
}
2021-08-22 14:44:33 +02:00
2021-08-23 15:33:49 +02:00
gpgFile := fmt . Sprintf ( "%s.gpg" , s . file )
2021-12-17 20:45:15 +01:00
s . registerHook ( hookLevelPlumbing , func ( error ) error {
2021-11-08 19:10:10 +01:00
if err := remove ( gpgFile ) ; err != nil {
return fmt . Errorf ( "encryptBackup: error removing gpg file: %w" , err )
}
s . logger . Infof ( "Removed GPG file `%s`." , gpgFile )
return nil
} )
2021-08-23 15:33:49 +02:00
outFile , err := os . Create ( gpgFile )
defer outFile . Close ( )
if err != nil {
return fmt . Errorf ( "encryptBackup: error opening out file: %w" , err )
}
2021-08-23 08:19:22 +02:00
2021-08-23 15:33:49 +02:00
_ , name := path . Split ( s . file )
2021-08-24 09:01:44 +02:00
dst , err := openpgp . SymmetricallyEncrypt ( outFile , [ ] byte ( s . c . GpgPassphrase ) , & openpgp . FileHints {
2021-08-22 14:44:33 +02:00
IsBinary : true ,
FileName : name ,
} , nil )
2021-08-23 15:33:49 +02:00
defer dst . Close ( )
2021-08-22 14:44:33 +02:00
if err != nil {
return fmt . Errorf ( "encryptBackup: error encrypting backup file: %w" , err )
}
2021-08-23 15:33:49 +02:00
src , err := os . Open ( s . file )
2021-08-22 14:44:33 +02:00
if err != nil {
2021-11-08 19:10:10 +01:00
return fmt . Errorf ( "encryptBackup: error opening backup file `%s`: %w" , s . file , err )
2021-08-22 14:44:33 +02:00
}
2021-08-23 15:33:49 +02:00
if _ , err := io . Copy ( dst , src ) ; err != nil {
return fmt . Errorf ( "encryptBackup: error writing ciphertext to file: %w" , err )
2021-08-22 14:44:33 +02:00
}
s . file = gpgFile
2021-08-23 07:07:44 +02:00
s . logger . Infof ( "Encrypted backup using given passphrase, saving as `%s`." , s . file )
2021-08-22 14:44:33 +02:00
return nil
2021-08-21 19:26:42 +02:00
}
2021-08-22 15:52:47 +02:00
// copyBackup makes sure the backup file is copied to both local and remote locations
// as per the given configuration.
2021-08-21 19:26:42 +02:00
func ( s * script ) copyBackup ( ) error {
2021-08-21 21:26:27 +02:00
_ , name := path . Split ( s . file )
2022-01-22 13:35:13 +01:00
if s . minioClient != nil {
2022-01-25 21:16:16 +01:00
if _ , err := s . minioClient . FPutObject ( context . Background ( ) , s . c . AwsS3BucketName , filepath . Join ( s . c . AwsS3Path , name ) , s . file , minio . PutObjectOptions {
2021-08-21 21:26:27 +02:00
ContentType : "application/tar+gzip" ,
2021-11-28 20:06:24 +01:00
} ) ; err != nil {
2021-08-21 21:26:27 +02:00
return fmt . Errorf ( "copyBackup: error uploading backup to remote storage: %w" , err )
}
2021-08-29 10:23:25 +02:00
s . logger . Infof ( "Uploaded a copy of backup `%s` to bucket `%s`." , s . file , s . c . AwsS3BucketName )
2021-08-21 21:26:27 +02:00
}
2021-08-22 15:04:44 +02:00
2022-01-22 13:29:21 +01:00
if s . webdavClient != nil {
bytes , err := os . ReadFile ( s . file )
if err != nil {
return fmt . Errorf ( "copyBackup: error reading the file to be uploaded: %w" , err )
}
if err := s . webdavClient . MkdirAll ( s . c . WebdavPath , 0644 ) ; err != nil {
return fmt . Errorf ( "copyBackup: error creating directory '%s' on WebDAV server: %w" , s . c . WebdavPath , err )
}
if err := s . webdavClient . Write ( filepath . Join ( s . c . WebdavPath , name ) , bytes , 0644 ) ; err != nil {
return fmt . Errorf ( "copyBackup: error uploading the file to WebDAV server: %w" , err )
}
s . logger . Infof ( "Uploaded a copy of backup `%s` to WebDAV-URL '%s' at path '%s'." , s . file , s . c . WebdavUrl , s . c . WebdavPath )
}
2021-08-24 09:01:44 +02:00
if _ , err := os . Stat ( s . c . BackupArchive ) ; ! os . IsNotExist ( err ) {
2021-11-08 08:39:18 +01:00
if err := copyFile ( s . file , path . Join ( s . c . BackupArchive , name ) ) ; err != nil {
2021-08-22 20:42:25 +02:00
return fmt . Errorf ( "copyBackup: error copying file to local archive: %w" , err )
2021-08-21 21:26:27 +02:00
}
2021-08-29 10:23:25 +02:00
s . logger . Infof ( "Stored copy of backup `%s` in local archive `%s`." , s . file , s . c . BackupArchive )
2021-10-01 10:07:46 +02:00
if s . c . BackupLatestSymlink != "" {
symlink := path . Join ( s . c . BackupArchive , s . c . BackupLatestSymlink )
if _ , err := os . Lstat ( symlink ) ; err == nil {
os . Remove ( symlink )
}
if err := os . Symlink ( name , symlink ) ; err != nil {
return fmt . Errorf ( "copyBackup: error creating latest symlink: %w" , err )
}
s . logger . Infof ( "Created/Updated symlink `%s` for latest backup." , s . c . BackupLatestSymlink )
}
2021-08-21 21:26:27 +02:00
}
return nil
2021-08-21 19:26:42 +02:00
}
2021-08-22 15:52:47 +02:00
// pruneOldBackups rotates away backups from local and remote storages using
// the given configuration. In case the given configuration would delete all
// backups, it does nothing instead.
2021-08-22 11:02:10 +02:00
func ( s * script ) pruneOldBackups ( ) error {
2021-08-24 09:01:44 +02:00
if s . c . BackupRetentionDays < 0 {
2021-08-21 19:26:42 +02:00
return nil
}
2021-08-22 22:02:19 +02:00
2021-08-24 09:01:44 +02:00
if s . c . BackupPruningLeeway != 0 {
s . logger . Infof ( "Sleeping for %s before pruning backups." , s . c . BackupPruningLeeway )
time . Sleep ( s . c . BackupPruningLeeway )
2021-08-22 11:02:10 +02:00
}
2021-08-24 09:15:43 +02:00
deadline := time . Now ( ) . AddDate ( 0 , 0 , - int ( s . c . BackupRetentionDays ) )
2021-08-22 14:00:21 +02:00
2022-01-22 13:29:21 +01:00
// Prune minio/S3 backups
2022-01-22 13:35:13 +01:00
if s . minioClient != nil {
candidates := s . minioClient . ListObjects ( context . Background ( ) , s . c . AwsS3BucketName , minio . ListObjectsOptions {
2021-08-22 15:04:44 +02:00
WithMetadata : true ,
2021-08-24 09:01:44 +02:00
Prefix : s . c . BackupPruningPrefix ,
2021-08-22 15:04:44 +02:00
} )
var matches [ ] minio . ObjectInfo
2021-08-22 16:41:06 +02:00
var lenCandidates int
2021-08-22 15:04:44 +02:00
for candidate := range candidates {
2021-08-22 16:41:06 +02:00
lenCandidates ++
if candidate . Err != nil {
2021-08-23 18:46:49 +02:00
return fmt . Errorf (
"pruneOldBackups: error looking up candidates from remote storage: %w" ,
candidate . Err ,
)
2021-08-22 16:41:06 +02:00
}
2021-08-22 15:04:44 +02:00
if candidate . LastModified . Before ( deadline ) {
matches = append ( matches , candidate )
}
}
2021-08-22 16:41:06 +02:00
if len ( matches ) != 0 && len ( matches ) != lenCandidates {
2021-08-22 15:04:44 +02:00
objectsCh := make ( chan minio . ObjectInfo )
go func ( ) {
2021-08-22 16:41:06 +02:00
for _ , match := range matches {
objectsCh <- match
2021-08-22 15:04:44 +02:00
}
2021-08-22 15:52:47 +02:00
close ( objectsCh )
2021-08-22 15:04:44 +02:00
} ( )
2022-01-22 13:35:13 +01:00
errChan := s . minioClient . RemoveObjects ( context . Background ( ) , s . c . AwsS3BucketName , objectsCh , minio . RemoveObjectsOptions { } )
2021-08-29 19:39:51 +02:00
var removeErrors [ ] error
2021-08-22 15:04:44 +02:00
for result := range errChan {
if result . Err != nil {
2021-08-29 19:39:51 +02:00
removeErrors = append ( removeErrors , result . Err )
2021-08-22 15:04:44 +02:00
}
}
2021-08-29 19:39:51 +02:00
if len ( removeErrors ) != 0 {
2021-08-22 15:04:44 +02:00
return fmt . Errorf (
2021-08-22 22:02:19 +02:00
"pruneOldBackups: %d error(s) removing files from remote storage: %w" ,
2021-08-29 19:39:51 +02:00
len ( removeErrors ) ,
join ( removeErrors ... ) ,
2021-08-22 15:04:44 +02:00
)
}
2021-08-22 16:41:06 +02:00
s . logger . Infof (
2021-08-26 12:50:22 +02:00
"Pruned %d out of %d remote backup(s) as their age exceeded the configured retention period of %d days." ,
2021-08-22 16:41:06 +02:00
len ( matches ) ,
lenCandidates ,
2021-08-26 12:50:22 +02:00
s . c . BackupRetentionDays ,
2021-08-22 16:41:06 +02:00
)
} else if len ( matches ) != 0 && len ( matches ) == lenCandidates {
2021-08-22 19:37:48 +02:00
s . logger . Warnf (
2021-08-23 18:46:49 +02:00
"The current configuration would delete all %d remote backup copies." ,
2021-08-22 19:37:48 +02:00
len ( matches ) ,
)
2021-08-23 18:46:49 +02:00
s . logger . Warn ( "Refusing to do so, please check your configuration." )
2021-08-22 16:41:06 +02:00
} else {
2021-08-22 22:02:19 +02:00
s . logger . Infof ( "None of %d remote backup(s) were pruned." , lenCandidates )
2021-08-22 15:04:44 +02:00
}
2021-08-22 11:02:10 +02:00
}
2021-08-22 15:04:44 +02:00
2022-01-22 13:29:21 +01:00
// Prune WebDAV backups
if s . webdavClient != nil {
candidates , err := s . webdavClient . ReadDir ( s . c . WebdavPath )
if err != nil {
return fmt . Errorf ( "pruneOldBackups: error looking up candidates from remote storage: %w" , err )
}
var matches [ ] fs . FileInfo
var lenCandidates int
for _ , candidate := range candidates {
lenCandidates ++
if candidate . ModTime ( ) . Before ( deadline ) {
matches = append ( matches , candidate )
}
}
if len ( matches ) != 0 && len ( matches ) != lenCandidates {
for _ , match := range matches {
if err := s . webdavClient . Remove ( filepath . Join ( s . c . WebdavPath , match . Name ( ) ) ) ; err != nil {
return fmt . Errorf ( "pruneOldBackups: error removing a file from remote storage: %w" , err )
}
s . logger . Infof ( "Pruned %s from WebDAV: %s" , match . Name ( ) , filepath . Join ( s . c . WebdavUrl , s . c . WebdavPath ) )
}
s . logger . Infof ( "Pruned %d out of %d remote backup(s) as their age exceeded the configured retention period of %d days." , len ( matches ) , lenCandidates , s . c . BackupRetentionDays )
} else if len ( matches ) != 0 && len ( matches ) == lenCandidates {
s . logger . Warnf ( "The current configuration would delete all %d remote backup copies." , len ( matches ) )
s . logger . Warn ( "Refusing to do so, please check your configuration." )
} else {
s . logger . Infof ( "None of %d remote backup(s) were pruned." , lenCandidates )
}
}
// Prune local backups
2021-08-24 09:01:44 +02:00
if _ , err := os . Stat ( s . c . BackupArchive ) ; ! os . IsNotExist ( err ) {
2021-10-29 08:48:26 +02:00
globPattern := path . Join (
s . c . BackupArchive ,
fmt . Sprintf ( "%s*" , s . c . BackupPruningPrefix ) ,
2021-08-22 14:00:21 +02:00
)
2021-10-29 08:48:26 +02:00
globMatches , err := filepath . Glob ( globPattern )
2021-08-22 14:00:21 +02:00
if err != nil {
2021-08-22 15:04:44 +02:00
return fmt . Errorf (
2021-10-29 08:48:26 +02:00
"pruneOldBackups: error looking up matching files using pattern %s: %w" ,
globPattern ,
err ,
2021-08-22 15:04:44 +02:00
)
2021-08-22 14:00:21 +02:00
}
2021-11-02 06:40:37 +01:00
var candidates [ ] string
2021-10-29 08:48:26 +02:00
for _ , candidate := range globMatches {
2021-11-03 18:07:55 +01:00
fi , err := os . Lstat ( candidate )
2021-08-22 14:00:21 +02:00
if err != nil {
2021-08-22 15:04:44 +02:00
return fmt . Errorf (
2021-11-03 18:07:55 +01:00
"pruneOldBackups: error calling Lstat on file %s: %w" ,
2021-08-22 15:04:44 +02:00
candidate ,
err ,
)
2021-08-22 14:00:21 +02:00
}
2021-11-03 18:03:44 +01:00
if fi . Mode ( ) & os . ModeSymlink != os . ModeSymlink {
2021-11-02 06:40:37 +01:00
candidates = append ( candidates , candidate )
2021-10-29 08:48:26 +02:00
}
}
2021-08-22 14:00:21 +02:00
2021-11-02 06:40:37 +01:00
var matches [ ] string
2021-10-29 08:48:26 +02:00
for _ , candidate := range candidates {
2021-11-02 06:40:37 +01:00
fi , err := os . Stat ( candidate )
if err != nil {
return fmt . Errorf (
"pruneOldBackups: error calling stat on file %s: %w" ,
candidate ,
err ,
)
}
if fi . ModTime ( ) . Before ( deadline ) {
2021-08-23 09:10:49 +02:00
matches = append ( matches , candidate )
2021-08-22 14:00:21 +02:00
}
}
2021-08-22 16:41:06 +02:00
if len ( matches ) != 0 && len ( matches ) != len ( candidates ) {
2021-08-29 19:39:51 +02:00
var removeErrors [ ] error
2021-11-02 06:40:37 +01:00
for _ , match := range matches {
if err := os . Remove ( match ) ; err != nil {
2021-08-29 19:39:51 +02:00
removeErrors = append ( removeErrors , err )
2021-08-22 14:00:21 +02:00
}
}
2021-08-29 19:39:51 +02:00
if len ( removeErrors ) != 0 {
2021-08-22 15:04:44 +02:00
return fmt . Errorf (
2021-08-22 22:02:19 +02:00
"pruneOldBackups: %d error(s) deleting local files, starting with: %w" ,
2021-08-29 19:39:51 +02:00
len ( removeErrors ) ,
join ( removeErrors ... ) ,
2021-08-22 15:04:44 +02:00
)
}
2021-08-22 16:41:06 +02:00
s . logger . Infof (
2021-08-26 12:50:22 +02:00
"Pruned %d out of %d local backup(s) as their age exceeded the configured retention period of %d days." ,
2021-08-22 16:41:06 +02:00
len ( matches ) ,
len ( candidates ) ,
2021-08-26 12:50:22 +02:00
s . c . BackupRetentionDays ,
2021-08-22 16:41:06 +02:00
)
} else if len ( matches ) != 0 && len ( matches ) == len ( candidates ) {
2021-08-22 19:37:48 +02:00
s . logger . Warnf (
2021-08-23 18:46:49 +02:00
"The current configuration would delete all %d local backup copies." ,
2021-08-22 19:37:48 +02:00
len ( matches ) ,
)
2021-08-23 18:46:49 +02:00
s . logger . Warn ( "Refusing to do so, please check your configuration." )
2021-08-22 16:41:06 +02:00
} else {
2021-08-22 22:02:19 +02:00
s . logger . Infof ( "None of %d local backup(s) were pruned." , len ( candidates ) )
2021-08-22 14:00:21 +02:00
}
2021-08-22 11:02:10 +02:00
}
return nil
2021-08-21 19:05:49 +02:00
}
2021-09-09 12:05:57 +02:00
// runHooks runs all hooks that have been registered using the
2021-11-08 19:10:10 +01:00
// given levels in the defined ordering. In case executing a hook returns an
// error, the following hooks will still be run before the function returns.
2021-12-18 09:56:05 +01:00
func ( s * script ) runHooks ( err error ) error {
2021-12-17 20:54:25 +01:00
sort . SliceStable ( s . hooks , func ( i , j int ) bool {
return s . hooks [ i ] . level < s . hooks [ j ] . level
} )
2021-09-09 12:05:57 +02:00
var actionErrors [ ] error
2021-12-17 20:45:15 +01:00
for _ , hook := range s . hooks {
2021-12-18 09:56:05 +01:00
if hook . level > s . hookLevel {
2021-12-17 20:45:15 +01:00
continue
}
if actionErr := hook . action ( err ) ; actionErr != nil {
actionErrors = append ( actionErrors , fmt . Errorf ( "runHooks: error running hook: %w" , actionErr ) )
2021-09-09 12:05:57 +02:00
}
}
if len ( actionErrors ) != 0 {
return join ( actionErrors ... )
}
return nil
}
2021-09-12 08:54:33 +02:00
// must exits the script run prematurely in case the given error
2021-11-08 19:10:10 +01:00
// is non-nil.
2021-08-22 19:37:48 +02:00
func ( s * script ) must ( err error ) {
if err != nil {
2021-09-09 11:08:05 +02:00
s . logger . Errorf ( "Fatal error running backup: %s" , err )
2021-11-08 19:10:10 +01:00
panic ( err )
}
}
// remove removes the given file or directory from disk.
func remove ( location string ) error {
fi , err := os . Lstat ( location )
if err != nil {
if os . IsNotExist ( err ) {
return nil
2021-09-09 08:12:07 +02:00
}
2021-11-08 19:10:10 +01:00
return fmt . Errorf ( "remove: error checking for existence of `%s`: %w" , location , err )
2021-08-21 21:26:27 +02:00
}
2021-11-08 19:10:10 +01:00
if fi . IsDir ( ) {
err = os . RemoveAll ( location )
} else {
err = os . Remove ( location )
}
if err != nil {
return fmt . Errorf ( "remove: error removing `%s`: %w" , location , err )
}
return nil
2021-08-21 21:26:27 +02:00
}
2021-08-23 08:19:22 +02:00
// lock opens a lockfile at the given location, keeping it locked until the
// caller invokes the returned release func. When invoked while the file is
// still locked the function panics.
func lock ( lockfile string ) func ( ) error {
2021-08-23 18:46:49 +02:00
fileLock := flock . New ( lockfile )
acquired , err := fileLock . TryLock ( )
2021-08-23 08:19:22 +02:00
if err != nil {
panic ( err )
}
2021-08-23 18:46:49 +02:00
if ! acquired {
panic ( "unable to acquire file lock" )
2021-08-23 08:19:22 +02:00
}
2021-08-23 18:46:49 +02:00
return fileLock . Unlock
2021-08-23 08:19:22 +02:00
}
// copy creates a copy of the file located at `dst` at `src`.
2021-11-08 08:39:18 +01:00
func copyFile ( src , dst string ) error {
2021-08-21 21:26:27 +02:00
in , err := os . Open ( src )
if err != nil {
return err
}
defer in . Close ( )
out , err := os . Create ( dst )
if err != nil {
return err
}
_ , err = io . Copy ( out , in )
if err != nil {
2021-08-22 11:02:10 +02:00
out . Close ( )
2021-08-21 21:26:27 +02:00
return err
}
return out . Close ( )
}
2021-08-29 19:39:51 +02:00
// join takes a list of errors and joins them into a single error
func join ( errs ... error ) error {
if len ( errs ) == 1 {
return errs [ 0 ]
}
var msgs [ ] string
for _ , err := range errs {
if err == nil {
continue
}
msgs = append ( msgs , err . Error ( ) )
}
return errors . New ( "[" + strings . Join ( msgs , ", " ) + "]" )
}
2021-09-09 07:24:18 +02:00
// buffer takes an io.Writer and returns a wrapped version of the
// writer that writes to both the original target as well as the returned buffer
func buffer ( w io . Writer ) ( io . Writer , * bytes . Buffer ) {
buffering := & bufferingWriter { buf : bytes . Buffer { } , writer : w }
return buffering , & buffering . buf
}
type bufferingWriter struct {
buf bytes . Buffer
writer io . Writer
}
func ( b * bufferingWriter ) Write ( p [ ] byte ) ( n int , err error ) {
if n , err := b . buf . Write ( p ) ; err != nil {
return n , fmt . Errorf ( "bufferingWriter: error writing to buffer: %w" , err )
}
return b . writer . Write ( p )
}
2021-09-09 12:05:57 +02:00
// hook contains a queued action that can be trigger them when the script
// reaches a certain point (e.g. unsuccessful backup)
type hook struct {
2021-11-08 19:10:10 +01:00
level hookLevel
2021-12-17 17:44:22 +01:00
action func ( err error ) error
2021-09-09 12:05:57 +02:00
}
2021-11-08 19:10:10 +01:00
type hookLevel int
2021-09-09 12:05:57 +02:00
const (
2021-12-17 20:45:15 +01:00
hookLevelPlumbing hookLevel = iota
hookLevelError
2021-12-18 09:56:05 +01:00
hookLevelInfo
2021-09-09 12:05:57 +02:00
)
2021-12-17 20:45:15 +01:00
2021-12-18 09:56:05 +01:00
var hookLevels = map [ string ] hookLevel {
2021-12-17 20:45:15 +01:00
"info" : hookLevelInfo ,
"error" : hookLevelError ,
}