/* Copyright The Velero Contributors. 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 ( "context" "errors" "testing" "time" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" corev1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/builder" "github.com/vmware-tanzu/velero/pkg/repository" repomokes "github.com/vmware-tanzu/velero/pkg/repository/mocks" repotypes "github.com/vmware-tanzu/velero/pkg/repository/types" velerotest "github.com/vmware-tanzu/velero/pkg/test" "github.com/vmware-tanzu/velero/pkg/util/kube" "github.com/vmware-tanzu/velero/pkg/util/logging" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" clientFake "sigs.k8s.io/controller-runtime/pkg/client/fake" batchv1 "k8s.io/api/batch/v1" ) const testMaintenanceFrequency = 10 * time.Minute func mockBackupRepoReconciler(t *testing.T, mockOn string, arg interface{}, ret ...interface{}) *BackupRepoReconciler { t.Helper() mgr := &repomokes.Manager{} if mockOn != "" { mgr.On(mockOn, arg).Return(ret...) } return NewBackupRepoReconciler( velerov1api.DefaultNamespace, velerotest.NewLogger(), velerotest.NewFakeControllerRuntimeClient(t), mgr, testMaintenanceFrequency, "fake-repo-config", 3, "", kube.PodResources{}, logrus.InfoLevel, nil, ) } func mockBackupRepositoryCR() *velerov1api.BackupRepository { return &velerov1api.BackupRepository{ ObjectMeta: metav1.ObjectMeta{ Namespace: velerov1api.DefaultNamespace, Name: "repo", }, Spec: velerov1api.BackupRepositorySpec{ MaintenanceFrequency: metav1.Duration{Duration: testMaintenanceFrequency}, }, } } func TestPatchBackupRepository(t *testing.T) { rr := mockBackupRepositoryCR() reconciler := mockBackupRepoReconciler(t, "", nil, nil) err := reconciler.Client.Create(context.TODO(), rr) assert.NoError(t, err) err = reconciler.patchBackupRepository(context.Background(), rr, repoReady()) assert.NoError(t, err) assert.Equal(t, velerov1api.BackupRepositoryPhaseReady, rr.Status.Phase) err = reconciler.patchBackupRepository(context.Background(), rr, repoNotReady("not ready")) assert.NoError(t, err) assert.NotEqual(t, velerov1api.BackupRepositoryPhaseReady, rr.Status.Phase) } func TestCheckNotReadyRepo(t *testing.T) { rr := mockBackupRepositoryCR() rr.Spec.BackupStorageLocation = "default" rr.Spec.ResticIdentifier = "fake-identifier" rr.Spec.VolumeNamespace = "volume-ns-1" reconciler := mockBackupRepoReconciler(t, "PrepareRepo", rr, nil) err := reconciler.Client.Create(context.TODO(), rr) assert.NoError(t, err) locations := &velerov1api.BackupStorageLocation{ Spec: velerov1api.BackupStorageLocationSpec{ Config: map[string]string{"resticRepoPrefix": "s3:test.amazonaws.com/bucket/restic"}, }, ObjectMeta: metav1.ObjectMeta{ Namespace: velerov1api.DefaultNamespace, Name: rr.Spec.BackupStorageLocation, }, } err = reconciler.Client.Create(context.TODO(), locations) assert.NoError(t, err) _, err = reconciler.checkNotReadyRepo(context.TODO(), rr, reconciler.logger) assert.NoError(t, err) assert.Equal(t, velerov1api.BackupRepositoryPhaseReady, rr.Status.Phase) assert.Equal(t, "s3:test.amazonaws.com/bucket/restic/volume-ns-1", rr.Spec.ResticIdentifier) } func startMaintenanceJobFail(client.Client, context.Context, *velerov1api.BackupRepository, string, kube.PodResources, logrus.Level, *logging.FormatFlag, logrus.FieldLogger) (string, error) { return "", errors.New("fake-start-error") } func startMaintenanceJobSucceed(client.Client, context.Context, *velerov1api.BackupRepository, string, kube.PodResources, logrus.Level, *logging.FormatFlag, logrus.FieldLogger) (string, error) { return "fake-job-name", nil } func waitMaintenanceJobCompleteFail(client.Client, context.Context, string, string, logrus.FieldLogger) (velerov1api.BackupRepositoryMaintenanceStatus, error) { return velerov1api.BackupRepositoryMaintenanceStatus{}, errors.New("fake-wait-error") } func waitMaintenanceJobCompleteSucceed(client.Client, context.Context, string, string, logrus.FieldLogger) (velerov1api.BackupRepositoryMaintenanceStatus, error) { return velerov1api.BackupRepositoryMaintenanceStatus{ StartTimestamp: &metav1.Time{Time: time.Now()}, CompleteTimestamp: &metav1.Time{Time: time.Now().Add(time.Hour)}, }, nil } func TestRunMaintenanceIfDue(t *testing.T) { rr := mockBackupRepositoryCR() reconciler := mockBackupRepoReconciler(t, "", rr, nil) funcStartMaintenanceJob = startMaintenanceJobFail err := reconciler.Client.Create(context.TODO(), rr) assert.NoError(t, err) lastTm := rr.Status.LastMaintenanceTime history := rr.Status.RecentMaintenance err = reconciler.runMaintenanceIfDue(context.TODO(), rr, reconciler.logger) assert.NoError(t, err) assert.Equal(t, rr.Status.LastMaintenanceTime, lastTm) assert.NotEqual(t, rr.Status.RecentMaintenance, history) rr = mockBackupRepositoryCR() reconciler = mockBackupRepoReconciler(t, "", rr, nil) funcStartMaintenanceJob = startMaintenanceJobSucceed funcWaitMaintenanceJobComplete = waitMaintenanceJobCompleteFail err = reconciler.Client.Create(context.TODO(), rr) assert.NoError(t, err) lastTm = rr.Status.LastMaintenanceTime history = rr.Status.RecentMaintenance err = reconciler.runMaintenanceIfDue(context.TODO(), rr, reconciler.logger) assert.EqualError(t, err, "error waiting repo maintenance completion status: fake-wait-error") assert.Equal(t, rr.Status.LastMaintenanceTime, lastTm) assert.Equal(t, rr.Status.RecentMaintenance, history) rr = mockBackupRepositoryCR() reconciler = mockBackupRepoReconciler(t, "", rr, nil) funcStartMaintenanceJob = startMaintenanceJobSucceed funcWaitMaintenanceJobComplete = waitMaintenanceJobCompleteSucceed err = reconciler.Client.Create(context.TODO(), rr) assert.NoError(t, err) lastTm = rr.Status.LastMaintenanceTime history = rr.Status.RecentMaintenance err = reconciler.runMaintenanceIfDue(context.TODO(), rr, reconciler.logger) assert.NoError(t, err) assert.NotEqual(t, rr.Status.LastMaintenanceTime, lastTm) assert.NotEqual(t, rr.Status.RecentMaintenance, history) rr.Status.LastMaintenanceTime = &metav1.Time{Time: time.Now()} lastTm = rr.Status.LastMaintenanceTime history = rr.Status.RecentMaintenance err = reconciler.runMaintenanceIfDue(context.TODO(), rr, reconciler.logger) assert.NoError(t, err) assert.Equal(t, rr.Status.LastMaintenanceTime, lastTm) assert.Equal(t, rr.Status.RecentMaintenance, history) } func TestInitializeRepo(t *testing.T) { rr := mockBackupRepositoryCR() rr.Spec.BackupStorageLocation = "default" reconciler := mockBackupRepoReconciler(t, "PrepareRepo", rr, nil) err := reconciler.Client.Create(context.TODO(), rr) assert.NoError(t, err) locations := &velerov1api.BackupStorageLocation{ Spec: velerov1api.BackupStorageLocationSpec{ Config: map[string]string{"resticRepoPrefix": "s3:test.amazonaws.com/bucket/restic"}, }, ObjectMeta: metav1.ObjectMeta{ Namespace: velerov1api.DefaultNamespace, Name: rr.Spec.BackupStorageLocation, }, } err = reconciler.Client.Create(context.TODO(), locations) assert.NoError(t, err) err = reconciler.initializeRepo(context.TODO(), rr, reconciler.logger) assert.NoError(t, err) assert.Equal(t, velerov1api.BackupRepositoryPhaseReady, rr.Status.Phase) } func TestBackupRepoReconcile(t *testing.T) { tests := []struct { name string repo *velerov1api.BackupRepository expectNil bool }{ { name: "test on api server not found", repo: &velerov1api.BackupRepository{ ObjectMeta: metav1.ObjectMeta{ Namespace: velerov1api.DefaultNamespace, Name: "unknown", }, Spec: velerov1api.BackupRepositorySpec{ MaintenanceFrequency: metav1.Duration{Duration: testMaintenanceFrequency}, }, }, expectNil: true, }, { name: "test on initialize repo", repo: &velerov1api.BackupRepository{ ObjectMeta: metav1.ObjectMeta{ Namespace: velerov1api.DefaultNamespace, Name: "repo", }, Spec: velerov1api.BackupRepositorySpec{ MaintenanceFrequency: metav1.Duration{Duration: testMaintenanceFrequency}, }, }, expectNil: true, }, { name: "test on repo with new phase", repo: &velerov1api.BackupRepository{ ObjectMeta: metav1.ObjectMeta{ Namespace: velerov1api.DefaultNamespace, Name: "repo", }, Spec: velerov1api.BackupRepositorySpec{ MaintenanceFrequency: metav1.Duration{Duration: testMaintenanceFrequency}, }, Status: velerov1api.BackupRepositoryStatus{ Phase: velerov1api.BackupRepositoryPhaseNew, }, }, expectNil: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { reconciler := mockBackupRepoReconciler(t, "", test.repo, nil) err := reconciler.Client.Create(context.TODO(), test.repo) assert.NoError(t, err) _, err = reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: types.NamespacedName{Namespace: test.repo.Namespace, Name: test.repo.Name}}) if test.expectNil { assert.NoError(t, err) } else { assert.Error(t, err) } }) } } func TestGetRepositoryMaintenanceFrequency(t *testing.T) { tests := []struct { name string mgr repotypes.SnapshotIdentifier repo *velerov1api.BackupRepository freqReturn time.Duration freqError error userDefinedFreq time.Duration expectFreq time.Duration }{ { name: "user defined valid", userDefinedFreq: time.Hour, expectFreq: time.Hour, }, { name: "repo return valid", freqReturn: time.Hour * 2, expectFreq: time.Hour * 2, }, { name: "fall to default", userDefinedFreq: -1, freqError: errors.New("fake-error"), expectFreq: defaultMaintainFrequency, }, { name: "fall to default, no freq error", freqReturn: -1, expectFreq: defaultMaintainFrequency, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { mgr := repomokes.Manager{} mgr.On("DefaultMaintenanceFrequency", mock.Anything).Return(test.freqReturn, test.freqError) reconciler := NewBackupRepoReconciler( velerov1api.DefaultNamespace, velerotest.NewLogger(), velerotest.NewFakeControllerRuntimeClient(t), &mgr, test.userDefinedFreq, "", 3, "", kube.PodResources{}, logrus.InfoLevel, nil, ) freq := reconciler.getRepositoryMaintenanceFrequency(test.repo) assert.Equal(t, test.expectFreq, freq) }) } } func TestNeedInvalidBackupRepo(t *testing.T) { tests := []struct { name string oldBSL *velerov1api.BackupStorageLocation newBSL *velerov1api.BackupStorageLocation expect bool }{ { name: "no change", oldBSL: &velerov1api.BackupStorageLocation{ Spec: velerov1api.BackupStorageLocationSpec{ Provider: "old-provider", }, }, newBSL: &velerov1api.BackupStorageLocation{ Spec: velerov1api.BackupStorageLocationSpec{ Provider: "new-provider", }, }, expect: false, }, { name: "other part change", oldBSL: &velerov1api.BackupStorageLocation{}, newBSL: &velerov1api.BackupStorageLocation{}, expect: false, }, { name: "bucket change", oldBSL: &velerov1api.BackupStorageLocation{ Spec: velerov1api.BackupStorageLocationSpec{ StorageType: velerov1api.StorageType{ ObjectStorage: &velerov1api.ObjectStorageLocation{ Bucket: "old-bucket", }, }, }, }, newBSL: &velerov1api.BackupStorageLocation{ Spec: velerov1api.BackupStorageLocationSpec{ StorageType: velerov1api.StorageType{ ObjectStorage: &velerov1api.ObjectStorageLocation{ Bucket: "new-bucket", }, }, }, }, expect: true, }, { name: "prefix change", oldBSL: &velerov1api.BackupStorageLocation{ Spec: velerov1api.BackupStorageLocationSpec{ StorageType: velerov1api.StorageType{ ObjectStorage: &velerov1api.ObjectStorageLocation{ Prefix: "old-prefix", }, }, }, }, newBSL: &velerov1api.BackupStorageLocation{ Spec: velerov1api.BackupStorageLocationSpec{ StorageType: velerov1api.StorageType{ ObjectStorage: &velerov1api.ObjectStorageLocation{ Prefix: "new-prefix", }, }, }, }, expect: true, }, { name: "CACert change", oldBSL: &velerov1api.BackupStorageLocation{ Spec: velerov1api.BackupStorageLocationSpec{ StorageType: velerov1api.StorageType{ ObjectStorage: &velerov1api.ObjectStorageLocation{ CACert: []byte{0x11, 0x12, 0x13}, }, }, }, }, newBSL: &velerov1api.BackupStorageLocation{ Spec: velerov1api.BackupStorageLocationSpec{ StorageType: velerov1api.StorageType{ ObjectStorage: &velerov1api.ObjectStorageLocation{ CACert: []byte{0x21, 0x22, 0x23}, }, }, }, }, expect: true, }, { name: "config change", oldBSL: &velerov1api.BackupStorageLocation{ Spec: velerov1api.BackupStorageLocationSpec{ Config: map[string]string{ "key1": "value1", }, }, }, newBSL: &velerov1api.BackupStorageLocation{ Spec: velerov1api.BackupStorageLocationSpec{ Config: map[string]string{ "key2": "value2", }, }, }, expect: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { reconciler := NewBackupRepoReconciler( velerov1api.DefaultNamespace, velerotest.NewLogger(), velerotest.NewFakeControllerRuntimeClient(t), nil, time.Duration(0), "", 3, "", kube.PodResources{}, logrus.InfoLevel, nil) need := reconciler.needInvalidBackupRepo(test.oldBSL, test.newBSL) assert.Equal(t, test.expect, need) }) } } func TestGetBackupRepositoryConfig(t *testing.T) { configWithNoData := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: "config-1", Namespace: velerov1api.DefaultNamespace, }, } configWithWrongData := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: "config-1", Namespace: velerov1api.DefaultNamespace, }, Data: map[string]string{ "fake-repo-type": "", }, } configWithData := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: "config-1", Namespace: velerov1api.DefaultNamespace, }, Data: map[string]string{ "fake-repo-type": "{\"cacheLimitMB\": 1000, \"enableCompression\": true}", "fake-repo-type-1": "{\"cacheLimitMB\": 1, \"enableCompression\": false}", }, } tests := []struct { name string congiName string repoName string repoType string kubeClientObj []runtime.Object expectedErr string expectedResult map[string]string }{ { name: "empty configName", }, { name: "get error", congiName: "config-1", expectedErr: "error getting configMap config-1: configmaps \"config-1\" not found", }, { name: "no config for repo", congiName: "config-1", repoName: "fake-repo", repoType: "fake-repo-type", kubeClientObj: []runtime.Object{ configWithNoData, }, }, { name: "unmarshall error", congiName: "config-1", repoName: "fake-repo", repoType: "fake-repo-type", kubeClientObj: []runtime.Object{ configWithWrongData, }, expectedErr: "error unmarshalling config data from config-1 for repo fake-repo, repo type fake-repo-type: unexpected end of JSON input", }, { name: "succeed", congiName: "config-1", repoName: "fake-repo", repoType: "fake-repo-type", kubeClientObj: []runtime.Object{ configWithData, }, expectedResult: map[string]string{ "cacheLimitMB": "1000", "enableCompression": "true", }, }, } scheme := runtime.NewScheme() corev1.AddToScheme(scheme) for _, test := range tests { t.Run(test.name, func(t *testing.T) { fakeClientBuilder := clientFake.NewClientBuilder() fakeClientBuilder = fakeClientBuilder.WithScheme(scheme) fakeClient := fakeClientBuilder.WithRuntimeObjects(test.kubeClientObj...).Build() result, err := getBackupRepositoryConfig(context.Background(), fakeClient, test.congiName, velerov1api.DefaultNamespace, test.repoName, test.repoType, velerotest.NewLogger()) if test.expectedErr != "" { assert.EqualError(t, err, test.expectedErr) } else { assert.NoError(t, err) assert.Equal(t, test.expectedResult, result) } }) } } func TestUpdateRepoMaintenanceHistory(t *testing.T) { standardTime := time.Now() backupRepoWithoutHistory := &velerov1api.BackupRepository{ ObjectMeta: metav1.ObjectMeta{ Namespace: velerov1api.DefaultNamespace, Name: "repo", }, } backupRepoWithHistory := &velerov1api.BackupRepository{ ObjectMeta: metav1.ObjectMeta{ Namespace: velerov1api.DefaultNamespace, Name: "repo", }, Status: velerov1api.BackupRepositoryStatus{ RecentMaintenance: []velerov1api.BackupRepositoryMaintenanceStatus{ { StartTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 24)}, CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 23)}, Message: "fake-history-message-1", }, }, }, } backupRepoWithFullHistory := &velerov1api.BackupRepository{ ObjectMeta: metav1.ObjectMeta{ Namespace: velerov1api.DefaultNamespace, Name: "repo", }, Status: velerov1api.BackupRepositoryStatus{ RecentMaintenance: []velerov1api.BackupRepositoryMaintenanceStatus{ { StartTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 24)}, CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 23)}, Message: "fake-history-message-2", }, { StartTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 22)}, CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 21)}, Message: "fake-history-message-3", }, { StartTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 20)}, CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 19)}, Message: "fake-history-message-4", }, }, }, } backupRepoWithOverFullHistory := &velerov1api.BackupRepository{ ObjectMeta: metav1.ObjectMeta{ Namespace: velerov1api.DefaultNamespace, Name: "repo", }, Status: velerov1api.BackupRepositoryStatus{ RecentMaintenance: []velerov1api.BackupRepositoryMaintenanceStatus{ { StartTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 24)}, CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 23)}, Message: "fake-history-message-5", }, { StartTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 22)}, CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 21)}, Message: "fake-history-message-6", }, { StartTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 20)}, CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 19)}, Message: "fake-history-message-7", }, { StartTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 18)}, CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 17)}, Message: "fake-history-message-8", }, }, }, } tests := []struct { name string backupRepo *velerov1api.BackupRepository result velerov1api.BackupRepositoryMaintenanceResult expectedHistory []velerov1api.BackupRepositoryMaintenanceStatus }{ { name: "empty history", backupRepo: backupRepoWithoutHistory, result: velerov1api.BackupRepositoryMaintenanceSucceeded, expectedHistory: []velerov1api.BackupRepositoryMaintenanceStatus{ { StartTimestamp: &metav1.Time{Time: standardTime}, CompleteTimestamp: &metav1.Time{Time: standardTime.Add(time.Hour)}, Message: "fake-message-0", }, }, }, { name: "less than history queue length", backupRepo: backupRepoWithHistory, result: velerov1api.BackupRepositoryMaintenanceSucceeded, expectedHistory: []velerov1api.BackupRepositoryMaintenanceStatus{ { StartTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 24)}, CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 23)}, Message: "fake-history-message-1", }, { StartTimestamp: &metav1.Time{Time: standardTime}, CompleteTimestamp: &metav1.Time{Time: standardTime.Add(time.Hour)}, Message: "fake-message-0", }, }, }, { name: "full history", backupRepo: backupRepoWithFullHistory, result: velerov1api.BackupRepositoryMaintenanceFailed, expectedHistory: []velerov1api.BackupRepositoryMaintenanceStatus{ { StartTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 22)}, CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 21)}, Message: "fake-history-message-3", }, { StartTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 20)}, CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 19)}, Message: "fake-history-message-4", }, { StartTimestamp: &metav1.Time{Time: standardTime}, CompleteTimestamp: &metav1.Time{Time: standardTime.Add(time.Hour)}, Message: "fake-message-0", }, }, }, { name: "over full history", backupRepo: backupRepoWithOverFullHistory, result: velerov1api.BackupRepositoryMaintenanceFailed, expectedHistory: []velerov1api.BackupRepositoryMaintenanceStatus{ { StartTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 20)}, CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 19)}, Message: "fake-history-message-7", }, { StartTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 18)}, CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 17)}, Message: "fake-history-message-8", }, { StartTimestamp: &metav1.Time{Time: standardTime}, CompleteTimestamp: &metav1.Time{Time: standardTime.Add(time.Hour)}, Message: "fake-message-0", }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { updateRepoMaintenanceHistory(test.backupRepo, test.result, &metav1.Time{Time: standardTime}, &metav1.Time{Time: standardTime.Add(time.Hour)}, "fake-message-0") for at := range test.backupRepo.Status.RecentMaintenance { assert.Equal(t, test.expectedHistory[at].StartTimestamp.Time, test.backupRepo.Status.RecentMaintenance[at].StartTimestamp.Time) assert.Equal(t, test.expectedHistory[at].CompleteTimestamp.Time, test.backupRepo.Status.RecentMaintenance[at].CompleteTimestamp.Time) assert.Equal(t, test.expectedHistory[at].Message, test.backupRepo.Status.RecentMaintenance[at].Message) } }) } } func TestRecallMaintenance(t *testing.T) { now := time.Now() schemeFail := runtime.NewScheme() velerov1api.AddToScheme(schemeFail) scheme := runtime.NewScheme() batchv1.AddToScheme(scheme) corev1.AddToScheme(scheme) velerov1api.AddToScheme(scheme) jobSucceeded := &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "job1", Namespace: velerov1api.DefaultNamespace, Labels: map[string]string{repository.RepositoryNameLabel: "repo"}, CreationTimestamp: metav1.Time{Time: now.Add(time.Hour)}, }, Status: batchv1.JobStatus{ StartTime: &metav1.Time{Time: now.Add(time.Hour)}, CompletionTime: &metav1.Time{Time: now.Add(time.Hour * 2)}, Succeeded: 1, }, } jobPodSucceeded := builder.ForPod(velerov1api.DefaultNamespace, "job1").Labels(map[string]string{"job-name": "job1"}).ContainerStatuses(&v1.ContainerStatus{ State: v1.ContainerState{ Terminated: &v1.ContainerStateTerminated{}, }, }).Result() tests := []struct { name string kubeClientObj []runtime.Object runtimeScheme *runtime.Scheme repoLastMatainTime metav1.Time expectNewHistory bool expectTimeUpdate bool expectedErr string }{ { name: "wait completion error", runtimeScheme: schemeFail, expectedErr: "error waiting incomplete repo maintenance job for repo repo: error listing maintenance job for repo repo: no kind is registered for the type v1.JobList in scheme \"pkg/runtime/scheme.go:100\"", }, { name: "no consolidate result", runtimeScheme: scheme, }, { name: "no update last time", runtimeScheme: scheme, kubeClientObj: []runtime.Object{ jobSucceeded, jobPodSucceeded, }, repoLastMatainTime: metav1.Time{Time: now.Add(time.Hour * 5)}, expectNewHistory: true, }, { name: "update last time", runtimeScheme: scheme, kubeClientObj: []runtime.Object{ jobSucceeded, jobPodSucceeded, }, expectNewHistory: true, expectTimeUpdate: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { r := mockBackupRepoReconciler(t, "", nil, nil) backupRepo := mockBackupRepositoryCR() backupRepo.Status.LastMaintenanceTime = &test.repoLastMatainTime test.kubeClientObj = append(test.kubeClientObj, backupRepo) fakeClientBuilder := fake.NewClientBuilder() fakeClientBuilder = fakeClientBuilder.WithScheme(test.runtimeScheme) fakeClient := fakeClientBuilder.WithRuntimeObjects(test.kubeClientObj...).Build() r.Client = fakeClient lastTm := backupRepo.Status.LastMaintenanceTime history := backupRepo.Status.RecentMaintenance err := r.recallMaintenance(context.TODO(), backupRepo, velerotest.NewLogger()) if test.expectedErr != "" { assert.EqualError(t, err, test.expectedErr) } else { assert.NoError(t, err) if test.expectNewHistory { assert.NotEqual(t, history, backupRepo.Status.RecentMaintenance) } if test.expectTimeUpdate { assert.NotEqual(t, lastTm, backupRepo.Status.LastMaintenanceTime) } } }) } } func TestConsolidateHistory(t *testing.T) { now := time.Now() tests := []struct { name string cur []velerov1api.BackupRepositoryMaintenanceStatus coming []velerov1api.BackupRepositoryMaintenanceStatus expected []velerov1api.BackupRepositoryMaintenanceStatus }{ { name: "zero coming", cur: []velerov1api.BackupRepositoryMaintenanceStatus{ { StartTimestamp: &metav1.Time{Time: now}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour)}, Result: velerov1api.BackupRepositoryMaintenanceSucceeded, Message: "fake-maintenance-message", }, }, expected: nil, }, { name: "identical coming", cur: []velerov1api.BackupRepositoryMaintenanceStatus{ { StartTimestamp: &metav1.Time{Time: now}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour)}, Result: velerov1api.BackupRepositoryMaintenanceSucceeded, Message: "fake-maintenance-message", }, }, coming: []velerov1api.BackupRepositoryMaintenanceStatus{ { StartTimestamp: &metav1.Time{Time: now}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour)}, }, }, expected: nil, }, { name: "less than limit", cur: []velerov1api.BackupRepositoryMaintenanceStatus{ { StartTimestamp: &metav1.Time{Time: now}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour)}, Result: velerov1api.BackupRepositoryMaintenanceSucceeded, Message: "fake-maintenance-message", }, { StartTimestamp: &metav1.Time{Time: now.Add(time.Hour)}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)}, Result: velerov1api.BackupRepositoryMaintenanceFailed, Message: "fake-maintenance-message-2", }, }, coming: []velerov1api.BackupRepositoryMaintenanceStatus{ { StartTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 3)}, Result: velerov1api.BackupRepositoryMaintenanceSucceeded, Message: "fake-maintenance-message-3", }, }, expected: []velerov1api.BackupRepositoryMaintenanceStatus{ { StartTimestamp: &metav1.Time{Time: now}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour)}, Result: velerov1api.BackupRepositoryMaintenanceSucceeded, Message: "fake-maintenance-message", }, { StartTimestamp: &metav1.Time{Time: now.Add(time.Hour)}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)}, Result: velerov1api.BackupRepositoryMaintenanceFailed, Message: "fake-maintenance-message-2", }, { StartTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 3)}, Result: velerov1api.BackupRepositoryMaintenanceSucceeded, Message: "fake-maintenance-message-3", }, }, }, { name: "more than limit", cur: []velerov1api.BackupRepositoryMaintenanceStatus{ { StartTimestamp: &metav1.Time{Time: now}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour)}, Result: velerov1api.BackupRepositoryMaintenanceSucceeded, Message: "fake-maintenance-message", }, { StartTimestamp: &metav1.Time{Time: now.Add(time.Hour)}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)}, Result: velerov1api.BackupRepositoryMaintenanceFailed, Message: "fake-maintenance-message-2", }, }, coming: []velerov1api.BackupRepositoryMaintenanceStatus{ { StartTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 3)}, Result: velerov1api.BackupRepositoryMaintenanceSucceeded, Message: "fake-maintenance-message-3", }, { StartTimestamp: &metav1.Time{Time: now.Add(time.Hour * 3)}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 4)}, Result: velerov1api.BackupRepositoryMaintenanceSucceeded, Message: "fake-maintenance-message-4", }, }, expected: []velerov1api.BackupRepositoryMaintenanceStatus{ { StartTimestamp: &metav1.Time{Time: now.Add(time.Hour)}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)}, Result: velerov1api.BackupRepositoryMaintenanceFailed, Message: "fake-maintenance-message-2", }, { StartTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 3)}, Result: velerov1api.BackupRepositoryMaintenanceSucceeded, Message: "fake-maintenance-message-3", }, { StartTimestamp: &metav1.Time{Time: now.Add(time.Hour * 3)}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 4)}, Result: velerov1api.BackupRepositoryMaintenanceSucceeded, Message: "fake-maintenance-message-4", }, }, }, { name: "more than limit 2", cur: []velerov1api.BackupRepositoryMaintenanceStatus{ { StartTimestamp: &metav1.Time{Time: now}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour)}, Result: velerov1api.BackupRepositoryMaintenanceSucceeded, Message: "fake-maintenance-message", }, { StartTimestamp: &metav1.Time{Time: now.Add(time.Hour)}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)}, Result: velerov1api.BackupRepositoryMaintenanceFailed, Message: "fake-maintenance-message-2", }, { StartTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 3)}, Result: velerov1api.BackupRepositoryMaintenanceSucceeded, Message: "fake-maintenance-message-3", }, }, coming: []velerov1api.BackupRepositoryMaintenanceStatus{ { StartTimestamp: &metav1.Time{Time: now.Add(time.Hour)}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)}, Result: velerov1api.BackupRepositoryMaintenanceFailed, Message: "fake-maintenance-message-2", }, { StartTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 3)}, Result: velerov1api.BackupRepositoryMaintenanceSucceeded, Message: "fake-maintenance-message-3", }, { StartTimestamp: &metav1.Time{Time: now.Add(time.Hour * 3)}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 4)}, Result: velerov1api.BackupRepositoryMaintenanceSucceeded, Message: "fake-maintenance-message-4", }, }, expected: []velerov1api.BackupRepositoryMaintenanceStatus{ { StartTimestamp: &metav1.Time{Time: now.Add(time.Hour)}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)}, Result: velerov1api.BackupRepositoryMaintenanceFailed, Message: "fake-maintenance-message-2", }, { StartTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 3)}, Result: velerov1api.BackupRepositoryMaintenanceSucceeded, Message: "fake-maintenance-message-3", }, { StartTimestamp: &metav1.Time{Time: now.Add(time.Hour * 3)}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 4)}, Result: velerov1api.BackupRepositoryMaintenanceSucceeded, Message: "fake-maintenance-message-4", }, }, }, { name: "coming is not used", cur: []velerov1api.BackupRepositoryMaintenanceStatus{ { StartTimestamp: &metav1.Time{Time: now.Add(time.Hour)}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)}, Result: velerov1api.BackupRepositoryMaintenanceSucceeded, Message: "fake-maintenance-message", }, { StartTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 3)}, Result: velerov1api.BackupRepositoryMaintenanceSucceeded, Message: "fake-maintenance-message-3", }, { StartTimestamp: &metav1.Time{Time: now.Add(time.Hour * 3)}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 4)}, Result: velerov1api.BackupRepositoryMaintenanceSucceeded, Message: "fake-maintenance-message-4", }, }, coming: []velerov1api.BackupRepositoryMaintenanceStatus{ { StartTimestamp: &metav1.Time{Time: now}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour)}, }, }, expected: nil, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { consolidated := consolidateHistory(test.coming, test.cur) if test.expected == nil { assert.Nil(t, consolidated) } else { assert.Len(t, consolidated, len(test.expected)) for i := 0; i < len(test.expected); i++ { assert.Equal(t, *test.expected[i].StartTimestamp, *consolidated[i].StartTimestamp) assert.Equal(t, *test.expected[i].CompleteTimestamp, *consolidated[i].CompleteTimestamp) assert.Equal(t, test.expected[i].Result, consolidated[i].Result) assert.Equal(t, test.expected[i].Message, consolidated[i].Message) } assert.Nil(t, consolidateHistory(test.coming, consolidated)) } }) } } func TestGetLastMaintenanceTimeFromHistory(t *testing.T) { now := time.Now() tests := []struct { name string history []velerov1api.BackupRepositoryMaintenanceStatus expected time.Time }{ { name: "first one is nil", history: []velerov1api.BackupRepositoryMaintenanceStatus{ { StartTimestamp: &metav1.Time{Time: now}, Result: velerov1api.BackupRepositoryMaintenanceSucceeded, Message: "fake-maintenance-message", }, { StartTimestamp: &metav1.Time{Time: now.Add(time.Hour)}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)}, Result: velerov1api.BackupRepositoryMaintenanceFailed, Message: "fake-maintenance-message-2", }, { StartTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 3)}, Result: velerov1api.BackupRepositoryMaintenanceSucceeded, Message: "fake-maintenance-message-3", }, }, expected: now.Add(time.Hour * 3), }, { name: "another one is nil", history: []velerov1api.BackupRepositoryMaintenanceStatus{ { StartTimestamp: &metav1.Time{Time: now}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour)}, Result: velerov1api.BackupRepositoryMaintenanceSucceeded, Message: "fake-maintenance-message", }, { StartTimestamp: &metav1.Time{Time: now.Add(time.Hour)}, Result: velerov1api.BackupRepositoryMaintenanceFailed, Message: "fake-maintenance-message-2", }, { StartTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 3)}, Result: velerov1api.BackupRepositoryMaintenanceSucceeded, Message: "fake-maintenance-message-3", }, }, expected: now.Add(time.Hour * 3), }, { name: "disordered", history: []velerov1api.BackupRepositoryMaintenanceStatus{ { StartTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 3)}, Result: velerov1api.BackupRepositoryMaintenanceSucceeded, Message: "fake-maintenance-message-3", }, { StartTimestamp: &metav1.Time{Time: now}, CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour)}, Result: velerov1api.BackupRepositoryMaintenanceSucceeded, Message: "fake-maintenance-message", }, { StartTimestamp: &metav1.Time{Time: now.Add(time.Hour)}, Result: velerov1api.BackupRepositoryMaintenanceFailed, Message: "fake-maintenance-message-2", }, }, expected: now.Add(time.Hour * 3), }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { time := getLastMaintenanceTimeFromHistory(test.history) assert.Equal(t, test.expected, time.Time) }) } }