diff --git a/.gitignore b/.gitignore index befa29e2..c45a9fa8 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,5 @@ go.work .Trashes ehthumbs.db Thumbs.db + +vendor/ \ No newline at end of file diff --git a/cmd/non-admin/backup/backup_test.go b/cmd/non-admin/backup/backup_test.go index 10178ff9..f1f08866 100644 --- a/cmd/non-admin/backup/backup_test.go +++ b/cmd/non-admin/backup/backup_test.go @@ -100,41 +100,35 @@ func TestNonAdminBackupCommands(t *testing.T) { name: "nonadmin get backup help", args: []string{"nonadmin", "get", "backup", "--help"}, expectContains: []string{ - "Get one or more non-admin resources", - "backup", + "Get one or more non-admin backups", }, }, { name: "nonadmin create backup help", args: []string{"nonadmin", "create", "backup", "--help"}, expectContains: []string{ - "Create non-admin resources", - "backup", - "bsl", + "Create a non-admin backup", }, }, { name: "nonadmin delete backup help", args: []string{"nonadmin", "delete", "backup", "--help"}, expectContains: []string{ - "Delete non-admin resources", - "backup", + "Delete one or more non-admin backups", }, }, { name: "nonadmin describe backup help", args: []string{"nonadmin", "describe", "backup", "--help"}, expectContains: []string{ - "Describe non-admin resources", - "backup", + "Describe a non-admin backup", }, }, { name: "nonadmin logs backup help", args: []string{"nonadmin", "logs", "backup", "--help"}, expectContains: []string{ - "Get logs for non-admin resources", - "backup", + "Show logs for a non-admin backup", }, }, // Shorthand verb-noun order tests @@ -142,41 +136,35 @@ func TestNonAdminBackupCommands(t *testing.T) { name: "na get backup help", args: []string{"na", "get", "backup", "--help"}, expectContains: []string{ - "Get one or more non-admin resources", - "backup", + "Get one or more non-admin backups", }, }, { name: "na create backup help", args: []string{"na", "create", "backup", "--help"}, expectContains: []string{ - "Create non-admin resources", - "backup", - "bsl", + "Create a non-admin backup", }, }, { name: "na delete backup help", args: []string{"na", "delete", "backup", "--help"}, expectContains: []string{ - "Delete non-admin resources", - "backup", + "Delete one or more non-admin backups", }, }, { name: "na describe backup help", args: []string{"na", "describe", "backup", "--help"}, expectContains: []string{ - "Describe non-admin resources", - "backup", + "Describe a non-admin backup", }, }, { name: "na logs backup help", args: []string{"na", "logs", "backup", "--help"}, expectContains: []string{ - "Get logs for non-admin resources", - "backup", + "Show logs for a non-admin backup", }, }, } @@ -400,13 +388,13 @@ func TestVerbNounOrderExamples(t *testing.T) { }) t.Run("verb commands with specific resources show proper examples", func(t *testing.T) { - // Test that verb commands with specific resources show examples + // Test that verb commands with specific resources show examples (noun-first format from underlying commands) expectedExamples := []string{ - "kubectl oadp nonadmin get backup my-backup", - "kubectl oadp nonadmin create backup my-backup", - "kubectl oadp nonadmin delete backup my-backup", - "kubectl oadp nonadmin describe backup my-backup", - "kubectl oadp nonadmin logs backup my-backup", + "kubectl oadp nonadmin backup get", + "kubectl oadp nonadmin backup create backup1", + "kubectl oadp nonadmin backup delete my-backup", + "kubectl oadp nonadmin backup describe my-backup", + "kubectl oadp nonadmin backup logs my-backup", } commands := [][]string{ @@ -510,6 +498,19 @@ func TestNonAdminBackupDeleteAllFlagExamples(t *testing.T) { []string{"nonadmin", "backup", "delete", "--help"}, expectedPatterns) }) + + t.Run("delete help has examples section", func(t *testing.T) { + // Test that examples section exists and shows various delete patterns + expectedExamples := []string{ + "kubectl oadp nonadmin backup delete my-backup", + "kubectl oadp nonadmin backup delete --all", + "kubectl oadp nonadmin backup delete my-backup --confirm", + } + + testutil.TestHelpCommand(t, binaryPath, + []string{"nonadmin", "backup", "delete", "--help"}, + expectedExamples) + }) } // TestNonAdminBackupDeleteAllFlagBehavior tests the behavioral aspects of the --all flag diff --git a/cmd/non-admin/backup/delete.go b/cmd/non-admin/backup/delete.go index 0385b51e..0ee5eca8 100644 --- a/cmd/non-admin/backup/delete.go +++ b/cmd/non-admin/backup/delete.go @@ -57,6 +57,17 @@ func NewDeleteCommand(f client.Factory, use string) *cobra.Command { cmd.CheckError(o.Validate()) cmd.CheckError(o.Run()) }, + Example: ` # Delete a specific backup + kubectl oadp nonadmin backup delete my-backup + + # Delete multiple backups + kubectl oadp nonadmin backup delete backup1 backup2 backup3 + + # Delete all backups in the current namespace + kubectl oadp nonadmin backup delete --all + + # Delete without confirmation prompt + kubectl oadp nonadmin backup delete my-backup --confirm`, } o.BindFlags(c.Flags()) diff --git a/cmd/non-admin/backup/describe.go b/cmd/non-admin/backup/describe.go index 22214b29..59c92b76 100644 --- a/cmd/non-admin/backup/describe.go +++ b/cmd/non-admin/backup/describe.go @@ -72,7 +72,7 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command { } // Print in Velero-style format - printNonAdminBackupDetails(cmd, &nab, kbClient, backupName, userNamespace, effectiveTimeout) + printNonAdminBackupDetails(cmd, &nab, kbClient, backupName, userNamespace, effectiveTimeout, details) // Add detailed output if --details flag is set if details { @@ -98,7 +98,7 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command { } // printNonAdminBackupDetails prints backup details in Velero admin describe format -func printNonAdminBackupDetails(cmd *cobra.Command, nab *nacv1alpha1.NonAdminBackup, kbClient kbclient.Client, backupName string, userNamespace string, timeout time.Duration) { +func printNonAdminBackupDetails(cmd *cobra.Command, nab *nacv1alpha1.NonAdminBackup, kbClient kbclient.Client, backupName string, userNamespace string, timeout time.Duration, showDetails bool) { out := cmd.OutOrStdout() // Get Velero backup reference if available @@ -322,22 +322,24 @@ func printNonAdminBackupDetails(cmd *cobra.Command, nab *nacv1alpha1.NonAdminBac fmt.Fprintf(out, "\n") - // Fetch and display Resource List - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() + // Fetch and display Resource List (only if showDetails is true) + if showDetails { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() - resourceList, err := shared.ProcessDownloadRequest(ctx, kbClient, shared.DownloadRequestOptions{ - BackupName: backupName, - DataType: "BackupResourceList", - Namespace: userNamespace, - HTTPTimeout: timeout, - }) + resourceList, err := shared.ProcessDownloadRequest(ctx, kbClient, shared.DownloadRequestOptions{ + BackupName: backupName, + DataType: "BackupResourceList", + Namespace: userNamespace, + HTTPTimeout: timeout, + }) - if err == nil && resourceList != "" { - if formattedList := formatResourceList(resourceList); formattedList != "" { - fmt.Fprintf(out, "Resource List:\n") - fmt.Fprintf(out, "%s\n", formattedList) - fmt.Fprintf(out, "\n") + if err == nil && resourceList != "" { + if formattedList := formatResourceList(resourceList); formattedList != "" { + fmt.Fprintf(out, "Resource List:\n") + fmt.Fprintf(out, "%s\n", formattedList) + fmt.Fprintf(out, "\n") + } } } diff --git a/cmd/non-admin/backup/get.go b/cmd/non-admin/backup/get.go index a1118b88..97561821 100644 --- a/cmd/non-admin/backup/get.go +++ b/cmd/non-admin/backup/get.go @@ -20,11 +20,11 @@ import ( "fmt" "time" + "github.com/migtools/oadp-cli/cmd/non-admin/output" "github.com/migtools/oadp-cli/cmd/shared" nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" "github.com/spf13/cobra" "github.com/vmware-tanzu/velero/pkg/client" - "github.com/vmware-tanzu/velero/pkg/cmd/util/output" kbclient "sigs.k8s.io/controller-runtime/pkg/client" ) diff --git a/cmd/non-admin/bsl/bsl.go b/cmd/non-admin/bsl/bsl.go index b8eed279..cd6a2150 100644 --- a/cmd/non-admin/bsl/bsl.go +++ b/cmd/non-admin/bsl/bsl.go @@ -30,7 +30,7 @@ func NewBSLCommand(f client.Factory) *cobra.Command { } c.AddCommand( - NewCreateCommand(f), + NewCreateCommand(f, "create"), NewGetCommand(f, "get"), ) diff --git a/cmd/non-admin/bsl/bsl_test.go b/cmd/non-admin/bsl/bsl_test.go index 777a89a7..17279c4d 100644 --- a/cmd/non-admin/bsl/bsl_test.go +++ b/cmd/non-admin/bsl/bsl_test.go @@ -73,16 +73,14 @@ func TestNonAdminBSLCommands(t *testing.T) { name: "nonadmin get bsl help", args: []string{"nonadmin", "get", "bsl", "--help"}, expectContains: []string{ - "Get one or more non-admin resources", - "bsl", + "Get one or more non-admin backup storage locations", }, }, { name: "nonadmin create bsl help", args: []string{"nonadmin", "create", "bsl", "--help"}, expectContains: []string{ - "Create non-admin resources", - "bsl", + "Create a non-admin backup storage location", }, }, // Shorthand verb-noun order tests @@ -90,16 +88,14 @@ func TestNonAdminBSLCommands(t *testing.T) { name: "na get bsl help", args: []string{"na", "get", "bsl", "--help"}, expectContains: []string{ - "Get one or more non-admin resources", - "bsl", + "Get one or more non-admin backup storage locations", }, }, { name: "na create bsl help", args: []string{"na", "create", "bsl", "--help"}, expectContains: []string{ - "Create non-admin resources", - "bsl", + "Create a non-admin backup storage location", }, }, } @@ -297,7 +293,7 @@ func TestVerbNounOrderBSLExamples(t *testing.T) { t.Run("get bsl with specific resource shows proper examples", func(t *testing.T) { expectedExamples := []string{ - "kubectl oadp nonadmin get bsl", + "kubectl oadp nonadmin bsl get", // Shows noun-first format from underlying command } testutil.TestHelpCommand(t, binaryPath, @@ -307,7 +303,7 @@ func TestVerbNounOrderBSLExamples(t *testing.T) { t.Run("create bsl with specific resource shows proper examples", func(t *testing.T) { expectedExamples := []string{ - "kubectl oadp nonadmin create bsl", + "kubectl oadp nonadmin bsl create", // Shows noun-first format from underlying command } testutil.TestHelpCommand(t, binaryPath, diff --git a/cmd/non-admin/bsl/create.go b/cmd/non-admin/bsl/create.go index 0fc0c5a7..f216faa8 100644 --- a/cmd/non-admin/bsl/create.go +++ b/cmd/non-admin/bsl/create.go @@ -36,11 +36,11 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func NewCreateCommand(f client.Factory) *cobra.Command { +func NewCreateCommand(f client.Factory, use string) *cobra.Command { o := NewCreateOptions() c := &cobra.Command{ - Use: "create NAME", + Use: use + " NAME", Short: "Create a non-admin backup storage location", Args: cobra.ExactArgs(1), Run: func(c *cobra.Command, args []string) { diff --git a/cmd/non-admin/bsl/get.go b/cmd/non-admin/bsl/get.go index 8db5f29e..5e1a503d 100644 --- a/cmd/non-admin/bsl/get.go +++ b/cmd/non-admin/bsl/get.go @@ -21,11 +21,11 @@ import ( "fmt" "time" + "github.com/migtools/oadp-cli/cmd/non-admin/output" "github.com/migtools/oadp-cli/cmd/shared" nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" "github.com/spf13/cobra" "github.com/vmware-tanzu/velero/pkg/client" - "github.com/vmware-tanzu/velero/pkg/cmd/util/output" kbclient "sigs.k8s.io/controller-runtime/pkg/client" ) diff --git a/cmd/non-admin/nonadmin_test.go b/cmd/non-admin/nonadmin_test.go index d675abc3..5c647702 100644 --- a/cmd/non-admin/nonadmin_test.go +++ b/cmd/non-admin/nonadmin_test.go @@ -110,50 +110,42 @@ func TestNonAdminCommands(t *testing.T) { name: "nonadmin get backup help", args: []string{"nonadmin", "get", "backup", "--help"}, expectContains: []string{ - "Get one or more non-admin resources", - "backup", + "Get one or more non-admin backups", }, }, { name: "nonadmin create backup help", args: []string{"nonadmin", "create", "backup", "--help"}, expectContains: []string{ - "Create non-admin resources", - "backup", - "bsl", + "Create a non-admin backup", }, }, { name: "nonadmin delete backup help", args: []string{"nonadmin", "delete", "backup", "--help"}, expectContains: []string{ - "Delete non-admin resources", - "backup", + "Delete one or more non-admin backups", }, }, { name: "nonadmin describe backup help", args: []string{"nonadmin", "describe", "backup", "--help"}, expectContains: []string{ - "Describe non-admin resources", - "backup", + "Describe a non-admin backup", }, }, { name: "nonadmin logs backup help", args: []string{"nonadmin", "logs", "backup", "--help"}, expectContains: []string{ - "Get logs for non-admin resources", - "backup", + "Show logs for a non-admin backup", }, }, { name: "nonadmin create bsl help", args: []string{"nonadmin", "create", "bsl", "--help"}, expectContains: []string{ - "Create non-admin resources", - "backup", - "bsl", + "Create a non-admin backup storage location", }, }, // Shorthand tests for verb-noun order @@ -169,9 +161,7 @@ func TestNonAdminCommands(t *testing.T) { name: "na create backup help", args: []string{"na", "create", "backup", "--help"}, expectContains: []string{ - "Create non-admin resources", - "backup", - "bsl", + "Create a non-admin backup", }, }, } diff --git a/cmd/non-admin/output/output.go b/cmd/non-admin/output/output.go new file mode 100644 index 00000000..a27eca01 --- /dev/null +++ b/cmd/non-admin/output/output.go @@ -0,0 +1,148 @@ +/* +Copyright 2025 The OADP CLI 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 output + +import ( + "bytes" + "fmt" + "io" + + nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + velerooutput "github.com/vmware-tanzu/velero/pkg/cmd/util/output" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" +) + +// NonAdminScheme returns a runtime.Scheme with NonAdmin types registered +func NonAdminScheme() *runtime.Scheme { + scheme := runtime.NewScheme() + + // Add NonAdmin types + if err := nacv1alpha1.AddToScheme(scheme); err != nil { + panic(fmt.Sprintf("failed to add NonAdmin types to scheme: %v", err)) + } + + // Add Velero types for compatibility + if err := velerov1api.AddToScheme(scheme); err != nil { + panic(fmt.Sprintf("failed to add Velero types to scheme: %v", err)) + } + + return scheme +} + +// BindFlags wraps Velero's BindFlags to add output flags +func BindFlags(flags *pflag.FlagSet) { + velerooutput.BindFlags(flags) +} + +// ClearOutputFlagDefault wraps Velero's ClearOutputFlagDefault +func ClearOutputFlagDefault(cmd *cobra.Command) { + velerooutput.ClearOutputFlagDefault(cmd) +} + +// PrintWithFormat prints the provided object in the format specified by +// the command's flags. This is a custom implementation for nonadmin commands +// that supports NonAdmin CRD types (NonAdminBackup, NonAdminRestore, etc.) +func PrintWithFormat(c *cobra.Command, obj runtime.Object) (bool, error) { + format := velerooutput.GetOutputFlagValue(c) + if format == "" { + return false, nil + } + + switch format { + case "json", "yaml": + return printEncoded(obj, format) + case "table": + // Table format is not supported by this function + // The caller should handle table printing + return false, nil + } + + return false, errors.Errorf("unsupported output format %q; valid values are 'table', 'json', and 'yaml'", format) +} + +func printEncoded(obj runtime.Object, format string) (bool, error) { + // assume we're printing obj + toPrint := obj + + if meta.IsListType(obj) { + list, _ := meta.ExtractList(obj) + if len(list) == 1 { + // if obj was a list and there was only 1 item, just print that 1 instead of a list + toPrint = list[0] + } + } + + encoded, err := encode(toPrint, format) + if err != nil { + return false, err + } + + fmt.Println(string(encoded)) + + return true, nil +} + +func encode(obj runtime.Object, format string) ([]byte, error) { + buf := new(bytes.Buffer) + + if err := encodeTo(obj, format, buf); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +func encodeTo(obj runtime.Object, format string, w io.Writer) error { + encoder, err := encoderFor(format, obj) + if err != nil { + return err + } + + return errors.WithStack(encoder.Encode(obj, w)) +} + +func encoderFor(format string, obj runtime.Object) (runtime.Encoder, error) { + var encoder runtime.Encoder + + // Use NonAdminScheme instead of Velero's scheme + codecFactory := serializer.NewCodecFactory(NonAdminScheme()) + + desiredMediaType := fmt.Sprintf("application/%s", format) + serializerInfo, found := runtime.SerializerInfoForMediaType(codecFactory.SupportedMediaTypes(), desiredMediaType) + if !found { + return nil, errors.Errorf("unable to locate an encoder for %q", desiredMediaType) + } + if serializerInfo.PrettySerializer != nil { + encoder = serializerInfo.PrettySerializer + } else { + encoder = serializerInfo.Serializer + } + + if !obj.GetObjectKind().GroupVersionKind().Empty() { + return encoder, nil + } + + // Use the appropriate GroupVersion for encoding + // For NonAdmin types, use nacv1alpha1.GroupVersion + encoder = codecFactory.EncoderForVersion(encoder, nacv1alpha1.GroupVersion) + return encoder, nil +} diff --git a/cmd/non-admin/output/output_test.go b/cmd/non-admin/output/output_test.go new file mode 100644 index 00000000..4a130fa6 --- /dev/null +++ b/cmd/non-admin/output/output_test.go @@ -0,0 +1,520 @@ +/* +Copyright 2025 The OADP CLI 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 output + +import ( + "bytes" + "io" + "os" + "strings" + "testing" + + nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" + "github.com/spf13/cobra" + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func TestNonAdminScheme(t *testing.T) { + scheme := NonAdminScheme() + + tests := []struct { + name string + gvk schema.GroupVersionKind + objType runtime.Object + }{ + { + name: "NonAdminBackup is registered", + gvk: schema.GroupVersionKind{ + Group: "oadp.openshift.io", + Version: "v1alpha1", + Kind: "NonAdminBackup", + }, + objType: &nacv1alpha1.NonAdminBackup{}, + }, + { + name: "NonAdminBackupList is registered", + gvk: schema.GroupVersionKind{ + Group: "oadp.openshift.io", + Version: "v1alpha1", + Kind: "NonAdminBackupList", + }, + objType: &nacv1alpha1.NonAdminBackupList{}, + }, + { + name: "NonAdminRestore is registered", + gvk: schema.GroupVersionKind{ + Group: "oadp.openshift.io", + Version: "v1alpha1", + Kind: "NonAdminRestore", + }, + objType: &nacv1alpha1.NonAdminRestore{}, + }, + { + name: "NonAdminRestoreList is registered", + gvk: schema.GroupVersionKind{ + Group: "oadp.openshift.io", + Version: "v1alpha1", + Kind: "NonAdminRestoreList", + }, + objType: &nacv1alpha1.NonAdminRestoreList{}, + }, + { + name: "NonAdminBackupStorageLocation is registered", + gvk: schema.GroupVersionKind{ + Group: "oadp.openshift.io", + Version: "v1alpha1", + Kind: "NonAdminBackupStorageLocation", + }, + objType: &nacv1alpha1.NonAdminBackupStorageLocation{}, + }, + { + name: "Velero Backup is registered", + gvk: schema.GroupVersionKind{ + Group: "velero.io", + Version: "v1", + Kind: "Backup", + }, + objType: &velerov1api.Backup{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Check if the type is recognized by the scheme + gvks, _, err := scheme.ObjectKinds(tt.objType) + if err != nil { + t.Fatalf("Failed to get ObjectKinds: %v", err) + } + + found := false + for _, gvk := range gvks { + if gvk.Group == tt.gvk.Group && gvk.Version == tt.gvk.Version && gvk.Kind == tt.gvk.Kind { + found = true + break + } + } + + if !found { + t.Errorf("Expected GVK %v to be registered in scheme, but it was not found", tt.gvk) + } + }) + } +} + +func TestBindFlags(t *testing.T) { + cmd := &cobra.Command{} + BindFlags(cmd.Flags()) + + // Check that the output flag is bound + outputFlag := cmd.Flags().Lookup("output") + if outputFlag == nil { + t.Fatal("Expected 'output' flag to be bound, but it was not found") + } + + // Check that the label-columns flag is bound + labelColumnsFlag := cmd.Flags().Lookup("label-columns") + if labelColumnsFlag == nil { + t.Fatal("Expected 'label-columns' flag to be bound, but it was not found") + } + + // Check that the show-labels flag is bound + showLabelsFlag := cmd.Flags().Lookup("show-labels") + if showLabelsFlag == nil { + t.Fatal("Expected 'show-labels' flag to be bound, but it was not found") + } +} + +func TestClearOutputFlagDefault(t *testing.T) { + cmd := &cobra.Command{} + BindFlags(cmd.Flags()) + + // Initially, the default should be "table" + outputFlag := cmd.Flags().Lookup("output") + if outputFlag.DefValue != "table" { + t.Errorf("Expected default value to be 'table', got %q", outputFlag.DefValue) + } + + // Clear the default + ClearOutputFlagDefault(cmd) + + // After clearing, the default should be empty + if outputFlag.DefValue != "" { + t.Errorf("Expected default value to be empty after clearing, got %q", outputFlag.DefValue) + } +} + +func TestPrintWithFormat(t *testing.T) { + tests := []struct { + name string + outputFormat string + obj runtime.Object + expectPrinted bool + expectError bool + validateOutput func(t *testing.T, output string) + }{ + { + name: "empty format returns false", + outputFormat: "", + obj: createTestBackup("test-backup"), + expectPrinted: false, + expectError: false, + }, + { + name: "yaml format", + outputFormat: "yaml", + obj: createTestBackup("test-backup"), + expectPrinted: true, + expectError: false, + validateOutput: func(t *testing.T, output string) { + if !strings.Contains(output, "apiVersion: oadp.openshift.io/v1alpha1") { + t.Error("Expected YAML output to contain apiVersion") + } + if !strings.Contains(output, "kind: NonAdminBackup") { + t.Error("Expected YAML output to contain kind") + } + if !strings.Contains(output, "name: test-backup") { + t.Error("Expected YAML output to contain name") + } + }, + }, + { + name: "json format", + outputFormat: "json", + obj: createTestBackup("test-backup"), + expectPrinted: true, + expectError: false, + validateOutput: func(t *testing.T, output string) { + if !strings.Contains(output, `"apiVersion": "oadp.openshift.io/v1alpha1"`) { + t.Error("Expected JSON output to contain apiVersion") + } + if !strings.Contains(output, `"kind": "NonAdminBackup"`) { + t.Error("Expected JSON output to contain kind") + } + if !strings.Contains(output, `"name": "test-backup"`) { + t.Error("Expected JSON output to contain name") + } + }, + }, + { + name: "table format returns false", + outputFormat: "table", + obj: createTestBackup("test-backup"), + expectPrinted: false, + expectError: false, + }, + { + name: "invalid format returns error", + outputFormat: "invalid", + obj: createTestBackup("test-backup"), + expectPrinted: false, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a command with the output flag + cmd := &cobra.Command{} + BindFlags(cmd.Flags()) + if tt.outputFormat != "" { + if err := cmd.Flags().Set("output", tt.outputFormat); err != nil { + t.Fatalf("Failed to set output flag: %v", err) + } + } + + // Capture stdout + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + printed, err := PrintWithFormat(cmd, tt.obj) + + // Restore stdout and read captured output + w.Close() + os.Stdout = oldStdout + var buf bytes.Buffer + if _, err := io.Copy(&buf, r); err != nil { + t.Fatalf("Failed to read output: %v", err) + } + output := buf.String() + + // Check error expectation + if tt.expectError && err == nil { + t.Error("Expected error but got none") + } + if !tt.expectError && err != nil { + t.Errorf("Unexpected error: %v", err) + } + + // Check printed expectation + if printed != tt.expectPrinted { + t.Errorf("Expected printed=%v, got %v", tt.expectPrinted, printed) + } + + // Validate output if provided + if tt.validateOutput != nil { + tt.validateOutput(t, output) + } + }) + } +} + +func TestPrintWithFormatList(t *testing.T) { + tests := []struct { + name string + outputFormat string + obj runtime.Object + expectSingle bool // Should single item list be printed as single object? + validateCount func(t *testing.T, output string) + }{ + { + name: "single item list printed as single object in yaml", + outputFormat: "yaml", + obj: &nacv1alpha1.NonAdminBackupList{ + Items: []nacv1alpha1.NonAdminBackup{ + *createTestBackup("backup-1"), + }, + }, + expectSingle: true, + validateCount: func(t *testing.T, output string) { + // Single object should not have "items:" field + if strings.Contains(output, "items:") { + t.Error("Single item from list should not contain 'items:' field") + } + if !strings.Contains(output, "name: backup-1") { + t.Error("Expected output to contain backup name") + } + }, + }, + { + name: "multiple item list printed as list in yaml", + outputFormat: "yaml", + obj: &nacv1alpha1.NonAdminBackupList{ + Items: []nacv1alpha1.NonAdminBackup{ + *createTestBackup("backup-1"), + *createTestBackup("backup-2"), + }, + }, + expectSingle: false, + validateCount: func(t *testing.T, output string) { + // Multiple objects should have "items:" field + if !strings.Contains(output, "items:") { + t.Error("Multiple items should contain 'items:' field") + } + if !strings.Contains(output, "name: backup-1") { + t.Error("Expected output to contain first backup name") + } + if !strings.Contains(output, "name: backup-2") { + t.Error("Expected output to contain second backup name") + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := &cobra.Command{} + BindFlags(cmd.Flags()) + if err := cmd.Flags().Set("output", tt.outputFormat); err != nil { + t.Fatalf("Failed to set output flag: %v", err) + } + + // Capture stdout + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + _, err := PrintWithFormat(cmd, tt.obj) + + // Restore stdout and read captured output + w.Close() + os.Stdout = oldStdout + var buf bytes.Buffer + if _, copyErr := io.Copy(&buf, r); copyErr != nil { + t.Fatalf("Failed to read output: %v", copyErr) + } + output := buf.String() + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if tt.validateCount != nil { + tt.validateCount(t, output) + } + }) + } +} + +func TestEncode(t *testing.T) { + tests := []struct { + name string + obj runtime.Object + format string + validate func(t *testing.T, data []byte) + }{ + { + name: "encode NonAdminBackup to yaml", + obj: createTestBackup("test-backup"), + format: "yaml", + validate: func(t *testing.T, data []byte) { + output := string(data) + if !strings.Contains(output, "apiVersion: oadp.openshift.io/v1alpha1") { + t.Error("Expected YAML to contain apiVersion") + } + if !strings.Contains(output, "kind: NonAdminBackup") { + t.Error("Expected YAML to contain kind") + } + }, + }, + { + name: "encode NonAdminBackup to json", + obj: createTestBackup("test-backup"), + format: "json", + validate: func(t *testing.T, data []byte) { + output := string(data) + if !strings.Contains(output, `"apiVersion"`) { + t.Error("Expected JSON to contain apiVersion") + } + if !strings.Contains(output, `"kind"`) { + t.Error("Expected JSON to contain kind") + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := encode(tt.obj, tt.format) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if tt.validate != nil { + tt.validate(t, data) + } + }) + } +} + +func TestEncoderFor(t *testing.T) { + tests := []struct { + name string + format string + obj runtime.Object + expectError bool + }{ + { + name: "yaml encoder", + format: "yaml", + obj: createTestBackup("test"), + expectError: false, + }, + { + name: "json encoder", + format: "json", + obj: createTestBackup("test"), + expectError: false, + }, + { + name: "invalid format", + format: "xml", + obj: createTestBackup("test"), + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + encoder, err := encoderFor(tt.format, tt.obj) + + if tt.expectError { + if err == nil { + t.Error("Expected error but got none") + } + } else { + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if encoder == nil { + t.Error("Expected encoder but got nil") + } + } + }) + } +} + +func TestIsListType(t *testing.T) { + tests := []struct { + name string + obj runtime.Object + isList bool + }{ + { + name: "NonAdminBackupList is a list", + obj: &nacv1alpha1.NonAdminBackupList{}, + isList: true, + }, + { + name: "NonAdminBackup is not a list", + obj: &nacv1alpha1.NonAdminBackup{}, + isList: false, + }, + { + name: "NonAdminRestoreList is a list", + obj: &nacv1alpha1.NonAdminRestoreList{}, + isList: true, + }, + { + name: "NonAdminRestore is not a list", + obj: &nacv1alpha1.NonAdminRestore{}, + isList: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + isList := meta.IsListType(tt.obj) + if isList != tt.isList { + t.Errorf("Expected IsListType=%v, got %v", tt.isList, isList) + } + }) + } +} + +// Helper function to create a test NonAdminBackup +func createTestBackup(name string) *nacv1alpha1.NonAdminBackup { + return &nacv1alpha1.NonAdminBackup{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "oadp.openshift.io/v1alpha1", + Kind: "NonAdminBackup", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: "test-namespace", + }, + Spec: nacv1alpha1.NonAdminBackupSpec{ + BackupSpec: &velerov1api.BackupSpec{ + IncludedNamespaces: []string{"test-namespace"}, + }, + }, + } +} diff --git a/cmd/non-admin/restore/delete.go b/cmd/non-admin/restore/delete.go index 3422a40a..8fad3b1d 100644 --- a/cmd/non-admin/restore/delete.go +++ b/cmd/non-admin/restore/delete.go @@ -57,6 +57,17 @@ func NewDeleteCommand(f client.Factory, use string) *cobra.Command { cmd.CheckError(o.Validate()) cmd.CheckError(o.Run()) }, + Example: ` # Delete a specific restore + kubectl oadp nonadmin restore delete my-restore + + # Delete multiple restores + kubectl oadp nonadmin restore delete restore1 restore2 restore3 + + # Delete all restores in the current namespace + kubectl oadp nonadmin restore delete --all + + # Delete without confirmation prompt + kubectl oadp nonadmin restore delete my-restore --confirm`, } o.BindFlags(c.Flags()) diff --git a/cmd/non-admin/restore/get.go b/cmd/non-admin/restore/get.go index 6b14224c..2c9e655d 100644 --- a/cmd/non-admin/restore/get.go +++ b/cmd/non-admin/restore/get.go @@ -20,11 +20,11 @@ import ( "fmt" "time" + "github.com/migtools/oadp-cli/cmd/non-admin/output" "github.com/migtools/oadp-cli/cmd/shared" nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" "github.com/spf13/cobra" "github.com/vmware-tanzu/velero/pkg/client" - "github.com/vmware-tanzu/velero/pkg/cmd/util/output" kbclient "sigs.k8s.io/controller-runtime/pkg/client" ) diff --git a/cmd/non-admin/restore/restore_test.go b/cmd/non-admin/restore/restore_test.go index c25ba9cf..308ca3b2 100644 --- a/cmd/non-admin/restore/restore_test.go +++ b/cmd/non-admin/restore/restore_test.go @@ -79,16 +79,14 @@ func TestNonAdminRestoreCommands(t *testing.T) { name: "nonadmin get restore help", args: []string{"nonadmin", "get", "restore", "--help"}, expectContains: []string{ - "Get one or more non-admin resources", - "restore", + "Get one or more non-admin restores", }, }, { name: "nonadmin create restore help", args: []string{"nonadmin", "create", "restore", "--help"}, expectContains: []string{ - "Create non-admin resources", - "restore", + "Create a non-admin restore", }, }, // Shorthand verb-noun order tests @@ -96,16 +94,14 @@ func TestNonAdminRestoreCommands(t *testing.T) { name: "na get restore help", args: []string{"na", "get", "restore", "--help"}, expectContains: []string{ - "Get one or more non-admin resources", - "restore", + "Get one or more non-admin restores", }, }, { name: "na create restore help", args: []string{"na", "create", "restore", "--help"}, expectContains: []string{ - "Create non-admin resources", - "restore", + "Create a non-admin restore", }, }, } @@ -318,13 +314,13 @@ func TestVerbNounOrderRestoreExamples(t *testing.T) { }) t.Run("verb commands with specific resources show proper examples", func(t *testing.T) { - // Test that verb commands with specific resources show examples + // Test that verb commands with specific resources show examples (noun-first format from underlying commands) expectedExamples := []string{ - "kubectl oadp nonadmin get restore my-restore", - "kubectl oadp nonadmin create restore my-restore", - "kubectl oadp nonadmin describe restore my-restore", - "kubectl oadp nonadmin logs restore my-restore", - "kubectl oadp nonadmin delete restore my-restore", + "kubectl oadp nonadmin restore get", + "kubectl oadp nonadmin restore create", + "kubectl oadp nonadmin restore describe my-restore", + "kubectl oadp nonadmin restore logs my-restore", + "kubectl oadp nonadmin restore delete my-restore", } commands := [][]string{ @@ -397,16 +393,14 @@ func TestNonAdminRestoreDescribeCommands(t *testing.T) { name: "nonadmin describe restore help - verb-noun order", args: []string{"nonadmin", "describe", "restore", "--help"}, expectContains: []string{ - "Describe non-admin resources", - "restore", + "Describe a non-admin restore", }, }, { name: "na describe restore help - shorthand", args: []string{"na", "describe", "restore", "--help"}, expectContains: []string{ - "Describe non-admin resources", - "restore", + "Describe a non-admin restore", }, }, } @@ -439,16 +433,14 @@ func TestNonAdminRestoreLogsCommands(t *testing.T) { name: "nonadmin logs restore help - verb-noun order", args: []string{"nonadmin", "logs", "restore", "--help"}, expectContains: []string{ - "Get logs for non-admin resources", - "restore", + "Show logs for a non-admin restore", }, }, { name: "na logs restore help - shorthand", args: []string{"na", "logs", "restore", "--help"}, expectContains: []string{ - "Get logs for non-admin resources", - "restore", + "Show logs for a non-admin restore", }, }, } @@ -482,16 +474,14 @@ func TestNonAdminRestoreDeleteCommands(t *testing.T) { name: "nonadmin delete restore help - verb-noun order", args: []string{"nonadmin", "delete", "restore", "--help"}, expectContains: []string{ - "Delete non-admin resources", - "restore", + "Delete one or more non-admin restores", }, }, { name: "na delete restore help - shorthand", args: []string{"na", "delete", "restore", "--help"}, expectContains: []string{ - "Delete non-admin resources", - "restore", + "Delete one or more non-admin restores", }, }, } @@ -518,4 +508,17 @@ func TestNonAdminRestoreDeleteAllFlag(t *testing.T) { []string{"nonadmin", "restore", "delete", "--help"}, []string{"--confirm", "Skip confirmation"}) }) + + t.Run("delete help has examples section", func(t *testing.T) { + // Test that examples section exists and shows various delete patterns + expectedExamples := []string{ + "kubectl oadp nonadmin restore delete my-restore", + "kubectl oadp nonadmin restore delete --all", + "kubectl oadp nonadmin restore delete my-restore --confirm", + } + + testutil.TestHelpCommand(t, binaryPath, + []string{"nonadmin", "restore", "delete", "--help"}, + expectedExamples) + }) } diff --git a/cmd/non-admin/verbs/builder.go b/cmd/non-admin/verbs/builder.go deleted file mode 100644 index e6b9e9f8..00000000 --- a/cmd/non-admin/verbs/builder.go +++ /dev/null @@ -1,184 +0,0 @@ -/* -Copyright 2025 The OADP CLI 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 verbs - -import ( - "fmt" - - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "github.com/vmware-tanzu/velero/pkg/client" -) - -// NonAdminResourceHandler defines functions to get the main command and its subcommand for a resource type. -type NonAdminResourceHandler struct { - GetCommandFunc func(client.Factory) *cobra.Command - GetSubCommandFunc func(*cobra.Command) *cobra.Command -} - -// NonAdminVerbBuilder helps construct verb-based commands dynamically for non-admin resources. -type NonAdminVerbBuilder struct { - factory client.Factory - resourceRegistry map[string]NonAdminResourceHandler -} - -// NewNonAdminVerbBuilder creates a new NonAdminVerbBuilder instance. -func NewNonAdminVerbBuilder(factory client.Factory) *NonAdminVerbBuilder { - return &NonAdminVerbBuilder{ - factory: factory, - resourceRegistry: make(map[string]NonAdminResourceHandler), - } -} - -// RegisterResource registers a resource type with its handler functions. -func (vb *NonAdminVerbBuilder) RegisterResource(resourceType string, handler NonAdminResourceHandler) { - vb.resourceRegistry[resourceType] = handler -} - -// NonAdminVerbConfig holds configuration for a verb command. -type NonAdminVerbConfig struct { - Use string - Short string - Long string - Example string -} - -// BuildVerbCommand constructs a cobra.Command for a verb, delegating to registered noun commands. -func (vb *NonAdminVerbBuilder) BuildVerbCommand(config NonAdminVerbConfig) *cobra.Command { - verbCmd := &cobra.Command{ - Use: config.Use, - Short: config.Short, - Long: config.Long, - Args: cobra.MinimumNArgs(1), - RunE: vb.runEFunc(config.Use), - Example: config.Example, - } - - vb.addFlagsFromResources(verbCmd, config.Use) - - return verbCmd -} - -func (vb *NonAdminVerbBuilder) runEFunc(verb string) func(cmd *cobra.Command, args []string) error { - return func(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return fmt.Errorf("resource type required") - } - - resourceType := args[0] - remainingArgs := args[1:] - - handler, ok := vb.resourceRegistry[resourceType] - if !ok { - return fmt.Errorf("unknown resource type: %s", resourceType) - } - - // Get the main command for the resource (e.g., "backup" command) - resourceCmd := handler.GetCommandFunc(vb.factory) - if resourceCmd == nil { - return fmt.Errorf("%s command not found for resource type %s", verb, resourceType) - } - - // Get the specific subcommand for the verb (e.g., "backup get" command) - subCmd := handler.GetSubCommandFunc(resourceCmd) - if subCmd == nil { - return fmt.Errorf("%s %s command not found", resourceType, verb) - } - - // Add flags to remaining args so they get passed to the delegated command - remainingArgs = vb.addFlagsToArgs(cmd, remainingArgs) - - // Create a new command instance to avoid argument inheritance - newSubCmd := vb.createCommandInstance(subCmd) - newSubCmd.SetArgs(remainingArgs) - - return newSubCmd.Execute() - } -} - -// addFlagsToArgs adds flags from the verb command to the remaining args -func (vb *NonAdminVerbBuilder) addFlagsToArgs(cmd *cobra.Command, remainingArgs []string) []string { - // Use Visit instead of VisitAll to only process flags that were actually set - cmd.Flags().Visit(func(flag *pflag.Flag) { - flagValue := flag.Value.String() - flagType := flag.Value.Type() - - switch flagType { - case "string", "map": - remainingArgs = append(remainingArgs, "--"+flag.Name, flagValue) - case "bool": - if flagValue == "true" { - remainingArgs = append(remainingArgs, "--"+flag.Name) - } - case "stringArray", "stringSlice": - // Handle string array/slice flags - remainingArgs = append(remainingArgs, "--"+flag.Name, flagValue) - default: - // For any other flag types, try to add them as string values - // This handles custom types that implement pflag.Value - if flagValue != "" { - remainingArgs = append(remainingArgs, "--"+flag.Name, flagValue) - } - } - }) - return remainingArgs -} - -// createCommandInstance creates a new cobra.Command instance from an existing one to avoid argument/flag inheritance issues. -func (vb *NonAdminVerbBuilder) createCommandInstance(originalCmd *cobra.Command) *cobra.Command { - newCmd := &cobra.Command{ - Use: originalCmd.Use, - Short: originalCmd.Short, - Long: originalCmd.Long, - Run: originalCmd.Run, - RunE: originalCmd.RunE, - } - - // Copy flags from the original command - originalCmd.Flags().VisitAll(func(flag *pflag.Flag) { - newCmd.Flags().AddFlag(flag) - }) - // Also copy persistent flags - originalCmd.PersistentFlags().VisitAll(func(flag *pflag.Flag) { - newCmd.PersistentFlags().AddFlag(flag) - }) - return newCmd -} - -// addFlagsFromResources adds flags from all registered resources to the verb command -func (vb *NonAdminVerbBuilder) addFlagsFromResources(verbCmd *cobra.Command, verb string) { - addedFlags := make(map[string]bool) - - for _, handler := range vb.resourceRegistry { - resourceCmd := handler.GetCommandFunc(vb.factory) - if resourceCmd == nil { - continue - } - - // Add flags from the specific verb subcommand (e.g., "backup create" flags to "create" command) - // This ensures flags are recognized at the verb level - subCmd := handler.GetSubCommandFunc(resourceCmd) - if subCmd != nil { - subCmd.Flags().VisitAll(func(flag *pflag.Flag) { - if !addedFlags[flag.Name] { - verbCmd.Flags().AddFlag(flag) - addedFlags[flag.Name] = true - } - }) - } - } -} diff --git a/cmd/non-admin/verbs/builder_test.go b/cmd/non-admin/verbs/builder_test.go deleted file mode 100644 index 2c4a86c8..00000000 --- a/cmd/non-admin/verbs/builder_test.go +++ /dev/null @@ -1,444 +0,0 @@ -/* -Copyright 2025 The OADP CLI 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 verbs - -import ( - "bytes" - "strings" - "testing" - - "github.com/spf13/cobra" - "github.com/vmware-tanzu/velero/pkg/client" -) - -// TestNonAdminVerbBuilder_RegisterResource tests resource registration -func TestNonAdminVerbBuilder_RegisterResource(t *testing.T) { - builder := NewNonAdminVerbBuilder(nil) - - handler := NonAdminResourceHandler{ - GetCommandFunc: func(f client.Factory) *cobra.Command { - return &cobra.Command{Use: "test"} - }, - GetSubCommandFunc: func(cmd *cobra.Command) *cobra.Command { - return &cobra.Command{Use: "get"} - }, - } - - builder.RegisterResource("test", handler) - - if _, exists := builder.resourceRegistry["test"]; !exists { - t.Error("Expected resource 'test' to be registered") - } -} - -// TestNonAdminVerbBuilder_BuildVerbCommand tests basic verb command creation -func TestNonAdminVerbBuilder_BuildVerbCommand(t *testing.T) { - builder := NewNonAdminVerbBuilder(nil) - - config := NonAdminVerbConfig{ - Use: "create", - Short: "Create resources", - Long: "Create non-admin resources", - Example: "kubectl oadp nonadmin create backup", - } - - cmd := builder.BuildVerbCommand(config) - - if cmd.Use != "create" { - t.Errorf("Expected Use to be 'create', got %s", cmd.Use) - } - if cmd.Short != "Create resources" { - t.Errorf("Expected Short to be 'Create resources', got %s", cmd.Short) - } - if cmd.Long != "Create non-admin resources" { - t.Errorf("Expected Long description, got %s", cmd.Long) - } - if cmd.Example != "kubectl oadp nonadmin create backup" { - t.Errorf("Expected Example, got %s", cmd.Example) - } -} - -// TestNonAdminVerbBuilder_FlagPassing tests that flags are passed correctly when using verb-first order -func TestNonAdminVerbBuilder_FlagPassing(t *testing.T) { - tests := []struct { - name string - flagName string - flagValue string - flagType string - expectedInArg string - }{ - { - name: "string flag passed", - flagName: "storage-location", - flagValue: "aws-backup", - flagType: "string", - expectedInArg: "--storage-location", - }, - { - name: "label flag passed", - flagName: "labels", - flagValue: "app=test", - flagType: "map", - expectedInArg: "--labels", - }, - { - name: "include-resources flag passed", - flagName: "include-resources", - flagValue: "deployments,services", - flagType: "stringArray", - expectedInArg: "--include-resources", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Track what flag value was received in the delegated command - var flagReceived bool - var receivedValue string - - // Create a mock subcommand that captures the flag value - mockSubCmd := &cobra.Command{ - Use: "create NAME", - Short: "Create a backup", - RunE: func(cmd *cobra.Command, args []string) error { - flag := cmd.Flags().Lookup(tt.flagName) - if flag != nil && flag.Changed { - flagReceived = true - receivedValue = flag.Value.String() - } - return nil - }, - } - - // Add the flag to the mock subcommand - switch tt.flagType { - case "string": - mockSubCmd.Flags().String(tt.flagName, "", "test flag") - case "map": - mockSubCmd.Flags().StringToString(tt.flagName, nil, "test flag") - case "stringArray": - mockSubCmd.Flags().StringArray(tt.flagName, nil, "test flag") - } - - // Create mock resource handler - mockResourceCmd := &cobra.Command{ - Use: "backup", - Short: "Work with backups", - } - mockResourceCmd.AddCommand(mockSubCmd) - - handler := NonAdminResourceHandler{ - GetCommandFunc: func(f client.Factory) *cobra.Command { - return mockResourceCmd - }, - GetSubCommandFunc: func(cmd *cobra.Command) *cobra.Command { - return mockSubCmd - }, - } - - // Build the verb command - builder := NewNonAdminVerbBuilder(nil) - builder.RegisterResource("backup", handler) - - verbCmd := builder.BuildVerbCommand(NonAdminVerbConfig{ - Use: "create", - Short: "Create resources", - }) - - // Set the flag value on the verb command and execute - verbCmd.SetArgs([]string{"backup", "test-backup", "--" + tt.flagName + "=" + tt.flagValue}) - - // Execute the command - err := verbCmd.Execute() - if err != nil { - t.Logf("Command execution error (expected if no cluster): %v", err) - } - - // Verify the flag was passed to the delegated command - if !flagReceived { - t.Errorf("Expected flag %s to be passed to delegated command, but it wasn't received", - tt.flagName) - } else { - t.Logf("Flag %s successfully passed with value: %s", tt.flagName, receivedValue) - } - }) - } -} - -// TestNonAdminVerbBuilder_BoolFlagPassing tests boolean flag handling -func TestNonAdminVerbBuilder_BoolFlagPassing(t *testing.T) { - tests := []struct { - name string - flagName string - flagValue bool - shouldAppear bool - }{ - { - name: "bool flag true", - flagName: "force", - flagValue: true, - shouldAppear: true, - }, - { - name: "bool flag false", - flagName: "force", - flagValue: false, - shouldAppear: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var flagReceived bool - - mockSubCmd := &cobra.Command{ - Use: "create NAME", - Short: "Create a backup", - RunE: func(cmd *cobra.Command, args []string) error { - flag := cmd.Flags().Lookup(tt.flagName) - if flag != nil && flag.Changed { - flagReceived = true - } - return nil - }, - } - mockSubCmd.Flags().Bool(tt.flagName, false, "test flag") - - mockResourceCmd := &cobra.Command{Use: "backup"} - mockResourceCmd.AddCommand(mockSubCmd) - - handler := NonAdminResourceHandler{ - GetCommandFunc: func(f client.Factory) *cobra.Command { return mockResourceCmd }, - GetSubCommandFunc: func(cmd *cobra.Command) *cobra.Command { return mockSubCmd }, - } - - builder := NewNonAdminVerbBuilder(nil) - builder.RegisterResource("backup", handler) - - verbCmd := builder.BuildVerbCommand(NonAdminVerbConfig{Use: "create"}) - - args := []string{"backup", "test-backup"} - if tt.flagValue { - args = append(args, "--"+tt.flagName) - } - verbCmd.SetArgs(args) - - err := verbCmd.Execute() - if err != nil { - t.Logf("Command execution error (expected if no cluster): %v", err) - } - - if tt.shouldAppear && !flagReceived { - t.Errorf("Expected flag --%s to be passed when value is %v", tt.flagName, tt.flagValue) - } - if !tt.shouldAppear && flagReceived { - t.Errorf("Expected flag --%s NOT to be passed when value is %v", tt.flagName, tt.flagValue) - } - }) - } -} - -// TestNonAdminVerbBuilder_UnknownResourceType tests error handling for unknown resources -func TestNonAdminVerbBuilder_UnknownResourceType(t *testing.T) { - builder := NewNonAdminVerbBuilder(nil) - - verbCmd := builder.BuildVerbCommand(NonAdminVerbConfig{ - Use: "get", - Short: "Get resources", - }) - - // Try to use an unregistered resource type - verbCmd.SetArgs([]string{"unknown-resource", "test-name"}) - - err := verbCmd.Execute() - if err == nil { - t.Error("Expected error for unknown resource type, got nil") - } - - if !strings.Contains(err.Error(), "unknown resource type") { - t.Errorf("Expected error message containing 'unknown resource type', got: %v", err) - } - - t.Logf("Got expected error: %v", err) -} - -// TestNonAdminVerbBuilder_MissingResourceType tests error when no resource type is provided -func TestNonAdminVerbBuilder_MissingResourceType(t *testing.T) { - builder := NewNonAdminVerbBuilder(nil) - - verbCmd := builder.BuildVerbCommand(NonAdminVerbConfig{ - Use: "get", - Short: "Get resources", - }) - - // Execute without any arguments - verbCmd.SetArgs([]string{}) - - var stderr bytes.Buffer - verbCmd.SetErr(&stderr) - - err := verbCmd.Execute() - if err == nil { - t.Error("Expected error when no resource type provided, got nil") - } - - t.Logf("Got expected error: %v", err) -} - -// TestNonAdminVerbBuilder_AddFlagsFromResources tests that flags from registered resources are added to verb command -func TestNonAdminVerbBuilder_AddFlagsFromResources(t *testing.T) { - // Create a mock subcommand with specific flags - mockSubCmd := &cobra.Command{ - Use: "create NAME", - Short: "Create a backup", - } - mockSubCmd.Flags().String("storage-location", "", "storage location") - mockSubCmd.Flags().StringArray("include-resources", nil, "resources to include") - mockSubCmd.Flags().Bool("force", false, "force creation") - - mockResourceCmd := &cobra.Command{Use: "backup"} - mockResourceCmd.AddCommand(mockSubCmd) - - handler := NonAdminResourceHandler{ - GetCommandFunc: func(f client.Factory) *cobra.Command { return mockResourceCmd }, - GetSubCommandFunc: func(cmd *cobra.Command) *cobra.Command { return mockSubCmd }, - } - - builder := NewNonAdminVerbBuilder(nil) - builder.RegisterResource("backup", handler) - - verbCmd := builder.BuildVerbCommand(NonAdminVerbConfig{ - Use: "create", - Short: "Create resources", - }) - - // Verify that flags from the backup create command were added to the verb command - expectedFlags := []string{"storage-location", "include-resources", "force"} - for _, flagName := range expectedFlags { - flag := verbCmd.Flags().Lookup(flagName) - if flag == nil { - t.Errorf("Expected flag '%s' to be added to verb command, but it wasn't found", flagName) - } else { - t.Logf("Flag '%s' successfully added to verb command", flagName) - } - } -} - -// TestNonAdminVerbBuilder_MultipleResourceTypes tests verb command with multiple resource types -func TestNonAdminVerbBuilder_MultipleResourceTypes(t *testing.T) { - builder := NewNonAdminVerbBuilder(nil) - - // Register backup resource - mockBackupSubCmd := &cobra.Command{Use: "create NAME", Short: "Create a backup"} - mockBackupCmd := &cobra.Command{Use: "backup"} - mockBackupCmd.AddCommand(mockBackupSubCmd) - - builder.RegisterResource("backup", NonAdminResourceHandler{ - GetCommandFunc: func(f client.Factory) *cobra.Command { return mockBackupCmd }, - GetSubCommandFunc: func(cmd *cobra.Command) *cobra.Command { return mockBackupSubCmd }, - }) - - // Register bsl resource - mockBSLSubCmd := &cobra.Command{Use: "create NAME", Short: "Create a BSL"} - mockBSLCmd := &cobra.Command{Use: "bsl"} - mockBSLCmd.AddCommand(mockBSLSubCmd) - - builder.RegisterResource("bsl", NonAdminResourceHandler{ - GetCommandFunc: func(f client.Factory) *cobra.Command { return mockBSLCmd }, - GetSubCommandFunc: func(cmd *cobra.Command) *cobra.Command { return mockBSLSubCmd }, - }) - - // Verify both resource types are registered - if len(builder.resourceRegistry) != 2 { - t.Errorf("Expected 2 registered resources, got %d", len(builder.resourceRegistry)) - } - - // Test that both resource types work with the verb command - resourceTypes := []string{"backup", "bsl"} - for _, resourceType := range resourceTypes { - t.Run("resource_"+resourceType, func(t *testing.T) { - testVerbCmd := builder.BuildVerbCommand(NonAdminVerbConfig{ - Use: "create", - Short: "Create resources", - }) - - testVerbCmd.SetArgs([]string{resourceType, "test-name"}) - err := testVerbCmd.Execute() - // Error is expected (no cluster), but it should not be "unknown resource type" - if err != nil && strings.Contains(err.Error(), "unknown resource type") { - t.Errorf("Resource type '%s' should be recognized, got error: %v", resourceType, err) - } - }) - } -} - -// TestNonAdminVerbBuilder_CreateCommandInstance tests command instance creation -func TestNonAdminVerbBuilder_CreateCommandInstance(t *testing.T) { - builder := NewNonAdminVerbBuilder(nil) - - originalCmd := &cobra.Command{ - Use: "create NAME", - Short: "Create a resource", - Long: "Create a non-admin resource", - } - originalCmd.Flags().String("test-flag", "default", "test flag") - originalCmd.PersistentFlags().String("persistent-flag", "default", "persistent flag") - - newCmd := builder.createCommandInstance(originalCmd) - - // Verify basic fields are copied - if newCmd.Use != originalCmd.Use { - t.Errorf("Expected Use to be %s, got %s", originalCmd.Use, newCmd.Use) - } - if newCmd.Short != originalCmd.Short { - t.Errorf("Expected Short to be %s, got %s", originalCmd.Short, newCmd.Short) - } - if newCmd.Long != originalCmd.Long { - t.Errorf("Expected Long to be %s, got %s", originalCmd.Long, newCmd.Long) - } - - // Verify flags are copied - if newCmd.Flags().Lookup("test-flag") == nil { - t.Error("Expected test-flag to be copied to new command") - } - if newCmd.PersistentFlags().Lookup("persistent-flag") == nil { - t.Error("Expected persistent-flag to be copied to new command") - } -} - -// TestNonAdminVerbBuilder_NilHandler tests handling of nil handlers -func TestNonAdminVerbBuilder_NilHandler(t *testing.T) { - builder := NewNonAdminVerbBuilder(nil) - - // Register a handler that returns nil for GetCommandFunc - builder.RegisterResource("nil-resource", NonAdminResourceHandler{ - GetCommandFunc: func(f client.Factory) *cobra.Command { return nil }, - GetSubCommandFunc: func(cmd *cobra.Command) *cobra.Command { return nil }, - }) - - verbCmd := builder.BuildVerbCommand(NonAdminVerbConfig{Use: "get"}) - verbCmd.SetArgs([]string{"nil-resource", "test-name"}) - - err := verbCmd.Execute() - if err == nil { - t.Error("Expected error when handler returns nil command, got nil") - } - - if !strings.Contains(err.Error(), "command not found") { - t.Logf("Got error (as expected): %v", err) - } -} diff --git a/cmd/non-admin/verbs/registry.go b/cmd/non-admin/verbs/registry.go deleted file mode 100644 index f03d1e0c..00000000 --- a/cmd/non-admin/verbs/registry.go +++ /dev/null @@ -1,77 +0,0 @@ -/* -Copyright 2025 The OADP CLI 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 verbs - -import ( - "github.com/migtools/oadp-cli/cmd/non-admin/backup" - "github.com/migtools/oadp-cli/cmd/non-admin/bsl" - "github.com/migtools/oadp-cli/cmd/non-admin/restore" - "github.com/spf13/cobra" - "github.com/vmware-tanzu/velero/pkg/client" -) - -// RegisterBackupResources registers backup resource for a specific verb -func RegisterBackupResources(builder *NonAdminVerbBuilder, verb string) { - builder.RegisterResource("backup", NonAdminResourceHandler{ - GetCommandFunc: func(factory client.Factory) *cobra.Command { - return backup.NewBackupCommand(factory) - }, - GetSubCommandFunc: func(resourceCmd *cobra.Command) *cobra.Command { - return getSubCommand(resourceCmd, verb) - }, - }) -} - -// RegisterRestoreResources registers restore resource for a specific verb -func RegisterRestoreResources(builder *NonAdminVerbBuilder, verb string) { - // Only register restore for supported verbs: create, get, describe, logs, delete - if verb == "create" || verb == "get" || verb == "describe" || verb == "logs" || verb == "delete" { - builder.RegisterResource("restore", NonAdminResourceHandler{ - GetCommandFunc: func(factory client.Factory) *cobra.Command { - return restore.NewRestoreCommand(factory) - }, - GetSubCommandFunc: func(resourceCmd *cobra.Command) *cobra.Command { - return getSubCommand(resourceCmd, verb) - }, - }) - } -} - -// RegisterBSLResources registers bsl resource for a specific verb -func RegisterBSLResources(builder *NonAdminVerbBuilder, verb string) { - // Only register BSL for supported verbs: create, get - if verb == "create" || verb == "get" { - builder.RegisterResource("bsl", NonAdminResourceHandler{ - GetCommandFunc: func(factory client.Factory) *cobra.Command { - return bsl.NewBSLCommand(factory) - }, - GetSubCommandFunc: func(resourceCmd *cobra.Command) *cobra.Command { - return getSubCommand(resourceCmd, verb) - }, - }) - } -} - -// getSubCommand finds a subcommand by name -func getSubCommand(parentCmd *cobra.Command, subCommandName string) *cobra.Command { - for _, subCmd := range parentCmd.Commands() { - if subCmd.Name() == subCommandName { - return subCmd - } - } - return nil -} diff --git a/cmd/non-admin/verbs/verbs.go b/cmd/non-admin/verbs/verbs.go index bb510851..c00c2d3b 100644 --- a/cmd/non-admin/verbs/verbs.go +++ b/cmd/non-admin/verbs/verbs.go @@ -19,16 +19,15 @@ package verbs import ( "github.com/spf13/cobra" "github.com/vmware-tanzu/velero/pkg/client" + + "github.com/migtools/oadp-cli/cmd/non-admin/backup" + "github.com/migtools/oadp-cli/cmd/non-admin/bsl" + "github.com/migtools/oadp-cli/cmd/non-admin/restore" ) // NewGetCommand creates the "get" verb command that delegates to noun commands func NewGetCommand(factory client.Factory) *cobra.Command { - builder := NewNonAdminVerbBuilder(factory) - RegisterBackupResources(builder, "get") - RegisterRestoreResources(builder, "get") - RegisterBSLResources(builder, "get") - - return builder.BuildVerbCommand(NonAdminVerbConfig{ + c := &cobra.Command{ Use: "get", Short: "Get one or more non-admin resources", Long: "Get one or more non-admin resources. This is a verb-based command that delegates to the appropriate noun command.", @@ -49,17 +48,20 @@ func NewGetCommand(factory client.Factory) *cobra.Command { # Get a specific backup storage location kubectl oadp nonadmin get bsl my-storage`, - }) + } + + c.AddCommand( + backup.NewGetCommand(factory, "backup"), + restore.NewGetCommand(factory, "restore"), + bsl.NewGetCommand(factory, "bsl"), + ) + + return c } // NewCreateCommand creates the "create" verb command that delegates to noun commands func NewCreateCommand(factory client.Factory) *cobra.Command { - builder := NewNonAdminVerbBuilder(factory) - RegisterBackupResources(builder, "create") - RegisterRestoreResources(builder, "create") - RegisterBSLResources(builder, "create") - - return builder.BuildVerbCommand(NonAdminVerbConfig{ + c := &cobra.Command{ Use: "create", Short: "Create non-admin resources", Long: "Create non-admin resources. This is a verb-based command that delegates to the appropriate noun command.", @@ -71,17 +73,20 @@ func NewCreateCommand(factory client.Factory) *cobra.Command { # Create a backup storage location kubectl oadp nonadmin create bsl my-bsl`, - }) + } + + c.AddCommand( + backup.NewCreateCommand(factory, "backup"), + restore.NewCreateCommand(factory, "restore"), + bsl.NewCreateCommand(factory, "bsl"), + ) + + return c } // NewDeleteCommand creates the "delete" verb command that delegates to noun commands func NewDeleteCommand(factory client.Factory) *cobra.Command { - builder := NewNonAdminVerbBuilder(factory) - RegisterBackupResources(builder, "delete") - RegisterRestoreResources(builder, "delete") - RegisterBSLResources(builder, "delete") - - return builder.BuildVerbCommand(NonAdminVerbConfig{ + c := &cobra.Command{ Use: "delete", Short: "Delete non-admin resources", Long: "Delete non-admin resources. This is a verb-based command that delegates to the appropriate noun command.", @@ -90,17 +95,19 @@ func NewDeleteCommand(factory client.Factory) *cobra.Command { # Delete a non-admin restore kubectl oadp nonadmin delete restore my-restore`, - }) + } + + c.AddCommand( + backup.NewDeleteCommand(factory, "backup"), + restore.NewDeleteCommand(factory, "restore"), + ) + + return c } // NewDescribeCommand creates the "describe" verb command that delegates to noun commands func NewDescribeCommand(factory client.Factory) *cobra.Command { - builder := NewNonAdminVerbBuilder(factory) - RegisterBackupResources(builder, "describe") - RegisterRestoreResources(builder, "describe") - RegisterBSLResources(builder, "describe") - - return builder.BuildVerbCommand(NonAdminVerbConfig{ + c := &cobra.Command{ Use: "describe", Short: "Describe non-admin resources", Long: "Describe non-admin resources. This is a verb-based command that delegates to the appropriate noun command.", @@ -109,17 +116,19 @@ func NewDescribeCommand(factory client.Factory) *cobra.Command { # Describe a non-admin restore kubectl oadp nonadmin describe restore my-restore`, - }) + } + + c.AddCommand( + backup.NewDescribeCommand(factory, "backup"), + restore.NewDescribeCommand(factory, "restore"), + ) + + return c } // NewLogsCommand creates the "logs" verb command that delegates to noun commands func NewLogsCommand(factory client.Factory) *cobra.Command { - builder := NewNonAdminVerbBuilder(factory) - RegisterBackupResources(builder, "logs") - RegisterRestoreResources(builder, "logs") - RegisterBSLResources(builder, "logs") - - return builder.BuildVerbCommand(NonAdminVerbConfig{ + c := &cobra.Command{ Use: "logs", Short: "Get logs for non-admin resources", Long: "Get logs for non-admin resources. This is a verb-based command that delegates to the appropriate noun command.", @@ -128,5 +137,12 @@ func NewLogsCommand(factory client.Factory) *cobra.Command { # Get logs for a non-admin restore kubectl oadp nonadmin logs restore my-restore`, - }) + } + + c.AddCommand( + backup.NewLogsCommand(factory, "backup"), + restore.NewLogsCommand(factory, "restore"), + ) + + return c }