Consolidate config value resolution at top level (#705)

* Consolidate env expansion at top level

* Move handling of deprecated config into resolve method

* Handle template interpolation in resolve method

* Clean up and document resolve function
This commit is contained in:
Frederik Ring
2026-01-04 21:03:43 +01:00
committed by GitHub
parent f9f89050d7
commit 8b6585ad30
6 changed files with 102 additions and 83 deletions

View File

@@ -4,12 +4,15 @@
package main package main
import ( import (
"bytes"
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"os" "os"
"regexp" "regexp"
"strconv" "strconv"
"strings"
"text/template"
"time" "time"
"github.com/offen/docker-volume-backup/internal/errwrap" "github.com/offen/docker-volume-backup/internal/errwrap"
@@ -225,3 +228,92 @@ func (c *Config) applyEnv() (func() error, error) {
} }
return unset, nil return unset, nil
} }
// resolve is responsible for performing all implicit logic that transforms a configuration object
// into what is actually being used at runtime. E.g. environment variables are expanded or
// deprecated config options are transposed into their up to date successors. The caller is
// responsible for calling the returned reset function after usage of the config is done.
func (c *Config) resolve() (reset func() error, warnings []string, err error) {
reset, aErr := c.applyEnv()
if aErr != nil {
err = errwrap.Wrap(aErr, "error applying env")
return
}
if c.BackupFilenameExpand {
c.BackupFilename = os.ExpandEnv(c.BackupFilename)
c.BackupLatestSymlink = os.ExpandEnv(c.BackupLatestSymlink)
c.BackupPruningPrefix = os.ExpandEnv(c.BackupPruningPrefix)
}
if c.EmailNotificationRecipient != "" {
emailURL := fmt.Sprintf(
"smtp://%s:%s@%s:%d/?from=%s&to=%s",
c.EmailSMTPUsername,
c.EmailSMTPPassword,
c.EmailSMTPHost,
c.EmailSMTPPort,
c.EmailNotificationSender,
c.EmailNotificationRecipient,
)
c.NotificationURLs = append(c.NotificationURLs, emailURL)
warnings = append(warnings,
"Using EMAIL_* keys for providing notification configuration has been deprecated and will be removed in the next major version.",
"Please use NOTIFICATION_URLS instead. Refer to the README for an upgrade guide.",
)
}
if c.BackupFromSnapshot {
warnings = append(warnings,
"Using BACKUP_FROM_SNAPSHOT has been deprecated and will be removed in the next major version.",
"Please use `archive-pre` and `archive-post` commands to prepare your backup sources. Refer to the documentation for an upgrade guide.",
)
}
if c.BackupStopDuringBackupLabel != "" && c.BackupStopContainerLabel != "" {
err = errwrap.Wrap(nil, "both BACKUP_STOP_DURING_BACKUP_LABEL and BACKUP_STOP_CONTAINER_LABEL have been set, cannot continue")
return
}
if c.BackupStopContainerLabel != "" {
warnings = append(warnings,
"Using BACKUP_STOP_CONTAINER_LABEL has been deprecated and will be removed in the next major version.",
"Please use BACKUP_STOP_DURING_BACKUP_LABEL instead. Refer to the docs for an upgrade guide.",
)
c.BackupStopDuringBackupLabel = c.BackupStopContainerLabel
}
tmplFileName, tErr := template.New("extension").Parse(c.BackupFilename)
if tErr != nil {
err = errwrap.Wrap(tErr, "unable to parse backup file extension template")
return
}
var bf bytes.Buffer
if tErr := tmplFileName.Execute(&bf, map[string]string{
"Extension": func() string {
if c.BackupCompression == "none" {
return "tar"
}
return fmt.Sprintf("tar.%s", c.BackupCompression)
}(),
}); tErr != nil {
err = errwrap.Wrap(tErr, "error executing backup file extension template")
return
}
c.BackupFilename = bf.String()
if c.AzureStorageEndpoint != "" {
endpointTemplate, tErr := template.New("endpoint").Parse(c.AzureStorageEndpoint)
if tErr != nil {
err = errwrap.Wrap(tErr, "error parsing endpoint template")
return
}
var ep bytes.Buffer
if tErr := endpointTemplate.Execute(&ep, map[string]string{"AccountName": c.AzureStorageAccountName}); tErr != nil {
err = errwrap.Wrap(tErr, "error executing endpoint template")
return
}
c.AzureStorageEndpoint = fmt.Sprintf("%s/", strings.TrimSuffix(ep.String(), "/"))
}
return
}

View File

