diff --git a/config/config.go b/config/config.go index d6ab1e6..eeea023 100644 --- a/config/config.go +++ b/config/config.go @@ -250,6 +250,7 @@ type PruneKeepLastN struct { type PruneKeepRegex struct { // FIXME rename to KeepRegex Type string `yaml:"type"` Regex string `yaml:"regex"` + Negate bool `yaml:"negate,optional,default=false"` } type LoggingOutletEnum struct { diff --git a/daemon/pruner/pruner_test.go b/daemon/pruner/pruner_test.go index d2bd703..47d8f41 100644 --- a/daemon/pruner/pruner_test.go +++ b/daemon/pruner/pruner_test.go @@ -179,7 +179,7 @@ func TestPruner_Prune(t *testing.T) { }, } - keepRules := []pruning.KeepRule{pruning.MustKeepRegex("^keep")} + keepRules := []pruning.KeepRule{pruning.MustKeepRegex("^keep", false)} p := Pruner{ args: args{ diff --git a/docs/configuration/prune.rst b/docs/configuration/prune.rst index 6ed9a82..ef40ce3 100644 --- a/docs/configuration/prune.rst +++ b/docs/configuration/prune.rst @@ -146,12 +146,22 @@ Policy ``regex`` - type: push pruning: keep_receiver: + # keep all snapshots with prefix zrepl_ or manual_ - type: regex regex: "^(zrepl|manual)_.*" - ... + + - type: push + snapshotting: + prefix: zrepl_ + pruning: + keep_sender: + # keep all snapshots that were not created by zrepl + - type: regex + negate: true + regex: "^zrepl_.*" ``regex`` keeps all snapshots whose names are matched by the regular expressionin ``regex``. Like all other regular expression fields in prune policies, zrepl uses Go's `regexp.Regexp `_ Perl-compatible regular expressions (`Syntax `_). - +The optional `negate` boolean field inverts the semantics: Use it if you want to keep all snapshots that *do not* match the given regex. diff --git a/pruning/keep_regex.go b/pruning/keep_regex.go index f8a5956..bf45864 100644 --- a/pruning/keep_regex.go +++ b/pruning/keep_regex.go @@ -6,20 +6,21 @@ import ( type KeepRegex struct { expr *regexp.Regexp + negate bool } var _ KeepRule = &KeepRegex{} -func NewKeepRegex(expr string) (*KeepRegex, error) { +func NewKeepRegex(expr string, negate bool) (*KeepRegex, error) { re, err := regexp.Compile(expr) if err != nil { return nil, err } - return &KeepRegex{re}, nil + return &KeepRegex{re, negate}, nil } -func MustKeepRegex(expr string) *KeepRegex { - k, err := NewKeepRegex(expr) +func MustKeepRegex(expr string, negate bool) *KeepRegex { + k, err := NewKeepRegex(expr, negate) if err != nil { panic(err) } @@ -28,6 +29,10 @@ func MustKeepRegex(expr string) *KeepRegex { func (k *KeepRegex) KeepRule(snaps []Snapshot) []Snapshot { return filterSnapList(snaps, func(s Snapshot) bool { - return k.expr.FindStringIndex(s.Name()) == nil + if k.negate { + return k.expr.FindStringIndex(s.Name()) != nil + } else { + return k.expr.FindStringIndex(s.Name()) == nil + } }) } diff --git a/pruning/keep_regex_test.go b/pruning/keep_regex_test.go new file mode 100644 index 0000000..1437290 --- /dev/null +++ b/pruning/keep_regex_test.go @@ -0,0 +1,32 @@ +package pruning + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestKeepRegexNegation(t *testing.T) { + + noneg := MustKeepRegex("^zrepl_", false) + neg := MustKeepRegex("^zrepl_", true) + + snaps := []Snapshot{ + stubSnap{name: "zrepl_foobar"}, + stubSnap{name: "zrepl"}, + stubSnap{name: "barfoo"}, + } + + destroyNonNeg := snapshotList(noneg.KeepRule(snaps)) + t.Logf("non-negated rule destroys: %#v", destroyNonNeg.NameList()) + assert.True(t, destroyNonNeg.ContainsName("zrepl")) + assert.True(t, destroyNonNeg.ContainsName("barfoo")) + assert.False(t, destroyNonNeg.ContainsName("zrepl_foobar")) + + destroyNeg := snapshotList(neg.KeepRule(snaps)) + t.Logf("negated rule destroys: %#v", destroyNeg.NameList()) + assert.False(t, destroyNeg.ContainsName("zrepl")) + assert.False(t, destroyNeg.ContainsName("barfoo")) + assert.True(t, destroyNeg.ContainsName("zrepl_foobar")) + +} diff --git a/pruning/pruning.go b/pruning/pruning.go index 2c4dec9..5f05ede 100644 --- a/pruning/pruning.go +++ b/pruning/pruning.go @@ -60,7 +60,7 @@ func RuleFromConfig(in config.PruningEnum) (KeepRule, error) { case *config.PruneKeepLastN: return NewKeepLastN(v.Count) case *config.PruneKeepRegex: - return NewKeepRegex(v.Regex) + return NewKeepRegex(v.Regex, v.Negate) case *config.PruneGrid: return NewKeepGrid(v) default: diff --git a/pruning/pruning_test.go b/pruning/pruning_test.go index 678dc50..2755dde 100644 --- a/pruning/pruning_test.go +++ b/pruning/pruning_test.go @@ -24,6 +24,25 @@ type testCase struct { expDestroyAlternatives []map[string]bool } +type snapshotList []Snapshot + +func (l snapshotList) ContainsName(n string) bool { + for _, s := range l { + if s.Name() == n { + return true + } + } + return false +} + +func (l snapshotList) NameList() []string { + res := make([]string, len(l)) + for i, s := range l { + res[i] = s.Name() + } + return res +} + func testTable(tcs map[string]testCase, t *testing.T) { mapEqual := func(a, b map[string]bool) bool { if len(a) != len(b) { @@ -79,7 +98,7 @@ func TestPruneSnapshots(t *testing.T) { "simple": { inputs: inputs["s1"], rules: []KeepRule{ - MustKeepRegex("foo_"), + MustKeepRegex("foo_", false), }, expDestroy: map[string]bool{ "bar_123": true, @@ -88,16 +107,16 @@ func TestPruneSnapshots(t *testing.T) { "multipleRules": { inputs: inputs["s1"], rules: []KeepRule{ - MustKeepRegex("foo_"), - MustKeepRegex("bar_"), + MustKeepRegex("foo_", false), + MustKeepRegex("bar_", false), }, expDestroy: map[string]bool{}, }, "onlyThoseRemovedByAllAreRemoved": { inputs: inputs["s1"], rules: []KeepRule{ - MustKeepRegex("notInS1"), // would remove all - MustKeepRegex("bar_"), // would remove all but bar_, i.e. foo_.* + MustKeepRegex("notInS1", false), // would remove all + MustKeepRegex("bar_", false), // would remove all but bar_, i.e. foo_.* }, expDestroy: map[string]bool{ "foo_123": true, @@ -117,7 +136,7 @@ func TestPruneSnapshots(t *testing.T) { "noSnaps": { inputs: []Snapshot{}, rules: []KeepRule{ - MustKeepRegex("foo_"), + MustKeepRegex("foo_", false), }, expDestroy: map[string]bool{}, },