Skip to content

Commit 57eadd0

Browse files
suaas21tamalsaha
authored andcommitted
Implement RetentionPolicy for VolumeSnapshot (#926)
1 parent db709fd commit 57eadd0

File tree

13 files changed

+968
-1
lines changed

13 files changed

+968
-1
lines changed

pkg/cmds/create_volumesnapshot.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"stash.appscode.dev/stash/pkg/restic"
2323
"stash.appscode.dev/stash/pkg/status"
2424
"stash.appscode.dev/stash/pkg/util"
25+
"stash.appscode.dev/stash/pkg/volumesnapshot"
2526
)
2627

2728
type VSoption struct {
@@ -139,6 +140,12 @@ func (opt *VSoption) createVolumeSnapshot() (*restic.BackupOutput, error) {
139140
}
140141

141142
}
143+
144+
err = volumesnapshot.CleanupSnapshots(backupConfig.Spec.RetentionPolicy, backupOutput.HostBackupStats, backupSession.Namespace, opt.snapshotClient)
145+
if err != nil {
146+
return nil, err
147+
}
148+
142149
return backupOutput, nil
143150
}
144151

pkg/rbac/volume_snapshot.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ func ensureVolumeSnapshotterJobClusterRole(kubeClient kubernetes.Interface, labe
8080
{
8181
APIGroups: []string{crdv1.GroupName},
8282
Resources: []string{"volumesnapshots", "volumesnapshotcontents", "volumesnapshotclasses"},
83-
Verbs: []string{"create", "get", "list", "watch", "patch"},
83+
Verbs: []string{"create", "get", "list", "watch", "patch", "delete"},
8484
},
8585
}
8686
return in
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package volumesnapshot
2+
3+
import (
4+
"sort"
5+
"time"
6+
7+
vs_api "github.com/kubernetes-csi/external-snapshotter/pkg/apis/volumesnapshot/v1alpha1"
8+
vs_cs "github.com/kubernetes-csi/external-snapshotter/pkg/client/clientset/versioned"
9+
kerr "k8s.io/apimachinery/pkg/api/errors"
10+
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11+
"stash.appscode.dev/stash/apis/stash/v1alpha1"
12+
"stash.appscode.dev/stash/apis/stash/v1beta1"
13+
)
14+
15+
// Some of the code of this file has been copied from restic/restic repository.
16+
// ref: https://github.com/restic/restic/blob/604b18aa7426148a55f76307ca729e829ff6b61d/internal/restic/snapshot_policy.go#L152:6
17+
18+
// isPolicyEmpty returns true if no policy has been configured (all values zero).
19+
func isPolicyEmpty(policy v1alpha1.RetentionPolicy) bool {
20+
if policy.KeepLast > 0 ||
21+
policy.KeepHourly > 0 ||
22+
policy.KeepDaily > 0 ||
23+
policy.KeepMonthly > 0 ||
24+
policy.KeepWeekly > 0 ||
25+
policy.KeepYearly > 0 {
26+
return true
27+
}
28+
return false
29+
}
30+
31+
// ymdh returns an integer in the form YYYYMMDDHH.
32+
func ymdh(d time.Time, _ int) int {
33+
return d.Year()*1000000 + int(d.Month())*10000 + d.Day()*100 + d.Hour()
34+
}
35+
36+
// ymd returns an integer in the form YYYYMMDD.
37+
func ymd(d time.Time, _ int) int {
38+
return d.Year()*10000 + int(d.Month())*100 + d.Day()
39+
}
40+
41+
// yw returns an integer in the form YYYYWW, where WW is the week number.
42+
func yw(d time.Time, _ int) int {
43+
year, week := d.ISOWeek()
44+
return year*100 + week
45+
}
46+
47+
// ym returns an integer in the form YYYYMM.
48+
func ym(d time.Time, _ int) int {
49+
return d.Year()*100 + int(d.Month())
50+
}
51+
52+
// y returns the year of d.
53+
func y(d time.Time, _ int) int {
54+
return d.Year()
55+
}
56+
57+
// always returns a unique number for d.
58+
func always(d time.Time, nr int) int {
59+
return nr
60+
}
61+
62+
type VolumeSnapshot struct {
63+
VolumeSnap vs_api.VolumeSnapshot
64+
}
65+
66+
type VolumeSnapshots []VolumeSnapshot
67+
68+
func (vs VolumeSnapshots) Len() int {
69+
return len(vs)
70+
}
71+
func (vs VolumeSnapshots) Less(i, j int) bool {
72+
return vs[i].VolumeSnap.CreationTimestamp.Time.After(vs[j].VolumeSnap.CreationTimestamp.Time)
73+
}
74+
func (vs VolumeSnapshots) Swap(i, j int) {
75+
vs[i], vs[j] = vs[j], vs[i]
76+
}
77+
78+
// ApplyRetentionPolicy do the following steps:
79+
// 1. sorts all the VolumeSnapshot according to CreationTimeStamp.
80+
// 2. then list that are to be kept and removed according to the policy.
81+
// 3. remove VolumeSnapshot that are not necessary according to RetentionPolicy
82+
func applyRetentionPolicy(policy v1alpha1.RetentionPolicy, volumeSnapshots VolumeSnapshots, namespace string, vsClient vs_cs.Interface) error {
83+
84+
// sorts the VolumeSnapshots according to CreationTimeStamp
85+
sort.Sort(VolumeSnapshots(volumeSnapshots))
86+
87+
if !isPolicyEmpty(policy) {
88+
return nil
89+
}
90+
91+
var buckets = [6]struct {
92+
Count int
93+
LastAdded func(d time.Time, nr int) int
94+
Last int
95+
}{
96+
{policy.KeepLast, always, -1},
97+
{policy.KeepHourly, ymdh, -1},
98+
{policy.KeepDaily, ymd, -1},
99+
{policy.KeepWeekly, yw, -1},
100+
{policy.KeepMonthly, ym, -1},
101+
{policy.KeepYearly, y, -1},
102+
}
103+
104+
var kept, removed VolumeSnapshots
105+
for nr, vs := range volumeSnapshots {
106+
var keepSnap bool
107+
// keep VolumeSnapshot that are matched with the policy
108+
for i, b := range buckets {
109+
if b.Count > 0 {
110+
val := b.LastAdded(vs.VolumeSnap.CreationTimestamp.Time, nr)
111+
if val != b.Last {
112+
keepSnap = true
113+
buckets[i].Last = val
114+
buckets[i].Count--
115+
}
116+
}
117+
}
118+
119+
if keepSnap {
120+
kept = append(kept, vs)
121+
} else {
122+
removed = append(removed, vs)
123+
}
124+
}
125+
126+
for _, vs := range removed {
127+
err := vsClient.SnapshotV1alpha1().VolumeSnapshots(namespace).Delete(vs.VolumeSnap.Name, &v1.DeleteOptions{})
128+
if err != nil {
129+
if kerr.IsNotFound(err) {
130+
return nil
131+
}
132+
return err
133+
}
134+
}
135+
136+
return nil
137+
}
138+
139+
func CleanupSnapshots(policy v1alpha1.RetentionPolicy, hostBackupStats []v1beta1.HostBackupStats, namespace string, vsClient vs_cs.Interface) error {
140+
vsList, err := vsClient.SnapshotV1alpha1().VolumeSnapshots(namespace).List(v1.ListOptions{})
141+
if err != nil {
142+
if kerr.IsNotFound(err) || len(vsList.Items) == 0 {
143+
return nil
144+
}
145+
return err
146+
}
147+
// filter VolumeSnapshots according to PVC source
148+
// then apply RetentionPolicy rule
149+
for _, host := range hostBackupStats {
150+
var volumeSnapshots VolumeSnapshots
151+
for _, vs := range vsList.Items {
152+
if host.Hostname == vs.Spec.Source.Name {
153+
volumeSnapshots = append(volumeSnapshots, VolumeSnapshot{VolumeSnap: vs})
154+
}
155+
}
156+
err := applyRetentionPolicy(policy, volumeSnapshots, namespace, vsClient)
157+
if err != nil {
158+
return err
159+
}
160+
}
161+
162+
return nil
163+
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package volumesnapshot
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
"time"
7+
8+
"github.com/appscode/go/strings"
9+
type_util "github.com/appscode/go/types"
10+
crdv1 "github.com/kubernetes-csi/external-snapshotter/pkg/apis/volumesnapshot/v1alpha1"
11+
vsfake "github.com/kubernetes-csi/external-snapshotter/pkg/client/clientset/versioned/fake"
12+
v1 "k8s.io/api/core/v1"
13+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
"k8s.io/apimachinery/pkg/runtime"
15+
"k8s.io/apimachinery/pkg/types"
16+
"stash.appscode.dev/stash/apis"
17+
"stash.appscode.dev/stash/apis/stash/v1alpha1"
18+
"stash.appscode.dev/stash/apis/stash/v1beta1"
19+
)
20+
21+
type snapInfo struct {
22+
name string
23+
creationTime string
24+
pvcName string
25+
}
26+
27+
type testInfo struct {
28+
description string
29+
policy v1alpha1.RetentionPolicy
30+
hostBackupStats []v1beta1.HostBackupStats
31+
expectedSnapshots []string
32+
}
33+
34+
const testNamespace = "vs-retention-policy-test"
35+
36+
func TestCleanupSnapshots(t *testing.T) {
37+
38+
snapMeta := []snapInfo{
39+
{name: "snap-1", creationTime: "2019-12-10T05:36:07Z", pvcName: "pvc-1"},
40+
{name: "snap-2", creationTime: "2019-11-10T05:36:07Z", pvcName: "pvc-1"},
41+
{name: "snap-3", creationTime: "2019-10-10T05:36:07Z", pvcName: "pvc-1"},
42+
{name: "snap-4", creationTime: "2019-10-10T05:30:07Z", pvcName: "pvc-1"},
43+
{name: "snap-5", creationTime: "2019-10-10T05:15:07Z", pvcName: "pvc-1"},
44+
{name: "snap-6", creationTime: "2018-10-11T05:36:07Z", pvcName: "pvc-1"},
45+
{name: "snap-7", creationTime: "2018-10-10T05:36:07Z", pvcName: "pvc-1"},
46+
{name: "snap-8", creationTime: "2017-12-10T05:36:07Z", pvcName: "pvc-1"},
47+
{name: "snap-9", creationTime: "2017-10-10T05:36:07Z", pvcName: "pvc-1"},
48+
{name: "snap-10", creationTime: "2019-12-10T05:36:07Z", pvcName: "pvc-2"},
49+
{name: "snap-11", creationTime: "2019-10-10T05:36:07Z", pvcName: "pvc-2"},
50+
{name: "snap-12", creationTime: "2019-10-10T05:36:07Z", pvcName: "pvc-2"},
51+
{name: "snap-13", creationTime: "2019-10-09T05:30:07Z", pvcName: "pvc-2"},
52+
}
53+
54+
testCases := []testInfo{
55+
{
56+
description: "No Policy",
57+
policy: v1alpha1.RetentionPolicy{},
58+
hostBackupStats: []v1beta1.HostBackupStats{{Hostname: "pvc-1"}, {Hostname: "pvc-2"}},
59+
expectedSnapshots: []string{"snap-1", "snap-2", "snap-3", "snap-4", "snap-5", "snap-6", "snap-7", "snap-8", "snap-9", "snap-10", "snap-11", "snap-12", "snap-13"}, // should keep all snapshots
60+
},
61+
{
62+
description: "KeepLast",
63+
policy: v1alpha1.RetentionPolicy{KeepLast: 3},
64+
hostBackupStats: []v1beta1.HostBackupStats{{Hostname: "pvc-1"}, {Hostname: "pvc-2"}},
65+
expectedSnapshots: []string{"snap-1", "snap-2", "snap-3", "snap-10", "snap-11", "snap-12"}, // should keep last 3 snapshots of claim 1 and last 3 snapshots of claim 2
66+
},
67+
{
68+
description: "KeepHourly",
69+
policy: v1alpha1.RetentionPolicy{KeepHourly: 3},
70+
hostBackupStats: []v1beta1.HostBackupStats{{Hostname: "pvc-1"}, {Hostname: "pvc-2"}},
71+
expectedSnapshots: []string{"snap-1", "snap-2", "snap-3", "snap-10", "snap-11", "snap-13"},
72+
},
73+
{
74+
description: "KeepDaily",
75+
policy: v1alpha1.RetentionPolicy{KeepDaily: 3},
76+
hostBackupStats: []v1beta1.HostBackupStats{{Hostname: "pvc-1"}, {Hostname: "pvc-2"}},
77+
expectedSnapshots: []string{"snap-1", "snap-2", "snap-3", "snap-10", "snap-11", "snap-13"},
78+
},
79+
{
80+
description: "KeepMonthly",
81+
policy: v1alpha1.RetentionPolicy{KeepMonthly: 3},
82+
hostBackupStats: []v1beta1.HostBackupStats{{Hostname: "pvc-1"}, {Hostname: "pvc-2"}},
83+
expectedSnapshots: []string{"snap-1", "snap-2", "snap-3", "snap-10", "snap-11"},
84+
},
85+
{
86+
description: "KeepYearly",
87+
policy: v1alpha1.RetentionPolicy{KeepYearly: 3},
88+
hostBackupStats: []v1beta1.HostBackupStats{{Hostname: "pvc-1"}, {Hostname: "pvc-2"}},
89+
expectedSnapshots: []string{"snap-1", "snap-6", "snap-8", "snap-10"},
90+
},
91+
{
92+
description: "KeepLast & KeepDaily",
93+
policy: v1alpha1.RetentionPolicy{KeepLast: 3, KeepDaily: 3},
94+
hostBackupStats: []v1beta1.HostBackupStats{{Hostname: "pvc-1"}, {Hostname: "pvc-2"}},
95+
expectedSnapshots: []string{"snap-1", "snap-2", "snap-3", "snap-10", "snap-11", "snap-12", "snap-13"},
96+
},
97+
{
98+
description: "KeepWeekly & KeepMonthly",
99+
policy: v1alpha1.RetentionPolicy{KeepWeekly: 2, KeepMonthly: 2},
100+
hostBackupStats: []v1beta1.HostBackupStats{{Hostname: "pvc-1"}, {Hostname: "pvc-2"}},
101+
expectedSnapshots: []string{"snap-1", "snap-2", "snap-10", "snap-11"},
102+
},
103+
{
104+
description: "KeepWeekly & KeepMonthly & KeepYearly",
105+
policy: v1alpha1.RetentionPolicy{KeepWeekly: 2, KeepMonthly: 3, KeepYearly: 4},
106+
hostBackupStats: []v1beta1.HostBackupStats{{Hostname: "pvc-1"}, {Hostname: "pvc-2"}},
107+
expectedSnapshots: []string{"snap-1", "snap-2", "snap-3", "snap-6", "snap-8", "snap-10", "snap-11"},
108+
},
109+
}
110+
111+
for _, test := range testCases {
112+
t.Run(test.description, func(t *testing.T) {
113+
volumeSnasphots, err := getVolumeSnapshots(snapMeta)
114+
if err != nil {
115+
t.Errorf("Failed to generate VolumeSnasphots. Reason: %v", err)
116+
return
117+
}
118+
vsClient := vsfake.NewSimpleClientset(volumeSnasphots...)
119+
err = CleanupSnapshots(test.policy, test.hostBackupStats, testNamespace, vsClient)
120+
if err != nil {
121+
t.Errorf("Failed to cleanup VolumeSnapshots. Reason: %v", err)
122+
return
123+
}
124+
vsList, err := vsClient.SnapshotV1alpha1().VolumeSnapshots(testNamespace).List(metav1.ListOptions{})
125+
if err != nil {
126+
t.Errorf("Failed to list remaining VolumeSnapshots. Reason: %v", err)
127+
return
128+
}
129+
if len(test.expectedSnapshots) != len(vsList.Items) {
130+
var remainingSnapshots []string
131+
for i := range vsList.Items {
132+
remainingSnapshots = append(remainingSnapshots, vsList.Items[i].Name)
133+
}
134+
135+
t.Errorf("Remaining VolumeSnapshot number did not match with expected number."+
136+
"\nExpected: %d"+
137+
"\nFound: %d"+
138+
"\nExpected Snapshots: %q"+
139+
"\nRemaining Snapshots: %q", len(test.expectedSnapshots), len(vsList.Items), test.expectedSnapshots, remainingSnapshots)
140+
return
141+
}
142+
143+
for _, vs := range vsList.Items {
144+
if !strings.Contains(test.expectedSnapshots, vs.Name) {
145+
t.Errorf("VolumeSnapshot %s should be deleted according to retention-policy: %s.", vs.Name, test.description)
146+
}
147+
}
148+
})
149+
}
150+
151+
}
152+
153+
func getVolumeSnapshots(snapMetas []snapInfo) ([]runtime.Object, error) {
154+
snapshots := make([]runtime.Object, 0)
155+
for i := range snapMetas {
156+
snapshot, err := newSnapshot(snapMetas[i])
157+
if err != nil {
158+
return nil, err
159+
}
160+
161+
snapshots = append(snapshots, snapshot)
162+
}
163+
return snapshots, nil
164+
}
165+
166+
func newSnapshot(snapMeta snapInfo) (*crdv1.VolumeSnapshot, error) {
167+
creationTimestamp, err := time.Parse(time.RFC3339, snapMeta.creationTime)
168+
if err != nil {
169+
return nil, err
170+
}
171+
172+
return &crdv1.VolumeSnapshot{
173+
ObjectMeta: metav1.ObjectMeta{
174+
Name: snapMeta.name,
175+
Namespace: testNamespace,
176+
UID: types.UID(snapMeta.name),
177+
ResourceVersion: "1",
178+
SelfLink: "/apis/snapshot.storage.k8s.io/v1alpha1/namespaces/" + testNamespace + "/volumesnapshots/" + snapMeta.name,
179+
CreationTimestamp: metav1.Time{Time: creationTimestamp},
180+
},
181+
Spec: crdv1.VolumeSnapshotSpec{
182+
VolumeSnapshotClassName: type_util.StringP("standard"),
183+
SnapshotContentName: fmt.Sprintf("snapshot-content-%s", snapMeta.name),
184+
Source: &v1.TypedLocalObjectReference{
185+
Name: snapMeta.pvcName,
186+
Kind: apis.KindPersistentVolumeClaim,
187+
},
188+
},
189+
Status: crdv1.VolumeSnapshotStatus{
190+
ReadyToUse: true,
191+
Error: nil,
192+
},
193+
}, nil
194+
}

0 commit comments

Comments
 (0)