[#373] pruning: add optional regex field to last_n rule

fixes #373
This commit is contained in:
Christian Schwarz 2020-08-31 14:05:32 +02:00
parent 428a60870a
commit b1f8cdf385
5 changed files with 76 additions and 18 deletions

View File

@ -324,6 +324,7 @@ type PruneKeepNotReplicated struct {
type PruneKeepLastN struct { type PruneKeepLastN struct {
Type string `yaml:"type"` Type string `yaml:"type"`
Count int `yaml:"count"` Count int `yaml:"count"`
Regex string `yaml:"regex,optional"`
} }
type PruneKeepRegex struct { // FIXME rename to KeepRegex type PruneKeepRegex struct { // FIXME rename to KeepRegex

View File

@ -175,9 +175,11 @@ Policy ``last_n``
keep_receiver: keep_receiver:
- type: last_n - type: last_n
count: 10 count: 10
regex: ^zrepl_.*$ # optional
... ...
``last_n`` keeps the last ``count`` snapshots (last = youngest = most recent creation date). ``last_n`` filters the snapshot list by ``regex``, then keeps the last ``count`` snapshots in that list (last = youngest = most recent creation date)
All snapshots that don't match ``regex`` or exceed ``count`` in the filtered list are destroyed unless matched by other rules.
.. _prune-keep-regex: .. _prune-keep-regex:

View File

@ -1,6 +1,7 @@
package pruning package pruning
import ( import (
"regexp"
"sort" "sort"
"strings" "strings"
@ -9,13 +10,26 @@ import (
type KeepLastN struct { type KeepLastN struct {
n int n int
re *regexp.Regexp
} }
func NewKeepLastN(n int) (*KeepLastN, error) { func MustKeepLastN(n int, regex string) *KeepLastN {
k, err := NewKeepLastN(n, regex)
if err != nil {
panic(err)
}
return k
}
func NewKeepLastN(n int, regex string) (*KeepLastN, error) {
if n <= 0 { if n <= 0 {
return nil, errors.Errorf("must specify positive number as 'keep last count', got %d", n) return nil, errors.Errorf("must specify positive number as 'keep last count', got %d", n)
} }
return &KeepLastN{n}, nil re, err := regexp.Compile(regex)
if err != nil {
return nil, errors.Errorf("invalid regex %q: %s", regex, err)
}
return &KeepLastN{n, re}, nil
} }
func (k KeepLastN) KeepRule(snaps []Snapshot) (destroyList []Snapshot) { func (k KeepLastN) KeepRule(snaps []Snapshot) (destroyList []Snapshot) {
@ -24,17 +38,25 @@ func (k KeepLastN) KeepRule(snaps []Snapshot) (destroyList []Snapshot) {
return []Snapshot{} return []Snapshot{}
} }
res := shallowCopySnapList(snaps) matching, notMatching := partitionSnapList(snaps, func(snapshot Snapshot) bool {
return k.re.MatchString(snapshot.Name())
})
// snaps that don't match the regex are not kept by this rule
destroyList = append(destroyList, notMatching...)
sort.Slice(res, func(i, j int) bool { if len(matching) == 0 {
return destroyList
}
sort.Slice(matching, func(i, j int) bool {
// by date (youngest first) // by date (youngest first)
id, jd := res[i].Date(), res[j].Date() id, jd := matching[i].Date(), matching[j].Date()
if !id.Equal(jd) { if !id.Equal(jd) {
return id.After(jd) return id.After(jd)
} }
// then lexicographically descending (e.g. b, a) // then lexicographically descending (e.g. b, a)
return strings.Compare(res[i].Name(), res[j].Name()) == 1 return strings.Compare(matching[i].Name(), matching[j].Name()) == 1
}) })
destroyList = append(destroyList, matching[k.n:]...)
return res[k.n:] return destroyList
} }

View File

@ -5,6 +5,7 @@ import (
"time" "time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestKeepLastN(t *testing.T) { func TestKeepLastN(t *testing.T) {
@ -28,7 +29,7 @@ func TestKeepLastN(t *testing.T) {
"keep2": { "keep2": {
inputs: inputs["s1"], inputs: inputs["s1"],
rules: []KeepRule{ rules: []KeepRule{
KeepLastN{2}, MustKeepLastN(2, ""),
}, },
expDestroy: map[string]bool{ expDestroy: map[string]bool{
"1": true, "2": true, "3": true, "1": true, "2": true, "3": true,
@ -37,34 +38,66 @@ func TestKeepLastN(t *testing.T) {
"keep1OfTwoWithSameTime": { // Keep one of two with same time "keep1OfTwoWithSameTime": { // Keep one of two with same time
inputs: inputs["s1"], inputs: inputs["s1"],
rules: []KeepRule{ rules: []KeepRule{
KeepLastN{1}, MustKeepLastN(1, ""),
}, },
expDestroy: map[string]bool{"1": true, "2": true, "3": true, "4": true}, expDestroy: map[string]bool{"1": true, "2": true, "3": true, "4": true},
}, },
"keepMany": { "keepMany": {
inputs: inputs["s1"], inputs: inputs["s1"],
rules: []KeepRule{ rules: []KeepRule{
KeepLastN{100}, MustKeepLastN(100, ""),
}, },
expDestroy: map[string]bool{}, expDestroy: map[string]bool{},
}, },
"empty": { "empty_input": {
inputs: inputs["s2"], inputs: inputs["s2"],
rules: []KeepRule{ rules: []KeepRule{
KeepLastN{100}, MustKeepLastN(100, ""),
}, },
expDestroy: map[string]bool{}, expDestroy: map[string]bool{},
}, },
"empty_regex": {
inputs: inputs["s1"],
rules: []KeepRule{
MustKeepLastN(4, ""),
},
expDestroy: map[string]bool{
"1": true,
},
},
"multiple_regexes": {
inputs: []Snapshot{
stubSnap{"a1", false, o(10)},
stubSnap{"b1", false, o(11)},
stubSnap{"a2", false, o(20)},
stubSnap{"b2", false, o(21)},
stubSnap{"a3", false, o(30)},
stubSnap{"b3", false, o(31)},
},
rules: []KeepRule{
MustKeepLastN(2, "^a"),
MustKeepLastN(2, "^b"),
},
expDestroy: map[string]bool{
"a1": true,
"b1": true,
},
},
} }
testTable(tcs, t) testTable(tcs, t)
t.Run("mustBePositive", func(t *testing.T) { t.Run("mustBePositive", func(t *testing.T) {
var err error var err error
_, err = NewKeepLastN(0) _, err = NewKeepLastN(0, "foo")
assert.Error(t, err) assert.Error(t, err)
_, err = NewKeepLastN(-5) _, err = NewKeepLastN(-5, "foo")
assert.Error(t, err) assert.Error(t, err)
}) })
t.Run("emptyRegexAllowed", func(t *testing.T) {
_, err := NewKeepLastN(23, "")
require.NoError(t, err)
})
} }

View File

@ -60,7 +60,7 @@ func RuleFromConfig(in config.PruningEnum) (KeepRule, error) {
case *config.PruneKeepNotReplicated: case *config.PruneKeepNotReplicated:
return NewKeepNotReplicated(), nil return NewKeepNotReplicated(), nil
case *config.PruneKeepLastN: case *config.PruneKeepLastN:
return NewKeepLastN(v.Count) return NewKeepLastN(v.Count, v.Regex)
case *config.PruneKeepRegex: case *config.PruneKeepRegex:
return NewKeepRegex(v.Regex, v.Negate) return NewKeepRegex(v.Regex, v.Negate)
case *config.PruneGrid: case *config.PruneGrid: