Refactor restic repository

Signed-off-by: Ming <mqiu@vmware.com>
pull/4859/head
Ming 2022-04-25 17:48:38 +08:00
parent 3ec96e2eac
commit 06d3d731ed
11 changed files with 482 additions and 174 deletions

View File

@ -0,0 +1 @@
Convert Restic Repository resource/controller to the Kubebuilder framework

View File

@ -16,9 +16,16 @@ spec:
singular: resticrepository
scope: Namespaced
versions:
- name: v1
- additionalPrinterColumns:
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1
schema:
openAPIV3Schema:
description: TODO(2.0) After converting all resources to use the runtime-controller
client, the genclient and k8s:deepcopy markers will no longer be needed
and should be removed.
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
@ -81,6 +88,8 @@ spec:
type: object
served: true
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""

View File

@ -17,10 +17,6 @@ spec:
scope: Namespaced
versions:
- additionalPrinterColumns:
- description: Name of the schedule
jsonPath: .metadata.name
name: Name
type: string
- description: Status of the schedule
jsonPath: .status.phase
name: Status

File diff suppressed because one or more lines are too long

View File

@ -72,6 +72,26 @@ rules:
- get
- patch
- update
- apiGroups:
- velero.io
resources:
- resticrepositories
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- velero.io
resources:
- resticrepositories/status
verbs:
- get
- patch
- update
- apiGroups:
- velero.io
resources:

View File

