parent
3ec96e2eac
commit
06d3d731ed
|
@ -0,0 +1 @@
|
|||
Convert Restic Repository resource/controller to the Kubebuilder framework
|
|
@ -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: ""
|
||||
|
|
|
@ -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
|
@ -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:
|
||||
|
|
|
@ -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 {
|
|
@ -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"
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue