diff --git a/pkg/builder/delete_backup_request_builder.go b/pkg/builder/delete_backup_request_builder.go new file mode 100644 index 000000000..4788795a3 --- /dev/null +++ b/pkg/builder/delete_backup_request_builder.go @@ -0,0 +1,59 @@ +package builder + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" +) + +// DeleteBackupRequestBuilder builds DeleteBackupRequest objects +type DeleteBackupRequestBuilder struct { + object *velerov1api.DeleteBackupRequest +} + +// ForDeleteBackupRequest is the constructor for a DeleteBackupRequestBuilder. +func ForDeleteBackupRequest(ns, name string) *DeleteBackupRequestBuilder { + return &DeleteBackupRequestBuilder{ + object: &velerov1api.DeleteBackupRequest{ + TypeMeta: metav1.TypeMeta{ + APIVersion: velerov1api.SchemeGroupVersion.String(), + Kind: "DeleteBackupRequest", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns, + Name: name, + }, + }, + } +} + +// Result returns the built DeleteBackupRequest. +func (b *DeleteBackupRequestBuilder) Result() *velerov1api.DeleteBackupRequest { + return b.object +} + +// ObjectMeta applies functional options to the DeleteBackupRequest's ObjectMeta. +func (b *DeleteBackupRequestBuilder) ObjectMeta(opts ...ObjectMetaOpt) *DeleteBackupRequestBuilder { + for _, opt := range opts { + opt(b.object) + } + return b +} + +// BackupName sets the DeleteBackupRequest's backup name. +func (b *DeleteBackupRequestBuilder) BackupName(name string) *DeleteBackupRequestBuilder { + b.object.Spec.BackupName = name + return b +} + +// Phase sets the DeleteBackupRequest's phase. +func (b *DeleteBackupRequestBuilder) Phase(phase velerov1api.DeleteBackupRequestPhase) *DeleteBackupRequestBuilder { + b.object.Status.Phase = phase + return b +} + +// Errors sets the DeleteBackupRequest's errors. +func (b *DeleteBackupRequestBuilder) Errors(errors ...string) *DeleteBackupRequestBuilder { + b.object.Status.Errors = errors + return b +} diff --git a/pkg/builder/item_operation_builder.go b/pkg/builder/item_operation_builder.go new file mode 100644 index 000000000..8ca9d7506 --- /dev/null +++ b/pkg/builder/item_operation_builder.go @@ -0,0 +1,193 @@ +package builder + +import ( + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/vmware-tanzu/velero/pkg/itemoperation" + "github.com/vmware-tanzu/velero/pkg/plugin/velero" +) + +// OperationStatusBuilder builds OperationStatus objects +type OperationStatusBuilder struct { + object *itemoperation.OperationStatus +} + +// ForOperationStatus is the constructor for a OperationStatusBuilder. +func ForOperationStatus() *OperationStatusBuilder { + return &OperationStatusBuilder{ + object: &itemoperation.OperationStatus{}, + } +} + +// Result returns the built OperationStatus. +func (osb *OperationStatusBuilder) Result() *itemoperation.OperationStatus { + return osb.object +} + +// Phase sets the OperationStatus's phase. +func (osb *OperationStatusBuilder) Phase(phase itemoperation.OperationPhase) *OperationStatusBuilder { + osb.object.Phase = phase + return osb +} + +// Error sets the OperationStatus's error. +func (osb *OperationStatusBuilder) Error(err string) *OperationStatusBuilder { + osb.object.Error = err + return osb +} + +// Progress sets the OperationStatus's progress. +func (osb *OperationStatusBuilder) Progress(nComplete int64, nTotal int64, operationUnits string) *OperationStatusBuilder { + osb.object.NCompleted = nComplete + osb.object.NTotal = nTotal + osb.object.OperationUnits = operationUnits + return osb +} + +// Description sets the OperationStatus's description. +func (osb *OperationStatusBuilder) Description(desc string) *OperationStatusBuilder { + osb.object.Description = desc + return osb +} + +// Created sets the OperationStatus's creation timestamp. +func (osb *OperationStatusBuilder) Created(t time.Time) *OperationStatusBuilder { + osb.object.Created = &metav1.Time{Time: t} + return osb +} + +// Updated sets the OperationStatus's last update timestamp. +func (osb *OperationStatusBuilder) Updated(t time.Time) *OperationStatusBuilder { + osb.object.Updated = &metav1.Time{Time: t} + return osb +} + +// Started sets the OperationStatus's start timestamp. +func (osb *OperationStatusBuilder) Started(t time.Time) *OperationStatusBuilder { + osb.object.Started = &metav1.Time{Time: t} + return osb +} + +// BackupOperationBuilder builds BackupOperation objects +type BackupOperationBuilder struct { + object *itemoperation.BackupOperation +} + +// ForBackupOperation is the constructor for a BackupOperationBuilder. +func ForBackupOperation() *BackupOperationBuilder { + return &BackupOperationBuilder{ + object: &itemoperation.BackupOperation{}, + } +} + +// Result returns the built BackupOperation. +func (bb *BackupOperationBuilder) Result() *itemoperation.BackupOperation { + return bb.object +} + +// BackupName sets the BackupOperation's backup name. +func (bb *BackupOperationBuilder) BackupName(name string) *BackupOperationBuilder { + bb.object.Spec.BackupName = name + return bb +} + +// OperationID sets the BackupOperation's operation ID. +func (bb *BackupOperationBuilder) OperationID(id string) *BackupOperationBuilder { + bb.object.Spec.OperationID = id + return bb +} + +// Status sets the BackupOperation's status. +func (bb *BackupOperationBuilder) Status(status itemoperation.OperationStatus) *BackupOperationBuilder { + bb.object.Status = status + return bb +} + +// ResourceIdentifier sets the BackupOperation's resource identifier. +func (bb *BackupOperationBuilder) ResourceIdentifier(group, resource, ns, name string) *BackupOperationBuilder { + bb.object.Spec.ResourceIdentifier = velero.ResourceIdentifier{ + GroupResource: schema.GroupResource{ + Group: group, + Resource: resource, + }, + Namespace: ns, + Name: name, + } + return bb +} + +// BackupItemAction sets the BackupOperation's backup item action. +func (bb *BackupOperationBuilder) BackupItemAction(bia string) *BackupOperationBuilder { + bb.object.Spec.BackupItemAction = bia + return bb +} + +// PostOperationItem adds a post-operation item to the BackupOperation's list of post-operation items. +func (bb *BackupOperationBuilder) PostOperationItem(group, resource, ns, name string) *BackupOperationBuilder { + bb.object.Spec.PostOperationItems = append(bb.object.Spec.PostOperationItems, velero.ResourceIdentifier{ + GroupResource: schema.GroupResource{ + Group: group, + Resource: resource, + }, + Namespace: ns, + Name: name, + }) + return bb +} + +// RestoreOperationBuilder builds RestoreOperation objects +type RestoreOperationBuilder struct { + object *itemoperation.RestoreOperation +} + +// ForRestoreOperation is the constructor for a RestoreOperationBuilder. +func ForRestoreOperation() *RestoreOperationBuilder { + return &RestoreOperationBuilder{ + object: &itemoperation.RestoreOperation{}, + } +} + +// Result returns the built RestoreOperation. +func (rb *RestoreOperationBuilder) Result() *itemoperation.RestoreOperation { + return rb.object +} + +// RestoreName sets the RestoreOperation's restore name. +func (rb *RestoreOperationBuilder) RestoreName(name string) *RestoreOperationBuilder { + rb.object.Spec.RestoreName = name + return rb +} + +// OperationID sets the RestoreOperation's operation ID. +func (rb *RestoreOperationBuilder) OperationID(id string) *RestoreOperationBuilder { + rb.object.Spec.OperationID = id + return rb +} + +// RestoreItemAction sets the RestoreOperation's restore item action. +func (rb *RestoreOperationBuilder) RestoreItemAction(ria string) *RestoreOperationBuilder { + rb.object.Spec.RestoreItemAction = ria + return rb +} + +// Status sets the RestoreOperation's status. +func (rb *RestoreOperationBuilder) Status(status itemoperation.OperationStatus) *RestoreOperationBuilder { + rb.object.Status = status + return rb +} + +// ResourceIdentifier sets the RestoreOperation's resource identifier. +func (rb *RestoreOperationBuilder) ResourceIdentifier(group, resource, ns, name string) *RestoreOperationBuilder { + rb.object.Spec.ResourceIdentifier = velero.ResourceIdentifier{ + GroupResource: schema.GroupResource{ + Group: group, + Resource: resource, + }, + Namespace: ns, + Name: name, + } + return rb +} diff --git a/pkg/builder/object_meta.go b/pkg/builder/object_meta.go index 6df1afadc..90730e4be 100644 --- a/pkg/builder/object_meta.go +++ b/pkg/builder/object_meta.go @@ -153,3 +153,10 @@ func WithManagedFields(val []metav1.ManagedFieldsEntry) func(obj metav1.Object) obj.SetManagedFields(val) } } + +// WithCreationTimestamp is a functional option that applies the specified creationTimestamp +func WithCreationTimestamp(t time.Time) func(obj metav1.Object) { + return func(obj metav1.Object) { + obj.SetCreationTimestamp(metav1.Time{Time: t}) + } +} diff --git a/pkg/builder/pod_volume_backup_builder.go b/pkg/builder/pod_volume_backup_builder.go index 2b15d5e19..14e57a063 100644 --- a/pkg/builder/pod_volume_backup_builder.go +++ b/pkg/builder/pod_volume_backup_builder.go @@ -53,7 +53,6 @@ func (b *PodVolumeBackupBuilder) ObjectMeta(opts ...ObjectMetaOpt) *PodVolumeBac for _, opt := range opts { opt(b.object) } - return b } diff --git a/pkg/builder/pod_volume_restore_builder.go b/pkg/builder/pod_volume_restore_builder.go new file mode 100644 index 000000000..c131a0384 --- /dev/null +++ b/pkg/builder/pod_volume_restore_builder.go @@ -0,0 +1,83 @@ +package builder + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" +) + +// PodVolumeRestoreBuilder builds PodVolumeRestore objects. +type PodVolumeRestoreBuilder struct { + object *velerov1api.PodVolumeRestore +} + +// ForPodVolumeRestore is the constructor for a PodVolumeRestoreBuilder. +func ForPodVolumeRestore(ns, name string) *PodVolumeRestoreBuilder { + return &PodVolumeRestoreBuilder{ + object: &velerov1api.PodVolumeRestore{ + TypeMeta: metav1.TypeMeta{ + APIVersion: velerov1api.SchemeGroupVersion.String(), + Kind: "PodVolumeRestore", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns, + Name: name, + }, + }, + } +} + +// Result returns the built PodVolumeRestore. +func (b *PodVolumeRestoreBuilder) Result() *velerov1api.PodVolumeRestore { + return b.object +} + +// ObjectMeta applies functional options to the PodVolumeRestore's ObjectMeta. +func (b *PodVolumeRestoreBuilder) ObjectMeta(opts ...ObjectMetaOpt) *PodVolumeRestoreBuilder { + for _, opt := range opts { + opt(b.object) + } + return b +} + +// Phase sets the PodVolumeRestore's phase. +func (b *PodVolumeRestoreBuilder) Phase(phase velerov1api.PodVolumeRestorePhase) *PodVolumeRestoreBuilder { + b.object.Status.Phase = phase + return b +} + +// BackupStorageLocation sets the PodVolumeRestore's backup storage location. +func (b *PodVolumeRestoreBuilder) BackupStorageLocation(name string) *PodVolumeRestoreBuilder { + b.object.Spec.BackupStorageLocation = name + return b +} + +// SnapshotID sets the PodVolumeRestore's snapshot ID. +func (b *PodVolumeRestoreBuilder) SnapshotID(snapshotID string) *PodVolumeRestoreBuilder { + b.object.Spec.SnapshotID = snapshotID + return b +} + +// PodName sets the name of the pod associated with this PodVolumeRestore. +func (b *PodVolumeRestoreBuilder) PodName(name string) *PodVolumeRestoreBuilder { + b.object.Spec.Pod.Name = name + return b +} + +// PodNamespace sets the name of the pod associated with this PodVolumeRestore. +func (b *PodVolumeRestoreBuilder) PodNamespace(ns string) *PodVolumeRestoreBuilder { + b.object.Spec.Pod.Namespace = ns + return b +} + +// Volume sets the name of the volume associated with this PodVolumeRestore. +func (b *PodVolumeRestoreBuilder) Volume(volume string) *PodVolumeRestoreBuilder { + b.object.Spec.Volume = volume + return b +} + +// UploaderType sets the type of uploader to use for this PodVolumeRestore. +func (b *PodVolumeRestoreBuilder) UploaderType(uploaderType string) *PodVolumeRestoreBuilder { + b.object.Spec.UploaderType = uploaderType + return b +} diff --git a/pkg/cmd/util/output/backup_describer_test.go b/pkg/cmd/util/output/backup_describer_test.go index 074c72bbe..f4ea3319f 100644 --- a/pkg/cmd/util/output/backup_describer_test.go +++ b/pkg/cmd/util/output/backup_describer_test.go @@ -6,6 +6,10 @@ import ( "text/tabwriter" "time" + "github.com/vmware-tanzu/velero/pkg/itemoperation" + + "github.com/stretchr/testify/require" + snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" @@ -46,7 +50,31 @@ func TestDescribeBackupSpec(t *testing.T) { TTL(72 * time.Hour). CSISnapshotTimeout(10 * time.Minute). DataMover("mover"). - Result().Spec + Hooks(velerov1api.BackupHooks{ + Resources: []velerov1api.BackupResourceHookSpec{ + { + Name: "hook-1", + PreHooks: []velerov1api.BackupResourceHook{ + { + Exec: &velerov1api.ExecHook{ + Container: "hook-container-1", + Command: []string{"pre"}, + OnError: velerov1api.HookErrorModeContinue, + }, + }, + }, + PostHooks: []velerov1api.BackupResourceHook{ + { + Exec: &velerov1api.ExecHook{ + Container: "hook-container-1", + Command: []string{"post"}, + OnError: velerov1api.HookErrorModeContinue, + }, + }, + }, + }, + }, + }).Result().Spec expect1 := `Namespaces: Included: inc-ns-1, inc-ns-2 @@ -70,7 +98,30 @@ TTL: 72h0m0s CSISnapshotTimeout: 10m0s ItemOperationTimeout: 0s -Hooks: +Hooks: + Resources: + hook-1: + Namespaces: + Included: inc-ns-1, inc-ns-2 + Excluded: exc-ns-1, exc-ns-2 + + Resources: + Included: inc-res-1, inc-res-2 + Excluded: exc-res-1, exc-res-2 + + Label selector: + + Pre Exec Hook: + Container: hook-container-1 + Command: pre + On Error: Continue + Timeout: 0s + + Post Exec Hook: + Container: hook-container-1 + Command: post + On Error: Continue + Timeout: 0s ` input2 := builder.ForBackup("test-ns", "test-backup-2"). @@ -112,13 +163,94 @@ ItemOperationTimeout: 0s Hooks: ` + input3 := builder.ForBackup("test-ns", "test-backup-3"). + StorageLocation("backup-location"). + OrderedResources(map[string]string{ + "kind1": "rs1-1, rs1-2", + }).Hooks(velerov1api.BackupHooks{ + Resources: []velerov1api.BackupResourceHookSpec{ + { + Name: "hook-1", + PreHooks: []velerov1api.BackupResourceHook{ + { + Exec: &velerov1api.ExecHook{ + Container: "hook-container-1", + Command: []string{"pre"}, + OnError: velerov1api.HookErrorModeContinue, + }, + }, + }, + PostHooks: []velerov1api.BackupResourceHook{ + { + Exec: &velerov1api.ExecHook{ + Container: "hook-container-1", + Command: []string{"post"}, + OnError: velerov1api.HookErrorModeContinue, + }, + }, + }, + }, + }, + }).Result().Spec + + expect3 := `Namespaces: + Included: * + Excluded: + +Resources: + Included: * + Excluded: + Cluster-scoped: auto + +Label selector: + +Storage Location: backup-location + +Velero-Native Snapshot PVs: auto +Snapshot Move Data: auto +Data Mover: + +TTL: 0s + +CSISnapshotTimeout: 0s +ItemOperationTimeout: 0s + +Hooks: + Resources: + hook-1: + Namespaces: + Included: * + Excluded: + + Resources: + Included: * + Excluded: + + Label selector: + + Pre Exec Hook: + Container: hook-container-1 + Command: pre + On Error: Continue + Timeout: 0s + + Post Exec Hook: + Container: hook-container-1 + Command: post + On Error: Continue + Timeout: 0s + +OrderedResources: + kind1: rs1-1, rs1-2 +` + testcases := []struct { name string input velerov1api.BackupSpec expect string }{ { - name: "old resource filter", + name: "old resource filter with hooks", input: input1, expect: expect1, }, @@ -127,6 +259,11 @@ Hooks: input: input2, expect: expect2, }, + { + name: "old resource filter with hooks and ordered resources", + input: input3, + expect: expect3, + }, } for _, tc := range testcases { @@ -164,7 +301,6 @@ func TestDescribeSnapshot(t *testing.T) { func TestDescribePodVolumeBackups(t *testing.T) { pvb1 := builder.ForPodVolumeBackup("test-ns", "test-pvb1"). - BackupStorageLocation("backup-location"). UploaderType("kopia"). Phase(velerov1api.PodVolumeBackupPhaseCompleted). BackupStorageLocation("bsl-1"). @@ -173,7 +309,6 @@ func TestDescribePodVolumeBackups(t *testing.T) { PodNamespace("pod-ns-1"). SnapshotID("snap-1").Result() pvb2 := builder.ForPodVolumeBackup("test-ns1", "test-pvb2"). - BackupStorageLocation("backup-location"). UploaderType("kopia"). Phase(velerov1api.PodVolumeBackupPhaseCompleted). BackupStorageLocation("bsl-1"). @@ -289,3 +424,99 @@ Snapshot Content Name: vsc-1 }) } } + +func TestDescribeDeleteBackupRequests(t *testing.T) { + t1, err1 := time.Parse("2006-Jan-02", "2023-Jun-26") + require.Nil(t, err1) + dbr1 := builder.ForDeleteBackupRequest("velero", "dbr1"). + ObjectMeta(builder.WithCreationTimestamp(t1)). + BackupName("bak-1"). + Phase(velerov1api.DeleteBackupRequestPhaseProcessed). + Errors("some error").Result() + t2, err2 := time.Parse("2006-Jan-02", "2023-Jun-25") + require.Nil(t, err2) + dbr2 := builder.ForDeleteBackupRequest("velero", "dbr2"). + ObjectMeta(builder.WithCreationTimestamp(t2)). + BackupName("bak-2"). + Phase(velerov1api.DeleteBackupRequestPhaseInProgress).Result() + + testcases := []struct { + name string + input []velerov1api.DeleteBackupRequest + expect string + }{ + { + name: "empty list", + input: []velerov1api.DeleteBackupRequest{}, + expect: `Deletion Attempts: +`, + }, + { + name: "list with one failed and one in-progress request", + input: []velerov1api.DeleteBackupRequest{*dbr1, *dbr2}, + expect: `Deletion Attempts (1 failed): + 2023-06-26 00:00:00 +0000 UTC: Processed + Errors: + some error + + 2023-06-25 00:00:00 +0000 UTC: InProgress +`, + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(tt *testing.T) { + d := &Describer{ + Prefix: "", + out: &tabwriter.Writer{}, + buf: &bytes.Buffer{}, + } + d.out.Init(d.buf, 0, 8, 2, ' ', 0) + DescribeDeleteBackupRequests(d, tc.input) + d.out.Flush() + assert.Equal(tt, tc.expect, d.buf.String()) + }) + } +} + +func TestDescribeBackupItemOperation(t *testing.T) { + t1, err1 := time.Parse("2006-Jan-02", "2023-Jun-26") + require.Nil(t, err1) + t2, err2 := time.Parse("2006-Jan-02", "2023-Jun-25") + require.Nil(t, err2) + t3, err3 := time.Parse("2006-Jan-02", "2023-Jun-24") + require.Nil(t, err3) + input := builder.ForBackupOperation(). + BackupName("backup-1"). + OperationID("op-1"). + BackupItemAction("action-1"). + ResourceIdentifier("group", "rs-type", "ns", "rs-name"). + Status(*builder.ForOperationStatus(). + Phase(itemoperation.OperationPhaseFailed). + Error("operation error"). + Progress(50, 100, "bytes"). + Description("operation description"). + Created(t3). + Started(t2). + Updated(t1). + Result()).Result() + expected := ` Operation for rs-type.group ns/rs-name: + Backup Item Action Plugin: action-1 + Operation ID: op-1 + Phase: Failed + Operation Error: operation error + Progress: 50 of 100 complete (bytes) + Progress description: operation description + Created: 2023-06-24 00:00:00 +0000 UTC + Started: 2023-06-25 00:00:00 +0000 UTC + Updated: 2023-06-26 00:00:00 +0000 UTC +` + d := &Describer{ + Prefix: "", + out: &tabwriter.Writer{}, + buf: &bytes.Buffer{}, + } + d.out.Init(d.buf, 0, 8, 2, ' ', 0) + describeBackupItemOperation(d, input) + d.out.Flush() + assert.Equal(t, expected, d.buf.String()) +} diff --git a/pkg/cmd/util/output/backup_structured_describer_test.go b/pkg/cmd/util/output/backup_structured_describer_test.go index eb2aecd28..e58dc5d2a 100644 --- a/pkg/cmd/util/output/backup_structured_describer_test.go +++ b/pkg/cmd/util/output/backup_structured_describer_test.go @@ -5,6 +5,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" "github.com/vmware-tanzu/velero/pkg/features" @@ -31,7 +33,32 @@ func TestDescribeBackupInSF(t *testing.T) { StorageLocation("backup-location"). TTL(72 * time.Hour). CSISnapshotTimeout(10 * time.Minute). - DataMover("mover") + DataMover("mover"). + Hooks(velerov1api.BackupHooks{ + Resources: []velerov1api.BackupResourceHookSpec{ + { + Name: "hook-1", + PreHooks: []velerov1api.BackupResourceHook{ + { + Exec: &velerov1api.ExecHook{ + Container: "hook-container-1", + Command: []string{"pre"}, + OnError: velerov1api.HookErrorModeContinue, + }, + }, + }, + PostHooks: []velerov1api.BackupResourceHook{ + { + Exec: &velerov1api.ExecHook{ + Container: "hook-container-1", + Command: []string{"post"}, + OnError: velerov1api.HookErrorModeContinue, + }, + }, + }, + }, + }, + }) expect1 := map[string]interface{}{ "spec": map[string]interface{}{ @@ -51,13 +78,73 @@ func TestDescribeBackupInSF(t *testing.T) { "TTL": "72h0m0s", "CSISnapshotTimeout": "10m0s", "veleroSnapshotMoveData": "auto", + "hooks": map[string]interface{}{ + "resources": map[string]interface{}{ + "hook-1": map[string]interface{}{ + "labelSelector": emptyDisplay, + "namespaces": map[string]string{ + "included": "inc-ns-1, inc-ns-2", + "excluded": "exc-ns-1, exc-ns-2", + }, + "preExecHook": []map[string]interface{}{ + { + "container": "hook-container-1", + "command": "pre", + "onError:": velerov1api.HookErrorModeContinue, + "timeout": "0s", + }, + }, + "postExecHook": []map[string]interface{}{ + { + "container": "hook-container-1", + "command": "post", + "onError:": velerov1api.HookErrorModeContinue, + "timeout": "0s", + }, + }, + "resources": map[string]string{ + "included": "inc-res-1, inc-res-2", + "excluded": "exc-res-1, exc-res-2", + }, + }, + }, + }, }, } DescribeBackupSpecInSF(sd, backupBuilder1.Result().Spec) assert.True(t, reflect.DeepEqual(sd.output, expect1)) - backupBuilder2 := builder.ForBackup("test-ns-2", "test-backup-2") - backupBuilder2.StorageLocation("backup-location") + backupBuilder2 := builder.ForBackup("test-ns-2", "test-backup-2"). + StorageLocation("backup-location"). + OrderedResources(map[string]string{ + "kind1": "rs1-1, rs1-2", + "kind2": "rs2-1, rs2-2", + }).Hooks(velerov1api.BackupHooks{ + Resources: []velerov1api.BackupResourceHookSpec{ + { + Name: "hook-1", + PreHooks: []velerov1api.BackupResourceHook{ + { + Exec: &velerov1api.ExecHook{ + Container: "hook-container-1", + Command: []string{"pre"}, + OnError: velerov1api.HookErrorModeContinue, + }, + }, + }, + PostHooks: []velerov1api.BackupResourceHook{ + { + Exec: &velerov1api.ExecHook{ + Container: "hook-container-1", + Command: []string{"post"}, + OnError: velerov1api.HookErrorModeContinue, + }, + }, + }, + }, + }, + }) + expect2 := map[string]interface{}{ "spec": map[string]interface{}{ "namespaces": map[string]interface{}{ @@ -76,6 +163,41 @@ func TestDescribeBackupInSF(t *testing.T) { "TTL": "0s", "CSISnapshotTimeout": "0s", "veleroSnapshotMoveData": "auto", + "hooks": map[string]interface{}{ + "resources": map[string]interface{}{ + "hook-1": map[string]interface{}{ + "labelSelector": emptyDisplay, + "namespaces": map[string]string{ + "included": "*", + "excluded": emptyDisplay, + }, + "preExecHook": []map[string]interface{}{ + { + "container": "hook-container-1", + "command": "pre", + "onError:": velerov1api.HookErrorModeContinue, + "timeout": "0s", + }, + }, + "postExecHook": []map[string]interface{}{ + { + "container": "hook-container-1", + "command": "post", + "onError:": velerov1api.HookErrorModeContinue, + "timeout": "0s", + }, + }, + "resources": map[string]string{ + "included": "*", + "excluded": emptyDisplay, + }, + }, + }, + }, + "orderedResources": map[string]string{ + "kind1": "rs1-1, rs1-2", + "kind2": "rs2-1, rs2-2", + }, }, } DescribeBackupSpecInSF(sd, backupBuilder2.Result().Spec) @@ -250,3 +372,83 @@ func TestDescribeBackupResultInSF(t *testing.T) { describeResultInSF(got, input) assert.True(t, reflect.DeepEqual(got, expect)) } + +func TestDescribeDeleteBackupRequestsInSF(t *testing.T) { + t1, err1 := time.Parse("2006-Jan-02", "2023-Jun-26") + require.Nil(t, err1) + dbr1 := builder.ForDeleteBackupRequest("velero", "dbr1"). + ObjectMeta(builder.WithCreationTimestamp(t1)). + BackupName("bak-1"). + Phase(velerov1api.DeleteBackupRequestPhaseProcessed). + Errors("some error").Result() + t2, err2 := time.Parse("2006-Jan-02", "2023-Jun-25") + require.Nil(t, err2) + dbr2 := builder.ForDeleteBackupRequest("velero", "dbr2"). + ObjectMeta(builder.WithCreationTimestamp(t2)). + BackupName("bak-2"). + Phase(velerov1api.DeleteBackupRequestPhaseInProgress).Result() + + testcases := []struct { + name string + input []velerov1api.DeleteBackupRequest + expect map[string]interface{} + }{ + { + name: "empty list", + input: []velerov1api.DeleteBackupRequest{}, + expect: map[string]interface{}{ + "deletionAttempts": map[string]interface{}{ + "deleteBackupRequests": []map[string]interface{}{}, + }, + }, + }, + { + name: "list with one failed and one in-progress request", + input: []velerov1api.DeleteBackupRequest{*dbr1, *dbr2}, + expect: map[string]interface{}{ + "deletionAttempts": map[string]interface{}{ + "failed": int(1), + "deleteBackupRequests": []map[string]interface{}{ + { + "creationTimestamp": t1.String(), + "phase": velerov1api.DeleteBackupRequestPhaseProcessed, + "errors": []string{ + "some error", + }, + }, + { + "creationTimestamp": t2.String(), + "phase": velerov1api.DeleteBackupRequestPhaseInProgress, + }, + }, + }, + }, + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(tt *testing.T) { + sd := &StructuredDescriber{ + output: make(map[string]interface{}), + format: "", + } + DescribeDeleteBackupRequestsInSF(sd, tc.input) + assert.True(tt, reflect.DeepEqual(sd.output, tc.expect)) + }) + } + +} + +func TestDescribeSnapshotInSF(t *testing.T) { + res := map[string]interface{}{} + iops := int64(100) + describeSnapshotInSF("pv-1", "snapshot-1", "ebs", "us-east-2", &iops, res) + expect := map[string]interface{}{ + "pv-1": map[string]string{ + "snapshotID": "snapshot-1", + "type": "ebs", + "availabilityZone": "us-east-2", + "IOPS": "100", + }, + } + assert.True(t, reflect.DeepEqual(expect, res)) +} diff --git a/pkg/cmd/util/output/describe.go b/pkg/cmd/util/output/describe.go index b6a44b676..d4f6e9e4f 100644 --- a/pkg/cmd/util/output/describe.go +++ b/pkg/cmd/util/output/describe.go @@ -47,15 +47,6 @@ func Describe(fn func(d *Describer)) string { return d.buf.String() } -func NewDescriber(minwidth, tabwidth, padding int, padchar byte, flags uint) *Describer { - d := &Describer{ - out: new(tabwriter.Writer), - buf: new(bytes.Buffer), - } - d.out.Init(d.buf, minwidth, tabwidth, padding, padchar, flags) - return d -} - func (d *Describer) Printf(msg string, args ...interface{}) { fmt.Fprint(d.out, d.Prefix) fmt.Fprintf(d.out, msg, args...) diff --git a/pkg/cmd/util/output/describe_test.go b/pkg/cmd/util/output/describe_test.go index 9385aa107..45becf873 100644 --- a/pkg/cmd/util/output/describe_test.go +++ b/pkg/cmd/util/output/describe_test.go @@ -3,6 +3,7 @@ package output import ( "bytes" "fmt" + "reflect" "testing" "text/tabwriter" @@ -132,3 +133,36 @@ func TestStructuredDescriber_JSONEncode(t *testing.T) { }) } } + +func TestStructuredDescriber_DescribeMetadata(t *testing.T) { + d := NewStructuredDescriber("") + input := metav1.ObjectMeta{ + Name: "test", + Namespace: "test-ns", + Labels: map[string]string{ + "label-1": "v1", + "label-2": "v2", + }, + Annotations: map[string]string{ + "annotation-1": "v1", + "annotation-2": "v2", + }, + } + expect := map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "test", + "namespace": "test-ns", + "labels": map[string]string{ + "label-1": "v1", + "label-2": "v2", + }, + "annotations": map[string]string{ + "annotation-1": "v1", + "annotation-2": "v2", + }, + }, + } + d.DescribeMetadata(input) + + assert.True(t, reflect.DeepEqual(expect, d.output)) +} diff --git a/pkg/cmd/util/output/restore_describer_test.go b/pkg/cmd/util/output/restore_describer_test.go new file mode 100644 index 000000000..dff7aa96d --- /dev/null +++ b/pkg/cmd/util/output/restore_describer_test.go @@ -0,0 +1,183 @@ +package output + +import ( + "bytes" + "testing" + "text/tabwriter" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/builder" + "github.com/vmware-tanzu/velero/pkg/itemoperation" + "github.com/vmware-tanzu/velero/pkg/util/results" +) + +func TestDescribeResult(t *testing.T) { + testcases := []struct { + name string + inputName string + inputResult results.Result + expect string + }{ + { + name: "result without ns warns", + inputName: "restore-1", + inputResult: results.Result{ + Velero: []string{"velero-msg-1", "velero-msg-2"}, + Cluster: []string{"cluster-msg-1", "cluster-msg-2"}, + Namespaces: map[string][]string{}, + }, + expect: `restore-1: + Velero: velero-msg-1 + velero-msg-2 + Cluster: cluster-msg-1 + cluster-msg-2 + Namespaces: +`, + }, + { + name: "result with ns warns", + inputName: "restore-2", + inputResult: results.Result{ + Velero: []string{"velero-msg-1", "velero-msg-2"}, + Cluster: []string{"cluster-msg-1", "cluster-msg-2"}, + Namespaces: map[string][]string{ + "ns-1": {"ns-1-warn-1", "ns-1-warn-2"}, + }, + }, + expect: `restore-2: + Velero: velero-msg-1 + velero-msg-2 + Cluster: cluster-msg-1 + cluster-msg-2 + Namespaces: + ns-1: ns-1-warn-1 + ns-1-warn-2 +`, + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(tt *testing.T) { + d := &Describer{ + Prefix: "", + out: &tabwriter.Writer{}, + buf: &bytes.Buffer{}, + } + d.out.Init(d.buf, 0, 8, 2, ' ', 0) + describeResult(d, tc.inputName, tc.inputResult) + d.out.Flush() + assert.Equal(tt, tc.expect, d.buf.String()) + }) + } +} + +func TestDescribeRestoreItemOperation(t *testing.T) { + t1, err1 := time.Parse("2006-Jan-02", "2023-Jun-26") + require.Nil(t, err1) + t2, err2 := time.Parse("2006-Jan-02", "2023-Jun-25") + require.Nil(t, err2) + t3, err3 := time.Parse("2006-Jan-02", "2023-Jun-24") + require.Nil(t, err3) + input := builder.ForRestoreOperation(). + RestoreName("restore-1"). + OperationID("op-1"). + RestoreItemAction("action-1"). + ResourceIdentifier("group", "rs-type", "ns", "rs-name"). + Status(*builder.ForOperationStatus(). + Phase(itemoperation.OperationPhaseFailed). + Error("operation error"). + Progress(50, 100, "bytes"). + Description("operation description"). + Created(t3). + Started(t2). + Updated(t1). + Result()).Result() + expected := ` Operation for rs-type.group ns/rs-name: + Restore Item Action Plugin: action-1 + Operation ID: op-1 + Phase: Failed + Operation Error: operation error + Progress: 50 of 100 complete (bytes) + Progress description: operation description + Created: 2023-06-24 00:00:00 +0000 UTC + Started: 2023-06-25 00:00:00 +0000 UTC + Updated: 2023-06-26 00:00:00 +0000 UTC +` + d := &Describer{ + Prefix: "", + out: &tabwriter.Writer{}, + buf: &bytes.Buffer{}, + } + d.out.Init(d.buf, 0, 8, 2, ' ', 0) + describeRestoreItemOperation(d, input) + d.out.Flush() + assert.Equal(t, expected, d.buf.String()) +} + +func TestDescribePodVolumeRestores(t *testing.T) { + pvr1 := builder.ForPodVolumeRestore("velero", "pvr-1"). + UploaderType("kopia"). + Phase(velerov1api.PodVolumeRestorePhaseCompleted). + BackupStorageLocation("bsl-1"). + Volume("vol-1"). + PodName("pod-1"). + PodNamespace("pod-ns-1"). + SnapshotID("snap-1").Result() + pvr2 := builder.ForPodVolumeRestore("velero", "pvr-2"). + UploaderType("kopia"). + Phase(velerov1api.PodVolumeRestorePhaseCompleted). + BackupStorageLocation("bsl-1"). + Volume("vol-2"). + PodName("pod-2"). + PodNamespace("pod-ns-1"). + SnapshotID("snap-2").Result() + + testcases := []struct { + name string + inputPVRList []velerov1api.PodVolumeRestore + inputDetails bool + expect string + }{ + { + name: "empty list", + inputPVRList: []velerov1api.PodVolumeRestore{}, + inputDetails: true, + expect: ``, + }, + { + name: "2 completed pvrs no details", + inputPVRList: []velerov1api.PodVolumeRestore{*pvr1, *pvr2}, + inputDetails: false, + expect: `kopia Restores (specify --details for more information): + Completed: 2 +`, + }, + { + name: "2 completed pvrs with details", + inputPVRList: []velerov1api.PodVolumeRestore{*pvr1, *pvr2}, + inputDetails: true, + expect: `kopia Restores: + Completed: + pod-ns-1/pod-1: vol-1 + pod-ns-1/pod-2: vol-2 +`, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(tt *testing.T) { + d := &Describer{ + Prefix: "", + out: &tabwriter.Writer{}, + buf: &bytes.Buffer{}, + } + d.out.Init(d.buf, 0, 8, 2, ' ', 0) + describePodVolumeRestores(d, tc.inputPVRList, tc.inputDetails) + d.out.Flush() + assert.Equal(tt, tc.expect, d.buf.String()) + }) + } +} diff --git a/pkg/cmd/util/output/schedule_describe_test.go b/pkg/cmd/util/output/schedule_describe_test.go new file mode 100644 index 000000000..bcf71ad0d --- /dev/null +++ b/pkg/cmd/util/output/schedule_describe_test.go @@ -0,0 +1,123 @@ +package output + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/builder" +) + +func TestDescribeSchedule(t *testing.T) { + input1 := builder.ForSchedule("velero", "schedule-1"). + Phase(velerov1api.SchedulePhaseFailedValidation). + ValidationError("validation failed").Result() + expect1 := `Name: schedule-1 +Namespace: velero +Labels: +Annotations: + +Phase: FailedValidation + +Validation errors: validation failed + +Paused: false + +Schedule: + +Backup Template: + Namespaces: + Included: * + Excluded: + + Resources: + Included: * + Excluded: + Cluster-scoped: auto + + Label selector: + + Storage Location: + + Velero-Native Snapshot PVs: auto + Snapshot Move Data: auto + Data Mover: + + TTL: 0s + + CSISnapshotTimeout: 0s + ItemOperationTimeout: 0s + + Hooks: + +Last Backup: +` + + input2 := builder.ForSchedule("velero", "schedule-2"). + Phase(velerov1api.SchedulePhaseEnabled). + CronSchedule("0 0 * * *"). + Template(builder.ForBackup("velero", "backup-1").Result().Spec). + LastBackupTime("2023-06-25 15:04:05").Result() + expect2 := `Name: schedule-2 +Namespace: velero +Labels: +Annotations: + +Phase: Enabled + +Paused: false + +Schedule: 0 0 * * * + +Backup Template: + Namespaces: + Included: * + Excluded: + + Resources: + Included: * + Excluded: + Cluster-scoped: auto + + Label selector: + + Storage Location: + + Velero-Native Snapshot PVs: auto + Snapshot Move Data: auto + Data Mover: + + TTL: 0s + + CSISnapshotTimeout: 0s + ItemOperationTimeout: 0s + + Hooks: + +Last Backup: 2023-06-25 15:04:05 +0000 UTC +` + + testcases := []struct { + name string + input *velerov1api.Schedule + expect string + }{ + { + name: "schedule failed in validation", + input: input1, + expect: expect1, + }, + { + name: "schedule enabled", + input: input2, + expect: expect2, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(tt *testing.T) { + assert.Equal(tt, tc.expect, DescribeSchedule(tc.input)) + }) + } +}