/* Copyright 2017 Heptio Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package controller import ( "bytes" "errors" "io" "io/ioutil" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "k8s.io/apimachinery/pkg/util/clock" "k8s.io/apimachinery/pkg/util/sets" api "github.com/heptio/ark/pkg/apis/ark/v1" "github.com/heptio/ark/pkg/generated/clientset/fake" informers "github.com/heptio/ark/pkg/generated/informers/externalversions" . "github.com/heptio/ark/pkg/util/test" ) type gcTest struct { name string bucket string backups map[string][]*api.Backup snapshots sets.String expectedBackupsRemaining map[string]sets.String expectedSnapshotsRemaining sets.String } func TestGarbageCollect(t *testing.T) { fakeClock := clock.NewFakeClock(time.Now()) tests := []gcTest{ gcTest{ name: "basic-expired", bucket: "bucket-1", backups: map[string][]*api.Backup{ "bucket-1": []*api.Backup{ NewTestBackup().WithName("backup-1"). WithExpiration(fakeClock.Now().Add(-1*time.Second)). WithSnapshot("pv-1", "snapshot-1"). WithSnapshot("pv-2", "snapshot-2"). Backup, }, }, snapshots: sets.NewString("snapshot-1", "snapshot-2"), expectedBackupsRemaining: make(map[string]sets.String), expectedSnapshotsRemaining: sets.NewString(), }, gcTest{ name: "basic-unexpired", bucket: "bucket-1", backups: map[string][]*api.Backup{ "bucket-1": []*api.Backup{ NewTestBackup().WithName("backup-1"). WithExpiration(fakeClock.Now().Add(1*time.Minute)). WithSnapshot("pv-1", "snapshot-1"). WithSnapshot("pv-2", "snapshot-2"). Backup, }, }, snapshots: sets.NewString("snapshot-1", "snapshot-2"), expectedBackupsRemaining: map[string]sets.String{ "bucket-1": sets.NewString("backup-1"), }, expectedSnapshotsRemaining: sets.NewString("snapshot-1", "snapshot-2"), }, gcTest{ name: "one expired, one unexpired", bucket: "bucket-1", backups: map[string][]*api.Backup{ "bucket-1": []*api.Backup{ NewTestBackup().WithName("backup-1"). WithExpiration(fakeClock.Now().Add(-1*time.Minute)). WithSnapshot("pv-1", "snapshot-1"). WithSnapshot("pv-2", "snapshot-2"). Backup, NewTestBackup().WithName("backup-2"). WithExpiration(fakeClock.Now().Add(1*time.Minute)). WithSnapshot("pv-3", "snapshot-3"). WithSnapshot("pv-4", "snapshot-4"). Backup, }, }, snapshots: sets.NewString("snapshot-1", "snapshot-2", "snapshot-3", "snapshot-4"), expectedBackupsRemaining: map[string]sets.String{ "bucket-1": sets.NewString("backup-2"), }, expectedSnapshotsRemaining: sets.NewString("snapshot-3", "snapshot-4"), }, gcTest{ name: "none expired in target bucket", bucket: "bucket-2", backups: map[string][]*api.Backup{ "bucket-1": []*api.Backup{ NewTestBackup().WithName("backup-1"). WithExpiration(fakeClock.Now().Add(-1*time.Minute)). WithSnapshot("pv-1", "snapshot-1"). WithSnapshot("pv-2", "snapshot-2"). Backup, }, "bucket-2": []*api.Backup{ NewTestBackup().WithName("backup-2"). WithExpiration(fakeClock.Now().Add(1*time.Minute)). WithSnapshot("pv-3", "snapshot-3"). WithSnapshot("pv-4", "snapshot-4"). Backup, }, }, snapshots: sets.NewString("snapshot-1", "snapshot-2", "snapshot-3", "snapshot-4"), expectedBackupsRemaining: map[string]sets.String{ "bucket-1": sets.NewString("backup-1"), "bucket-2": sets.NewString("backup-2"), }, expectedSnapshotsRemaining: sets.NewString("snapshot-1", "snapshot-2", "snapshot-3", "snapshot-4"), }, gcTest{ name: "orphan snapshots", bucket: "bucket-1", backups: map[string][]*api.Backup{ "bucket-1": []*api.Backup{ NewTestBackup().WithName("backup-1"). WithExpiration(fakeClock.Now().Add(-1*time.Minute)). WithSnapshot("pv-1", "snapshot-1"). WithSnapshot("pv-2", "snapshot-2"). Backup, }, }, snapshots: sets.NewString("snapshot-1", "snapshot-2", "snapshot-3", "snapshot-4"), expectedBackupsRemaining: make(map[string]sets.String), expectedSnapshotsRemaining: sets.NewString("snapshot-3", "snapshot-4"), }, } for _, test := range tests { backupService := &fakeBackupService{} snapshotService := &FakeSnapshotService{} t.Run(test.name, func(t *testing.T) { backupService.backupsByBucket = make(map[string][]*api.Backup) for bucket, backups := range test.backups { data := make([]*api.Backup, 0, len(backups)) for _, backup := range backups { data = append(data, backup) } backupService.backupsByBucket[bucket] = data } snapshotService.SnapshotsTaken = test.snapshots var ( client = fake.NewSimpleClientset() sharedInformers = informers.NewSharedInformerFactory(client, 0) ) controller := NewGCController( backupService, snapshotService, test.bucket, 1*time.Millisecond, sharedInformers.Ark().V1().Backups(), client.ArkV1(), ).(*gcController) controller.clock = fakeClock controller.cleanBackups() // verify every bucket has the backups we expect for bucket, backups := range backupService.backupsByBucket { // if actual and expected are both empty, no further verification needed if len(backups) == 0 && len(test.expectedBackupsRemaining[bucket]) == 0 { continue } // get all the actual backups remaining in this bucket backupNames := sets.NewString() for _, backup := range backupService.backupsByBucket[bucket] { backupNames.Insert(backup.Name) } assert.Equal(t, test.expectedBackupsRemaining[bucket], backupNames) } assert.Equal(t, test.expectedSnapshotsRemaining, snapshotService.SnapshotsTaken) }) } } func TestGarbageCollectPicksUpBackupUponExpiration(t *testing.T) { var ( backupService = &fakeBackupService{} snapshotService = &FakeSnapshotService{} fakeClock = clock.NewFakeClock(time.Now()) assert = assert.New(t) ) scenario := gcTest{ name: "basic-expired", bucket: "bucket-1", backups: map[string][]*api.Backup{ "bucket-1": []*api.Backup{ NewTestBackup().WithName("backup-1"). WithExpiration(fakeClock.Now().Add(1*time.Second)). WithSnapshot("pv-1", "snapshot-1"). WithSnapshot("pv-2", "snapshot-2"). Backup, }, }, snapshots: sets.NewString("snapshot-1", "snapshot-2"), } backupService.backupsByBucket = make(map[string][]*api.Backup) for bucket, backups := range scenario.backups { data := make([]*api.Backup, 0, len(backups)) for _, backup := range backups { data = append(data, backup) } backupService.backupsByBucket[bucket] = data } snapshotService.SnapshotsTaken = scenario.snapshots var ( client = fake.NewSimpleClientset() sharedInformers = informers.NewSharedInformerFactory(client, 0) ) controller := NewGCController( backupService, snapshotService, scenario.bucket, 1*time.Millisecond, sharedInformers.Ark().V1().Backups(), client.ArkV1(), ).(*gcController) controller.clock = fakeClock // PASS 1 controller.cleanBackups() assert.Equal(scenario.backups, backupService.backupsByBucket, "backups should not be garbage-collected yet.") assert.Equal(scenario.snapshots, snapshotService.SnapshotsTaken, "snapshots should not be garbage-collected yet.") // PASS 2 fakeClock.Step(1 * time.Minute) controller.cleanBackups() assert.Equal(0, len(backupService.backupsByBucket[scenario.bucket]), "backups should have been garbage-collected.") assert.Equal(0, len(snapshotService.SnapshotsTaken), "snapshots should have been garbage-collected.") } type fakeBackupService struct { backupsByBucket map[string][]*api.Backup mock.Mock } func (s *fakeBackupService) GetAllBackups(bucket string) ([]*api.Backup, error) { backups, found := s.backupsByBucket[bucket] if !found { return nil, errors.New("bucket not found") } return backups, nil } func (bs *fakeBackupService) UploadBackup(bucket, name string, metadata, backup io.ReadSeeker) error { args := bs.Called(bucket, name, metadata, backup) return args.Error(0) } func (s *fakeBackupService) DownloadBackup(bucket, name string) (io.ReadCloser, error) { return ioutil.NopCloser(bytes.NewReader([]byte("hello world"))), nil } func (s *fakeBackupService) DeleteBackup(bucket, backupName string) error { backups, err := s.GetAllBackups(bucket) if err != nil { return err } deleteIdx := -1 for i, backup := range backups { if backup.Name == backupName { deleteIdx = i break } } if deleteIdx == -1 { return errors.New("backup not found") } s.backupsByBucket[bucket] = append(s.backupsByBucket[bucket][0:deleteIdx], s.backupsByBucket[bucket][deleteIdx+1:]...) return nil }