Files
velero/pkg/controller/backup_repository_controller_test.go
Tiger Kaovilai 1daa685e7d Make ResticIdentifier optional for kopia repositories (#8987)
The ResticIdentifier field in BackupRepository is only relevant for restic
repositories. For kopia repositories, this field is unused and should be
omitted. This change:

- Adds omitempty tag to ResticIdentifier field in BackupRepository CRD
- Updates controller to only populate ResticIdentifier for restic repos
- Adds tests to verify behavior for both restic and kopia repository types

This ensures backward compatibility while properly handling kopia repositories
that don't require a restic-compatible identifier.

Signed-off-by: Tiger Kaovilai <tkaovila@redhat.com>
2025-07-24 22:25:09 -04:00

1725 lines
57 KiB
Go

/*
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"
"github.com/stretchr/testify/require"
corev1api "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/clock"
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"
"github.com/vmware-tanzu/velero/pkg/repository/maintenance"
repomaintenance "github.com/vmware-tanzu/velero/pkg/repository/maintenance"
repomanager "github.com/vmware-tanzu/velero/pkg/repository/manager"
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"
clientFake "sigs.k8s.io/controller-runtime/pkg/client/fake"
batchv1api "k8s.io/api/batch/v1"
)
const testMaintenanceFrequency = 10 * time.Minute
func mockBackupRepoReconciler(t *testing.T, mockOn string, arg any, ret ...any) *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(t.Context(), rr)
require.NoError(t, err)
err = reconciler.patchBackupRepository(t.Context(), rr, repoReady())
require.NoError(t, err)
assert.Equal(t, velerov1api.BackupRepositoryPhaseReady, rr.Status.Phase)
err = reconciler.patchBackupRepository(t.Context(), rr, repoNotReady("not ready"))
require.NoError(t, err)
assert.NotEqual(t, velerov1api.BackupRepositoryPhaseReady, rr.Status.Phase)
}
func TestCheckNotReadyRepo(t *testing.T) {
// Test for restic repository
t.Run("restic repository", func(t *testing.T) {
rr := mockBackupRepositoryCR()
rr.Spec.BackupStorageLocation = "default"
rr.Spec.ResticIdentifier = "fake-identifier"
rr.Spec.VolumeNamespace = "volume-ns-1"
rr.Spec.RepositoryType = velerov1api.BackupRepositoryTypeRestic
reconciler := mockBackupRepoReconciler(t, "PrepareRepo", rr, nil)
err := reconciler.Client.Create(t.Context(), rr)
require.NoError(t, err)
location := 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.checkNotReadyRepo(t.Context(), rr, &location, reconciler.logger)
require.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)
})
// Test for kopia repository
t.Run("kopia repository", func(t *testing.T) {
rr := mockBackupRepositoryCR()
rr.Spec.BackupStorageLocation = "default"
rr.Spec.VolumeNamespace = "volume-ns-1"
rr.Spec.RepositoryType = velerov1api.BackupRepositoryTypeKopia
reconciler := mockBackupRepoReconciler(t, "PrepareRepo", rr, nil)
err := reconciler.Client.Create(t.Context(), rr)
require.NoError(t, err)
location := 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.checkNotReadyRepo(t.Context(), rr, &location, reconciler.logger)
require.NoError(t, err)
assert.Equal(t, velerov1api.BackupRepositoryPhaseReady, rr.Status.Phase)
// ResticIdentifier should remain empty for kopia
assert.Empty(t, rr.Spec.ResticIdentifier)
})
// Test for empty repository type (defaults to restic)
t.Run("empty repository type", func(t *testing.T) {
rr := mockBackupRepositoryCR()
rr.Spec.BackupStorageLocation = "default"
rr.Spec.ResticIdentifier = "fake-identifier"
rr.Spec.VolumeNamespace = "volume-ns-1"
// Deliberately leave RepositoryType empty
reconciler := mockBackupRepoReconciler(t, "PrepareRepo", rr, nil)
err := reconciler.Client.Create(t.Context(), rr)
require.NoError(t, err)
location := 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.checkNotReadyRepo(t.Context(), rr, &location, reconciler.logger)
require.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 waitMaintenanceJobCompleteFunc(now time.Time, result velerov1api.BackupRepositoryMaintenanceResult, message string) func(client.Client, context.Context, string, string, logrus.FieldLogger) (velerov1api.BackupRepositoryMaintenanceStatus, error) {
completionTimeStamp := &metav1.Time{Time: now.Add(time.Hour)}
if result == velerov1api.BackupRepositoryMaintenanceFailed {
completionTimeStamp = nil
}
return func(client.Client, context.Context, string, string, logrus.FieldLogger) (velerov1api.BackupRepositoryMaintenanceStatus, error) {
return velerov1api.BackupRepositoryMaintenanceStatus{
StartTimestamp: &metav1.Time{Time: now},
CompleteTimestamp: completionTimeStamp,
Result: result,
Message: message,
}, nil
}
}
type fakeClock struct {
now time.Time
}
func (f *fakeClock) After(time.Duration) <-chan time.Time {
return nil
}
func (f *fakeClock) NewTicker(time.Duration) clock.Ticker {
return nil
}
func (f *fakeClock) NewTimer(time.Duration) clock.Timer {
return nil
}
func (f *fakeClock) Now() time.Time {
return f.now
}
func (f *fakeClock) Since(time.Time) time.Duration {
return 0
}
func (f *fakeClock) Sleep(time.Duration) {}
func (f *fakeClock) Tick(time.Duration) <-chan time.Time {
return nil
}
func (f *fakeClock) AfterFunc(time.Duration, func()) clock.Timer {
return nil
}
func TestRunMaintenanceIfDue(t *testing.T) {
now := time.Now().Round(time.Second)
tests := []struct {
name string
repo *velerov1api.BackupRepository
startJobFunc func(client.Client, context.Context, *velerov1api.BackupRepository, string, kube.PodResources, logrus.Level, *logging.FormatFlag, logrus.FieldLogger) (string, error)
waitJobFunc func(client.Client, context.Context, string, string, logrus.FieldLogger) (velerov1api.BackupRepositoryMaintenanceStatus, error)
expectedMaintenanceTime time.Time
expectedHistory []velerov1api.BackupRepositoryMaintenanceStatus
expectedErr string
}{
{
name: "not due",
repo: &velerov1api.BackupRepository{
ObjectMeta: metav1.ObjectMeta{
Namespace: velerov1api.DefaultNamespace,
Name: "repo",
},
Spec: velerov1api.BackupRepositorySpec{
MaintenanceFrequency: metav1.Duration{Duration: time.Hour},
},
Status: velerov1api.BackupRepositoryStatus{
LastMaintenanceTime: &metav1.Time{Time: now},
RecentMaintenance: []velerov1api.BackupRepositoryMaintenanceStatus{
{
StartTimestamp: &metav1.Time{Time: now.Add(-time.Hour)},
CompleteTimestamp: &metav1.Time{Time: now},
Result: velerov1api.BackupRepositoryMaintenanceSucceeded,
},
},
},
},
expectedMaintenanceTime: now,
expectedHistory: []velerov1api.BackupRepositoryMaintenanceStatus{
{
StartTimestamp: &metav1.Time{Time: now.Add(-time.Hour)},
CompleteTimestamp: &metav1.Time{Time: now},
Result: velerov1api.BackupRepositoryMaintenanceSucceeded,
},
},
},
{
name: "start failed",
repo: &velerov1api.BackupRepository{
ObjectMeta: metav1.ObjectMeta{
Namespace: velerov1api.DefaultNamespace,
Name: "repo",
},
Spec: velerov1api.BackupRepositorySpec{
MaintenanceFrequency: metav1.Duration{Duration: time.Hour},
},
Status: velerov1api.BackupRepositoryStatus{
LastMaintenanceTime: &metav1.Time{Time: now.Add(-time.Hour - time.Minute)},
RecentMaintenance: []velerov1api.BackupRepositoryMaintenanceStatus{
{
StartTimestamp: &metav1.Time{Time: now.Add(-time.Hour * 2)},
CompleteTimestamp: &metav1.Time{Time: now.Add(-time.Hour)},
Result: velerov1api.BackupRepositoryMaintenanceSucceeded,
},
},
},
},
startJobFunc: startMaintenanceJobFail,
expectedMaintenanceTime: now.Add(-time.Hour - time.Minute),
expectedHistory: []velerov1api.BackupRepositoryMaintenanceStatus{
{
StartTimestamp: &metav1.Time{Time: now.Add(-time.Hour * 2)},
CompleteTimestamp: &metav1.Time{Time: now.Add(-time.Hour)},
Result: velerov1api.BackupRepositoryMaintenanceSucceeded,
},
{
StartTimestamp: &metav1.Time{Time: now},
Result: velerov1api.BackupRepositoryMaintenanceFailed,
Message: "Failed to start maintenance job, err: fake-start-error",
},
},
},
{
name: "wait failed",
repo: &velerov1api.BackupRepository{
ObjectMeta: metav1.ObjectMeta{
Namespace: velerov1api.DefaultNamespace,
Name: "repo",
},
Spec: velerov1api.BackupRepositorySpec{
MaintenanceFrequency: metav1.Duration{Duration: time.Hour},
},
Status: velerov1api.BackupRepositoryStatus{
LastMaintenanceTime: &metav1.Time{Time: now.Add(-time.Hour - time.Minute)},
RecentMaintenance: []velerov1api.BackupRepositoryMaintenanceStatus{
{
StartTimestamp: &metav1.Time{Time: now.Add(-time.Hour * 2)},
CompleteTimestamp: &metav1.Time{Time: now.Add(-time.Hour)},
Result: velerov1api.BackupRepositoryMaintenanceSucceeded,
},
},
},
},
startJobFunc: startMaintenanceJobSucceed,
waitJobFunc: waitMaintenanceJobCompleteFail,
expectedErr: "error waiting repo maintenance completion status: fake-wait-error",
expectedMaintenanceTime: now.Add(-time.Hour - time.Minute),
expectedHistory: []velerov1api.BackupRepositoryMaintenanceStatus{
{
StartTimestamp: &metav1.Time{Time: now.Add(-time.Hour * 2)},
CompleteTimestamp: &metav1.Time{Time: now.Add(-time.Hour)},
Result: velerov1api.BackupRepositoryMaintenanceSucceeded,
},
},
},
{
name: "maintenance failed",
repo: &velerov1api.BackupRepository{
ObjectMeta: metav1.ObjectMeta{
Namespace: velerov1api.DefaultNamespace,
Name: "repo",
},
Spec: velerov1api.BackupRepositorySpec{
MaintenanceFrequency: metav1.Duration{Duration: time.Hour},
},
Status: velerov1api.BackupRepositoryStatus{
LastMaintenanceTime: &metav1.Time{Time: now.Add(-time.Hour - time.Minute)},
RecentMaintenance: []velerov1api.BackupRepositoryMaintenanceStatus{
{
StartTimestamp: &metav1.Time{Time: now.Add(-time.Hour * 2)},
CompleteTimestamp: &metav1.Time{Time: now.Add(-time.Hour)},
Result: velerov1api.BackupRepositoryMaintenanceSucceeded,
},
},
},
},
startJobFunc: startMaintenanceJobSucceed,
waitJobFunc: waitMaintenanceJobCompleteFunc(now, velerov1api.BackupRepositoryMaintenanceFailed, "fake-maintenance-message"),
expectedMaintenanceTime: now.Add(-time.Hour - time.Minute),
expectedHistory: []velerov1api.BackupRepositoryMaintenanceStatus{
{
StartTimestamp: &metav1.Time{Time: now.Add(-time.Hour * 2)},
CompleteTimestamp: &metav1.Time{Time: now.Add(-time.Hour)},
Result: velerov1api.BackupRepositoryMaintenanceSucceeded,
},
{
StartTimestamp: &metav1.Time{Time: now},
Result: velerov1api.BackupRepositoryMaintenanceFailed,
Message: "fake-maintenance-message",
},
},
},
{
name: "maintenance succeeded",
repo: &velerov1api.BackupRepository{
ObjectMeta: metav1.ObjectMeta{
Namespace: velerov1api.DefaultNamespace,
Name: "repo",
},
Spec: velerov1api.BackupRepositorySpec{
MaintenanceFrequency: metav1.Duration{Duration: time.Hour},
},
Status: velerov1api.BackupRepositoryStatus{
LastMaintenanceTime: &metav1.Time{Time: now.Add(-time.Hour - time.Minute)},
RecentMaintenance: []velerov1api.BackupRepositoryMaintenanceStatus{
{
StartTimestamp: &metav1.Time{Time: now.Add(-time.Hour * 2)},
CompleteTimestamp: &metav1.Time{Time: now.Add(-time.Hour)},
Result: velerov1api.BackupRepositoryMaintenanceSucceeded,
},
},
},
},
startJobFunc: startMaintenanceJobSucceed,
waitJobFunc: waitMaintenanceJobCompleteFunc(now, velerov1api.BackupRepositoryMaintenanceSucceeded, ""),
expectedMaintenanceTime: now.Add(time.Hour),
expectedHistory: []velerov1api.BackupRepositoryMaintenanceStatus{
{
StartTimestamp: &metav1.Time{Time: now.Add(-time.Hour * 2)},
CompleteTimestamp: &metav1.Time{Time: now.Add(-time.Hour)},
Result: velerov1api.BackupRepositoryMaintenanceSucceeded,
},
{
StartTimestamp: &metav1.Time{Time: now},
CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour)},
Result: velerov1api.BackupRepositoryMaintenanceSucceeded,
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
reconciler := mockBackupRepoReconciler(t, "", test.repo, nil)
reconciler.clock = &fakeClock{now}
err := reconciler.Client.Create(t.Context(), test.repo)
require.NoError(t, err)
funcStartMaintenanceJob = test.startJobFunc
funcWaitMaintenanceJobComplete = test.waitJobFunc
err = reconciler.runMaintenanceIfDue(t.Context(), test.repo, velerotest.NewLogger())
if test.expectedErr == "" {
require.NoError(t, err)
}
assert.Equal(t, test.expectedMaintenanceTime, test.repo.Status.LastMaintenanceTime.Time)
assert.Len(t, test.repo.Status.RecentMaintenance, len(test.expectedHistory))
for i := 0; i < len(test.expectedHistory); i++ {
assert.Equal(t, test.expectedHistory[i].StartTimestamp.Time, test.repo.Status.RecentMaintenance[i].StartTimestamp.Time)
if test.expectedHistory[i].CompleteTimestamp == nil {
assert.Nil(t, test.repo.Status.RecentMaintenance[i].CompleteTimestamp)
} else {
assert.Equal(t, test.expectedHistory[i].CompleteTimestamp.Time, test.repo.Status.RecentMaintenance[i].CompleteTimestamp.Time)
}
assert.Equal(t, test.expectedHistory[i].Result, test.repo.Status.RecentMaintenance[i].Result)
assert.Equal(t, test.expectedHistory[i].Message, test.repo.Status.RecentMaintenance[i].Message)
}
})
}
}
func TestInitializeRepo(t *testing.T) {
rr := mockBackupRepositoryCR()
rr.Spec.BackupStorageLocation = "default"
reconciler := mockBackupRepoReconciler(t, "PrepareRepo", rr, nil)
err := reconciler.Client.Create(t.Context(), rr)
require.NoError(t, err)
location := 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.initializeRepo(t.Context(), rr, &location, reconciler.logger)
require.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(t.Context(), test.repo)
require.NoError(t, err)
_, err = reconciler.Reconcile(t.Context(), ctrl.Request{NamespacedName: types.NamespacedName{Namespace: test.repo.Namespace, Name: "repo"}})
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 := &corev1api.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "config-1",
Namespace: velerov1api.DefaultNamespace,
},
}
configWithWrongData := &corev1api.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "config-1",
Namespace: velerov1api.DefaultNamespace,
},
Data: map[string]string{
"fake-repo-type": "",
},
}
configWithData := &corev1api.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "config-1",
Namespace: velerov1api.DefaultNamespace,
},
Data: map[string]string{
"fake-repo-type": "{\"cacheLimitMB\": 1000, \"enableCompression\": true, \"fullMaintenanceInterval\": \"fastGC\"}",
"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",
"fullMaintenanceInterval": "fastGC",
},
},
}
scheme := runtime.NewScheme()
corev1api.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(t.Context(), fakeClient, test.congiName, velerov1api.DefaultNamespace, test.repoName, test.repoType, velerotest.NewLogger())
if test.expectedErr != "" {
assert.EqualError(t, err, test.expectedErr)
} else {
require.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.BackupRepositoryMaintenanceSucceeded,
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.BackupRepositoryMaintenanceSucceeded,
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().Round(time.Second)
schemeFail := runtime.NewScheme()
velerov1api.AddToScheme(schemeFail)
scheme := runtime.NewScheme()
batchv1api.AddToScheme(scheme)
corev1api.AddToScheme(scheme)
velerov1api.AddToScheme(scheme)
jobSucceeded := &batchv1api.Job{
ObjectMeta: metav1.ObjectMeta{
Name: "job1",
Namespace: velerov1api.DefaultNamespace,
Labels: map[string]string{maintenance.RepositoryNameLabel: "repo"},
CreationTimestamp: metav1.Time{Time: now.Add(time.Hour)},
},
Status: batchv1api.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(&corev1api.ContainerStatus{
State: corev1api.ContainerState{
Terminated: &corev1api.ContainerStateTerminated{},
},
}).Result()
tests := []struct {
name string
kubeClientObj []runtime.Object
runtimeScheme *runtime.Scheme
repoLastMatainTime metav1.Time
expectNewHistory []velerov1api.BackupRepositoryMaintenanceStatus
expectTimeUpdate *metav1.Time
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: []velerov1api.BackupRepositoryMaintenanceStatus{
{
StartTimestamp: &metav1.Time{Time: now.Add(time.Hour)},
CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)},
Result: velerov1api.BackupRepositoryMaintenanceSucceeded,
},
},
},
{
name: "update last time",
runtimeScheme: scheme,
kubeClientObj: []runtime.Object{
jobSucceeded,
jobPodSucceeded,
},
expectNewHistory: []velerov1api.BackupRepositoryMaintenanceStatus{
{
StartTimestamp: &metav1.Time{Time: now.Add(time.Hour)},
CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)},
Result: velerov1api.BackupRepositoryMaintenanceSucceeded,
},
},
expectTimeUpdate: &metav1.Time{Time: now.Add(time.Hour * 2)},
},
}
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 := clientFake.NewClientBuilder()
fakeClientBuilder = fakeClientBuilder.WithScheme(test.runtimeScheme)
fakeClient := fakeClientBuilder.WithRuntimeObjects(test.kubeClientObj...).Build()
r.Client = fakeClient
lastTm := backupRepo.Status.LastMaintenanceTime
err := r.recallMaintenance(t.Context(), backupRepo, velerotest.NewLogger())
if test.expectedErr != "" {
assert.EqualError(t, err, test.expectedErr)
} else {
assert.NoError(t, err)
if test.expectNewHistory == nil {
assert.Nil(t, backupRepo.Status.RecentMaintenance)
} else {
assert.Len(t, backupRepo.Status.RecentMaintenance, len(test.expectNewHistory))
for i := 0; i < len(test.expectNewHistory); i++ {
assert.Equal(t, test.expectNewHistory[i].StartTimestamp.Time, backupRepo.Status.RecentMaintenance[i].StartTimestamp.Time)
assert.Equal(t, test.expectNewHistory[i].CompleteTimestamp.Time, backupRepo.Status.RecentMaintenance[i].CompleteTimestamp.Time)
assert.Equal(t, test.expectNewHistory[i].Result, backupRepo.Status.RecentMaintenance[i].Result)
assert.Equal(t, test.expectNewHistory[i].Message, backupRepo.Status.RecentMaintenance[i].Message)
}
}
if test.expectTimeUpdate != nil {
assert.Equal(t, test.expectTimeUpdate.Time, backupRepo.Status.LastMaintenanceTime.Time)
} else {
assert.Equal(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.BackupRepositoryMaintenanceSucceeded,
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.BackupRepositoryMaintenanceSucceeded,
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.BackupRepositoryMaintenanceSucceeded,
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.BackupRepositoryMaintenanceSucceeded,
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.BackupRepositoryMaintenanceSucceeded,
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.BackupRepositoryMaintenanceSucceeded,
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.BackupRepositoryMaintenanceSucceeded,
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.BackupRepositoryMaintenanceFailed,
Message: "fake-maintenance-message",
},
{
StartTimestamp: &metav1.Time{Time: now.Add(time.Hour)},
CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)},
Result: velerov1api.BackupRepositoryMaintenanceSucceeded,
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)
})
}
}
// This test verify the BackupRepository controller will keep no more jobs
// than the number of test case's keptJobNumber.
func TestDeleteOldMaintenanceJob(t *testing.T) {
now := time.Now().Round(time.Second)
tests := []struct {
name string
repo *velerov1api.BackupRepository
keptJobNumber int // The BackupRepository controller's keepLatestMaintenanceJobs parameter
expectNil bool
maintenanceJobs []batchv1api.Job
bsl *velerov1api.BackupStorageLocation
}{
{
name: "test maintenance job cleaning when repo is ready",
repo: &velerov1api.BackupRepository{
ObjectMeta: metav1.ObjectMeta{
Namespace: velerov1api.DefaultNamespace,
Name: "repo",
},
Spec: velerov1api.BackupRepositorySpec{
MaintenanceFrequency: metav1.Duration{Duration: testMaintenanceFrequency},
BackupStorageLocation: "default",
},
Status: velerov1api.BackupRepositoryStatus{
LastMaintenanceTime: &metav1.Time{Time: time.Now()},
RecentMaintenance: []velerov1api.BackupRepositoryMaintenanceStatus{
{
StartTimestamp: &metav1.Time{Time: now.Add(-time.Minute)},
CompleteTimestamp: &metav1.Time{Time: now},
Result: velerov1api.BackupRepositoryMaintenanceSucceeded,
},
}, Phase: velerov1api.BackupRepositoryPhaseReady,
},
},
keptJobNumber: 1,
expectNil: true,
maintenanceJobs: []batchv1api.Job{
*builder.ForJob("velero", "job-01").ObjectMeta(builder.WithLabels(repomaintenance.RepositoryNameLabel, "repo")).Succeeded(1).Result(),
*builder.ForJob("velero", "job-02").ObjectMeta(builder.WithLabels(repomaintenance.RepositoryNameLabel, "repo")).Succeeded(1).Result(),
},
bsl: builder.ForBackupStorageLocation("velero", "default").Result(),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
crClient := velerotest.NewFakeControllerRuntimeClient(t, test.repo, test.bsl)
for _, job := range test.maintenanceJobs {
require.NoError(t, crClient.Create(t.Context(), &job))
}
repoLocker := repository.NewRepoLocker()
mgr := repomanager.NewManager("", crClient, repoLocker, nil, nil, nil)
reconciler := NewBackupRepoReconciler(
velerov1api.DefaultNamespace,
velerotest.NewLogger(),
crClient,
mgr,
time.Duration(0),
"",
test.keptJobNumber,
"",
kube.PodResources{},
logrus.InfoLevel,
nil,
)
_, err := reconciler.Reconcile(t.Context(), ctrl.Request{NamespacedName: types.NamespacedName{Namespace: test.repo.Namespace, Name: "repo"}})
if test.expectNil {
require.NoError(t, err)
} else {
require.Error(t, err)
}
if len(test.maintenanceJobs) > 0 {
jobList := new(batchv1api.JobList)
require.NoError(t, reconciler.Client.List(t.Context(), jobList, &client.ListOptions{Namespace: "velero"}))
assert.Len(t, jobList.Items, 1)
}
})
}
}
func TestInitializeRepoWithRepositoryTypes(t *testing.T) {
scheme := runtime.NewScheme()
corev1api.AddToScheme(scheme)
velerov1api.AddToScheme(scheme)
// Test for restic repository
t.Run("restic repository", func(t *testing.T) {
rr := mockBackupRepositoryCR()
rr.Spec.BackupStorageLocation = "default"
rr.Spec.VolumeNamespace = "volume-ns-1"
rr.Spec.RepositoryType = velerov1api.BackupRepositoryTypeRestic
location := &velerov1api.BackupStorageLocation{
ObjectMeta: metav1.ObjectMeta{
Namespace: velerov1api.DefaultNamespace,
Name: "default",
},
Spec: velerov1api.BackupStorageLocationSpec{
Provider: "aws",
StorageType: velerov1api.StorageType{
ObjectStorage: &velerov1api.ObjectStorageLocation{
Bucket: "test-bucket",
Prefix: "test-prefix",
},
},
Config: map[string]string{
"region": "us-east-1",
},
},
}
fakeClient := clientFake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(rr, location).Build()
mgr := &repomokes.Manager{}
mgr.On("PrepareRepo", rr).Return(nil)
reconciler := NewBackupRepoReconciler(
velerov1api.DefaultNamespace,
velerotest.NewLogger(),
fakeClient,
mgr,
testMaintenanceFrequency,
"",
3,
"",
kube.PodResources{},
logrus.InfoLevel,
nil,
)
err := reconciler.initializeRepo(t.Context(), rr, location, reconciler.logger)
require.NoError(t, err)
// Verify ResticIdentifier is set for restic
assert.NotEmpty(t, rr.Spec.ResticIdentifier)
assert.Contains(t, rr.Spec.ResticIdentifier, "volume-ns-1")
assert.Equal(t, velerov1api.BackupRepositoryPhaseReady, rr.Status.Phase)
})
// Test for kopia repository
t.Run("kopia repository", func(t *testing.T) {
rr := mockBackupRepositoryCR()
rr.Spec.BackupStorageLocation = "default"
rr.Spec.VolumeNamespace = "volume-ns-1"
rr.Spec.RepositoryType = velerov1api.BackupRepositoryTypeKopia
location := &velerov1api.BackupStorageLocation{
ObjectMeta: metav1.ObjectMeta{
Namespace: velerov1api.DefaultNamespace,
Name: "default",
},
Spec: velerov1api.BackupStorageLocationSpec{
Provider: "aws",
StorageType: velerov1api.StorageType{
ObjectStorage: &velerov1api.ObjectStorageLocation{
Bucket: "test-bucket",
Prefix: "test-prefix",
},
},
Config: map[string]string{
"region": "us-east-1",
},
},
}
fakeClient := clientFake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(rr, location).Build()
mgr := &repomokes.Manager{}
mgr.On("PrepareRepo", rr).Return(nil)
reconciler := NewBackupRepoReconciler(
velerov1api.DefaultNamespace,
velerotest.NewLogger(),
fakeClient,
mgr,
testMaintenanceFrequency,
"",
3,
"",
kube.PodResources{},
logrus.InfoLevel,
nil,
)
err := reconciler.initializeRepo(t.Context(), rr, location, reconciler.logger)
require.NoError(t, err)
// Verify ResticIdentifier is NOT set for kopia
assert.Empty(t, rr.Spec.ResticIdentifier)
assert.Equal(t, velerov1api.BackupRepositoryPhaseReady, rr.Status.Phase)
})
// Test for empty repository type (defaults to restic)
t.Run("empty repository type", func(t *testing.T) {
rr := mockBackupRepositoryCR()
rr.Spec.BackupStorageLocation = "default"
rr.Spec.VolumeNamespace = "volume-ns-1"
// Leave RepositoryType empty
location := &velerov1api.BackupStorageLocation{
ObjectMeta: metav1.ObjectMeta{
Namespace: velerov1api.DefaultNamespace,
Name: "default",
},
Spec: velerov1api.BackupStorageLocationSpec{
Provider: "aws",
StorageType: velerov1api.StorageType{
ObjectStorage: &velerov1api.ObjectStorageLocation{
Bucket: "test-bucket",
Prefix: "test-prefix",
},
},
Config: map[string]string{
"region": "us-east-1",
},
},
}
fakeClient := clientFake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(rr, location).Build()
mgr := &repomokes.Manager{}
mgr.On("PrepareRepo", rr).Return(nil)
reconciler := NewBackupRepoReconciler(
velerov1api.DefaultNamespace,
velerotest.NewLogger(),
fakeClient,
mgr,
testMaintenanceFrequency,
"",
3,
"",
kube.PodResources{},
logrus.InfoLevel,
nil,
)
err := reconciler.initializeRepo(t.Context(), rr, location, reconciler.logger)
require.NoError(t, err)
// Verify ResticIdentifier is set when type is empty (defaults to restic)
assert.NotEmpty(t, rr.Spec.ResticIdentifier)
assert.Contains(t, rr.Spec.ResticIdentifier, "volume-ns-1")
assert.Equal(t, velerov1api.BackupRepositoryPhaseReady, rr.Status.Phase)
})
}