diff --git a/pkg/builder/pod_volume_backup_builder.go b/pkg/builder/pod_volume_backup_builder.go index 052342a72..2b15d5e19 100644 --- a/pkg/builder/pod_volume_backup_builder.go +++ b/pkg/builder/pod_volume_backup_builder.go @@ -98,3 +98,9 @@ func (b *PodVolumeBackupBuilder) Volume(volume string) *PodVolumeBackupBuilder { b.object.Spec.Volume = volume return b } + +// UploaderType sets the type of uploader to use for this PodVolumeBackup. +func (b *PodVolumeBackupBuilder) UploaderType(uploaderType string) *PodVolumeBackupBuilder { + b.object.Spec.UploaderType = uploaderType + return b +} diff --git a/pkg/builder/volume_snapshot_content_builder.go b/pkg/builder/volume_snapshot_content_builder.go index 936eb74c5..734eeedf3 100644 --- a/pkg/builder/volume_snapshot_content_builder.go +++ b/pkg/builder/volume_snapshot_content_builder.go @@ -48,8 +48,8 @@ func (v *VolumeSnapshotContentBuilder) Result() *snapshotv1api.VolumeSnapshotCon } // Status initiates VolumeSnapshotContent's status. -func (v *VolumeSnapshotContentBuilder) Status() *VolumeSnapshotContentBuilder { - v.object.Status = &snapshotv1api.VolumeSnapshotContentStatus{} +func (v *VolumeSnapshotContentBuilder) Status(status *snapshotv1api.VolumeSnapshotContentStatus) *VolumeSnapshotContentBuilder { + v.object.Status = status return v } diff --git a/pkg/cmd/util/output/backup_describer_test.go b/pkg/cmd/util/output/backup_describer_test.go new file mode 100644 index 000000000..074c72bbe --- /dev/null +++ b/pkg/cmd/util/output/backup_describer_test.go @@ -0,0 +1,291 @@ +package output + +import ( + "bytes" + "testing" + "text/tabwriter" + "time" + + snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + + "github.com/vmware-tanzu/velero/pkg/builder" + "github.com/vmware-tanzu/velero/pkg/features" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" +) + +func TestDescribeResourcePolicies(t *testing.T) { + input := &v1.TypedLocalObjectReference{ + Kind: "configmap", + Name: "test-resource-policy", + } + d := &Describer{ + Prefix: "", + out: &tabwriter.Writer{}, + buf: &bytes.Buffer{}, + } + d.out.Init(d.buf, 0, 8, 2, ' ', 0) + DescribeResourcePolicies(d, input) + d.out.Flush() + expect := `Resource policies: + Type: configmap + Name: test-resource-policy +` + assert.Equal(t, expect, d.buf.String()) +} + +func TestDescribeBackupSpec(t *testing.T) { + input1 := builder.ForBackup("test-ns", "test-backup-1"). + IncludedNamespaces("inc-ns-1", "inc-ns-2"). + ExcludedNamespaces("exc-ns-1", "exc-ns-2"). + IncludedResources("inc-res-1", "inc-res-2"). + ExcludedResources("exc-res-1", "exc-res-2"). + StorageLocation("backup-location"). + TTL(72 * time.Hour). + CSISnapshotTimeout(10 * time.Minute). + DataMover("mover"). + Result().Spec + + expect1 := `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 + Cluster-scoped: auto + +Label selector: + +Storage Location: backup-location + +Velero-Native Snapshot PVs: auto +Snapshot Move Data: auto +Data Mover: mover + +TTL: 72h0m0s + +CSISnapshotTimeout: 10m0s +ItemOperationTimeout: 0s + +Hooks: +` + + input2 := builder.ForBackup("test-ns", "test-backup-2"). + IncludedNamespaces("inc-ns-1", "inc-ns-2"). + ExcludedNamespaces("exc-ns-1", "exc-ns-2"). + IncludedClusterScopedResources("inc-cluster-res-1", "inc-cluster-res-2"). + IncludedNamespaceScopedResources("inc-ns-res-1", "inc-ns-res-2"). + ExcludedClusterScopedResources("exc-cluster-res-1", "exc-cluster-res-2"). + ExcludedNamespaceScopedResources("exc-ns-res-1", "exc-ns-res-2"). + StorageLocation("backup-location"). + TTL(72 * time.Hour). + CSISnapshotTimeout(10 * time.Minute). + DataMover("mover"). + Result().Spec + + expect2 := `Namespaces: + Included: inc-ns-1, inc-ns-2 + Excluded: exc-ns-1, exc-ns-2 + +Resources: + Included cluster-scoped: inc-cluster-res-1, inc-cluster-res-2 + Excluded cluster-scoped: exc-cluster-res-1, exc-cluster-res-2 + Included namespace-scoped: inc-ns-res-1, inc-ns-res-2 + Excluded namespace-scoped: exc-ns-res-1, exc-ns-res-2 + +Label selector: + +Storage Location: backup-location + +Velero-Native Snapshot PVs: auto +Snapshot Move Data: auto +Data Mover: mover + +TTL: 72h0m0s + +CSISnapshotTimeout: 10m0s +ItemOperationTimeout: 0s + +Hooks: +` + + testcases := []struct { + name string + input velerov1api.BackupSpec + expect string + }{ + { + name: "old resource filter", + input: input1, + expect: expect1, + }, + { + name: "new resource filter", + input: input2, + expect: expect2, + }, + } + + 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) + DescribeBackupSpec(d, tc.input) + d.out.Flush() + assert.Equal(tt, tc.expect, d.buf.String()) + }) + } +} + +func TestDescribeSnapshot(t *testing.T) { + d := &Describer{ + Prefix: "", + out: &tabwriter.Writer{}, + buf: &bytes.Buffer{}, + } + d.out.Init(d.buf, 0, 8, 2, ' ', 0) + describeSnapshot(d, "pv-1", "snapshot-1", "ebs", "us-east-2", nil) + expect1 := ` pv-1: + Snapshot ID: snapshot-1 + Type: ebs + Availability Zone: us-east-2 + IOPS: +` + d.out.Flush() + assert.Equal(t, expect1, d.buf.String()) +} + +func TestDescribePodVolumeBackups(t *testing.T) { + pvb1 := builder.ForPodVolumeBackup("test-ns", "test-pvb1"). + BackupStorageLocation("backup-location"). + UploaderType("kopia"). + Phase(velerov1api.PodVolumeBackupPhaseCompleted). + BackupStorageLocation("bsl-1"). + Volume("vol-1"). + PodName("pod-1"). + 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"). + Volume("vol-2"). + PodName("pod-2"). + PodNamespace("pod-ns-1"). + SnapshotID("snap-2").Result() + + testcases := []struct { + name string + inputPVBList []velerov1api.PodVolumeBackup + inputDetails bool + expect string + }{ + { + name: "empty list", + inputPVBList: []velerov1api.PodVolumeBackup{}, + inputDetails: true, + expect: ``, + }, + { + name: "2 completed pvbs no details", + inputPVBList: []velerov1api.PodVolumeBackup{*pvb1, *pvb2}, + inputDetails: false, + expect: `kopia Backups (specify --details for more information): + Completed: 2 +`, + }, + { + name: "2 completed pvbs with details", + inputPVBList: []velerov1api.PodVolumeBackup{*pvb1, *pvb2}, + inputDetails: true, + expect: `kopia Backups: + 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) + DescribePodVolumeBackups(d, tc.inputPVBList, tc.inputDetails) + d.out.Flush() + assert.Equal(tt, tc.expect, d.buf.String()) + }) + } +} + +func TestDescribeCSIVolumeSnapshots(t *testing.T) { + features.Enable(velerov1api.CSIFeatureFlag) + defer func() { + features.Disable(velerov1api.CSIFeatureFlag) + }() + handle := "handle-1" + readyToUse := true + size := int64(1024) + vsc1 := builder.ForVolumeSnapshotContent("vsc-1"). + Status(&snapshotv1api.VolumeSnapshotContentStatus{ + SnapshotHandle: &handle, + ReadyToUse: &readyToUse, + RestoreSize: &size, + }).Result() + testcases := []struct { + name string + inputVSCList []snapshotv1api.VolumeSnapshotContent + inputDetails bool + expect string + }{ + { + name: "empty list", + inputVSCList: []snapshotv1api.VolumeSnapshotContent{}, + inputDetails: false, + expect: `CSI Volume Snapshots: +`, + }, + { + name: "1 vsc no details", + inputVSCList: []snapshotv1api.VolumeSnapshotContent{*vsc1}, + inputDetails: false, + expect: `CSI Volume Snapshots: 1 included (specify --details for more information) +`, + }, + { + name: "1 vsc with details", + inputVSCList: []snapshotv1api.VolumeSnapshotContent{*vsc1}, + inputDetails: true, + expect: `CSI Volume Snapshots: +Snapshot Content Name: vsc-1 + Storage Snapshot ID: handle-1 + Snapshot Size (bytes): 1024 + Ready to use: true +`, + }, + } + 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) + DescribeCSIVolumeSnapshots(d, tc.inputDetails, tc.inputVSCList) + d.out.Flush() + assert.Equal(tt, tc.expect, d.buf.String()) + }) + } +} diff --git a/pkg/cmd/util/output/backup_structured_describer.go b/pkg/cmd/util/output/backup_structured_describer.go index ad711aabc..896eeafdd 100644 --- a/pkg/cmd/util/output/backup_structured_describer.go +++ b/pkg/cmd/util/output/backup_structured_describer.go @@ -437,14 +437,13 @@ func DescribeCSIVolumeSnapshotsInSF(d *StructuredDescriber, details bool, volume // the field of 'CSI Volume Snapshots Details' displays the content of CSI Volume Snapshots if !details { CSIVolumeSnapshotsInfo["CSIVolumeSnapshotsCount"] = len(volumeSnapshotContents) - return + } else { + vscDetails := make(map[string]interface{}) + for _, vsc := range volumeSnapshotContents { + DescribeVSCInSF(details, vsc, vscDetails) + } + CSIVolumeSnapshotsInfo["CSIVolumeSnapshotsDetails"] = vscDetails } - - vscDetails := make(map[string]interface{}) - for _, vsc := range volumeSnapshotContents { - DescribeVSCInSF(details, vsc, vscDetails) - } - CSIVolumeSnapshotsInfo["CSIVolumeSnapshotsDetails"] = vscDetails d.Describe("CSIVolumeSnapshots", CSIVolumeSnapshotsInfo) } diff --git a/pkg/cmd/util/output/backup_structured_describer_test.go b/pkg/cmd/util/output/backup_structured_describer_test.go new file mode 100644 index 000000000..eb2aecd28 --- /dev/null +++ b/pkg/cmd/util/output/backup_structured_describer_test.go @@ -0,0 +1,252 @@ +package output + +import ( + "reflect" + "testing" + "time" + + v1 "k8s.io/api/core/v1" + + "github.com/vmware-tanzu/velero/pkg/features" + "github.com/vmware-tanzu/velero/pkg/util/results" + + "github.com/stretchr/testify/assert" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/builder" + + snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1" +) + +func TestDescribeBackupInSF(t *testing.T) { + sd := &StructuredDescriber{ + output: make(map[string]interface{}), + format: "", + } + backupBuilder1 := builder.ForBackup("test-ns", "test-backup") + backupBuilder1.IncludedNamespaces("inc-ns-1", "inc-ns-2"). + ExcludedNamespaces("exc-ns-1", "exc-ns-2"). + IncludedResources("inc-res-1", "inc-res-2"). + ExcludedResources("exc-res-1", "exc-res-2"). + StorageLocation("backup-location"). + TTL(72 * time.Hour). + CSISnapshotTimeout(10 * time.Minute). + DataMover("mover") + + expect1 := map[string]interface{}{ + "spec": map[string]interface{}{ + "namespaces": map[string]interface{}{ + "included": "inc-ns-1, inc-ns-2", + "excluded": "exc-ns-1, exc-ns-2", + }, + "resources": map[string]string{ + "included": "inc-res-1, inc-res-2", + "excluded": "exc-res-1, exc-res-2", + "clusterScoped": "auto", + }, + "dataMover": "mover", + "labelSelector": emptyDisplay, + "storageLocation": "backup-location", + "veleroNativeSnapshotPVs": "auto", + "TTL": "72h0m0s", + "CSISnapshotTimeout": "10m0s", + "veleroSnapshotMoveData": "auto", + }, + } + 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") + expect2 := map[string]interface{}{ + "spec": map[string]interface{}{ + "namespaces": map[string]interface{}{ + "included": "*", + "excluded": emptyDisplay, + }, + "resources": map[string]string{ + "included": "*", + "excluded": emptyDisplay, + "clusterScoped": "auto", + }, + "dataMover": emptyDisplay, + "labelSelector": emptyDisplay, + "storageLocation": "backup-location", + "veleroNativeSnapshotPVs": "auto", + "TTL": "0s", + "CSISnapshotTimeout": "0s", + "veleroSnapshotMoveData": "auto", + }, + } + DescribeBackupSpecInSF(sd, backupBuilder2.Result().Spec) + assert.True(t, reflect.DeepEqual(sd.output, expect2)) +} + +func TestDescribePodVolumeBackupsInSF(t *testing.T) { + pvbBuilder1 := builder.ForPodVolumeBackup("test-ns1", "test-pvb1") + pvb1 := pvbBuilder1.BackupStorageLocation("backup-location"). + UploaderType("kopia"). + Phase(velerov1api.PodVolumeBackupPhaseCompleted). + BackupStorageLocation("bsl-1"). + Volume("vol-1"). + PodName("pod-1"). + PodNamespace("pod-ns-1"). + SnapshotID("snap-1").Result() + + pvbBuilder2 := builder.ForPodVolumeBackup("test-ns1", "test-pvb2") + pvb2 := pvbBuilder2.BackupStorageLocation("backup-location"). + UploaderType("kopia"). + Phase(velerov1api.PodVolumeBackupPhaseCompleted). + BackupStorageLocation("bsl-1"). + Volume("vol-2"). + PodName("pod-2"). + PodNamespace("pod-ns-1"). + SnapshotID("snap-2").Result() + + testcases := []struct { + name string + inputPVBList []velerov1api.PodVolumeBackup + inputDetails bool + expect map[string]interface{} + }{ + { + name: "empty list", + inputPVBList: []velerov1api.PodVolumeBackup{}, + inputDetails: false, + expect: map[string]interface{}{}, + }, + { + name: "2 completed pvbs", + inputPVBList: []velerov1api.PodVolumeBackup{*pvb1, *pvb2}, + inputDetails: true, + expect: map[string]interface{}{ + "podVolumeBackups": map[string]interface{}{ + "podVolumeBackupsDetails": map[string]interface{}{ + "Completed": []map[string]string{ + {"pod-ns-1/pod-1": "vol-1"}, + {"pod-ns-1/pod-2": "vol-2"}, + }, + }, + "type": "kopia", + }, + }, + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(tt *testing.T) { + sd := &StructuredDescriber{ + output: make(map[string]interface{}), + format: "", + } + DescribePodVolumeBackupsInSF(sd, tc.inputPVBList, tc.inputDetails) + assert.True(tt, reflect.DeepEqual(sd.output, tc.expect)) + }) + } +} + +func TestDescribeCSIVolumeSnapshotsInSF(t *testing.T) { + features.Enable(velerov1api.CSIFeatureFlag) + defer func() { + features.Disable(velerov1api.CSIFeatureFlag) + }() + + vscBuilder1 := builder.ForVolumeSnapshotContent("vsc-1") + handle := "handle-1" + readyToUse := true + size := int64(1024) + vsc1 := vscBuilder1.Status(&snapshotv1api.VolumeSnapshotContentStatus{ + SnapshotHandle: &handle, + ReadyToUse: &readyToUse, + RestoreSize: &size, + }).Result() + + testcases := []struct { + name string + inputVSCList []snapshotv1api.VolumeSnapshotContent + inputDetails bool + expect map[string]interface{} + }{ + { + name: "empty list", + inputVSCList: []snapshotv1api.VolumeSnapshotContent{}, + inputDetails: false, + expect: map[string]interface{}{}, + }, + { + name: "1 vsc no detail", + inputVSCList: []snapshotv1api.VolumeSnapshotContent{*vsc1}, + inputDetails: false, + expect: map[string]interface{}{ + "CSIVolumeSnapshots": map[string]interface{}{ + "CSIVolumeSnapshotsCount": 1, + }, + }, + }, + { + name: "1 vsc with detail", + inputVSCList: []snapshotv1api.VolumeSnapshotContent{*vsc1}, + inputDetails: true, + expect: map[string]interface{}{ + "CSIVolumeSnapshots": map[string]interface{}{ + "CSIVolumeSnapshotsDetails": map[string]interface{}{ + "vsc-1": map[string]interface{}{ + "readyToUse": true, + "snapshotSize(bytes)": int64(1024), + "storageSnapshotID": "handle-1", + }, + }, + }, + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(tt *testing.T) { + sd := &StructuredDescriber{ + output: make(map[string]interface{}), + format: "", + } + DescribeCSIVolumeSnapshotsInSF(sd, tc.inputDetails, tc.inputVSCList) + assert.True(tt, reflect.DeepEqual(sd.output, tc.expect)) + }) + } +} + +func TestDescribeResourcePoliciesInSF(t *testing.T) { + input := &v1.TypedLocalObjectReference{ + Kind: "configmap", + Name: "resource-policy-1", + } + expect := map[string]interface{}{ + "resourcePolicies": map[string]interface{}{ + "type": "configmap", + "name": "resource-policy-1", + }, + } + sd := &StructuredDescriber{ + output: make(map[string]interface{}), + format: "", + } + DescribeResourcePoliciesInSF(sd, input) + assert.True(t, reflect.DeepEqual(sd.output, expect)) +} + +func TestDescribeBackupResultInSF(t *testing.T) { + input := results.Result{ + Velero: []string{"msg-1", "msg-2"}, + Cluster: []string{"cluster-1", "cluster-2"}, + Namespaces: map[string][]string{ + "ns-1": {"ns-1-msg-1", "ns-1-msg-2"}, + }, + } + got := map[string]interface{}{} + expect := map[string]interface{}{ + "velero": []string{"msg-1", "msg-2"}, + "cluster": []string{"cluster-1", "cluster-2"}, + "namespace": map[string][]string{ + "ns-1": {"ns-1-msg-1", "ns-1-msg-2"}, + }, + } + describeResultInSF(got, input) + assert.True(t, reflect.DeepEqual(got, expect)) +} diff --git a/pkg/cmd/util/output/describe_test.go b/pkg/cmd/util/output/describe_test.go new file mode 100644 index 000000000..9385aa107 --- /dev/null +++ b/pkg/cmd/util/output/describe_test.go @@ -0,0 +1,134 @@ +package output + +import ( + "bytes" + "fmt" + "testing" + "text/tabwriter" + + "github.com/fatih/color" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestBoolPointerString(t *testing.T) { + trueStr := "true" + falseStr := "FALSE" + nilStr := "nul" + truee := true + falsee := false + testcases := []struct { + name string + input *bool + expect string + }{ + { + name: "nil", + input: nil, + expect: nilStr, + }, + { + name: "true", + input: &truee, + expect: trueStr, + }, + { + name: "false", + input: &falsee, + expect: falseStr, + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + got := BoolPointerString(tc.input, falseStr, trueStr, nilStr) + assert.Equal(t, tc.expect, got) + }) + } +} + +func TestDescriber_DescribeMetadata(t *testing.T) { + input := metav1.ObjectMeta{ + Name: "test", + Namespace: "test-ns", + Labels: map[string]string{ + "test-key2": "v2", + "test-key0": "v0", + }, + Annotations: nil, + } + expect := new(bytes.Buffer) + d := &Describer{ + Prefix: "pref-", + out: &tabwriter.Writer{}, + buf: &bytes.Buffer{}, + } + d.out.Init(d.buf, 0, 8, 0, ' ', 0) + fmt.Fprintf(expect, "pref-Name: %s\n", color.New(color.Bold).SprintFunc()("test")) + fmt.Fprintf(expect, "pref-Namespace: %s\n", "test-ns") + fmt.Fprintf(expect, "pref-Labels: %s\n", "pref-test-key0=v0") + fmt.Fprintf(expect, "pref- test-key2=v2\n") + fmt.Fprintf(expect, "pref-Annotations:%s\n", "pref-") + d.DescribeMetadata(input) + d.out.Flush() + assert.Equal(t, expect.String(), d.buf.String()) +} + +func TestDescriber_DescribeSlice(t *testing.T) { + input := []string{"a", "b", "c"} + expect := new(bytes.Buffer) + d := &Describer{ + Prefix: "pref-", + out: &tabwriter.Writer{}, + buf: &bytes.Buffer{}, + } + d.out.Init(d.buf, 0, 8, 0, ' ', 0) + fmt.Fprintf(expect, "pref-test:pref-a\n") + fmt.Fprintf(expect, "pref- b\n") + fmt.Fprintf(expect, "pref- c\n") + d.DescribeSlice(4, "test", input) + d.out.Flush() + assert.Equal(t, expect.String(), d.buf.String()) + var input2 []string + expect2 := new(bytes.Buffer) + d2 := &Describer{ + Prefix: "pref-", + out: &tabwriter.Writer{}, + buf: &bytes.Buffer{}, + } + d2.out.Init(d2.buf, 0, 4, 0, ' ', 0) + fmt.Fprintf(expect2, "pref-test:pref-\n") + d2.DescribeSlice(4, "test", input2) + d2.out.Flush() + assert.Equal(t, expect2.String(), d2.buf.String()) +} + +func TestStructuredDescriber_JSONEncode(t *testing.T) { + testcases := []struct { + name string + inputMap map[string]interface{} + expect string + }{ + { + name: "invalid json", + inputMap: map[string]interface{}{}, + expect: "{}\n", + }, + { + name: "valid json", + inputMap: map[string]interface{}{"k1": "v1"}, + expect: `{ + "k1": "v1" +} +`, + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(tt *testing.T) { + d := &StructuredDescriber{ + output: tc.inputMap, + } + got := d.JSONEncode() + assert.Equal(tt, tc.expect, got) + }) + } +} diff --git a/pkg/cmd/util/output/output_test.go b/pkg/cmd/util/output/output_test.go new file mode 100644 index 000000000..ba9c27843 --- /dev/null +++ b/pkg/cmd/util/output/output_test.go @@ -0,0 +1,382 @@ +package output + +import ( + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/runtime" + + velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + + "testing" +) + +func TestBindFlags(t *testing.T) { + cmd := &cobra.Command{} + BindFlags(cmd.Flags()) + assert.NotNil(t, cmd.Flags().Lookup("output")) + assert.NotNil(t, cmd.Flags().Lookup("label-columns")) + assert.NotNil(t, cmd.Flags().Lookup("show-labels")) + assert.Nil(t, cmd.Flags().Lookup("not-exist")) +} + +func TestBindFlagsSimple(t *testing.T) { + cmd := &cobra.Command{} + BindFlagsSimple(cmd.Flags()) + assert.NotNil(t, cmd.Flags().Lookup("output")) + assert.Nil(t, cmd.Flags().Lookup("label-columns")) + assert.Nil(t, cmd.Flags().Lookup("show-labels")) +} + +func TestClearOutputFlagDefault(t *testing.T) { + cmd := &cobra.Command{} + ClearOutputFlagDefault(cmd) + assert.Nil(t, cmd.Flags().Lookup("output")) + BindFlags(cmd.Flags()) + cmd.Flags().Set("output", "json") + ClearOutputFlagDefault(cmd) + assert.Equal(t, "", cmd.Flags().Lookup("output").Value.String()) +} + +func cmdWithFormat(use string, format string) *cobra.Command { + cmd := &cobra.Command{ + Use: use, + } + BindFlags(cmd.Flags()) + cmd.Flags().Set("output", format) + return cmd +} + +func TestValidateFlags(t *testing.T) { + testcases := []struct { + name string + input *cobra.Command + hasErr bool + }{ + { + name: "unknown format", + input: cmdWithFormat("whatever", "unknown"), + hasErr: true, + }, + { + name: "json format", + input: cmdWithFormat("whatever", "json"), + hasErr: false, + }, + { + name: "yaml format", + input: cmdWithFormat("whatever", "yaml"), + hasErr: false, + }, + { + name: "empty format", + input: cmdWithFormat("whatever", ""), + hasErr: false, + }, + { + name: "install with table format", + input: cmdWithFormat("install", "table"), + hasErr: true, + }, + { + name: "other with table format", + input: cmdWithFormat("other", "table"), + hasErr: false, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + err := ValidateFlags(tc.input) + if tc.hasErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestPrintWithFormat(t *testing.T) { + testcases := []struct { + name string + input struct { + cmd *cobra.Command + obj runtime.Object + } + hasErr bool + printed bool + }{ + { + name: "empty format", + input: struct { + cmd *cobra.Command + obj runtime.Object + }{ + cmd: cmdWithFormat("describe", ""), + }, + hasErr: false, + printed: false, + }, + { + name: "json format backup", + input: struct { + cmd *cobra.Command + obj runtime.Object + }{ + cmd: cmdWithFormat("describe", "json"), + obj: &velerov1.Backup{}, + }, + hasErr: false, + printed: true, + }, + { + name: "table format backup", + input: struct { + cmd *cobra.Command + obj runtime.Object + }{ + cmd: cmdWithFormat("describe", "table"), + obj: &velerov1.Backup{}, + }, + hasErr: false, + printed: true, + }, + { + name: "json format backup list", + input: struct { + cmd *cobra.Command + obj runtime.Object + }{ + cmd: cmdWithFormat("describe", "json"), + obj: &velerov1.BackupList{ + Items: []velerov1.Backup{ + {}, + }, + }, + }, + hasErr: false, + printed: true, + }, + { + name: "table format backup list", + input: struct { + cmd *cobra.Command + obj runtime.Object + }{ + cmd: cmdWithFormat("describe", "table"), + obj: &velerov1.BackupList{ + Items: []velerov1.Backup{ + {}, + }, + }, + }, + hasErr: false, + printed: true, + }, + { + name: "table format backup", + input: struct { + cmd *cobra.Command + obj runtime.Object + }{ + cmd: cmdWithFormat("describe", "table"), + obj: &velerov1.Backup{}, + }, + hasErr: false, + printed: true, + }, + { + name: "table format restore list", + input: struct { + cmd *cobra.Command + obj runtime.Object + }{ + cmd: cmdWithFormat("describe", "table"), + obj: &velerov1.RestoreList{ + Items: []velerov1.Restore{ + {}, + }, + }, + }, + hasErr: false, + printed: true, + }, + { + name: "table format restore", + input: struct { + cmd *cobra.Command + obj runtime.Object + }{ + cmd: cmdWithFormat("describe", "table"), + obj: &velerov1.Restore{}, + }, + hasErr: false, + printed: true, + }, + { + name: "table format schedule list", + input: struct { + cmd *cobra.Command + obj runtime.Object + }{ + cmd: cmdWithFormat("describe", "table"), + obj: &velerov1.ScheduleList{ + Items: []velerov1.Schedule{ + {}, + }, + }, + }, + hasErr: false, + printed: true, + }, + { + name: "table format schedule", + input: struct { + cmd *cobra.Command + obj runtime.Object + }{ + cmd: cmdWithFormat("describe", "table"), + obj: &velerov1.Schedule{}, + }, + hasErr: false, + printed: true, + }, + { + name: "table format backup repository list", + input: struct { + cmd *cobra.Command + obj runtime.Object + }{ + cmd: cmdWithFormat("describe", "table"), + obj: &velerov1.BackupRepositoryList{ + Items: []velerov1.BackupRepository{ + {}, + }, + }, + }, + hasErr: false, + printed: true, + }, + { + name: "table format backup repository", + input: struct { + cmd *cobra.Command + obj runtime.Object + }{ + cmd: cmdWithFormat("describe", "table"), + obj: &velerov1.BackupRepository{}, + }, + hasErr: false, + printed: true, + }, + { + name: "table format backup location list", + input: struct { + cmd *cobra.Command + obj runtime.Object + }{ + cmd: cmdWithFormat("describe", "table"), + obj: &velerov1.BackupStorageLocationList{ + Items: []velerov1.BackupStorageLocation{ + { + Spec: velerov1.BackupStorageLocationSpec{ + Provider: "aws", + StorageType: velerov1.StorageType{ + ObjectStorage: &velerov1.ObjectStorageLocation{ + Bucket: "bucket", + }, + }, + }, + }, + }, + }, + }, + hasErr: false, + printed: true, + }, + { + name: "table format backup location", + input: struct { + cmd *cobra.Command + obj runtime.Object + }{ + cmd: cmdWithFormat("describe", "table"), + obj: &velerov1.BackupStorageLocation{ + Spec: velerov1.BackupStorageLocationSpec{ + Provider: "aws", + StorageType: velerov1.StorageType{ + ObjectStorage: &velerov1.ObjectStorageLocation{ + Bucket: "bucket", + }, + }, + }, + }, + }, + hasErr: false, + printed: true, + }, + { + name: "table format volume snapshot location list", + input: struct { + cmd *cobra.Command + obj runtime.Object + }{ + cmd: cmdWithFormat("describe", "table"), + obj: &velerov1.VolumeSnapshotLocationList{ + Items: []velerov1.VolumeSnapshotLocation{ + {}, + }, + }, + }, + hasErr: false, + printed: true, + }, + { + name: "table format volume snapshot location", + input: struct { + cmd *cobra.Command + obj runtime.Object + }{ + cmd: cmdWithFormat("describe", "table"), + obj: &velerov1.VolumeSnapshotLocation{}, + }, + hasErr: false, + printed: true, + }, + { + name: "table format volume snapshot location", + input: struct { + cmd *cobra.Command + obj runtime.Object + }{ + cmd: cmdWithFormat("describe", "table"), + obj: &velerov1.VolumeSnapshotLocation{}, + }, + hasErr: false, + printed: true, + }, + { + name: "table format plugin list via server status", + input: struct { + cmd *cobra.Command + obj runtime.Object + }{ + cmd: cmdWithFormat("describe", "table"), + obj: &velerov1.ServerStatusRequest{}, + }, + hasErr: false, + printed: true, + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + p, err := PrintWithFormat(tc.input.cmd, tc.input.obj) + if tc.hasErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tc.printed, p) + }) + } +}