diff --git a/cmd/backup/config.go b/cmd/backup/config.go index 60717d1..7e59ff5 100644 --- a/cmd/backup/config.go +++ b/cmd/backup/config.go @@ -16,86 +16,66 @@ import ( // Config holds all configuration values that are expected to be set // by users. type Config struct { - AwsS3BucketName string `env:"AWS_S3_BUCKET_NAME"` - AwsS3Path string `env:"AWS_S3_PATH"` - AwsEndpoint string `envDefault:"s3.amazonaws.com"` - AwsEndpointProto string `envDefault:"https"` + AwsS3BucketName string + AwsS3Path string + AwsEndpoint string `default:"s3.amazonaws.com"` + AwsEndpointProto string AwsEndpointInsecure bool - AwsEndpointCACert CertDecoder `env:"AWS_ENDPOINT_CA_CERT"` + AwsEndpointCACert CertDecoder AwsStorageClass string - AwsAccessKeyID string `env:"AWS_ACCESS_KEY_ID"` - AwsAccessKeyIDFile string `env:"AWS_ACCESS_KEY_ID_FILE,file"` - AwsSecretAccessKey string `env:"AWS_SECRET_ACCESS_KEY"` - AwsSecretAccessKeyFile string `env:"AWS_SECRET_ACCESS_KEY_FILE,file"` + AwsAccessKeyID string + AwsSecretAccessKey string AwsIamRoleEndpoint string AwsPartSize int64 - BackupCompression CompressionType `envDefault:"gz"` - BackupSources string `envDefault:"/backup"` - BackupFilename string `envDefault:"backup-%Y-%m-%dT%H-%M-%S.{{ .Extension }}"` + BackupCompression CompressionType `default:"gz"` + BackupSources string `default:"/backup"` + BackupFilename string `default:"backup-%Y-%m-%dT%H-%M-%S.{{ .Extension }}"` BackupFilenameExpand bool BackupLatestSymlink string - BackupArchive string `envDefault:"/archive"` - BackupRetentionDays int32 `envDefault:"-1"` - BackupPruningLeeway time.Duration `envDefault:"1m"` + BackupArchive string `default:"/archive"` + BackupRetentionDays int32 `default:"-1"` + BackupPruningLeeway time.Duration `default:"1m"` BackupPruningPrefix string - BackupStopContainerLabel string `envDefault:"true"` + BackupStopContainerLabel string `default:"true"` BackupFromSnapshot bool BackupExcludeRegexp RegexpDecoder BackupSkipBackendsFromPrune []string - GpgPassphrase string `env:"GPG_PASSPHRASE"` - GpgPassphraseFile string `env:"GPG_PASSPHRASE_FILE,file"` - NotificationURLs []string `env:"NOTIFICATION_URLS"` - NotificationLevel string `envDefault:"error"` + GpgPassphrase string + NotificationURLs []string + NotificationLevel string `default:"error"` EmailNotificationRecipient string - EmailNotificationSender string `envDefault:"noreply@nohost"` - EmailSMTPHost string `env:"EMAIL_SMTP_HOST"` - EmailSMTPPort int `env:"EMAIL_SMTP_PORT" envDefault:"587"` - EmailSMTPUsername string `env:"EMAIL_SMTP_USERNAME"` - EmailSMTPPassword string `env:"EMAIL_SMTP_PASSWORD"` - EmailSMTPPasswordFile string `env:"EMAIL_SMTP_PASSWORD_FILE,file"` + EmailNotificationSender string `default:"noreply@nohost"` + EmailSMTPHost string + EmailSMTPPort int `default:"587"` + EmailSMTPUsername string + EmailSMTPPassword string WebdavUrl string WebdavUrlInsecure bool - WebdavPath string `envDefault:"/"` + WebdavPath string `default:"/"` WebdavUsername string - WebdavPassword string `env:"WEBDAV_PASSWORD"` - WebdavPasswordFile string `env:"WEBDAV_PASSWORD_FILE,file"` - SSHHostName string `env:"SSH_HOST_NAME"` - SSHPort string `env:"SSH_PORT" envDefault:"22"` - SSHUser string `env:"SSH_USER"` - SSHPassword string `env:"SSH_PASSWORD"` - SSHPasswordFile string `env:"SSH_PASSWORD_FILE,file"` - SSHIdentityFile string `env:"SSH_IDENTITY_FILE" envDefault:"/root/.ssh/id_rsa"` - SSHIdentityPassphrase string `env:"SSH_IDENTITY_PASSPHRASE"` - SSHIdentityPassphraseFile string `env:"SSH_IDENTITY_PASSPHRASE_FILE,file"` - SSHRemotePath string `env:"SSH_REMOTE_PATH"` + WebdavPassword string + SSHHostName string + SSHPort string `default:"22"` + SSHUser string + SSHPassword string + SSHIdentityFile string `default:"/root/.ssh/id_rsa"` + SSHIdentityPassphrase string + SSHRemotePath string ExecLabel string ExecForwardOutput bool - LockTimeout time.Duration `envDefault:"60m"` + LockTimeout time.Duration `default:"60m"` AzureStorageAccountName string AzureStoragePrimaryAccountKey string AzureStorageContainerName string AzureStoragePath string - AzureStorageEndpoint string `envDefault:"https://{{ .AccountName }}.blob.core.windows.net/"` - DropboxEndpoint string `envDefault:"https://api.dropbox.com/"` - DropboxOAuth2Endpoint string `env:"DROPBOX_OAUTH2_ENDPOINT" envDefault:"https://api.dropbox.com/"` - DropboxRefreshToken string `env:"DROPBOX_REFRESH_TOKEN"` - DropboxRefreshTokenFile string `env:"DROPBOX_REFRESH_TOKEN_FILE,file"` - DropboxAppKey string `env:"DROPBOX_APP_KEY"` - DropboxAppKeyFile string `env:"DROPBOX_APP_KEY_FILE,file"` - DropboxAppSecret string `env:"DROPBOX_APP_SECRET"` - DropboxAppSecretFile string `env:"DROPBOX_APP_SECRET_FILE,file"` + AzureStorageEndpoint string `default:"https://{{ .AccountName }}.blob.core.windows.net/"` + DropboxEndpoint string `default:"https://api.dropbox.com/"` + DropboxOAuth2Endpoint string `default:"https://api.dropbox.com/"` + DropboxRefreshToken string + DropboxAppKey string + DropboxAppSecret string DropboxRemotePath string - DropboxConcurrencyLevel NaturalNumber `envDefault:"6"` -} - -func (c *Config) getSecret(preferred string, fallback string) string { - if preferred != "" { - return preferred - } - if fallback != "" { - return fallback - } - return "" + DropboxConcurrencyLevel NaturalNumber `default:"6"` } type CompressionType string diff --git a/cmd/backup/script.go b/cmd/backup/script.go index 3d52857..6b0a3b6 100644 --- a/cmd/backup/script.go +++ b/cmd/backup/script.go @@ -28,7 +28,6 @@ import ( "github.com/offen/docker-volume-backup/internal/storage/webdav" "github.com/ProtonMail/go-crypto/openpgp" - "github.com/caarlos0/env/v9" "github.com/containrrr/shoutrrr" "github.com/containrrr/shoutrrr/pkg/router" "github.com/docker/docker/api/types" @@ -36,6 +35,7 @@ import ( "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/client" + "github.com/johnstairs/pathenvconfig" "github.com/leekchan/timeutil" "github.com/otiai10/copy" "golang.org/x/sync/errgroup" @@ -89,13 +89,12 @@ func newScript() (*script, error) { return nil }) - envOptions := env.Options{ - UseFieldNameByDefault: true, - } - if err := env.ParseWithOptions(s.c, envOptions); err != nil { + if err := pathenvconfig.Process("", s.c); err != nil { return nil, fmt.Errorf("newScript: failed to process configuration values: %w", err) } + fmt.Printf("Using configuration: %+v\n", s.c) // Debug + s.file = path.Join("/tmp", s.c.BackupFilename) tmplFileName, tErr := template.New("extension").Parse(s.file) @@ -140,13 +139,10 @@ func newScript() (*script, error) { } if s.c.AwsS3BucketName != "" { - accessKeyID := s.c.getSecret(s.c.AwsAccessKeyIDFile, s.c.AwsAccessKeyID) - secretAccessKey := s.c.getSecret(s.c.AwsSecretAccessKeyFile, s.c.AwsSecretAccessKey) - s3Config := s3.Config{ Endpoint: s.c.AwsEndpoint, - AccessKeyID: accessKeyID, - SecretAccessKey: secretAccessKey, + AccessKeyID: s.c.AwsAccessKeyID, + SecretAccessKey: s.c.AwsSecretAccessKey, IamRoleEndpoint: s.c.AwsIamRoleEndpoint, EndpointProto: s.c.AwsEndpointProto, EndpointInsecure: s.c.AwsEndpointInsecure, @@ -164,13 +160,11 @@ func newScript() (*script, error) { } if s.c.WebdavUrl != "" { - webdavPassword := s.c.getSecret(s.c.WebdavPasswordFile, s.c.WebdavPassword) - webDavConfig := webdav.Config{ URL: s.c.WebdavUrl, URLInsecure: s.c.WebdavUrlInsecure, Username: s.c.WebdavUsername, - Password: webdavPassword, + Password: s.c.WebdavPassword, RemotePath: s.c.WebdavPath, } if webdavBackend, err := webdav.NewStorageBackend(webDavConfig, logFunc); err != nil { @@ -181,16 +175,13 @@ func newScript() (*script, error) { } if s.c.SSHHostName != "" { - sshPassword := s.c.getSecret(s.c.SSHPasswordFile, s.c.SSHPassword) - sshIdentityPassphrase := s.c.getSecret(s.c.SSHIdentityPassphraseFile, s.c.SSHIdentityPassphrase) - sshConfig := ssh.Config{ HostName: s.c.SSHHostName, Port: s.c.SSHPort, User: s.c.SSHUser, - Password: sshPassword, + Password: s.c.SSHPassword, IdentityFile: s.c.SSHIdentityFile, - IdentityPassphrase: sshIdentityPassphrase, + IdentityPassphrase: s.c.SSHIdentityPassphrase, RemotePath: s.c.SSHRemotePath, } if sshBackend, err := ssh.NewStorageBackend(sshConfig, logFunc); err != nil { @@ -225,16 +216,12 @@ func newScript() (*script, error) { } if s.c.DropboxRefreshToken != "" && s.c.DropboxAppKey != "" && s.c.DropboxAppSecret != "" { - dropboxRefreshToken := s.c.getSecret(s.c.DropboxRefreshTokenFile, s.c.DropboxRefreshToken) - dropboxAppKey := s.c.getSecret(s.c.DropboxAppKeyFile, s.c.DropboxAppKey) - dropboxAppSecret := s.c.getSecret(s.c.DropboxAppSecretFile, s.c.DropboxAppSecret) - dropboxConfig := dropbox.Config{ Endpoint: s.c.DropboxEndpoint, OAuth2Endpoint: s.c.DropboxOAuth2Endpoint, - RefreshToken: dropboxRefreshToken, - AppKey: dropboxAppKey, - AppSecret: dropboxAppSecret, + RefreshToken: s.c.DropboxRefreshToken, + AppKey: s.c.DropboxAppKey, + AppSecret: s.c.DropboxAppSecret, RemotePath: s.c.DropboxRemotePath, ConcurrencyLevel: s.c.DropboxConcurrencyLevel.Int(), } @@ -246,12 +233,10 @@ func newScript() (*script, error) { } if s.c.EmailNotificationRecipient != "" { - smtpPassword := s.c.getSecret(s.c.EmailSMTPPasswordFile, s.c.EmailSMTPPassword) - emailURL := fmt.Sprintf( "smtp://%s:%s@%s:%d/?from=%s&to=%s", s.c.EmailSMTPUsername, - smtpPassword, + s.c.EmailSMTPPassword, s.c.EmailSMTPHost, s.c.EmailSMTPPort, s.c.EmailNotificationSender, @@ -512,8 +497,7 @@ func (s *script) createArchive() error { // In case no passphrase is given it returns early, leaving the backup file // untouched. func (s *script) encryptArchive() error { - gpgPassphrase := s.c.getSecret(s.c.GpgPassphraseFile, s.c.GpgPassphrase) - if gpgPassphrase == "" { + if s.c.GpgPassphrase == "" { return nil } @@ -535,7 +519,7 @@ func (s *script) encryptArchive() error { defer outFile.Close() _, name := path.Split(s.file) - dst, err := openpgp.SymmetricallyEncrypt(outFile, []byte(gpgPassphrase), &openpgp.FileHints{ + dst, err := openpgp.SymmetricallyEncrypt(outFile, []byte(s.c.GpgPassphrase), &openpgp.FileHints{ IsBinary: true, FileName: name, }, nil) diff --git a/go.mod b/go.mod index db55167..e24a399 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( require ( github.com/cloudflare/circl v1.3.3 // indirect + github.com/dlclark/regexp2 v1.4.0 // indirect github.com/golang/protobuf v1.5.2 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.28.1 // indirect @@ -33,7 +34,6 @@ require ( github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 // indirect github.com/Microsoft/go-winio v0.5.2 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 - github.com/caarlos0/env/v9 v9.0.0 github.com/docker/distribution v2.8.2+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect @@ -43,6 +43,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/google/uuid v1.3.0 // indirect + github.com/johnstairs/pathenvconfig v0.2.1 github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/kr/fs v0.1.0 // indirect diff --git a/go.sum b/go.sum index 3924b3c..7e4aec8 100644 --- a/go.sum +++ b/go.sum @@ -221,8 +221,6 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/caarlos0/env/v9 v9.0.0 h1:SI6JNsOA+y5gj9njpgybykATIylrRMklbs5ch6wO6pc= -github.com/caarlos0/env/v9 v9.0.0/go.mod h1:ye5mlCVMYh6tZ+vCgrs/B95sj88cg5Tlnc0XIzgZ020= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -254,6 +252,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= 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= @@ -442,6 +442,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1: github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jarcoal/httpmock v1.2.0 h1:gSvTxxFR/MEMfsGrvRbdfpRUMBStovlSRLw0Ep1bwwc= github.com/jarcoal/httpmock v1.2.0/go.mod h1:oCoTsnAz4+UoOUIf5lJOWV2QQIW5UoeUI6aM2YnWAZk= +github.com/johnstairs/pathenvconfig v0.2.1 h1:hay0C2ddNdej569b4GXwjwZyjRagei97ppjRW8XcvMQ= +github.com/johnstairs/pathenvconfig v0.2.1/go.mod h1:XqBReihWnlfmCkHqvJAfsRCfa/JJCYXZds8fwtuz8cM= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=