diff --git a/cmd/pruning/keep_helpers.go b/cmd/pruning/keep_helpers.go new file mode 100644 index 0000000..a756ce7 --- /dev/null +++ b/cmd/pruning/keep_helpers.go @@ -0,0 +1,17 @@ +package pruning + +func filterSnapList(snaps []Snapshot, predicate func(Snapshot) bool) []Snapshot { + r := make([]Snapshot, 0, len(snaps)) + for i := range snaps { + if predicate(snaps[i]) { + r = append(r, snaps[i]) + } + } + return r +} + +func shallowCopySnapList(snaps []Snapshot) []Snapshot { + c := make([]Snapshot, len(snaps)) + copy(c, snaps) + return c +} diff --git a/cmd/pruning/keep_helpers_test.go b/cmd/pruning/keep_helpers_test.go new file mode 100644 index 0000000..bf8b399 --- /dev/null +++ b/cmd/pruning/keep_helpers_test.go @@ -0,0 +1,22 @@ +package pruning + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestShallowCopySnapList(t *testing.T) { + + l1 := []Snapshot{ + stubSnap{name: "foo"}, + stubSnap{name: "bar"}, + } + l2 := shallowCopySnapList(l1) + + assert.Equal(t, l1, l2) + + l1[0] = stubSnap{name: "baz"} + assert.Equal(t, "baz", l1[0].Name()) + assert.Equal(t, "foo", l2[0].Name()) + +} diff --git a/cmd/pruning/keep_last_n.go b/cmd/pruning/keep_last_n.go new file mode 100644 index 0000000..5301141 --- /dev/null +++ b/cmd/pruning/keep_last_n.go @@ -0,0 +1,32 @@ +package pruning + +import ( + "github.com/pkg/errors" + "sort" +) + +type KeepLastN struct { + n int +} + +func NewKeepLastN(n int) (*KeepLastN, error) { + if n <= 0 { + return nil, errors.Errorf("must specify positive number as 'keep last count', got %d", n) + } + return &KeepLastN{n}, nil +} + +func (k KeepLastN) KeepRule(snaps []Snapshot) []Snapshot { + + if k.n > len(snaps) { + return snaps + } + + res := shallowCopySnapList(snaps) + + sort.Slice(res, func(i, j int) bool { + return res[i].Date().After(res[j].Date()) + }) + + return res[:k.n] +} diff --git a/cmd/pruning/keep_prefix.go b/cmd/pruning/keep_prefix.go new file mode 100644 index 0000000..f8a5956 --- /dev/null +++ b/cmd/pruning/keep_prefix.go @@ -0,0 +1,33 @@ +package pruning + +import ( + "regexp" +) + +type KeepRegex struct { + expr *regexp.Regexp +} + +var _ KeepRule = &KeepRegex{} + +func NewKeepRegex(expr string) (*KeepRegex, error) { + re, err := regexp.Compile(expr) + if err != nil { + return nil, err + } + return &KeepRegex{re}, nil +} + +func MustKeepRegex(expr string) *KeepRegex { + k, err := NewKeepRegex(expr) + if err != nil { + panic(err) + } + return k +} + +func (k *KeepRegex) KeepRule(snaps []Snapshot) []Snapshot { + return filterSnapList(snaps, func(s Snapshot) bool { + return k.expr.FindStringIndex(s.Name()) == nil + }) +} diff --git a/cmd/pruning/pruning.go b/cmd/pruning/pruning.go new file mode 100644 index 0000000..97b866b --- /dev/null +++ b/cmd/pruning/pruning.go @@ -0,0 +1,39 @@ +package pruning + +import ( + "time" +) + +type KeepRule interface { + KeepRule(snaps []Snapshot) []Snapshot +} + +type Snapshot interface { + Name() string + Replicated() bool + Date() time.Time +} + +func PruneSnapshots(snaps []Snapshot, keepRules []KeepRule) []Snapshot { + + if len(keepRules) == 0 { + return snaps + } + + remCount := make(map[Snapshot]int, len(snaps)) + for _, r := range keepRules { + ruleRems := r.KeepRule(snaps) + for _, ruleRem := range ruleRems { + remCount[ruleRem]++ + } + } + + remove := make([]Snapshot, 0, len(snaps)) + for snap, rc := range remCount { + if rc == len(keepRules) { + remove = append(remove, snap) + } + } + + return remove +} diff --git a/cmd/pruning/pruning_test.go b/cmd/pruning/pruning_test.go new file mode 100644 index 0000000..cd97311 --- /dev/null +++ b/cmd/pruning/pruning_test.go @@ -0,0 +1,88 @@ +package pruning + +import ( + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +type stubSnap struct { + name string + replicated bool + date time.Time +} + +func (s stubSnap) Name() string { return s.name } + +func (s stubSnap) Replicated() bool { return s.replicated } + +func (s stubSnap) Date() time.Time { return s.date } + +func TestPruneSnapshots(t *testing.T) { + + type testCase struct { + inputs []Snapshot + rules []KeepRule + exp, eff []Snapshot + } + + inputs := map[string][]Snapshot{ + "s1": []Snapshot{ + stubSnap{name: "foo_123"}, + stubSnap{name: "foo_456"}, + stubSnap{name: "bar_123"}, + }, + } + + tcs := map[string]testCase{ + "simple": { + inputs: inputs["s1"], + rules: []KeepRule{ + MustKeepRegex("foo_"), + }, + exp: []Snapshot{ + stubSnap{name: "bar_123"}, + }, + }, + "multipleRules": { + inputs: inputs["s1"], + rules: []KeepRule{ + MustKeepRegex("foo_"), + MustKeepRegex("bar_"), + }, + exp: []Snapshot{}, + }, + "onlyThoseRemovedByAllAreRemoved": { + inputs: inputs["s1"], + rules: []KeepRule{ + MustKeepRegex("notInS1"), // would remove all + MustKeepRegex("bar_"), // would remove all but bar_, i.e. foo_.* + }, + exp: []Snapshot{ + stubSnap{name: "foo_123"}, + stubSnap{name: "foo_456"}, + }, + }, + "noRulesKeepsAll": { + inputs: inputs["s1"], + rules: []KeepRule{}, + exp: inputs["s1"], + }, + "noSnaps": { + inputs: []Snapshot{}, + rules: []KeepRule{ + MustKeepRegex("foo_"), + }, + exp: []Snapshot{}, + }, + } + + for name := range tcs { + t.Run(name, func(t *testing.T) { + tc := tcs[name] + tc.eff = PruneSnapshots(tc.inputs, tc.rules) + assert.Equal(t, tc.exp, tc.eff) + }) + } + +}