@ -64,9 +64,15 @@ type ResticRepositoryStatus struct {
LastMaintenanceTime *metav1.Time `json:"lastMaintenanceTime,omitempty"`
}
// TODO(2.0) After converting all resources to use the runtime-controller client,
// the genclient and k8s:deepcopy markers will no longer be needed and should be removed.
// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +kubebuilder:object:root=true
// +kubebuilder:object:generate=true
// +kubebuilder:storageversion
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
type ResticRepository struct {
metav1.TypeMeta `json:",inline"`
@ -80,7 +86,12 @@ type ResticRepository struct {
Status ResticRepositoryStatus `json:"status,omitempty"`
}
// TODO(2.0) After converting all resources to use the runtime-controller client,
// the k8s:deepcopy marker will no longer be needed and should be removed.
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +kubebuilder:object:root=true
// +kubebuilder:rbac:groups=velero.io,resources=resticrepositories,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=velero.io,resources=resticrepositories/status,verbs=get;update;patch
// ResticRepositoryList is a list of ResticRepositories.
type ResticRepositoryList struct {

View File

@ -84,7 +84,6 @@ type ScheduleStatus struct {
// +kubebuilder:object:root=true
// +kubebuilder:storageversion
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Name",type="string",JSONPath=".metadata.name",description="Name of the schedule"
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.phase",description="Status of the schedule"
// +kubebuilder:printcolumn:name="Schedule",type="string",JSONPath=".spec.schedule",description="A Cron expression defining when to run the Backup"
// +kubebuilder:printcolumn:name="LastBackup",type="date",JSONPath=".status.lastBackup",description="The last time a Backup was run for this schedule"

View File

@ -739,29 +739,12 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string
}
}
resticRepoControllerRunInfo := func() controllerRunInfo {
resticRepoController := controller.NewResticRepositoryController(
s.logger,
s.sharedInformerFactory.Velero().V1().ResticRepositories(),
s.veleroClient.VeleroV1(),
s.mgr.GetClient(),
s.resticManager,
s.config.defaultResticMaintenanceFrequency,
)
return controllerRunInfo{
controller: resticRepoController,
numWorkers: defaultControllerWorkers,
}
}
enabledControllers := map[string]func() controllerRunInfo{
controller.BackupSync: backupSyncControllerRunInfo,
controller.Backup: backupControllerRunInfo,
controller.GarbageCollection: gcControllerRunInfo,
controller.BackupDeletion: deletionControllerRunInfo,
controller.Restore: restoreControllerRunInfo,
controller.ResticRepo: resticRepoControllerRunInfo,
}
// Note: all runtime type controllers that can be disabled are grouped separately, below:
enabledRuntimeControllers := make(map[string]struct{})
@ -833,6 +816,10 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string
s.logger.Fatal(err, "unable to create controller", "controller", controller.Schedule)
}
if err := controller.NewResticRepoConciler(s.namespace, s.logger, s.mgr.GetClient(), s.config.defaultResticMaintenanceFrequency, s.resticManager).SetupWithManager(s.mgr); err != nil {
s.logger.Fatal(err, "unable to create controller", "controller", controller.ResticRepo)
}
if _, ok := enabledRuntimeControllers[controller.ServerStatusRequest]; ok {
r := controller.ServerStatusRequestReconciler{
Scheme: s.mgr.GetScheme(),

View File

@ -18,59 +18,46 @@ package controller
import (
"context"
"encoding/json"
"strings"
"time"
jsonpatch "github.com/evanphx/json-patch"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/clock"
"k8s.io/client-go/tools/cache"
"sigs.k8s.io/cluster-api/util/patch"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
velerov1client "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/typed/velero/v1"
velerov1informers "github.com/vmware-tanzu/velero/pkg/generated/informers/externalversions/velero/v1"
velerov1listers "github.com/vmware-tanzu/velero/pkg/generated/listers/velero/v1"
"github.com/vmware-tanzu/velero/pkg/restic"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/vmware-tanzu/velero/pkg/util/kube"
)
type resticRepositoryController struct {
*genericController
const (
repoSyncPeriod = 5 * time.Minute
)
resticRepositoryClient velerov1client.ResticRepositoriesGetter
resticRepositoryLister velerov1listers.ResticRepositoryLister
kbClient client.Client
repositoryManager restic.RepositoryManager
type ResticRepoReconciler struct {
client.Client
namespace string
logger logrus.FieldLogger
clock clock.Clock
defaultMaintenanceFrequency time.Duration
clock clock.Clock
repositoryManager restic.RepositoryManager
}
// NewResticRepositoryController creates a new restic repository controller.
func NewResticRepositoryController(
logger logrus.FieldLogger,
resticRepositoryInformer velerov1informers.ResticRepositoryInformer,
resticRepositoryClient velerov1client.ResticRepositoriesGetter,
kbClient client.Client,
repositoryManager restic.RepositoryManager,
defaultMaintenanceFrequency time.Duration,
) Interface {
c := &resticRepositoryController{
genericController: newGenericController(ResticRepo, logger),
resticRepositoryClient: resticRepositoryClient,
resticRepositoryLister: resticRepositoryInformer.Lister(),
kbClient: kbClient,
repositoryManager: repositoryManager,
defaultMaintenanceFrequency: defaultMaintenanceFrequency,
clock: &clock.RealClock{},
func NewResticRepoConciler(namespace string, logger logrus.FieldLogger, client client.Client,
defaultMaintenanceFrequency time.Duration, repositoryManager restic.RepositoryManager) *ResticRepoReconciler {
c := &ResticRepoReconciler{
client,
namespace,
logger,
clock.RealClock{},
defaultMaintenanceFrequency,
repositoryManager,
}
if c.defaultMaintenanceFrequency <= 0 {
@ -78,124 +65,106 @@ func NewResticRepositoryController(
c.defaultMaintenanceFrequency = restic.DefaultMaintenanceFrequency
}
c.syncHandler = c.processQueueItem
resticRepositoryInformer.Informer().AddEventHandler(
cache.ResourceEventHandlerFuncs{
AddFunc: c.enqueue,
},
)
c.resyncPeriod = 5 * time.Minute
c.resyncFunc = c.enqueueAllRepositories
return c
}
// enqueueAllRepositories lists all restic repositories from cache and enqueues all
// of them so we can check each one for maintenance.
func (c *resticRepositoryController) enqueueAllRepositories() {
c.logger.Debug("resticRepositoryController.enqueueAllRepositories")
repos, err := c.resticRepositoryLister.List(labels.Everything())
if err != nil {
c.logger.WithError(errors.WithStack(err)).Error("error listing restic repositories")
return
}
for _, repo := range repos {
c.enqueue(repo)
}
func (r *ResticRepoReconciler) SetupWithManager(mgr ctrl.Manager) error {
s := kube.NewPeriodicalEnqueueSource(r.logger, mgr.GetClient(), &velerov1api.ResticRepositoryList{}, repoSyncPeriod)
return ctrl.NewControllerManagedBy(mgr).
For(&velerov1api.ResticRepository{}).
Watches(s, nil).
Complete(r)
}
func (c *resticRepositoryController) processQueueItem(key string) error {
log := c.logger.WithField("key", key)
log.Debug("Running processQueueItem")
func (r *ResticRepoReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := r.logger.WithField("resticRepo", req.String())
resticRepo := &velerov1api.ResticRepository{}
if err := r.Get(ctx, req.NamespacedName, resticRepo); err != nil {
if apierrors.IsNotFound(err) {
log.Warnf("restic repository %s in namespace %s is not found", req.Name, req.Namespace)
return ctrl.Result{}, nil
}
log.WithError(err).Error("error getting restic repository")
return ctrl.Result{}, err
}
ns, name, err := cache.SplitMetaNamespaceKey(key)
// Initialize the patch helper.
patchHelper, err := patch.NewHelper(resticRepo, r.Client)
if err != nil {
log.WithError(errors.WithStack(err)).Error("error splitting queue key")
return nil
log.WithError(err).Error("Error getting a patch helper to update restic repository resource")
return ctrl.Result{}, errors.WithStack(err)
}
log = c.logger.WithField("namespace", ns).WithField("name", name)
if resticRepo.Status.Phase == "" || resticRepo.Status.Phase == velerov1api.ResticRepositoryPhaseNew {
if err = r.initializeRepo(ctx, resticRepo, log, patchHelper); err != nil {
log.WithError(err).Error("error initialize repository")
return ctrl.Result{}, errors.WithStack(err)
}
req, err := c.resticRepositoryLister.ResticRepositories(ns).Get(name)
if apierrors.IsNotFound(err) {
log.Debug("Unable to find ResticRepository")
return nil
}
if err != nil {
return errors.Wrap(err, "error getting ResticRepository")
}
// Don't mutate the shared cache
reqCopy := req.DeepCopy()
if req.Status.Phase == "" || req.Status.Phase == velerov1api.ResticRepositoryPhaseNew {
return c.initializeRepo(reqCopy, log)
return ctrl.Result{}, nil
}
// If the repository is ready or not-ready, check it for stale locks, but if
// this fails for any reason, it's non-critical so we still continue on to the
// rest of the "process" logic.
log.Debug("Checking repository for stale locks")
if err := c.repositoryManager.UnlockRepo(reqCopy); err != nil {
if err := r.repositoryManager.UnlockRepo(resticRepo); err != nil {
log.WithError(err).Error("Error checking repository for stale locks")
}
switch req.Status.Phase {
switch resticRepo.Status.Phase {
case velerov1api.ResticRepositoryPhaseReady:
return c.runMaintenanceIfDue(reqCopy, log)
return ctrl.Result{}, r.runMaintenanceIfDue(ctx, resticRepo, patchHelper, log)
case velerov1api.ResticRepositoryPhaseNotReady:
return c.checkNotReadyRepo(reqCopy, log)
return ctrl.Result{}, r.checkNotReadyRepo(ctx, resticRepo, patchHelper, log)
}
return nil
return ctrl.Result{}, nil
}
func (c *resticRepositoryController) initializeRepo(req *velerov1api.ResticRepository, log logrus.FieldLogger) error {
func (r *ResticRepoReconciler) initializeRepo(ctx context.Context, req *velerov1api.ResticRepository, log logrus.FieldLogger, patchHelper *patch.Helper) error {
log.Info("Initializing restic repository")
// confirm the repo's BackupStorageLocation is valid
loc := &velerov1api.BackupStorageLocation{}
if err := c.kbClient.Get(context.Background(), client.ObjectKey{
if err := r.Get(context.Background(), client.ObjectKey{
Namespace: req.Namespace,
Name: req.Spec.BackupStorageLocation,
}, loc); err != nil {
return c.patchResticRepository(req, repoNotReady(err.Error()))
return r.patchResticRepository(ctx, req, patchHelper, log, repoNotReady(err.Error()))
}
repoIdentifier, err := restic.GetRepoIdentifier(loc, req.Spec.VolumeNamespace)
if err != nil {
return c.patchResticRepository(req, func(r *velerov1api.ResticRepository) {
r.Status.Message = err.Error()
r.Status.Phase = velerov1api.ResticRepositoryPhaseNotReady
return r.patchResticRepository(ctx, req, patchHelper, log, func(rr *velerov1api.ResticRepository) {
rr.Status.Message = err.Error()
rr.Status.Phase = velerov1api.ResticRepositoryPhaseNotReady
if r.Spec.MaintenanceFrequency.Duration <= 0 {
r.Spec.MaintenanceFrequency = metav1.Duration{Duration: c.defaultMaintenanceFrequency}
if rr.Spec.MaintenanceFrequency.Duration <= 0 {
rr.Spec.MaintenanceFrequency = metav1.Duration{Duration: r.defaultMaintenanceFrequency}
}
})
}
// defaulting - if the patch fails, return an error so the item is returned to the queue
if err := c.patchResticRepository(req, func(r *velerov1api.ResticRepository) {
r.Spec.ResticIdentifier = repoIdentifier
if err := r.patchResticRepository(ctx, req, patchHelper, log, func(rr *velerov1api.ResticRepository) {
rr.Spec.ResticIdentifier = repoIdentifier
if r.Spec.MaintenanceFrequency.Duration <= 0 {
r.Spec.MaintenanceFrequency = metav1.Duration{Duration: c.defaultMaintenanceFrequency}
if rr.Spec.MaintenanceFrequency.Duration <= 0 {
rr.Spec.MaintenanceFrequency = metav1.Duration{Duration: r.defaultMaintenanceFrequency}
}
}); err != nil {
return err
}
if err := ensureRepo(req, c.repositoryManager); err != nil {
return c.patchResticRepository(req, repoNotReady(err.Error()))
if err := ensureRepo(req, r.repositoryManager); err != nil {
return r.patchResticRepository(ctx, req, patchHelper, log, repoNotReady(err.Error()))
}
return c.patchResticRepository(req, func(req *velerov1api.ResticRepository) {
req.Status.Phase = velerov1api.ResticRepositoryPhaseReady
req.Status.LastMaintenanceTime = &metav1.Time{Time: time.Now()}
return r.patchResticRepository(ctx, req, patchHelper, log, func(rr *velerov1api.ResticRepository) {
rr.Status.Phase = velerov1api.ResticRepositoryPhaseReady
rr.Status.LastMaintenanceTime = &metav1.Time{Time: time.Now()}
})
}
@ -218,10 +187,10 @@ func ensureRepo(repo *velerov1api.ResticRepository, repoManager restic.Repositor
return nil
}
func (c *resticRepositoryController) runMaintenanceIfDue(req *velerov1api.ResticRepository, log logrus.FieldLogger) error {
func (r *ResticRepoReconciler) runMaintenanceIfDue(ctx context.Context, req *velerov1api.ResticRepository, patchHelper *patch.Helper, log logrus.FieldLogger) error {
log.Debug("resticRepositoryController.runMaintenanceIfDue")
now := c.clock.Now()
now := r.clock.Now()
if !dueForMaintenance(req, now) {
log.Debug("not due for maintenance")
@ -233,17 +202,15 @@ func (c *resticRepositoryController) runMaintenanceIfDue(req *velerov1api.Restic
// prune failures should be displayed in the `.status.message` field but
// should not cause the repo to move to `NotReady`.
log.Debug("Pruning repo")
if err := c.repositoryManager.PruneRepo(req); err != nil {
if err := r.repositoryManager.PruneRepo(req); err != nil {
log.WithError(err).Warn("error pruning repository")
if patchErr := c.patchResticRepository(req, func(r *velerov1api.ResticRepository) {
r.Status.Message = err.Error()
}); patchErr != nil {
if patchErr := patchHelper.Patch(ctx, req); patchErr != nil {
req.Status.Message = err.Error()
return patchErr
}
}
return c.patchResticRepository(req, func(req *velerov1api.ResticRepository) {
req.Status.LastMaintenanceTime = &metav1.Time{Time: now}
return r.patchResticRepository(ctx, req, patchHelper, log, func(rr *velerov1api.ResticRepository) {
rr.Status.LastMaintenanceTime = &metav1.Time{Time: now}
})
}
@ -251,7 +218,7 @@ func dueForMaintenance(req *velerov1api.ResticRepository, now time.Time) bool {
return req.Status.LastMaintenanceTime == nil || req.Status.LastMaintenanceTime.Add(req.Spec.MaintenanceFrequency.Duration).Before(now)
}
func (c *resticRepositoryController) checkNotReadyRepo(req *velerov1api.ResticRepository, log logrus.FieldLogger) error {
func (r *ResticRepoReconciler) checkNotReadyRepo(ctx context.Context, req *velerov1api.ResticRepository, patchHelper *patch.Helper, log logrus.FieldLogger) error {
// no identifier: can't possibly be ready, so just return
if req.Spec.ResticIdentifier == "" {
return nil
@ -261,11 +228,10 @@ func (c *resticRepositoryController) checkNotReadyRepo(req *velerov1api.ResticRe
// we need to ensure it (first check, if check fails, attempt to init)
// because we don't know if it's been successfully initialized yet.
if err := ensureRepo(req, c.repositoryManager); err != nil {
return c.patchResticRepository(req, repoNotReady(err.Error()))
if err := ensureRepo(req, r.repositoryManager); err != nil {
return r.patchResticRepository(ctx, req, patchHelper, log, repoNotReady(err.Error()))
}
return c.patchResticRepository(req, repoReady())
return r.patchResticRepository(ctx, req, patchHelper, log, repoReady())
}
func repoNotReady(msg string) func(*velerov1api.ResticRepository) {
@ -285,37 +251,11 @@ func repoReady() func(*velerov1api.ResticRepository) {
// patchResticRepository mutates req with the provided mutate function, and patches it
// through the Kube API. After executing this function, req will be updated with both
// the mutation and the results of the Patch() API call.
func (c *resticRepositoryController) patchResticRepository(req *velerov1api.ResticRepository, mutate func(*velerov1api.ResticRepository)) error {
// Record original json
oldData, err := json.Marshal(req)
if err != nil {
return errors.Wrap(err, "error marshalling original ResticRepository")
}
func (r *ResticRepoReconciler) patchResticRepository(ctx context.Context, req *velerov1api.ResticRepository, patchHelper *patch.Helper, log logrus.FieldLogger, mutate func(*velerov1api.ResticRepository)) error {
mutate(req)
// Record new json
newData, err := json.Marshal(req)
if err != nil {
return errors.Wrap(err, "error marshalling updated ResticRepository")
if err := patchHelper.Patch(ctx, req); err != nil {
log.WithError(err).Errorf("error updating restic repository resource %s in namespace %s with err %s", req.Name, req.Namespace, err.Error())
return err
}
patchBytes, err := jsonpatch.CreateMergePatch(oldData, newData)
if err != nil {
return errors.Wrap(err, "error creating json merge patch for ResticRepository")
}
// empty patch: don't apply
if string(patchBytes) == "{}" {
return nil
}
// patch, and if successful, update req
var patched *velerov1api.ResticRepository
if patched, err = c.resticRepositoryClient.ResticRepositories(req.Namespace).Patch(context.TODO(), req.Name, types.MergePatchType, patchBytes, metav1.PatchOptions{}); err != nil {
return errors.Wrap(err, "error patching ResticRepository")
}
req = patched
return nil
}

View File

@ -0,0 +1,198 @@
/*
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"
"testing"
"time"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/cluster-api/util/patch"
ctrl "sigs.k8s.io/controller-runtime"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
resticmokes "github.com/vmware-tanzu/velero/pkg/restic/mocks"
velerotest "github.com/vmware-tanzu/velero/pkg/test"
)
const defaultMaintenanceFrequency = 10 * time.Minute
func mockResticRepoReconciler(t *testing.T, rr *velerov1api.ResticRepository, mockOn string, arg interface{}, ret interface{}) *ResticRepoReconciler {
mgr := &resticmokes.RepositoryManager{}
if mockOn != "" {
mgr.On(mockOn, arg).Return(ret)
}
return NewResticRepoConciler(
velerov1api.DefaultNamespace,
velerotest.NewLogger(),
velerotest.NewFakeControllerRuntimeClient(t),
defaultMaintenanceFrequency,
mgr,
)
}
func mockResticRepositoryCR() *velerov1api.ResticRepository {
return &velerov1api.ResticRepository{
ObjectMeta: metav1.ObjectMeta{
Namespace: velerov1api.DefaultNamespace,
Name: "repo",
},
Spec: velerov1api.ResticRepositorySpec{
MaintenanceFrequency: metav1.Duration{defaultMaintenanceFrequency},
},
}
}
func TestPatchResticRepository(t *testing.T) {
rr := mockResticRepositoryCR()
reconciler := mockResticRepoReconciler(t, rr, "", nil, nil)
err := reconciler.Client.Create(context.TODO(), rr)
assert.NoError(t, err)
patchHelper, err := patch.NewHelper(rr, reconciler.Client)
assert.NoError(t, err)
err = reconciler.patchResticRepository(context.Background(), rr, patchHelper, reconciler.logger, repoReady())
assert.NoError(t, err)
assert.Equal(t, rr.Status.Phase, velerov1api.ResticRepositoryPhaseReady)
err = reconciler.patchResticRepository(context.Background(), rr, patchHelper, reconciler.logger, repoNotReady("not ready"))
assert.NoError(t, err)
assert.NotEqual(t, rr.Status.Phase, velerov1api.ResticRepositoryPhaseReady)
}
func TestCheckNotReadyRepo(t *testing.T) {
rr := mockResticRepositoryCR()
reconciler := mockResticRepoReconciler(t, rr, "ConnectToRepo", rr, nil)
err := reconciler.Client.Create(context.TODO(), rr)
assert.NoError(t, err)
patchHelper, err := patch.NewHelper(rr, reconciler.Client)
assert.NoError(t, err)
err = reconciler.checkNotReadyRepo(context.TODO(), rr, patchHelper, reconciler.logger)
assert.NoError(t, err)
assert.Equal(t, rr.Status.Phase, velerov1api.ResticRepositoryPhase(""))
rr.Spec.ResticIdentifier = "s3:test.amazonaws.com/bucket/restic"
err = reconciler.checkNotReadyRepo(context.TODO(), rr, patchHelper, reconciler.logger)
assert.NoError(t, err)
assert.Equal(t, rr.Status.Phase, velerov1api.ResticRepositoryPhaseReady)
}
func TestRunMaintenanceIfDue(t *testing.T) {
rr := mockResticRepositoryCR()
reconciler := mockResticRepoReconciler(t, rr, "PruneRepo", rr, nil)
err := reconciler.Client.Create(context.TODO(), rr)
assert.NoError(t, err)
patchHelper, err := patch.NewHelper(rr, reconciler.Client)
assert.NoError(t, err)
lastTm := rr.Status.LastMaintenanceTime
err = reconciler.runMaintenanceIfDue(context.TODO(), rr, patchHelper, reconciler.logger)
assert.NoError(t, err)
assert.NotEqual(t, rr.Status.LastMaintenanceTime, lastTm)
rr.Status.LastMaintenanceTime = &metav1.Time{Time: time.Now()}
lastTm = rr.Status.LastMaintenanceTime
err = reconciler.runMaintenanceIfDue(context.TODO(), rr, patchHelper, reconciler.logger)
assert.NoError(t, err)
assert.Equal(t, rr.Status.LastMaintenanceTime, lastTm)
}
func TestInitializeRepo(t *testing.T) {
rr := mockResticRepositoryCR()
rr.Spec.BackupStorageLocation = "default"
reconciler := mockResticRepoReconciler(t, rr, "ConnectToRepo", rr, nil)
err := reconciler.Client.Create(context.TODO(), rr)
assert.NoError(t, err)
patchHelper, err := patch.NewHelper(rr, reconciler.Client)
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, patchHelper)
assert.NoError(t, err)
assert.Equal(t, rr.Status.Phase, velerov1api.ResticRepositoryPhaseReady)
}
func TestResticRepoReconcile(t *testing.T) {
tests := []struct {
name string
repo *velerov1api.ResticRepository
expectNil bool
}{
{
name: "test on api server not found",
repo: &velerov1api.ResticRepository{
ObjectMeta: metav1.ObjectMeta{
Namespace: velerov1api.DefaultNamespace,
Name: "unknown",
},
Spec: velerov1api.ResticRepositorySpec{
MaintenanceFrequency: metav1.Duration{defaultMaintenanceFrequency},
},
},
expectNil: true,
},
{
name: "test on initialize repo",
repo: &velerov1api.ResticRepository{
ObjectMeta: metav1.ObjectMeta{
Namespace: velerov1api.DefaultNamespace,
Name: "repo",
},
Spec: velerov1api.ResticRepositorySpec{
MaintenanceFrequency: metav1.Duration{defaultMaintenanceFrequency},
},
},
expectNil: true,
},
{
name: "test on repo with new phase",
repo: &velerov1api.ResticRepository{
ObjectMeta: metav1.ObjectMeta{
Namespace: velerov1api.DefaultNamespace,
Name: "repo",
},
Spec: velerov1api.ResticRepositorySpec{
MaintenanceFrequency: metav1.Duration{defaultMaintenanceFrequency},
},
Status: velerov1api.ResticRepositoryStatus{
Phase: velerov1api.ResticRepositoryPhaseNew,
},
},
expectNil: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
reconciler := mockResticRepoReconciler(t, test.repo, "", 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)
}
})
}
}

View File

@ -0,0 +1,147 @@
/*
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.*/
// Code generated by mockery v2.10.2. DO NOT EDIT.
package mocks
import (
context "context"
mock "github.com/stretchr/testify/mock"
restic "github.com/vmware-tanzu/velero/pkg/restic"
v1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
)
// RepositoryManager is an autogenerated mock type for the RepositoryManager type
type RepositoryManager struct {
mock.Mock
}
// ConnectToRepo provides a mock function with given fields: repo
func (_m *RepositoryManager) ConnectToRepo(repo *v1.ResticRepository) error {
ret := _m.Called(repo)
var r0 error
if rf, ok := ret.Get(0).(func(*v1.ResticRepository) error); ok {
r0 = rf(repo)
} else {
r0 = ret.Error(0)
}
return r0
}
// Forget provides a mock function with given fields: _a0, _a1
func (_m *RepositoryManager) Forget(_a0 context.Context, _a1 restic.SnapshotIdentifier) error {
ret := _m.Called(_a0, _a1)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, restic.SnapshotIdentifier) error); ok {
r0 = rf(_a0, _a1)
} else {
r0 = ret.Error(0)
}
return r0
}
// InitRepo provides a mock function with given fields: repo
func (_m *RepositoryManager) InitRepo(repo *v1.ResticRepository) error {
ret := _m.Called(repo)
var r0 error
if rf, ok := ret.Get(0).(func(*v1.ResticRepository) error); ok {
r0 = rf(repo)
} else {
r0 = ret.Error(0)
}
return r0
}
// NewBackupper provides a mock function with given fields: _a0, _a1
func (_m *RepositoryManager) NewBackupper(_a0 context.Context, _a1 *v1.Backup) (restic.Backupper, error) {
ret := _m.Called(_a0, _a1)
var r0 restic.Backupper
if rf, ok := ret.Get(0).(func(context.Context, *v1.Backup) restic.Backupper); ok {
r0 = rf(_a0, _a1)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(restic.Backupper)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *v1.Backup) error); ok {
r1 = rf(_a0, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// NewRestorer provides a mock function with given fields: _a0, _a1
func (_m *RepositoryManager) NewRestorer(_a0 context.Context, _a1 *v1.Restore) (restic.Restorer, error) {
ret := _m.Called(_a0, _a1)
var r0 restic.Restorer
if rf, ok := ret.Get(0).(func(context.Context, *v1.Restore) restic.Restorer); ok {
r0 = rf(_a0, _a1)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(restic.Restorer)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *v1.Restore) error); ok {
r1 = rf(_a0, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PruneRepo provides a mock function with given fields: repo
func (_m *RepositoryManager) PruneRepo(repo *v1.ResticRepository) error {
ret := _m.Called(repo)
var r0 error
if rf, ok := ret.Get(0).(func(*v1.ResticRepository) error); ok {
r0 = rf(repo)
} else {
r0 = ret.Error(0)
}
return r0
}
// UnlockRepo provides a mock function with given fields: repo
func (_m *RepositoryManager) UnlockRepo(repo *v1.ResticRepository) error {
ret := _m.Called(repo)
var r0 error
if rf, ok := ret.Get(0).(func(*v1.ResticRepository) error); ok {
r0 = rf(repo)
} else {
r0 = ret.Error(0)
}
return r0
}