Skip to content

Commit

Permalink
Add restored resource list in the output of restore describe command
Browse files Browse the repository at this point in the history
Add restored resource list in the output of restore describe command

Fixes #5199

Signed-off-by: Wenkai Yin(尹文开) <yinw@vmware.com>
  • Loading branch information
ywk253100 committed Feb 28, 2023
1 parent c6fba55 commit eb4ecd3
Show file tree
Hide file tree
Showing 18 changed files with 353 additions and 53 deletions.
1 change: 1 addition & 0 deletions changelogs/unreleased/5867-ywk253100
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add restored resource list in the restore describe command
1 change: 1 addition & 0 deletions config/crd/v1/bases/velero.io_downloadrequests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ spec:
- BackupResourceList
- RestoreLog
- RestoreResults
- RestoreResourceList
- RestoreItemOperations
- CSIBackupVolumeSnapshots
- CSIBackupVolumeSnapshotContents
Expand Down
2 changes: 1 addition & 1 deletion config/crd/v1/crds/crds.go

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion pkg/apis/velero/v1/download_request_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ type DownloadRequestSpec struct {
}

// DownloadTargetKind represents what type of file to download.
// +kubebuilder:validation:Enum=BackupLog;BackupContents;BackupVolumeSnapshots;BackupItemOperations;BackupResourceList;RestoreLog;RestoreResults;RestoreItemOperations;CSIBackupVolumeSnapshots;CSIBackupVolumeSnapshotContents
// +kubebuilder:validation:Enum=BackupLog;BackupContents;BackupVolumeSnapshots;BackupItemOperations;BackupResourceList;RestoreLog;RestoreResults;RestoreResourceList;RestoreItemOperations;CSIBackupVolumeSnapshots;CSIBackupVolumeSnapshotContents
type DownloadTargetKind string

const (
Expand All @@ -36,6 +36,7 @@ const (
DownloadTargetKindBackupResourceList DownloadTargetKind = "BackupResourceList"
DownloadTargetKindRestoreLog DownloadTargetKind = "RestoreLog"
DownloadTargetKindRestoreResults DownloadTargetKind = "RestoreResults"
DownloadTargetKindRestoreResourceList DownloadTargetKind = "RestoreResourceList"
DownloadTargetKindRestoreItemOperations DownloadTargetKind = "RestoreItemOperations"
DownloadTargetKindCSIBackupVolumeSnapshots DownloadTargetKind = "CSIBackupVolumeSnapshots"
DownloadTargetKindCSIBackupVolumeSnapshotContents DownloadTargetKind = "CSIBackupVolumeSnapshotContents"
Expand Down
36 changes: 36 additions & 0 deletions pkg/cmd/util/output/restore_describer.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,11 @@ func DescribeRestore(ctx context.Context, kbClient kbclient.Client, restore *vel
d.Println()
d.Printf("Preserve Service NodePorts:\t%s\n", BoolPointerString(restore.Spec.PreserveNodePorts, "false", "true", "auto"))

d.Println()
if details {
describeRestoreResourceList(ctx, kbClient, d, restore, insecureSkipTLSVerify, caCertFile)
d.Println()
}
})
}

Expand Down Expand Up @@ -275,3 +280,34 @@ func groupRestoresByPhase(restores []velerov1api.PodVolumeRestore) map[string][]

return restoresByPhase
}