@@ -18,12 +18,6 @@ func (s *script) createArchive() error {
backupSources := s.c.BackupSources backupSources := s.c.BackupSources
if s.c.BackupFromSnapshot { if s.c.BackupFromSnapshot {
s.logger.Warn(
"Using BACKUP_FROM_SNAPSHOT has been deprecated and will be removed in the next major version.",
)
s.logger.Warn(
"Please use `archive-pre` and `archive-post` commands to prepare your backup sources. Refer to the documentation for an upgrade guide.",
)
backupSources = filepath.Join("/tmp", s.c.BackupSources) backupSources = filepath.Join("/tmp", s.c.BackupSources)
// copy before compressing guard against a situation where backup folder's content are still growing. // copy before compressing guard against a situation where backup folder's content are still growing.
s.registerHook(hookLevelPlumbing, func(error) error { s.registerHook(hookLevelPlumbing, func(error) error {

View File

@@ -43,7 +43,7 @@ func runScript(c *Config) (err error) {
} }
}() }()
unset, err := s.c.applyEnv() unset, warnings, err := s.c.resolve()
if err != nil { if err != nil {
return errwrap.Wrap(err, "error applying env") return errwrap.Wrap(err, "error applying env")
} }
@@ -52,6 +52,9 @@ func runScript(c *Config) (err error) {
err = errors.Join(err, errwrap.Wrap(derr, "error unsetting environment variables")) err = errors.Join(err, errwrap.Wrap(derr, "error unsetting environment variables"))
} }
}() }()
for _, w := range warnings {
s.logger.Warn(w)
}
if s.c != nil && s.c.BackupJitter > 0 { if s.c != nil && s.c.BackupJitter > 0 {
max := s.c.BackupJitter max := s.c.BackupJitter

View File

@@ -4,7 +4,6 @@
package main package main
import ( import (
"bytes"
"fmt" "fmt"
"log/slog" "log/slog"
"os" "os"
@@ -79,24 +78,6 @@ func (s *script) init() error {
return nil return nil
}) })
// Register notifications first so they can fire in case of other init errors. // Register notifications first so they can fire in case of other init errors.
if s.c.EmailNotificationRecipient != "" {
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)
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.",
)
}
hookLevel, ok := hookLevels[s.c.NotificationLevel] hookLevel, ok := hookLevels[s.c.NotificationLevel]
if !ok { if !ok {
@@ -143,30 +124,6 @@ func (s *script) init() 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 errwrap.Wrap(tErr, "unable to parse backup file extension template")
}
var bf bytes.Buffer
if tErr := tmplFileName.Execute(&bf, map[string]string{
"Extension": func() string {
if s.c.BackupCompression == "none" {
return "tar"
}
return fmt.Sprintf("tar.%s", s.c.BackupCompression)
}(),
}); tErr != nil {
return errwrap.Wrap(tErr, "error executing backup file extension template")
}
s.file = bf.String()
if s.c.BackupFilenameExpand {
s.file = os.ExpandEnv(s.file)
s.c.BackupLatestSymlink = os.ExpandEnv(s.c.BackupLatestSymlink)
s.c.BackupPruningPrefix = os.ExpandEnv(s.c.BackupPruningPrefix)
}
s.file = timeutil.Strftime(&s.stats.StartTime, s.file) s.file = timeutil.Strftime(&s.stats.StartTime, s.file)
_, err := os.Stat("/var/run/docker.sock") _, err := os.Stat("/var/run/docker.sock")

View File

@@ -8,7 +8,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"os"
"sync" "sync"
"time" "time"
@@ -113,23 +112,9 @@ func (s *script) stopContainersAndServices() (func() error, error) {
return noop, errwrap.Wrap(err, "error determining swarm state") return noop, errwrap.Wrap(err, "error determining swarm state")
} }
labelValue := s.c.BackupStopDuringBackupLabel
if s.c.BackupStopContainerLabel != "" {
s.logger.Warn(
"Using BACKUP_STOP_CONTAINER_LABEL has been deprecated and will be removed in the next major version.",
)
s.logger.Warn(
"Please use BACKUP_STOP_DURING_BACKUP_LABEL instead. Refer to the docs for an upgrade guide.",
)
if _, ok := os.LookupEnv("BACKUP_STOP_DURING_BACKUP_LABEL"); ok {
return noop, errwrap.Wrap(nil, "both BACKUP_STOP_DURING_BACKUP_LABEL and BACKUP_STOP_CONTAINER_LABEL have been set, cannot continue")
}
labelValue = s.c.BackupStopContainerLabel
}
stopDuringBackupLabel := fmt.Sprintf( stopDuringBackupLabel := fmt.Sprintf(
"docker-volume-backup.stop-during-backup=%s", "docker-volume-backup.stop-during-backup=%s",
labelValue, s.c.BackupStopDuringBackupLabel,
) )
stopDuringBackupNoRestartLabel := fmt.Sprintf( stopDuringBackupNoRestartLabel := fmt.Sprintf(
@@ -144,7 +129,7 @@ func (s *script) stopContainersAndServices() (func() error, error) {
var containersToStop []handledContainer var containersToStop []handledContainer
for _, c := range allContainers.Items { for _, c := range allContainers.Items {
hasStopDuringBackupLabel, hasStopDuringBackupNoRestartLabel, err := checkStopLabels(c.Labels, labelValue, s.c.BackupStopDuringBackupNoRestartLabel) hasStopDuringBackupLabel, hasStopDuringBackupNoRestartLabel, err := checkStopLabels(c.Labels, s.c.BackupStopDuringBackupLabel, s.c.BackupStopDuringBackupNoRestartLabel)
if err != nil { if err != nil {
return noop, errwrap.Wrap(err, "error querying for containers to stop") return noop, errwrap.Wrap(err, "error querying for containers to stop")
} }
@@ -169,7 +154,7 @@ func (s *script) stopContainersAndServices() (func() error, error) {
} }
for _, service := range allServices { for _, service := range allServices {
hasStopDuringBackupLabel, hasStopDuringBackupNoRestartLabel, err := checkStopLabels(service.Spec.Labels, labelValue, s.c.BackupStopDuringBackupNoRestartLabel) hasStopDuringBackupLabel, hasStopDuringBackupNoRestartLabel, err := checkStopLabels(service.Spec.Labels, s.c.BackupStopDuringBackupLabel, s.c.BackupStopDuringBackupNoRestartLabel)
if err != nil { if err != nil {
return noop, errwrap.Wrap(err, "error querying for services to scale down") return noop, errwrap.Wrap(err, "error querying for services to scale down")
} }

View File

@@ -4,16 +4,13 @@
package azure package azure
import ( import (
"bytes"
"context" "context"
"errors" "errors"
"fmt" "fmt"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"strings"
"sync" "sync"
"text/template"
"time" "time"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
@@ -49,16 +46,6 @@ func NewStorageBackend(opts Config, logFunc storage.Log) (storage.Backend, error
return nil, errwrap.Wrap(nil, "using primary account key and connection string are mutually exclusive") return nil, errwrap.Wrap(nil, "using primary account key and connection string are mutually exclusive")
} }
endpointTemplate, err := template.New("endpoint").Parse(opts.Endpoint)
if err != nil {
return nil, errwrap.Wrap(err, "error parsing endpoint template")
}
var ep bytes.Buffer
if err := endpointTemplate.Execute(&ep, opts); err != nil {
return nil, errwrap.Wrap(err, "error executing endpoint template")
}
normalizedEndpoint := fmt.Sprintf("%s/", strings.TrimSuffix(ep.String(), "/"))
var client *azblob.Client var client *azblob.Client
if opts.PrimaryAccountKey != "" { if opts.PrimaryAccountKey != "" {
cred, err := azblob.NewSharedKeyCredential(opts.AccountName, opts.PrimaryAccountKey) cred, err := azblob.NewSharedKeyCredential(opts.AccountName, opts.PrimaryAccountKey)
@@ -66,11 +53,12 @@ func NewStorageBackend(opts Config, logFunc storage.Log) (storage.Backend, error
return nil, errwrap.Wrap(err, "error creating shared key Azure credential") return nil, errwrap.Wrap(err, "error creating shared key Azure credential")
} }
client, err = azblob.NewClientWithSharedKeyCredential(normalizedEndpoint, cred, nil) client, err = azblob.NewClientWithSharedKeyCredential(opts.Endpoint, cred, nil)
if err != nil { if err != nil {
return nil, errwrap.Wrap(err, "error creating azure client from primary account key") return nil, errwrap.Wrap(err, "error creating azure client from primary account key")
} }
} else if opts.ConnectionString != "" { } else if opts.ConnectionString != "" {
var err error
client, err = azblob.NewClientFromConnectionString(opts.ConnectionString, nil) client, err = azblob.NewClientFromConnectionString(opts.ConnectionString, nil)
if err != nil { if err != nil {
return nil, errwrap.Wrap(err, "error creating azure client from connection string") return nil, errwrap.Wrap(err, "error creating azure client from connection string")
@@ -80,7 +68,7 @@ func NewStorageBackend(opts Config, logFunc storage.Log) (storage.Backend, error
if err != nil { if err != nil {
return nil, errwrap.Wrap(err, "error creating managed identity credential") return nil, errwrap.Wrap(err, "error creating managed identity credential")
} }
client, err = azblob.NewClient(normalizedEndpoint, cred, nil) client, err = azblob.NewClient(opts.Endpoint, cred, nil)
if err != nil { if err != nil {
return nil, errwrap.Wrap(err, "error creating azure client from managed identity") return nil, errwrap.Wrap(err, "error creating azure client from managed identity")
} }