Refuse an exclude pattern that does not compile when the config loads (closes #455)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
An exclude pattern that did not compile was skipped with a log warning during the backup, which then stored what it meant to leave out. Config.Validate now refuses such a pattern, global or per snapshot, naming the setting and quoting the pattern. The compile step moves to a new package, internal/exclude, which both the config and the scanner call. Model: opus-5-5
This commit was merged in pull request #456.
This commit is contained in:
1 parent
6a8bb2f389
commit
5ca0d46932
6 files changed
+294
-69
No files matched your search
@@ -396,6 +396,8 @@ func Parse(data []byte) (*Config, error) {
|
||||
// never the value. An empty list is accepted, because only snapshot
|
||||
// create needs a recipient and it checks for one itself
|
||||
// - At least one snapshot must be configured with at least one path
|
||||
// - Every exclude pattern, global or in a snapshot, must compile; errors
|
||||
// name the setting and quote the pattern
|
||||
// - Storage must be configured (either storage_url or s3.* fields)
|
||||
// - The index path must be absolute: a relative one would resolve against
|
||||
// the working directory, so each directory would get its own index
|
||||
@@ -416,18 +418,18 @@ func (c *Config) Validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
if len(c.Snapshots) == 0 {
|
||||
return errNoSnapshots
|
||||
err := c.validateSnapshots()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for name, snap := range c.Snapshots {
|
||||
if len(snap.Paths) == 0 {
|
||||
return fmt.Errorf("%w: %q", errSnapshotNoPaths, name)
|
||||
}
|
||||
err = c.validateExcludes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate storage configuration
|
||||
err := c.validateStorage()
|
||||
err = c.validateStorage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -466,6 +468,22 @@ func (c *Config) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateSnapshots refuses a config with no snapshot or with a snapshot
|
||||
// that has no path.
|
||||
func (c *Config) validateSnapshots() error {
|
||||
if len(c.Snapshots) == 0 {
|
||||
return errNoSnapshots
|
||||
}
|
||||
|
||||
for name, snap := range c.Snapshots {
|
||||
if len(snap.Paths) == 0 {
|
||||
return fmt.Errorf("%w: %q", errSnapshotNoPaths, name)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateAgeRecipient parses one age_recipients entry with the age library
|
||||
// and returns a value-free error on failure. A recipient string can be
|
||||
// sensitive (an operator may paste a secret key by mistake), so neither the
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"sneak.berlin/go/vaultik/internal/exclude"
|
||||
)
|
||||
|
||||
// validateExcludes refuses an exclude pattern, global or in a snapshot,
|
||||
// that does not compile, naming the setting and quoting the pattern.
|
||||
func (c *Config) validateExcludes() error {
|
||||
err := validateExcludeList("exclude", c.Exclude)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, name := range c.SnapshotNames() {
|
||||
err = validateExcludeList("snapshots."+name+".exclude",
|
||||
c.Snapshots[name].Exclude)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateExcludeList refuses the first pattern in patterns that does not
|
||||
// compile. setting is the name of the list, as in "exclude".
|
||||
func validateExcludeList(setting string, patterns []string) error {
|
||||
for i, pattern := range patterns {
|
||||
_, _, err := exclude.Compile(pattern)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s[%d]: %w", setting, i, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sneak.berlin/go/vaultik/internal/config"
|
||||
)
|
||||
|
||||
// TestLoadRefusesBadExcludePattern checks that an exclude pattern that does
|
||||
// not compile, global or in a snapshot, fails at load with the setting and
|
||||
// the pattern named, rather than the backup storing what it meant to leave
|
||||
// out.
|
||||
func TestLoadRefusesBadExcludePattern(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const storage = "storage_url: file:///tmp/vaultik-test-storage\n"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "global",
|
||||
config: "exclude: ['*.tmp', '.cache', 'build[']\n" +
|
||||
"snapshots:\n" +
|
||||
" home:\n" +
|
||||
" paths: [/tmp/vaultik-test-source]\n" + storage,
|
||||
wantErr: `exclude[2]: "build[" is not a valid exclude pattern`,
|
||||
},
|
||||
{
|
||||
name: "snapshot",
|
||||
config: "snapshots:\n" +
|
||||
" home:\n" +
|
||||
" paths: [/tmp/vaultik-test-source]\n" +
|
||||
" exclude: ['/a/**/x[']\n" + storage,
|
||||
wantErr: `snapshots.home.exclude[0]: "/a/**/x[" is not a valid exclude pattern`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
path := filepath.Join(t.TempDir(), "config.yml")
|
||||
|
||||
err := os.WriteFile(path, []byte(tt.config), 0o600)
|
||||
if err != nil {
|
||||
t.Fatalf("write config: %v", err)
|
||||
}
|
||||
|
||||
_, err = config.Load(path)
|
||||
if err == nil {
|
||||
t.Fatalf("Load() returned nil, want an error containing %q", tt.wantErr)
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), tt.wantErr) {
|
||||
t.Fatalf("Load() error = %q, want it to contain %q", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user