func describeRestoreResourceList(ctx context.Context, kbClient kbclient.Client, d *Describer, restore *velerov1api.Restore, insecureSkipTLSVerify bool, caCertPath string) {
buf := new(bytes.Buffer)
if err := downloadrequest.Stream(ctx, kbClient, restore.Namespace, restore.Name, velerov1api.DownloadTargetKindRestoreResourceList, buf, downloadRequestTimeout, insecureSkipTLSVerify, caCertPath); err != nil {
if err == downloadrequest.ErrNotFound {
d.Println("Resource List:\t<restore resource list not found>")
} else {
d.Printf("Resource List:\t<error getting restore resource list: %v>\n", err)
}
return
}

var resourceList map[string][]string
if err := json.NewDecoder(buf).Decode(&resourceList); err != nil {
d.Printf("Resource List:\t<error reading restore resource list: %v>\n", err)
return
}

d.Println("Resource List:")

// Sort GVKs in output
gvks := make([]string, 0, len(resourceList))
for gvk := range resourceList {
gvks = append(gvks, gvk)
}
sort.Strings(gvks)

for _, gvk := range gvks {
d.Printf("\t%s:\n\t\t- %s\n", gvk, strings.Join(resourceList[gvk], "\n\t\t- "))
}
}
3 changes: 2 additions & 1 deletion pkg/controller/download_request_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ func (r *downloadRequestReconciler) Reconcile(ctx context.Context, req ctrl.Requ
downloadRequest.Status.Expiration = &metav1.Time{Time: r.clock.Now().Add(persistence.DownloadURLTTL)}

if downloadRequest.Spec.Target.Kind == velerov1api.DownloadTargetKindRestoreLog ||
downloadRequest.Spec.Target.Kind == velerov1api.DownloadTargetKindRestoreResults {
downloadRequest.Spec.Target.Kind == velerov1api.DownloadTargetKindRestoreResults ||
downloadRequest.Spec.Target.Kind == velerov1api.DownloadTargetKindRestoreResourceList {
restore := &velerov1api.Restore{}
if err := r.client.Get(ctx, kbclient.ObjectKey{
Namespace: downloadRequest.Namespace,
Expand Down
26 changes: 25 additions & 1 deletion pkg/controller/restore_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@ func (r *restoreReconciler) runValidatedRestore(restore *api.Restore, info backu
for i := range podVolumeBackupList.Items {
podVolumeBackups = append(podVolumeBackups, &podVolumeBackupList.Items[i])
}
restoreReq := pkgrestore.Request{
restoreReq := &pkgrestore.Request{
Log: restoreLog,
Restore: restore,
Backup: info.backup,
Expand Down Expand Up @@ -539,6 +539,10 @@ func (r *restoreReconciler) runValidatedRestore(restore *api.Restore, info backu
r.logger.WithError(err).Error("Error uploading restore results to backup storage")
}

if err := putRestoredResourceList(restore, restoreReq.RestoredResourceList(), info.backupStore); err != nil {
r.logger.WithError(err).Error("Error uploading restored resource list to backup storage")
}

return nil
}

Expand Down Expand Up @@ -585,6 +589,26 @@ func putResults(restore *api.Restore, results map[string]results.Result, backupS
return nil
}

func putRestoredResourceList(restore *api.Restore, list map[string][]string, backupStore persistence.BackupStore) error {
buf := new(bytes.Buffer)
gzw := gzip.NewWriter(buf)
defer gzw.Close()

if err := json.NewEncoder(gzw).Encode(list); err != nil {
return errors.Wrap(err, "error encoding restored resource list to JSON")
}

if err := gzw.Close(); err != nil {
return errors.Wrap(err, "error closing gzip writer")
}

if err := backupStore.PutRestoredResourceList(restore.Name, buf); err != nil {
return err
}

return nil
}

func downloadToTempFile(backupName string, backupStore persistence.BackupStore, logger logrus.FieldLogger) (*os.File, error) {
readCloser, err := backupStore.GetBackupContents(backupName)
if err != nil {
Expand Down
5 changes: 3 additions & 2 deletions pkg/controller/restore_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,7 @@ func TestRestoreReconcile(t *testing.T) {
backupStore.On("PutRestoreLog", test.backup.Name, test.restore.Name, mock.Anything).Return(test.putRestoreLogErr)

backupStore.On("PutRestoreResults", test.backup.Name, test.restore.Name, mock.Anything).Return(nil)
backupStore.On("PutRestoredResourceList", test.restore.Name, mock.Anything).Return(nil)

volumeSnapshots := []*volume.Snapshot{
{
Expand Down Expand Up @@ -767,7 +768,7 @@ type fakeRestorer struct {
}

func (r *fakeRestorer) Restore(
info pkgrestore.Request,
info *pkgrestore.Request,
actions []riav2.RestoreItemAction,
volumeSnapshotterGetter pkgrestore.VolumeSnapshotterGetter,
) (results.Result, results.Result) {
Expand All @@ -778,7 +779,7 @@ func (r *fakeRestorer) Restore(
return res.Get(0).(results.Result), res.Get(1).(results.Result)
}

func (r *fakeRestorer) RestoreWithResolvers(req pkgrestore.Request,
func (r *fakeRestorer) RestoreWithResolvers(req *pkgrestore.Request,
resolver framework.RestoreItemActionResolverV2,
itemSnapshotterResolver framework.ItemSnapshotterResolver,
volumeSnapshotterGetter pkgrestore.VolumeSnapshotterGetter,
Expand Down
13 changes: 13 additions & 0 deletions pkg/persistence/mocks/backup_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -327,3 +327,16 @@ func (_m *BackupStore) GetRestoreItemOperations(name string) ([]*itemoperation.R
panic("implement me")
return nil, nil
}

func (_m *BackupStore) PutRestoredResourceList(restore string, results io.Reader) error {
ret := _m.Called(restore, results)

var r0 error
if rf, ok := ret.Get(0).(func(string, io.Reader) error); ok {
r0 = rf(restore, results)
} else {
r0 = ret.Error(0)
}

return r0
}
7 changes: 7 additions & 0 deletions pkg/persistence/object_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ type BackupStore interface {

PutRestoreLog(backup, restore string, log io.Reader) error
PutRestoreResults(backup, restore string, results io.Reader) error
PutRestoredResourceList(restore string, results io.Reader) error
PutRestoreItemOperations(backup, restore string, restoreItemOperations io.Reader) error
GetRestoreItemOperations(name string) ([]*itemoperation.RestoreOperation, error)
DeleteRestore(name string) error
Expand Down Expand Up @@ -537,6 +538,10 @@ func (s *objectBackupStore) PutRestoreResults(backup string, restore string, res
return s.objectStore.PutObject(s.bucket, s.layout.getRestoreResultsKey(restore), results)
}

func (s *objectBackupStore) PutRestoredResourceList(restore string, list io.Reader) error {
return s.objectStore.PutObject(s.bucket, s.layout.getRestoreResourceListKey(restore), list)
}

func (s *objectBackupStore) PutRestoreItemOperations(backup string, restore string, restoreItemOperations io.Reader) error {
return seekAndPutObject(s.objectStore, s.bucket, s.layout.getRestoreItemOperationsKey(restore), restoreItemOperations)
}
Expand All @@ -563,6 +568,8 @@ func (s *objectBackupStore) GetDownloadURL(target velerov1api.DownloadTarget) (s
return s.objectStore.CreateSignedURL(s.bucket, s.layout.getRestoreLogKey(target.Name), DownloadURLTTL)
case velerov1api.DownloadTargetKindRestoreResults:
return s.objectStore.CreateSignedURL(s.bucket, s.layout.getRestoreResultsKey(target.Name), DownloadURLTTL)
case velerov1api.DownloadTargetKindRestoreResourceList:
return s.objectStore.CreateSignedURL(s.bucket, s.layout.getRestoreResourceListKey(target.Name), DownloadURLTTL)
case velerov1api.DownloadTargetKindCSIBackupVolumeSnapshots:
return s.objectStore.CreateSignedURL(s.bucket, s.layout.getCSIVolumeSnapshotKey(target.Name), DownloadURLTTL)
case velerov1api.DownloadTargetKindCSIBackupVolumeSnapshotContents:
Expand Down
4 changes: 4 additions & 0 deletions pkg/persistence/object_store_layout.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ func (l *ObjectStoreLayout) getRestoreResultsKey(restore string) string {
return path.Join(l.subdirs["restores"], restore, fmt.Sprintf("restore-%s-results.gz", restore))
}

func (l *ObjectStoreLayout) getRestoreResourceListKey(restore string) string {
return path.Join(l.subdirs["restores"], restore, fmt.Sprintf("restore-%s-resource-list.json.gz", restore))
}

func (l *ObjectStoreLayout) getRestoreItemOperationsKey(restore string) string {
return path.Join(l.subdirs["restores"], restore, fmt.Sprintf("restore-%s-itemoperations.json.gz", restore))
}
Expand Down
3 changes: 3 additions & 0 deletions pkg/persistence/object_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,7 @@ func TestGetDownloadURL(t *testing.T) {
velerov1api.DownloadTargetKindRestoreLog: "restores/my-backup/restore-my-backup-logs.gz",
velerov1api.DownloadTargetKindRestoreResults: "restores/my-backup/restore-my-backup-results.gz",
velerov1api.DownloadTargetKindRestoreItemOperations: "restores/my-backup/restore-my-backup-itemoperations.json.gz",
velerov1api.DownloadTargetKindRestoreResourceList: "restores/my-backup/restore-my-backup-resource-list.json.gz",
},
},
{
Expand All @@ -632,6 +633,7 @@ func TestGetDownloadURL(t *testing.T) {
velerov1api.DownloadTargetKindRestoreLog: "velero-backups/restores/my-backup/restore-my-backup-logs.gz",
velerov1api.DownloadTargetKindRestoreResults: "velero-backups/restores/my-backup/restore-my-backup-results.gz",
velerov1api.DownloadTargetKindRestoreItemOperations: "velero-backups/restores/my-backup/restore-my-backup-itemoperations.json.gz",
velerov1api.DownloadTargetKindRestoreResourceList: "velero-backups/restores/my-backup/restore-my-backup-resource-list.json.gz",
},
},
{
Expand All @@ -641,6 +643,7 @@ func TestGetDownloadURL(t *testing.T) {
velerov1api.DownloadTargetKindRestoreLog: "restores/b-cool-20170913154901-20170913154902/restore-b-cool-20170913154901-20170913154902-logs.gz",
velerov1api.DownloadTargetKindRestoreResults: "restores/b-cool-20170913154901-20170913154902/restore-b-cool-20170913154901-20170913154902-results.gz",
velerov1api.DownloadTargetKindRestoreItemOperations: "restores/b-cool-20170913154901-20170913154902/restore-b-cool-20170913154901-20170913154902-itemoperations.json.gz",
velerov1api.DownloadTargetKindRestoreResourceList: "restores/b-cool-20170913154901-20170913154902/restore-b-cool-20170913154901-20170913154902-resource-list.json.gz",
},
},
}
Expand Down
79 changes: 79 additions & 0 deletions pkg/restore/request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
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 restore

import (
"fmt"
"io"
"sort"

"github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/runtime"

velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/volume"
)

const (
itemRestoreResultCreated = "created"
itemRestoreResultUpdated = "updated"
itemRestoreResultFailed = "failed"
itemRestoreResultSkipped = "skipped"
)

type itemKey struct {
resource string
namespace string
name string
}

func resourceKey(obj runtime.Object) string {
gvk := obj.GetObjectKind().GroupVersionKind()
return fmt.Sprintf("%s/%s", gvk.GroupVersion().String(), gvk.Kind)
}

type Request struct {
*velerov1api.Restore

Log logrus.FieldLogger
Backup *velerov1api.Backup
PodVolumeBackups []*velerov1api.PodVolumeBackup
VolumeSnapshots []*volume.Snapshot
BackupReader io.Reader
RestoredItems map[itemKey]string
}

// RestoredResourceList returns the list of restored resources grouped by the API
// Version and Kind
func (r *Request) RestoredResourceList() map[string][]string {
resources := map[string][]string{}
for i, action := range r.RestoredItems {
entry := i.name
if i.namespace != "" {
entry = fmt.Sprintf("%s/%s", i.namespace, i.name)
}
entry = fmt.Sprintf("%s(%s)", entry, action)
resources[i.resource] = append(resources[i.resource], entry)
}

// sort namespace/name entries for each GVK
for _, v := range resources {
sort.Strings(v)
}

return resources
}
67 changes: 67 additions & 0 deletions pkg/restore/request_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
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 restore

import (
"testing"

"github.com/stretchr/testify/assert"
coreV1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestResourceKey(t *testing.T) {
namespace := &coreV1.Namespace{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Namespace",
},
}
assert.Equal(t, "v1/Namespace", resourceKey(namespace))

cr := &coreV1.Namespace{
TypeMeta: metav1.TypeMeta{
APIVersion: "customized/v1",
Kind: "Cron",
},
}
assert.Equal(t, "customized/v1/Cron", resourceKey(cr))
}

func TestRestoredResourceList(t *testing.T) {
request := &Request{
RestoredItems: map[itemKey]string{
{
resource: "v1/Namespace",
namespace: "",
name: "default",
}: "created",
{
resource: "v1/ConfigMap",
namespace: "default",
name: "cm",
}: "skipped",
},
}

expected := map[string][]string{
"v1/ConfigMap": {"default/cm(skipped)"},
"v1/Namespace": {"default(created)"},
}

assert.EqualValues(t, expected, request.RestoredResourceList())
}
Loading

0 comments on commit eb4ecd3

Please sign in to comment.