Convert DownloadRequest resource/controller to kubebuilder (#3004)

* Migrate DownloadRequest types to kubebuilder

Signed-off-by: Carlisia <carlisia@vmware.com>

* Migrate controller to kubebuilder

Signed-off-by: Carlisia <carlisia@vmware.com>

* Migrate download request cli to kubebuilder

Signed-off-by: Carlisia <carlisia@vmware.com>

* Format w make update

Signed-off-by: Carlisia <carlisia@vmware.com>

* Remove download file

Signed-off-by: Carlisia <carlisia@vmware.com>

* Remove kubebuilder from backup/restore apis

Signed-off-by: Carlisia <carlisia@vmware.com>

* Fix test description

Signed-off-by: Carlisia <carlisia@vmware.com>

* Import cleanups

Signed-off-by: Carlisia <carlisia@vmware.com>

* Refactor for controller runtime version update

Signed-off-by: Carlisia <carlisia@vmware.com>

* Remove year from the copyright

Signed-off-by: Carlisia <carlisia@vmware.com>

* Check for expiration regardless of phase

Signed-off-by: Carlisia <carlisia@vmware.com>

* Fix typos and godoc

Signed-off-by: Carlisia <carlisia@vmware.com>

* Fix test setup and fix a test case

Signed-off-by: Carlisia <carlisia@vmware.com>
pull/3442/head
Carlisia Thompson 2021-03-01 10:28:46 -08:00 committed by GitHub
parent c80ad61bbc
commit 11bfe82342
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 542 additions and 818 deletions

View File

@ -32,7 +32,8 @@ spec:
singular: downloadrequest
preserveUnknownFields: false
scope: Namespaced
subresources: {}
subresources:
status: {}
validation:
openAPIV3Schema:
description: DownloadRequest is a request to download an artifact from backup

File diff suppressed because one or more lines are too long

View File

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

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// TODO(2.0) After converting all controllers to runttime-controller,
// TODO(2.0) After converting all controllers to runtime-controller,
// the functions in this file will no longer be needed and should be removed.
package managercontroller

View File

@ -90,7 +90,7 @@ type BackupStorageLocationStatus struct {
AccessMode BackupStorageLocationAccessMode `json:"accessMode,omitempty"`
}
// TODO(2.0) After converting all resources to use the runttime-controller client,
// 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
@ -116,7 +116,7 @@ type BackupStorageLocation struct {
Status BackupStorageLocationStatus `json:"status,omitempty"`
}
// TODO(2.0) After converting all resources to use the runttime-controller client,
// 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

View File

@ -1,5 +1,5 @@
/*
Copyright 2017 the Velero contributors.
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.
@ -77,8 +77,14 @@ type DownloadRequestStatus struct {
Expiration *metav1.Time `json:"expiration,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.
// +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="Status",type="string",JSONPath=".status.phase",description="DownloadRequest status such as New/Processed"
// +kubebuilder:printcolumn:name="Target Name",type="string",JSONPath=".spec.target.name",description="Name of the associated Kubernetes resource"
// +kubebuilder:printcolumn:name="Target Kind",type="string",JSONPath=".spec.target.kind",description="Type of file to download"
@ -99,7 +105,12 @@ type DownloadRequest struct {
Status DownloadRequestStatus `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=downloadrequests,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=velero.io,resources=downloadrequests/status,verbs=get;update;patch
// DownloadRequestList is a list of DownloadRequests.
type DownloadRequestList struct {

View File

@ -20,7 +20,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// TODO(2.0) After converting all resources to use the runttime-controller client,
// 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
@ -89,7 +89,7 @@ type ServerStatusRequestStatus struct {
Plugins []PluginInfo `json:"plugins,omitempty"`
}
// TODO(2.0) After converting all resources to use the runttime-controller client,
// 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

View File

@ -0,0 +1,62 @@
/*
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 builder
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
)
// DownloadRequestBuilder builds DownloadRequest objects.
type DownloadRequestBuilder struct {
object *velerov1api.DownloadRequest
}
// ForDownloadRequest is the constructor for a DownloadRequestBuilder.
func ForDownloadRequest(ns, name string) *DownloadRequestBuilder {
return &DownloadRequestBuilder{
object: &velerov1api.DownloadRequest{
TypeMeta: metav1.TypeMeta{
APIVersion: velerov1api.SchemeGroupVersion.String(),
Kind: "DownloadRequest",
},
ObjectMeta: metav1.ObjectMeta{
Namespace: ns,
Name: name,
},
},
}
}
// Result returns the built DownloadRequest.
func (b *DownloadRequestBuilder) Result() *velerov1api.DownloadRequest {
return b.object
}
// Phase sets the DownloadRequest's status phase.
func (b *DownloadRequestBuilder) Phase(phase velerov1api.DownloadRequestPhase) *DownloadRequestBuilder {
b.object.Status.Phase = phase
return b
}
// Target sets the DownloadRequest's target kind and target name.
func (b *DownloadRequestBuilder) Target(targetKind velerov1api.DownloadTargetKind, targetName string) *DownloadRequestBuilder {
b.object.Spec.Target.Kind = targetKind
b.object.Spec.Target.Name = targetName
return b
}

View File

@ -1,5 +1,5 @@
/*
Copyright 2020 the Velero contributors.
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.
@ -56,6 +56,9 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command {
veleroClient, err := f.Client()
cmd.CheckError(err)
kbClient, err := f.KubebuilderClient()
cmd.CheckError(err)
var backups *velerov1api.BackupList
if len(args) > 0 {
backups = new(velerov1api.BackupList)
@ -99,7 +102,7 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command {
}
}
s := output.DescribeBackup(&backup, deleteRequestList.Items, podVolumeBackupList.Items, vscList.Items, details, veleroClient, insecureSkipTLSVerify, caCertFile)
s := output.DescribeBackup(context.Background(), kbClient, &backup, deleteRequestList.Items, podVolumeBackupList.Items, vscList.Items, details, veleroClient, insecureSkipTLSVerify, caCertFile)
if first {
first = false
fmt.Print(s)

View File

@ -1,5 +1,5 @@
/*
Copyright 2020 the Velero contributors.
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.
@ -28,7 +28,7 @@ import (
"github.com/spf13/pflag"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/client"
"github.com/vmware-tanzu/velero/pkg/cmd"
"github.com/vmware-tanzu/velero/pkg/cmd/util/downloadrequest"
@ -115,7 +115,7 @@ func (o *DownloadOptions) Complete(args []string) error {
}
func (o *DownloadOptions) Run(c *cobra.Command, f client.Factory) error {
veleroClient, err := f.Client()
kbClient, err := f.KubebuilderClient()
cmd.CheckError(err)
backupDest, err := os.OpenFile(o.Output, o.writeOptions, 0600)
@ -124,7 +124,7 @@ func (o *DownloadOptions) Run(c *cobra.Command, f client.Factory) error {
}
defer backupDest.Close()
err = downloadrequest.Stream(veleroClient.VeleroV1(), f.Namespace(), o.Name, v1.DownloadTargetKindBackupContents, backupDest, o.Timeout, o.InsecureSkipTLSVerify, o.caCertFile)
err = downloadrequest.Stream(context.Background(), kbClient, f.Namespace(), o.Name, velerov1api.DownloadTargetKindBackupContents, backupDest, o.Timeout, o.InsecureSkipTLSVerify, o.caCertFile)
if err != nil {
os.Remove(o.Output)
cmd.CheckError(err)

View File

@ -1,5 +1,5 @@
/*
Copyright 2017 the Velero contributors.
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.
@ -26,7 +26,7 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/client"
"github.com/vmware-tanzu/velero/pkg/cmd"
"github.com/vmware-tanzu/velero/pkg/cmd/util/downloadrequest"
@ -52,6 +52,9 @@ func NewLogsCommand(f client.Factory) *cobra.Command {
veleroClient, err := f.Client()
cmd.CheckError(err)
kbClient, err := f.KubebuilderClient()
cmd.CheckError(err)
backup, err := veleroClient.VeleroV1().Backups(f.Namespace()).Get(context.TODO(), backupName, metav1.GetOptions{})
if apierrors.IsNotFound(err) {
cmd.Exit("Backup %q does not exist.", backupName)
@ -60,14 +63,14 @@ func NewLogsCommand(f client.Factory) *cobra.Command {
}
switch backup.Status.Phase {
case v1.BackupPhaseCompleted, v1.BackupPhasePartiallyFailed, v1.BackupPhaseFailed:
case velerov1api.BackupPhaseCompleted, velerov1api.BackupPhasePartiallyFailed, velerov1api.BackupPhaseFailed:
// terminal phases, do nothing.
default:
cmd.Exit("Logs for backup %q are not available until it's finished processing. Please wait "+
"until the backup has a phase of Completed or Failed and try again.", backupName)
}
err = downloadrequest.Stream(veleroClient.VeleroV1(), f.Namespace(), backupName, v1.DownloadTargetKindBackupLog, os.Stdout, timeout, insecureSkipTLSVerify, caCertFile)
err = downloadrequest.Stream(context.Background(), kbClient, f.Namespace(), backupName, velerov1api.DownloadTargetKindBackupLog, os.Stdout, timeout, insecureSkipTLSVerify, caCertFile)
cmd.CheckError(err)
},
}

View File

@ -1,5 +1,5 @@
/*
Copyright 2020 the Velero contributors.
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.
@ -24,7 +24,7 @@ import (
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/client"
"github.com/vmware-tanzu/velero/pkg/cmd"
"github.com/vmware-tanzu/velero/pkg/cmd/util/output"
@ -51,9 +51,12 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command {
veleroClient, err := f.Client()
cmd.CheckError(err)
var restores *api.RestoreList
kbClient, err := f.KubebuilderClient()
cmd.CheckError(err)
var restores *velerov1api.RestoreList
if len(args) > 0 {
restores = new(api.RestoreList)
restores = new(velerov1api.RestoreList)
for _, name := range args {
restore, err := veleroClient.VeleroV1().Restores(f.Namespace()).Get(context.TODO(), name, metav1.GetOptions{})
cmd.CheckError(err)
@ -72,7 +75,7 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command {
fmt.Fprintf(os.Stderr, "error getting PodVolumeRestores for restore %s: %v\n", restore.Name, err)
}
s := output.DescribeRestore(&restore, podvolumeRestoreList.Items, details, veleroClient, insecureSkipTLSVerify, caCertFile)
s := output.DescribeRestore(context.Background(), kbClient, &restore, podvolumeRestoreList.Items, details, veleroClient, insecureSkipTLSVerify, caCertFile)
if first {
first = false
fmt.Print(s)

View File

@ -1,5 +1,5 @@
/*
Copyright 2020 the Velero contributors.
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.
@ -26,7 +26,7 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/client"
"github.com/vmware-tanzu/velero/pkg/cmd"
"github.com/vmware-tanzu/velero/pkg/cmd/util/downloadrequest"
@ -52,6 +52,9 @@ func NewLogsCommand(f client.Factory) *cobra.Command {
veleroClient, err := f.Client()
cmd.CheckError(err)
kbClient, err := f.KubebuilderClient()
cmd.CheckError(err)
restore, err := veleroClient.VeleroV1().Restores(f.Namespace()).Get(context.TODO(), restoreName, metav1.GetOptions{})
if apierrors.IsNotFound(err) {
cmd.Exit("Restore %q does not exist.", restoreName)
@ -60,14 +63,14 @@ func NewLogsCommand(f client.Factory) *cobra.Command {
}
switch restore.Status.Phase {
case v1.RestorePhaseCompleted, v1.RestorePhaseFailed, v1.RestorePhasePartiallyFailed:
case velerov1api.RestorePhaseCompleted, velerov1api.RestorePhaseFailed, velerov1api.RestorePhasePartiallyFailed:
// terminal phases, don't exit.
default:
cmd.Exit("Logs for restore %q are not available until it's finished processing. Please wait "+
"until the restore has a phase of Completed or Failed and try again.", restoreName)
}
err = downloadrequest.Stream(veleroClient.VeleroV1(), f.Namespace(), restoreName, v1.DownloadTargetKindRestoreLog, os.Stdout, timeout, insecureSkipTLSVerify, caCertFile)
err = downloadrequest.Stream(context.Background(), kbClient, f.Namespace(), restoreName, velerov1api.DownloadTargetKindRestoreLog, os.Stdout, timeout, insecureSkipTLSVerify, caCertFile)
cmd.CheckError(err)
},
}

View File

@ -731,24 +731,6 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string
}
}
downloadrequestControllerRunInfo := func() controllerRunInfo {
downloadRequestController := controller.NewDownloadRequestController(
s.veleroClient.VeleroV1(),
s.sharedInformerFactory.Velero().V1().DownloadRequests(),
s.sharedInformerFactory.Velero().V1().Restores().Lister(),
s.mgr.GetClient(),
s.sharedInformerFactory.Velero().V1().Backups().Lister(),
newPluginManager,
persistence.NewObjectBackupStoreGetter(),
s.logger,
)
return controllerRunInfo{
controller: downloadRequestController,
numWorkers: defaultControllerWorkers,
}
}
enabledControllers := map[string]func() controllerRunInfo{
controller.BackupSync: backupSyncControllerRunInfo,
controller.Backup: backupControllerRunInfo,
@ -757,12 +739,11 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string
controller.BackupDeletion: deletionControllerRunInfo,
controller.Restore: restoreControllerRunInfo,
controller.ResticRepo: resticRepoControllerRunInfo,
controller.DownloadRequest: downloadrequestControllerRunInfo,
}
// Note: all runtime type controllers that can be disabled are grouped separately, below:
enabledRuntimeControllers := map[string]struct{}{
controller.ServerStatusRequest: {},
}
enabledRuntimeControllers := make(map[string]struct{})
enabledRuntimeControllers[controller.ServerStatusRequest] = struct{}{}
enabledRuntimeControllers[controller.DownloadRequest] = struct{}{}
if s.config.restoreOnly {
s.logger.Info("Restore only mode - not starting the backup, schedule, delete-backup, or GC controllers")
@ -839,6 +820,20 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string
}
}
if _, ok := enabledRuntimeControllers[controller.DownloadRequest]; ok {
r := controller.DownloadRequestReconciler{
Scheme: s.mgr.GetScheme(),
Client: s.mgr.GetClient(),
Clock: clock.RealClock{},
NewPluginManager: newPluginManager,
BackupStoreGetter: persistence.NewObjectBackupStoreGetter(),
Log: s.logger,
}
if err := r.SetupWithManager(s.mgr); err != nil {
s.logger.Fatal(err, "unable to create controller", "controller", controller.DownloadRequest)
}
}
// TODO(2.0): presuming all controllers and resources are converted to runtime-controller
// by v2.0, the block from this line and including the `s.mgr.Start() will be
// deprecated, since the manager auto-starts all the caches. Until then, we need to start the

View File

@ -1,5 +1,5 @@
/*
Copyright 2017 the Velero contributors.
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.
@ -29,74 +29,50 @@ import (
"time"
"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/apimachinery/pkg/util/wait"
kbclient "sigs.k8s.io/controller-runtime/pkg/client"
v1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
velerov1client "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/typed/velero/v1"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/builder"
)
// ErrNotFound is exported for external packages to check for when a file is
// not found
var ErrNotFound = errors.New("file not found")
func Stream(client velerov1client.DownloadRequestsGetter, namespace, name string, kind v1.DownloadTargetKind, w io.Writer, timeout time.Duration, insecureSkipTLSVerify bool, caCertFile string) error {
req := &v1.DownloadRequest{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: fmt.Sprintf("%s-%s", name, time.Now().Format("20060102150405")),
},
Spec: v1.DownloadRequestSpec{
Target: v1.DownloadTarget{
Kind: kind,
Name: name,
},
},
}
func Stream(ctx context.Context, kbClient kbclient.Client, namespace, name string, kind velerov1api.DownloadTargetKind, w io.Writer, timeout time.Duration, insecureSkipTLSVerify bool, caCertFile string) error {
reqName := fmt.Sprintf("%s-%s", name, time.Now().Format("20060102150405"))
created := builder.ForDownloadRequest(namespace, reqName).Target(kind, name).Result()
req, err := client.DownloadRequests(namespace).Create(context.TODO(), req, metav1.CreateOptions{})
if err != nil {
if err := kbClient.Create(context.Background(), created, &kbclient.CreateOptions{}); err != nil {
return errors.WithStack(err)
}
defer client.DownloadRequests(namespace).Delete(context.TODO(), req.Name, metav1.DeleteOptions{})
listOptions := metav1.ListOptions{
FieldSelector: "metadata.name=" + req.Name,
ResourceVersion: req.ResourceVersion,
}
watcher, err := client.DownloadRequests(namespace).Watch(context.TODO(), listOptions)
if err != nil {
return errors.WithStack(err)
}
defer watcher.Stop()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
expired := time.NewTimer(timeout)
defer expired.Stop()
key := kbclient.ObjectKey{Name: created.Name, Namespace: namespace}
checkFunc := func() {
updated := &velerov1api.DownloadRequest{}
if err := kbClient.Get(ctx, key, updated); err != nil {
return
}
Loop:
for {
select {
case <-expired.C:
return errors.New("timed out waiting for download URL")
case e := <-watcher.ResultChan():
updated, ok := e.Object.(*v1.DownloadRequest)
if !ok {
return errors.Errorf("unexpected type %T", e.Object)
}
// TODO: once the minimum supported Kubernetes version is v1.9.0, remove the following check.
// See http://issue.k8s.io/51046 for details.
if updated.Name != created.Name {
return
}
switch e.Type {
case watch.Deleted:
errors.New("download request was unexpectedly deleted")
case watch.Modified:
if updated.Status.DownloadURL != "" {
req = updated
break Loop
}
}
if updated.Status.DownloadURL != "" {
created = updated
cancel()
}
}
if req.Status.DownloadURL == "" {
wait.Until(checkFunc, 25*time.Millisecond, ctx.Done())
if created.Status.DownloadURL == "" {
return ErrNotFound
}
@ -134,7 +110,7 @@ Loop:
ExpectContinueTimeout: defaultTransport.ExpectContinueTimeout,
}
httpReq, err := http.NewRequest("GET", req.Status.DownloadURL, nil)
httpReq, err := http.NewRequest("GET", created.Status.DownloadURL, nil)
if err != nil {
return err
}
@ -164,7 +140,7 @@ Loop:
}
reader := resp.Body
if kind != v1.DownloadTargetKindBackupContents {
if kind != velerov1api.DownloadTargetKindBackupContents {
// need to decompress logs
gzipReader, err := gzip.NewReader(resp.Body)
if err != nil {

View File

@ -1,225 +0,0 @@
/*
Copyright 2017 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 downloadrequest
import (
"bytes"
"compress/gzip"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/watch"
core "k8s.io/client-go/testing"
v1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/fake"
)
func TestStream(t *testing.T) {
tests := []struct {
name string
kind v1.DownloadTargetKind
timeout time.Duration
createError error
watchError error
watchAdds []runtime.Object
watchModifies []runtime.Object
watchDeletes []runtime.Object
updateWithURL bool
statusCode int
body string
deleteError error
expectedError string
}{
{
name: "error creating req",
createError: errors.New("foo"),
kind: v1.DownloadTargetKindBackupLog,
expectedError: "foo",
},
{
name: "error creating watch",
watchError: errors.New("bar"),
kind: v1.DownloadTargetKindBackupLog,
expectedError: "bar",
},
{
name: "timed out",
kind: v1.DownloadTargetKindBackupLog,
timeout: time.Millisecond,
expectedError: "timed out waiting for download URL",
},
{
name: "unexpected watch type",
kind: v1.DownloadTargetKindBackupLog,
watchAdds: []runtime.Object{&v1.Backup{}},
expectedError: "unexpected type *v1.Backup",
},
{
name: "other requests added/updated/deleted first",
kind: v1.DownloadTargetKindBackupLog,
watchAdds: []runtime.Object{
newDownloadRequest("foo").DownloadRequest,
},
watchModifies: []runtime.Object{
newDownloadRequest("foo").DownloadRequest,
},
watchDeletes: []runtime.Object{
newDownloadRequest("foo").DownloadRequest,
},
updateWithURL: true,
statusCode: http.StatusOK,
body: "download body",
},
{
name: "http error",
kind: v1.DownloadTargetKindBackupLog,
updateWithURL: true,
statusCode: http.StatusInternalServerError,
body: "some error",
expectedError: "request failed: some error",
},
}
const testTimeout = 30 * time.Second
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
client := fake.NewSimpleClientset()
created := make(chan *v1.DownloadRequest, 1)
client.PrependReactor("create", "downloadrequests", func(action core.Action) (bool, runtime.Object, error) {
createAction := action.(core.CreateAction)
created <- createAction.GetObject().(*v1.DownloadRequest)
return true, createAction.GetObject(), test.createError
})
fakeWatch := watch.NewFake()
client.PrependWatchReactor("downloadrequests", core.DefaultWatchReactor(fakeWatch, test.watchError))
deleted := make(chan string, 1)
client.PrependReactor("delete", "downloadrequests", func(action core.Action) (bool, runtime.Object, error) {
deleteAction := action.(core.DeleteAction)
deleted <- deleteAction.GetName()
return true, nil, test.deleteError
})
timeout := test.timeout
if timeout == 0 {
timeout = testTimeout
}
var server *httptest.Server
var url string
if test.updateWithURL {
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(test.statusCode)
if test.statusCode == http.StatusOK {
gzipWriter := gzip.NewWriter(w)
fmt.Fprintf(gzipWriter, test.body)
gzipWriter.Close()
return
}
fmt.Fprintf(w, test.body)
}))
defer server.Close()
url = server.URL
}
output := new(bytes.Buffer)
errCh := make(chan error)
go func() {
err := Stream(client.VeleroV1(), "namespace", "name", test.kind, output, timeout, false, "")
errCh <- err
}()
for i := range test.watchAdds {
fakeWatch.Add(test.watchAdds[i])
}
for i := range test.watchModifies {
fakeWatch.Modify(test.watchModifies[i])
}
for i := range test.watchDeletes {
fakeWatch.Delete(test.watchDeletes[i])
}
var createdName string
if test.updateWithURL {
select {
case r := <-created:
createdName = r.Name
r.Status.DownloadURL = url
fakeWatch.Modify(r)
case <-time.After(testTimeout):
t.Fatalf("created object not received")
}
}
var err error
select {
case err = <-errCh:
case <-time.After(testTimeout):
t.Fatal("test timed out")
}
if test.expectedError != "" {
require.EqualError(t, err, test.expectedError)
return
}
require.NoError(t, err)
if test.statusCode != http.StatusOK {
assert.EqualError(t, err, "request failed: "+test.body)
return
}
assert.Equal(t, test.body, output.String())
select {
case name := <-deleted:
assert.Equal(t, createdName, name)
default:
t.Fatal("download request was not deleted")
}
})
}
}
type downloadRequest struct {
*v1.DownloadRequest
}
func newDownloadRequest(name string) *downloadRequest {
return &downloadRequest{
DownloadRequest: &v1.DownloadRequest{
ObjectMeta: metav1.ObjectMeta{
Namespace: v1.DefaultNamespace,
Name: name,
},
},
}
}

View File

@ -1,5 +1,5 @@
/*
Copyright 2021 the Velero contributors.
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.
@ -18,6 +18,7 @@ package output
import (
"bytes"
"context"
"encoding/json"
"fmt"
"sort"
@ -28,6 +29,7 @@ import (
snapshotv1beta1api "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1beta1"
"github.com/fatih/color"
kbclient "sigs.k8s.io/controller-runtime/pkg/client"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/cmd/util/downloadrequest"
@ -38,6 +40,8 @@ import (
// DescribeBackup describes a backup in human-readable format.
func DescribeBackup(
ctx context.Context,
kbClient kbclient.Client,
backup *velerov1api.Backup,
deleteRequests []velerov1api.DeleteBackupRequest,
podVolumeBackups []velerov1api.PodVolumeBackup,
@ -90,7 +94,7 @@ func DescribeBackup(
DescribeBackupSpec(d, backup.Spec)
d.Println()
DescribeBackupStatus(d, backup, details, veleroClient, insecureSkipTLSVerify, caCertFile)
DescribeBackupStatus(ctx, kbClient, d, backup, details, veleroClient, insecureSkipTLSVerify, caCertFile)
if len(deleteRequests) > 0 {
d.Println()
@ -241,7 +245,7 @@ func DescribeBackupSpec(d *Describer, spec velerov1api.BackupSpec) {
}
// DescribeBackupStatus describes a backup status in human-readable format.
func DescribeBackupStatus(d *Describer, backup *velerov1api.Backup, details bool, veleroClient clientset.Interface, insecureSkipTLSVerify bool, caCertPath string) {
func DescribeBackupStatus(ctx context.Context, kbClient kbclient.Client, d *Describer, backup *velerov1api.Backup, details bool, veleroClient clientset.Interface, insecureSkipTLSVerify bool, caCertPath string) {
status := backup.Status
// Status.Version has been deprecated, use Status.FormatVersion
@ -280,7 +284,7 @@ func DescribeBackupStatus(d *Describer, backup *velerov1api.Backup, details bool
}
if details {
describeBackupResourceList(d, backup, veleroClient, insecureSkipTLSVerify, caCertPath)
describeBackupResourceList(ctx, kbClient, d, backup, insecureSkipTLSVerify, caCertPath)
d.Println()
}
@ -291,7 +295,7 @@ func DescribeBackupStatus(d *Describer, backup *velerov1api.Backup, details bool
}
buf := new(bytes.Buffer)
if err := downloadrequest.Stream(veleroClient.VeleroV1(), backup.Namespace, backup.Name, velerov1api.DownloadTargetKindBackupVolumeSnapshots, buf, downloadRequestTimeout, insecureSkipTLSVerify, caCertPath); err != nil {
if err := downloadrequest.Stream(ctx, kbClient, backup.Namespace, backup.Name, velerov1api.DownloadTargetKindBackupVolumeSnapshots, buf, downloadRequestTimeout, insecureSkipTLSVerify, caCertPath); err != nil {
d.Printf("Velero-Native Snapshots:\t<error getting snapshot info: %v>\n", err)
return
}
@ -312,9 +316,9 @@ func DescribeBackupStatus(d *Describer, backup *velerov1api.Backup, details bool
d.Printf("Velero-Native Snapshots: <none included>\n")
}
func describeBackupResourceList(d *Describer, backup *velerov1api.Backup, veleroClient clientset.Interface, insecureSkipTLSVerify bool, caCertPath string) {
func describeBackupResourceList(ctx context.Context, kbClient kbclient.Client, d *Describer, backup *velerov1api.Backup, insecureSkipTLSVerify bool, caCertPath string) {
buf := new(bytes.Buffer)
if err := downloadrequest.Stream(veleroClient.VeleroV1(), backup.Namespace, backup.Name, velerov1api.DownloadTargetKindBackupResourceList, buf, downloadRequestTimeout, insecureSkipTLSVerify, caCertPath); err != nil {
if err := downloadrequest.Stream(ctx, kbClient, backup.Namespace, backup.Name, velerov1api.DownloadTargetKindBackupResourceList, buf, downloadRequestTimeout, insecureSkipTLSVerify, caCertPath); err != nil {
if err == downloadrequest.ErrNotFound {
// the backup resource list could be missing if (other reasons may exist as well):
// - the backup was taken prior to v1.1; or

View File

@ -1,5 +1,5 @@
/*
Copyright 2017, 2019 the Velero contributors.
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.
@ -18,40 +18,42 @@ package output
import (
"bytes"
"context"
"encoding/json"
"fmt"
"sort"
"strings"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kbclient "sigs.k8s.io/controller-runtime/pkg/client"
"github.com/fatih/color"
v1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/cmd/util/downloadrequest"
clientset "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned"
pkgrestore "github.com/vmware-tanzu/velero/pkg/restore"
)
func DescribeRestore(restore *v1.Restore, podVolumeRestores []v1.PodVolumeRestore, details bool, veleroClient clientset.Interface, insecureSkipTLSVerify bool, caCertFile string) string {
func DescribeRestore(ctx context.Context, kbClient kbclient.Client, restore *velerov1api.Restore, podVolumeRestores []velerov1api.PodVolumeRestore, details bool, veleroClient clientset.Interface, insecureSkipTLSVerify bool, caCertFile string) string {
return Describe(func(d *Describer) {
d.DescribeMetadata(restore.ObjectMeta)
d.Println()
phase := restore.Status.Phase
if phase == "" {
phase = v1.RestorePhaseNew
phase = velerov1api.RestorePhaseNew
}
phaseString := string(phase)
switch phase {
case v1.RestorePhaseCompleted:
case velerov1api.RestorePhaseCompleted:
phaseString = color.GreenString(phaseString)
case v1.RestorePhaseFailedValidation, v1.RestorePhasePartiallyFailed, v1.RestorePhaseFailed:
case velerov1api.RestorePhaseFailedValidation, velerov1api.RestorePhasePartiallyFailed, velerov1api.RestorePhaseFailed:
phaseString = color.RedString(phaseString)
}
resultsNote := ""
if phase == v1.RestorePhaseFailed || phase == v1.RestorePhasePartiallyFailed {
if phase == velerov1api.RestorePhaseFailed || phase == velerov1api.RestorePhasePartiallyFailed {
resultsNote = fmt.Sprintf(" (run 'velero restore logs %s' for more information)", restore.Name)
}
@ -79,7 +81,7 @@ func DescribeRestore(restore *v1.Restore, podVolumeRestores []v1.PodVolumeRestor
}
}
describeRestoreResults(d, restore, veleroClient, insecureSkipTLSVerify, caCertFile)
describeRestoreResults(ctx, kbClient, d, restore, insecureSkipTLSVerify, caCertFile)
d.Println()
d.Printf("Backup:\t%s\n", restore.Spec.BackupName)
@ -143,7 +145,7 @@ func DescribeRestore(restore *v1.Restore, podVolumeRestores []v1.PodVolumeRestor
})
}
func describeRestoreResults(d *Describer, restore *v1.Restore, veleroClient clientset.Interface, insecureSkipTLSVerify bool, caCertPath string) {
func describeRestoreResults(ctx context.Context, kbClient kbclient.Client, d *Describer, restore *velerov1api.Restore, insecureSkipTLSVerify bool, caCertPath string) {
if restore.Status.Warnings == 0 && restore.Status.Errors == 0 {
return
}
@ -151,7 +153,7 @@ func describeRestoreResults(d *Describer, restore *v1.Restore, veleroClient clie
var buf bytes.Buffer
var resultMap map[string]pkgrestore.Result
if err := downloadrequest.Stream(veleroClient.VeleroV1(), restore.Namespace, restore.Name, v1.DownloadTargetKindRestoreResults, &buf, downloadRequestTimeout, insecureSkipTLSVerify, caCertPath); err != nil {
if err := downloadrequest.Stream(ctx, kbClient, restore.Namespace, restore.Name, velerov1api.DownloadTargetKindRestoreResults, &buf, downloadRequestTimeout, insecureSkipTLSVerify, caCertPath); err != nil {
d.Printf("Warnings:\t<error getting warnings: %v>\n\nErrors:\t<error getting errors: %v>\n", err, err)
return
}
@ -186,7 +188,7 @@ func describeRestoreResult(d *Describer, name string, result pkgrestore.Result)
}
// describePodVolumeRestores describes pod volume restores in human-readable format.
func describePodVolumeRestores(d *Describer, restores []v1.PodVolumeRestore, details bool) {
func describePodVolumeRestores(d *Describer, restores []velerov1api.PodVolumeRestore, details bool) {
if details {
d.Printf("Restic Restores:\n")
} else {
@ -198,10 +200,10 @@ func describePodVolumeRestores(d *Describer, restores []v1.PodVolumeRestore, det
// go through phases in a specific order
for _, phase := range []string{
string(v1.PodVolumeRestorePhaseCompleted),
string(v1.PodVolumeRestorePhaseFailed),
string(velerov1api.PodVolumeRestorePhaseCompleted),
string(velerov1api.PodVolumeRestorePhaseFailed),
"In Progress",
string(v1.PodVolumeRestorePhaseNew),
string(velerov1api.PodVolumeRestorePhaseNew),
} {
if len(restoresByPhase[phase]) == 0 {
continue
@ -230,15 +232,15 @@ func describePodVolumeRestores(d *Describer, restores []v1.PodVolumeRestore, det
}
}
func groupRestoresByPhase(restores []v1.PodVolumeRestore) map[string][]v1.PodVolumeRestore {
restoresByPhase := make(map[string][]v1.PodVolumeRestore)
func groupRestoresByPhase(restores []velerov1api.PodVolumeRestore) map[string][]velerov1api.PodVolumeRestore {
restoresByPhase := make(map[string][]velerov1api.PodVolumeRestore)
phaseToGroup := map[v1.PodVolumeRestorePhase]string{
v1.PodVolumeRestorePhaseCompleted: string(v1.PodVolumeRestorePhaseCompleted),
v1.PodVolumeRestorePhaseFailed: string(v1.PodVolumeRestorePhaseFailed),
v1.PodVolumeRestorePhaseInProgress: "In Progress",
v1.PodVolumeRestorePhaseNew: string(v1.PodVolumeRestorePhaseNew),
"": string(v1.PodVolumeRestorePhaseNew),
phaseToGroup := map[velerov1api.PodVolumeRestorePhase]string{
velerov1api.PodVolumeRestorePhaseCompleted: string(velerov1api.PodVolumeRestorePhaseCompleted),
velerov1api.PodVolumeRestorePhaseFailed: string(velerov1api.PodVolumeRestorePhaseFailed),
velerov1api.PodVolumeRestorePhaseInProgress: "In Progress",
velerov1api.PodVolumeRestorePhaseNew: string(velerov1api.PodVolumeRestorePhaseNew),
"": string(velerov1api.PodVolumeRestorePhaseNew),
}
for _, restore := range restores {

View File

@ -1,5 +1,5 @@
/*
Copyright 2020 the Velero contributors.
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.

View File

@ -1,5 +1,5 @@
/*
Copyright 2020 the Velero contributors.
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.
@ -60,7 +60,7 @@ func (r *BackupStorageLocationReconciler) Reconcile(ctx context.Context, req ctr
locationList, err := storage.ListBackupStorageLocations(r.Ctx, r.Client, req.Namespace)
if err != nil {
log.WithError(err).Error("No backup storage locations found, at least one is required")
return ctrl.Result{Requeue: true}, err
return ctrl.Result{}, err
}
pluginManager := r.NewPluginManager(log)

View File

@ -1,5 +1,5 @@
/*
Copyright 2017 the Velero contributors.
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.
@ -18,230 +18,154 @@ package controller
import (
"context"
"encoding/json"
"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/runtime"
"k8s.io/apimachinery/pkg/util/clock"
"k8s.io/client-go/tools/cache"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/cluster-api/util/patch"
ctrl "sigs.k8s.io/controller-runtime"
kbclient "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/persistence"
"github.com/vmware-tanzu/velero/pkg/plugin/clientmgmt"
"github.com/vmware-tanzu/velero/pkg/util/kube"
)
type downloadRequestController struct {
*genericController
// DownloadRequestReconciler reconciles a DownloadRequest object
type DownloadRequestReconciler struct {
Scheme *runtime.Scheme
Client kbclient.Client
Clock clock.Clock
// use variables to refer to these functions so they can be
// replaced with fakes for testing.
NewPluginManager func(logrus.FieldLogger) clientmgmt.Manager
BackupStoreGetter persistence.ObjectBackupStoreGetter
downloadRequestClient velerov1client.DownloadRequestsGetter
downloadRequestLister velerov1listers.DownloadRequestLister
restoreLister velerov1listers.RestoreLister
clock clock.Clock
kbClient client.Client
backupLister velerov1listers.BackupLister
newPluginManager func(logrus.FieldLogger) clientmgmt.Manager
backupStoreGetter persistence.ObjectBackupStoreGetter
Log logrus.FieldLogger
}
// NewDownloadRequestController creates a new DownloadRequestController.
func NewDownloadRequestController(
downloadRequestClient velerov1client.DownloadRequestsGetter,
downloadRequestInformer velerov1informers.DownloadRequestInformer,
restoreLister velerov1listers.RestoreLister,
kbClient client.Client,
backupLister velerov1listers.BackupLister,
newPluginManager func(logrus.FieldLogger) clientmgmt.Manager,
backupStoreGetter persistence.ObjectBackupStoreGetter,
logger logrus.FieldLogger,
) Interface {
c := &downloadRequestController{
genericController: newGenericController(DownloadRequest, logger),
downloadRequestClient: downloadRequestClient,
downloadRequestLister: downloadRequestInformer.Lister(),
restoreLister: restoreLister,
kbClient: kbClient,
backupLister: backupLister,
// +kubebuilder:rbac:groups=velero.io,resources=downloadrequests,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=velero.io,resources=downloadrequests/status,verbs=get;update;patch
func (r *DownloadRequestReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := r.Log.WithFields(logrus.Fields{
"controller": "download-request",
"downloadRequest": req.NamespacedName,
})
// use variables to refer to these functions so they can be
// replaced with fakes for testing.
newPluginManager: newPluginManager,
backupStoreGetter: backupStoreGetter,
clock: &clock.RealClock{},
}
c.syncHandler = c.processDownloadRequest
downloadRequestInformer.Informer().AddEventHandler(
cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
key, err := cache.MetaNamespaceKeyFunc(obj)
if err != nil {
downloadRequest := obj.(*velerov1api.DownloadRequest)
c.logger.WithError(errors.WithStack(err)).
WithField(DownloadRequest, downloadRequest.Name).
Error("Error creating queue key, item not added to queue")
return
}
c.queue.Add(key)
},
},
)
return c
}
// processDownloadRequest is the default per-item sync handler. It generates a pre-signed URL for
// a new DownloadRequest or deletes the DownloadRequest if it has expired.
func (c *downloadRequestController) processDownloadRequest(key string) error {
log := c.logger.WithField("key", key)
log.Debug("Running processDownloadRequest")
ns, name, err := cache.SplitMetaNamespaceKey(key)
if err != nil {
log.WithError(err).Error("error splitting queue key")
return nil
}
downloadRequest, err := c.downloadRequestLister.DownloadRequests(ns).Get(name)
if apierrors.IsNotFound(err) {
log.Debug("Unable to find DownloadRequest")
return nil
}
if err != nil {
return errors.Wrap(err, "error getting DownloadRequest")
}
switch downloadRequest.Status.Phase {
case "", velerov1api.DownloadRequestPhaseNew:
return c.generatePreSignedURL(downloadRequest, log)
case velerov1api.DownloadRequestPhaseProcessed:
return c.deleteIfExpired(downloadRequest)
}
return nil
}
const signedURLTTL = 10 * time.Minute
// generatePreSignedURL generates a pre-signed URL for downloadRequest, changes the phase to
// Processed, and persists the changes to storage.
func (c *downloadRequestController) generatePreSignedURL(downloadRequest *velerov1api.DownloadRequest, log logrus.FieldLogger) error {
update := downloadRequest.DeepCopy()
var (
backupName string
err error
)
switch downloadRequest.Spec.Target.Kind {
case velerov1api.DownloadTargetKindRestoreLog, velerov1api.DownloadTargetKindRestoreResults:
restore, err := c.restoreLister.Restores(downloadRequest.Namespace).Get(downloadRequest.Spec.Target.Name)
if err != nil {
return errors.Wrap(err, "error getting Restore")
// Fetch the DownloadRequest instance.
log.Debug("Getting DownloadRequest")
downloadRequest := &velerov1api.DownloadRequest{}
if err := r.Client.Get(ctx, req.NamespacedName, downloadRequest); err != nil {
if apierrors.IsNotFound(err) {
log.Debug("Unable to find DownloadRequest")
return ctrl.Result{}, nil
}
backupName = restore.Spec.BackupName
default:
backupName = downloadRequest.Spec.Target.Name
log.WithError(err).Error("Error getting DownloadRequest")
return ctrl.Result{}, errors.WithStack(err)
}
backup, err := c.backupLister.Backups(downloadRequest.Namespace).Get(backupName)
// Initialize the patch helper.
patchHelper, err := patch.NewHelper(downloadRequest, r.Client)
if err != nil {
return errors.WithStack(err)
log.WithError(err).Error("Error getting a patch helper to update this resource")
return ctrl.Result{}, errors.WithStack(err)
}
backupLocation := &velerov1api.BackupStorageLocation{}
if err := c.kbClient.Get(context.Background(), client.ObjectKey{
Namespace: backup.Namespace,
Name: backup.Spec.StorageLocation,
}, backupLocation); err != nil {
return errors.WithStack(err)
defer func() {
// Always attempt to Patch the downloadRequest object and status after each reconciliation.
if err := patchHelper.Patch(ctx, downloadRequest); err != nil {
log.WithError(err).Error("Error updating download request")
return
}
}()
if downloadRequest.Status != (velerov1api.DownloadRequestStatus{}) && downloadRequest.Status.Expiration != nil {
if downloadRequest.Status.Expiration.Time.Before(r.Clock.Now()) {
// Delete any request that is expired, regardless of the phase: it is not
// worth proceeding and trying/retrying to find it.
log.Debug("DownloadRequest has expired - deleting")
if err := r.Client.Delete(ctx, downloadRequest); err != nil {
log.WithError(err).Error("Error deleting an expired download request")
return ctrl.Result{}, errors.WithStack(err)
}
return ctrl.Result{Requeue: false}, nil
} else if downloadRequest.Status.Phase == velerov1api.DownloadRequestPhaseProcessed {
// Requeue the request if is not yet expired and has already been processed before,
// since it might still be in use by the logs streaming and shouldn't
// be deleted until after its expiration.
log.Debug("DownloadRequest has not yet expired - requeueing")
return ctrl.Result{Requeue: true}, nil
}
}
pluginManager := c.newPluginManager(log)
defer pluginManager.CleanupClients()
// Process a brand new request.
backupName := downloadRequest.Spec.Target.Name
if downloadRequest.Status.Phase == "" || downloadRequest.Status.Phase == velerov1api.DownloadRequestPhaseNew {
backupStore, err := c.backupStoreGetter.Get(backupLocation, pluginManager, log)
if err != nil {
return errors.WithStack(err)
}
// Update the expiration.
downloadRequest.Status.Expiration = &metav1.Time{Time: r.Clock.Now().Add(persistence.DownloadURLTTL)}
if update.Status.DownloadURL, err = backupStore.GetDownloadURL(downloadRequest.Spec.Target); err != nil {
return err
}
update.Status.Phase = velerov1api.DownloadRequestPhaseProcessed
update.Status.Expiration = &metav1.Time{Time: c.clock.Now().Add(persistence.DownloadURLTTL)}
_, err = patchDownloadRequest(downloadRequest, update, c.downloadRequestClient)
return errors.WithStack(err)
}
// deleteIfExpired deletes downloadRequest if it has expired.
func (c *downloadRequestController) deleteIfExpired(downloadRequest *velerov1api.DownloadRequest) error {
log := c.logger.WithField("key", kube.NamespaceAndName(downloadRequest))
log.Info("checking for expiration of DownloadRequest")
if downloadRequest.Status.Expiration.Time.After(c.clock.Now()) {
log.Debug("DownloadRequest has not expired")
return nil
}
log.Debug("DownloadRequest has expired - deleting")
return errors.WithStack(c.downloadRequestClient.DownloadRequests(downloadRequest.Namespace).Delete(context.TODO(), downloadRequest.Name, metav1.DeleteOptions{}))
}
// resync requeues all the DownloadRequests in the lister's cache. This is mostly to handle deleting
// any expired requests that were not deleted as part of the normal client flow for whatever reason.
func (c *downloadRequestController) resync() {
list, err := c.downloadRequestLister.List(labels.Everything())
if err != nil {
c.logger.WithError(errors.WithStack(err)).Error("error listing download requests")
return
}
for _, dr := range list {
key, err := cache.MetaNamespaceKeyFunc(dr)
if err != nil {
c.logger.WithError(errors.WithStack(err)).WithField(DownloadRequest, dr.Name).Error("error generating key for download request")
continue
if downloadRequest.Spec.Target.Kind == velerov1api.DownloadTargetKindRestoreLog ||
downloadRequest.Spec.Target.Kind == velerov1api.DownloadTargetKindRestoreResults {
restore := &velerov1api.Restore{}
if err := r.Client.Get(ctx, kbclient.ObjectKey{
Namespace: downloadRequest.Namespace,
Name: downloadRequest.Spec.Target.Name,
}, restore); err != nil {
return ctrl.Result{}, errors.WithStack(err)
}
backupName = restore.Spec.BackupName
}
c.queue.Add(key)
backup := &velerov1api.Backup{}
if err := r.Client.Get(ctx, kbclient.ObjectKey{
Namespace: downloadRequest.Namespace,
Name: backupName,
}, backup); err != nil {
return ctrl.Result{}, errors.WithStack(err)
}
location := &velerov1api.BackupStorageLocation{}
if err := r.Client.Get(ctx, kbclient.ObjectKey{
Namespace: backup.Namespace,
Name: backup.Spec.StorageLocation,
}, location); err != nil {
return ctrl.Result{}, errors.WithStack(err)
}
pluginManager := r.NewPluginManager(log)
defer pluginManager.CleanupClients()
backupStore, err := r.BackupStoreGetter.Get(location, pluginManager, log)
if err != nil {
log.WithError(err).Error("Error getting a backup store")
return ctrl.Result{}, errors.WithStack(err)
}
if downloadRequest.Status.DownloadURL, err = backupStore.GetDownloadURL(downloadRequest.Spec.Target); err != nil {
return ctrl.Result{Requeue: true}, errors.WithStack(err)
}
downloadRequest.Status.Phase = velerov1api.DownloadRequestPhaseProcessed
// Update the expiration again to extend the time we wait (the TTL) to start after successfully processing the URL.
downloadRequest.Status.Expiration = &metav1.Time{Time: r.Clock.Now().Add(persistence.DownloadURLTTL)}
}
// Requeue is mostly to handle deleting any expired requests that were not
// deleted as part of the normal client flow for whatever reason.
return ctrl.Result{Requeue: true}, nil
}
func patchDownloadRequest(original, updated *velerov1api.DownloadRequest, client velerov1client.DownloadRequestsGetter) (*velerov1api.DownloadRequest, error) {
origBytes, err := json.Marshal(original)
if err != nil {
return nil, errors.Wrap(err, "error marshalling original download request")
}
updatedBytes, err := json.Marshal(updated)
if err != nil {
return nil, errors.Wrap(err, "error marshalling updated download request")
}
patchBytes, err := jsonpatch.CreateMergePatch(origBytes, updatedBytes)
if err != nil {
return nil, errors.Wrap(err, "error creating json merge patch for download request")
}
res, err := client.DownloadRequests(original.Namespace).Patch(context.TODO(), original.Name, types.MergePatchType, patchBytes, metav1.PatchOptions{})
if err != nil {
return nil, errors.Wrap(err, "error patching download request")
}
return res, nil
func (r *DownloadRequestReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&velerov1api.DownloadRequest{}).
Complete(r)
}

View File

@ -18,306 +18,249 @@ package controller
import (
"context"
"testing"
"time"
. "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/extensions/table"
. "github.com/onsi/gomega"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/clock"
"k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client"
ctrl "sigs.k8s.io/controller-runtime"
kbclient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/builder"
"github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/fake"
informers "github.com/vmware-tanzu/velero/pkg/generated/informers/externalversions"
persistencemocks "github.com/vmware-tanzu/velero/pkg/persistence/mocks"
"github.com/vmware-tanzu/velero/pkg/plugin/clientmgmt"
pluginmocks "github.com/vmware-tanzu/velero/pkg/plugin/mocks"
velerotest "github.com/vmware-tanzu/velero/pkg/test"
kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube"
)
type downloadRequestTestHarness struct {
client *fake.Clientset
informerFactory informers.SharedInformerFactory
pluginManager *pluginmocks.Manager
backupStore *persistencemocks.BackupStore
controller *downloadRequestController
}
func newDownloadRequestTestHarness(t *testing.T, fakeClient client.Client) *downloadRequestTestHarness {
var (
client = fake.NewSimpleClientset()
informerFactory = informers.NewSharedInformerFactory(client, 0)
pluginManager = new(pluginmocks.Manager)
backupStore = new(persistencemocks.BackupStore)
controller = NewDownloadRequestController(
client.VeleroV1(),
informerFactory.Velero().V1().DownloadRequests(),
informerFactory.Velero().V1().Restores().Lister(),
fakeClient,
informerFactory.Velero().V1().Backups().Lister(),
func(logrus.FieldLogger) clientmgmt.Manager { return pluginManager },
NewFakeSingleObjectBackupStoreGetter(backupStore),
velerotest.NewLogger(),
).(*downloadRequestController)
)
clockTime, err := time.Parse(time.RFC1123, time.RFC1123)
require.NoError(t, err)
controller.clock = clock.NewFakeClock(clockTime)
pluginManager.On("CleanupClients").Return()
return &downloadRequestTestHarness{
client: client,
informerFactory: informerFactory,
pluginManager: pluginManager,
backupStore: backupStore,
controller: controller,
var _ = Describe("Download Request Reconciler", func() {
type request struct {
downloadRequest *velerov1api.DownloadRequest
backup *velerov1api.Backup
restore *velerov1api.Restore
backupLocation *velerov1api.BackupStorageLocation
expired bool
expectedReconcileErr string
expectGetsURL bool
expectedRequeue ctrl.Result
}
}
func newDownloadRequest(phase velerov1api.DownloadRequestPhase, targetKind velerov1api.DownloadTargetKind, targetName string) *velerov1api.DownloadRequest {
return &velerov1api.DownloadRequest{
ObjectMeta: metav1.ObjectMeta{
Name: "a-download-request",
Namespace: velerov1api.DefaultNamespace,
},
Spec: velerov1api.DownloadRequestSpec{
Target: velerov1api.DownloadTarget{
Kind: targetKind,
Name: targetName,
},
},
Status: velerov1api.DownloadRequestStatus{
Phase: phase,
},
}
}
func newBackupLocation(name, provider, bucket string) *velerov1api.BackupStorageLocation {
return &velerov1api.BackupStorageLocation{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: velerov1api.DefaultNamespace,
},
Spec: velerov1api.BackupStorageLocationSpec{
Provider: provider,
StorageType: velerov1api.StorageType{
ObjectStorage: &velerov1api.ObjectStorageLocation{
Bucket: bucket,
},
},
},
}
}
func TestProcessDownloadRequest(t *testing.T) {
defaultBackup := func() *velerov1api.Backup {
return builder.ForBackup(velerov1api.DefaultNamespace, "a-backup").StorageLocation("a-location").Result()
}
tests := []struct {
name string
key string
downloadRequest *velerov1api.DownloadRequest
backup *velerov1api.Backup
restore *velerov1api.Restore
backupLocation *velerov1api.BackupStorageLocation
expired bool
expectedErr string
expectGetsURL bool
}{
{
name: "empty key returns without error",
key: "",
DescribeTable("a Download request",
func(test request) {
// now will be used to set the fake clock's time; capture
// it here so it can be referenced in the test case defs.
now, err := time.Parse(time.RFC1123, time.RFC1123)
Expect(err).To(BeNil())
now = now.Local()
rClock := clock.NewFakeClock(now)
const signedURLTTL = 10 * time.Minute
var (
pluginManager = &pluginmocks.Manager{}
backupStores = make(map[string]*persistencemocks.BackupStore)
)
pluginManager.On("CleanupClients").Return(nil)
Expect(test.downloadRequest).ToNot(BeNil())
// Set .status.expiration properly for all requests test cases that are
// meant to be expired. Since "expired" is relative to the controller's
// clock time, it's easier to do this here than as part of the test case definitions.
if test.expired {
test.downloadRequest.Status.Expiration = &metav1.Time{Time: rClock.Now().Add(-1 * time.Minute)}
}
fakeClient := fake.NewFakeClientWithScheme(scheme.Scheme)
err = fakeClient.Create(context.TODO(), test.downloadRequest)
Expect(err).To(BeNil())
if test.backup != nil {
err := fakeClient.Create(context.TODO(), test.backup)
Expect(err).To(BeNil())
}
if test.backupLocation != nil {
err := fakeClient.Create(context.TODO(), test.backupLocation)
Expect(err).To(BeNil())
backupStores[test.backupLocation.Name] = &persistencemocks.BackupStore{}
}
if test.restore != nil {
err := fakeClient.Create(context.TODO(), test.restore)
Expect(err).To(BeNil())
}
// Setup reconciler
Expect(velerov1api.AddToScheme(scheme.Scheme)).To(Succeed())
r := DownloadRequestReconciler{
Client: fakeClient,
Clock: rClock,
NewPluginManager: func(logrus.FieldLogger) clientmgmt.Manager { return pluginManager },
BackupStoreGetter: NewFakeObjectBackupStoreGetter(backupStores),
Log: velerotest.NewLogger(),
}
if test.backupLocation != nil && test.expectGetsURL {
backupStores[test.backupLocation.Name].On("GetDownloadURL", test.downloadRequest.Spec.Target).Return("a-url", nil)
}
actualResult, err := r.Reconcile(context.Background(), ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: velerov1api.DefaultNamespace,
Name: test.downloadRequest.Name,
},
})
Expect(actualResult).To(BeEquivalentTo(test.expectedRequeue))
if test.expectedReconcileErr == "" {
Expect(err).To(BeNil())
} else {
Expect(err.Error()).To(Equal(test.expectedReconcileErr))
}
instance := &velerov1api.DownloadRequest{}
err = r.Client.Get(ctx, kbclient.ObjectKey{Name: test.downloadRequest.Name, Namespace: test.downloadRequest.Namespace}, instance)
if test.expired {
Expect(instance).ToNot(Equal(test.downloadRequest))
Expect(apierrors.IsNotFound(err)).To(BeTrue())
} else {
if test.downloadRequest.Status.Phase == velerov1api.DownloadRequestPhaseProcessed {
Expect(instance).To(Equal(test.downloadRequest))
} else {
Expect(instance).ToNot(Equal(test.downloadRequest))
}
Expect(err).To(BeNil())
}
if test.expectGetsURL {
Expect(string(instance.Status.Phase)).To(Equal(string(velerov1api.DownloadRequestPhaseProcessed)))
Expect(instance.Status.DownloadURL).To(Equal("a-url"))
Expect(velerotest.TimesAreEqual(instance.Status.Expiration.Time, r.Clock.Now().Add(signedURLTTL))).To(BeTrue())
}
},
{
name: "bad key format returns without error",
key: "a/b/c",
},
{
name: "no download request for key returns without error",
key: "nonexistent/key",
},
{
name: "backup contents request for nonexistent backup returns an error",
downloadRequest: newDownloadRequest("", velerov1api.DownloadTargetKindBackupContents, "a-backup"),
backup: builder.ForBackup(velerov1api.DefaultNamespace, "non-matching-backup").StorageLocation("a-location").Result(),
backupLocation: newBackupLocation("a-location", "a-provider", "a-bucket"),
expectedErr: "backup.velero.io \"a-backup\" not found",
},
{
name: "restore log request for nonexistent restore returns an error",
downloadRequest: newDownloadRequest("", velerov1api.DownloadTargetKindRestoreLog, "a-backup-20170912150214"),
restore: builder.ForRestore(velerov1api.DefaultNamespace, "non-matching-restore").Phase(velerov1api.RestorePhaseCompleted).Backup("a-backup").Result(),
Entry("backup contents request for nonexistent backup returns an error", request{
downloadRequest: builder.ForDownloadRequest(velerov1api.DefaultNamespace, "a-download-request").Phase("").Target(velerov1api.DownloadTargetKindBackupContents, "a1-backup").Result(),
backup: builder.ForBackup(velerov1api.DefaultNamespace, "non-matching-backup").StorageLocation("a-location").Result(),
backupLocation: builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "a-location").Provider("a-provider").Bucket("a-bucket").Result(),
expectedReconcileErr: "backups.velero.io \"a1-backup\" not found",
expectedRequeue: ctrl.Result{Requeue: false},
}),
Entry("restore log request for nonexistent restore returns an error", request{
downloadRequest: builder.ForDownloadRequest(velerov1api.DefaultNamespace, "a-download-request").Phase("").Target(velerov1api.DownloadTargetKindRestoreLog, "a-backup-20170912150214").Result(),
restore: builder.ForRestore(velerov1api.DefaultNamespace, "non-matching-restore").Phase(velerov1api.RestorePhaseCompleted).Backup("a-backup").Result(),
backup: defaultBackup(),
backupLocation: builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "a-location").Provider("a-provider").Bucket("a-bucket").Result(),
expectedReconcileErr: "restores.velero.io \"a-backup-20170912150214\" not found",
expectedRequeue: ctrl.Result{Requeue: false},
}),
Entry("backup contents request for backup with nonexistent location returns an error", request{
downloadRequest: builder.ForDownloadRequest(velerov1api.DefaultNamespace, "a-download-request").Phase("").Target(velerov1api.DownloadTargetKindBackupContents, "a-backup").Result(),
backup: defaultBackup(),
backupLocation: builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "non-matching-location").Provider("a-provider").Bucket("a-bucket").Result(),
expectedReconcileErr: "backupstoragelocations.velero.io \"a-location\" not found",
expectedRequeue: ctrl.Result{Requeue: false},
}),
Entry("backup contents request with phase '' gets a url", request{
downloadRequest: builder.ForDownloadRequest(velerov1api.DefaultNamespace, "a-download-request").Phase("").Target(velerov1api.DownloadTargetKindBackupContents, "a-backup").Result(),
backup: defaultBackup(),
backupLocation: newBackupLocation("a-location", "a-provider", "a-bucket"),
expectedErr: "error getting Restore: restore.velero.io \"a-backup-20170912150214\" not found",
},
{
name: "backup contents request for backup with nonexistent location returns an error",
downloadRequest: newDownloadRequest("", velerov1api.DownloadTargetKindBackupContents, "a-backup"),
backup: defaultBackup(),
backupLocation: newBackupLocation("non-matching-location", "a-provider", "a-bucket"),
expectedErr: "backupstoragelocations.velero.io \"a-location\" not found",
},
{
name: "backup contents request with phase '' gets a url",
downloadRequest: newDownloadRequest("", velerov1api.DownloadTargetKindBackupContents, "a-backup"),
backup: defaultBackup(),
backupLocation: newBackupLocation("a-location", "a-provider", "a-bucket"),
backupLocation: builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "a-location").Provider("a-provider").Bucket("a-bucket").Result(),
expectGetsURL: true,
},
{
name: "backup contents request with phase 'New' gets a url",
downloadRequest: newDownloadRequest(velerov1api.DownloadRequestPhaseNew, velerov1api.DownloadTargetKindBackupContents, "a-backup"),
expectedRequeue: ctrl.Result{Requeue: true},
}),
Entry("backup contents request with phase 'New' gets a url", request{
downloadRequest: builder.ForDownloadRequest(velerov1api.DefaultNamespace, "a-download-request").Phase(velerov1api.DownloadRequestPhaseNew).Target(velerov1api.DownloadTargetKindBackupContents, "a-backup").Result(),
backup: defaultBackup(),
backupLocation: newBackupLocation("a-location", "a-provider", "a-bucket"),
backupLocation: builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "a-location").Provider("a-provider").Bucket("a-bucket").Result(),
expectGetsURL: true,
},
{
name: "backup log request with phase '' gets a url",
downloadRequest: newDownloadRequest("", velerov1api.DownloadTargetKindBackupLog, "a-backup"),
expectedRequeue: ctrl.Result{Requeue: true},
}),
Entry("backup log request with phase '' gets a url", request{
downloadRequest: builder.ForDownloadRequest(velerov1api.DefaultNamespace, "a-download-request").Phase("").Target(velerov1api.DownloadTargetKindBackupLog, "a-backup").Result(),
backup: defaultBackup(),
backupLocation: newBackupLocation("a-location", "a-provider", "a-bucket"),
backupLocation: builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "a-location").Provider("a-provider").Bucket("a-bucket").Result(),
expectGetsURL: true,
},
{
name: "backup log request with phase 'New' gets a url",
downloadRequest: newDownloadRequest(velerov1api.DownloadRequestPhaseNew, velerov1api.DownloadTargetKindBackupLog, "a-backup"),
expectedRequeue: ctrl.Result{Requeue: true},
}),
Entry("backup log request with phase 'New' gets a url", request{
downloadRequest: builder.ForDownloadRequest(velerov1api.DefaultNamespace, "a-download-request").Phase(velerov1api.DownloadRequestPhaseNew).Target(velerov1api.DownloadTargetKindBackupLog, "a-backup").Result(),
backup: defaultBackup(),
backupLocation: newBackupLocation("a-location", "a-provider", "a-bucket"),
backupLocation: builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "a-location").Provider("a-provider").Bucket("a-bucket").Result(),
expectGetsURL: true,
},
{
name: "restore log request with phase '' gets a url",
downloadRequest: newDownloadRequest("", velerov1api.DownloadTargetKindRestoreLog, "a-backup-20170912150214"),
expectedRequeue: ctrl.Result{Requeue: true},
}),
Entry("restore log request with phase '' gets a url", request{
downloadRequest: builder.ForDownloadRequest(velerov1api.DefaultNamespace, "a-download-request").Phase("").Target(velerov1api.DownloadTargetKindRestoreLog, "a-backup-20170912150214").Result(),
restore: builder.ForRestore(velerov1api.DefaultNamespace, "a-backup-20170912150214").Phase(velerov1api.RestorePhaseCompleted).Backup("a-backup").Result(),
backup: defaultBackup(),
backupLocation: newBackupLocation("a-location", "a-provider", "a-bucket"),
backupLocation: builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "a-location").Provider("a-provider").Bucket("a-bucket").Result(),
expectGetsURL: true,
},
{
name: "restore log request with phase 'New' gets a url",
downloadRequest: newDownloadRequest(velerov1api.DownloadRequestPhaseNew, velerov1api.DownloadTargetKindRestoreLog, "a-backup-20170912150214"),
expectedRequeue: ctrl.Result{Requeue: true},
}),
Entry("restore log request with phase 'New' gets a url", request{
downloadRequest: builder.ForDownloadRequest(velerov1api.DefaultNamespace, "a-download-request").Phase(velerov1api.DownloadRequestPhaseNew).Target(velerov1api.DownloadTargetKindRestoreLog, "a-backup-20170912150214").Result(),
backup: defaultBackup(),
restore: builder.ForRestore(velerov1api.DefaultNamespace, "a-backup-20170912150214").Phase(velerov1api.RestorePhaseCompleted).Backup("a-backup").Result(),
backupLocation: builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "a-location").Provider("a-provider").Bucket("a-bucket").Result(),
expectGetsURL: true,
expectedRequeue: ctrl.Result{Requeue: true},
}),
Entry("restore results request with phase '' gets a url", request{
downloadRequest: builder.ForDownloadRequest(velerov1api.DefaultNamespace, "a-download-request").Phase("").Target(velerov1api.DownloadTargetKindRestoreResults, "a-backup-20170912150214").Result(),
restore: builder.ForRestore(velerov1api.DefaultNamespace, "a-backup-20170912150214").Phase(velerov1api.RestorePhaseCompleted).Backup("a-backup").Result(),
backup: defaultBackup(),
backupLocation: newBackupLocation("a-location", "a-provider", "a-bucket"),
backupLocation: builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "a-location").Provider("a-provider").Bucket("a-bucket").Result(),
expectGetsURL: true,
},
{
name: "restore results request with phase '' gets a url",
downloadRequest: newDownloadRequest("", velerov1api.DownloadTargetKindRestoreResults, "a-backup-20170912150214"),
expectedRequeue: ctrl.Result{Requeue: true},
}),
Entry("restore results request with phase 'New' gets a url", request{
downloadRequest: builder.ForDownloadRequest(velerov1api.DefaultNamespace, "a-download-request").Phase(velerov1api.DownloadRequestPhaseNew).Target(velerov1api.DownloadTargetKindRestoreResults, "a-backup-20170912150214").Result(),
restore: builder.ForRestore(velerov1api.DefaultNamespace, "a-backup-20170912150214").Phase(velerov1api.RestorePhaseCompleted).Backup("a-backup").Result(),
backup: defaultBackup(),
backupLocation: newBackupLocation("a-location", "a-provider", "a-bucket"),
backupLocation: builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "a-location").Provider("a-provider").Bucket("a-bucket").Result(),
expectGetsURL: true,
},
{
name: "restore results request with phase 'New' gets a url",
downloadRequest: newDownloadRequest(velerov1api.DownloadRequestPhaseNew, velerov1api.DownloadTargetKindRestoreResults, "a-backup-20170912150214"),
restore: builder.ForRestore(velerov1api.DefaultNamespace, "a-backup-20170912150214").Phase(velerov1api.RestorePhaseCompleted).Backup("a-backup").Result(),
expectedRequeue: ctrl.Result{Requeue: true},
}),
Entry("request with phase 'Processed' and not expired is not deleted", request{
downloadRequest: builder.ForDownloadRequest(velerov1api.DefaultNamespace, "a-download-request").Phase(velerov1api.DownloadRequestPhaseProcessed).Target(velerov1api.DownloadTargetKindBackupLog, "a-backup-20170912150214").Result(),
backup: defaultBackup(),
backupLocation: newBackupLocation("a-location", "a-provider", "a-bucket"),
expectGetsURL: true,
},
{
name: "request with phase 'Processed' is not deleted if not expired",
downloadRequest: newDownloadRequest(velerov1api.DownloadRequestPhaseProcessed, velerov1api.DownloadTargetKindBackupLog, "a-backup-20170912150214"),
backup: defaultBackup(),
},
{
name: "request with phase 'Processed' is deleted if expired",
downloadRequest: newDownloadRequest(velerov1api.DownloadRequestPhaseProcessed, velerov1api.DownloadTargetKindBackupLog, "a-backup-20170912150214"),
expectedRequeue: ctrl.Result{Requeue: true},
}),
Entry("request with phase 'Processed' and expired is deleted", request{
downloadRequest: builder.ForDownloadRequest(velerov1api.DefaultNamespace, "a-download-request").Phase(velerov1api.DownloadRequestPhaseProcessed).Target(velerov1api.DownloadTargetKindBackupLog, "a-backup-20170912150214").Result(),
backup: defaultBackup(),
expired: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var fakeClient client.Client
if tc.backupLocation != nil {
fakeClient = velerotest.NewFakeControllerRuntimeClient(t, tc.backupLocation)
} else {
fakeClient = velerotest.NewFakeControllerRuntimeClient(t)
}
harness := newDownloadRequestTestHarness(t, fakeClient)
// set up test case data
// Set .status.expiration properly for processed requests. Since "expired" is relative to the controller's
// clock time, it's easier to do this here than as part of the test case definitions.
if tc.downloadRequest != nil && tc.downloadRequest.Status.Phase == velerov1api.DownloadRequestPhaseProcessed {
if tc.expired {
tc.downloadRequest.Status.Expiration = &metav1.Time{Time: harness.controller.clock.Now().Add(-1 * time.Minute)}
} else {
tc.downloadRequest.Status.Expiration = &metav1.Time{Time: harness.controller.clock.Now().Add(time.Minute)}
}
}
if tc.downloadRequest != nil {
require.NoError(t, harness.informerFactory.Velero().V1().DownloadRequests().Informer().GetStore().Add(tc.downloadRequest))
_, err := harness.client.VeleroV1().DownloadRequests(tc.downloadRequest.Namespace).Create(context.TODO(), tc.downloadRequest, metav1.CreateOptions{})
require.NoError(t, err)
}
if tc.restore != nil {
require.NoError(t, harness.informerFactory.Velero().V1().Restores().Informer().GetStore().Add(tc.restore))
}
if tc.backup != nil {
require.NoError(t, harness.informerFactory.Velero().V1().Backups().Informer().GetStore().Add(tc.backup))
}
if tc.expectGetsURL {
harness.backupStore.On("GetDownloadURL", tc.downloadRequest.Spec.Target).Return("a-url", nil)
}
// exercise method under test
key := tc.key
if key == "" && tc.downloadRequest != nil {
key = kubeutil.NamespaceAndName(tc.downloadRequest)
}
err := harness.controller.processDownloadRequest(key)
// verify results
if tc.expectedErr != "" {
require.Equal(t, tc.expectedErr, err.Error())
} else {
assert.Nil(t, err)
}
if tc.expectGetsURL {
output, err := harness.client.VeleroV1().DownloadRequests(tc.downloadRequest.Namespace).Get(context.TODO(), tc.downloadRequest.Name, metav1.GetOptions{})
require.NoError(t, err)
assert.Equal(t, string(velerov1api.DownloadRequestPhaseProcessed), string(output.Status.Phase))
assert.Equal(t, "a-url", output.Status.DownloadURL)
assert.True(t, velerotest.TimesAreEqual(harness.controller.clock.Now().Add(signedURLTTL), output.Status.Expiration.Time), "expiration does not match")
}
if tc.downloadRequest != nil && tc.downloadRequest.Status.Phase == velerov1api.DownloadRequestPhaseProcessed {
res, err := harness.client.VeleroV1().DownloadRequests(tc.downloadRequest.Namespace).Get(context.TODO(), tc.downloadRequest.Name, metav1.GetOptions{})
if tc.expired {
assert.True(t, apierrors.IsNotFound(err))
} else {
assert.NoError(t, err)
assert.Equal(t, tc.downloadRequest, res)
}
}
})
}
}
expectedRequeue: ctrl.Result{Requeue: false},
}),
Entry("request with phase '' and expired is deleted", request{
downloadRequest: builder.ForDownloadRequest(velerov1api.DefaultNamespace, "a-download-request").Phase("").Target(velerov1api.DownloadTargetKindBackupLog, "a-backup-20170912150214").Result(),
backup: defaultBackup(),
expired: true,
expectedRequeue: ctrl.Result{Requeue: false},
}),
Entry("request with phase 'New' and expired is deleted", request{
downloadRequest: builder.ForDownloadRequest(velerov1api.DefaultNamespace, "a-download-request").Phase(velerov1api.DownloadRequestPhaseNew).Target(velerov1api.DownloadTargetKindBackupLog, "a-backup-20170912150214").Result(),
backup: defaultBackup(),
expired: true,
expectedRequeue: ctrl.Result{Requeue: false},
}),
)
})

View File

@ -1,5 +1,5 @@
/*
Copyright 2018 the Velero contributors.
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.
@ -71,12 +71,11 @@ func (r *ServerStatusRequestReconciler) Reconcile(ctx context.Context, req ctrl.
statusRequest := &velerov1api.ServerStatusRequest{}
if err := r.Client.Get(r.Ctx, req.NamespacedName, statusRequest); err != nil {
if apierrors.IsNotFound(err) {
log.WithError(err).Error("ServerStatusRequest not found")
log.Debug("Unable to find ServerStatusRequest")
return ctrl.Result{}, nil
}
log.WithError(err).Error("Error getting ServerStatusRequest")
// Error reading the object - requeue the request.
return ctrl.Result{}, err
}