[#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 string `yaml:"type"`
Count int `yaml:"count"`
Regex string `yaml:"regex,optional"`
}
type PruneKeepRegex struct { // FIXME rename to KeepRegex

View File

@ -175,9 +175,11 @@ Policy ``last_n``
keep_receiver:
- type: last_n
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:

View File

@ -1,6 +1,7 @@
package pruning
import (
"regexp"
"sort"
"strings"
@ -8,14 +9,27 @@ import (
)
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 {
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) {
@ -24,17 +38,25 @@ func (k KeepLastN) KeepRule(snaps []Snapshot) (destroyList []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)
id, jd := res[i].Date(), res[j].Date()
id, jd := matching[i].Date(), matching[j].Date()
if !id.Equal(jd) {
return id.After(jd)
}
// 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
})
return res[k.n:]
destroyList = append(destroyList, matching[k.n:]...)
return destroyList
}

View File

@ -5,6 +5,7 @@ import (
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestKeepLastN(t *testing.T) {
@ -28,7 +29,7 @@ func TestKeepLastN(t *testing.T) {
"keep2": {
inputs: inputs["s1"],
rules: []KeepRule{
KeepLastN{2},
MustKeepLastN(2, ""),
},
expDestroy: map[string]bool{
"1": true, "2": true, "3": true,
@ -37,34 +38,66 @@ func TestKeepLastN(t *testing.T) {
"keep1OfTwoWithSameTime": { // Keep one of two with same time
inputs: inputs["s1"],
rules: []KeepRule{
KeepLastN{1},
MustKeepLastN(1, ""),
},
expDestroy: map[string]bool{"1": true, "2": true, "3": true, "4": true},
},
"keepMany": {
inputs: inputs["s1"],
rules: []KeepRule{
KeepLastN{100},
MustKeepLastN(100, ""),
},
expDestroy: map[string]bool{},
},
"empty": {
"empty_input": {
inputs: inputs["s2"],
rules: []KeepRule{
KeepLastN{100},
MustKeepLastN(100, ""),
},
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)
t.Run("mustBePositive", func(t *testing.T) {
var err error
_, err = NewKeepLastN(0)
_, err = NewKeepLastN(0, "foo")
assert.Error(t, err)
_, err = NewKeepLastN(-5)
_, err = NewKeepLastN(-5, "foo")
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:
return NewKeepNotReplicated(), nil
case *config.PruneKeepLastN:
return NewKeepLastN(v.Count)
return NewKeepLastN(v.Count, v.Regex)
case *config.PruneKeepRegex:
return NewKeepRegex(v.Regex, v.Negate)
case *config.PruneGrid: