diff --git a/cmd/non-admin/backup/backup_test.go b/cmd/non-admin/backup/backup_test.go index 91eb1fd9..8f39f3e3 100644 --- a/cmd/non-admin/backup/backup_test.go +++ b/cmd/non-admin/backup/backup_test.go @@ -97,6 +97,90 @@ func TestNonAdminBackupCommands(t *testing.T) { "logs", }, }, + // Verb-noun order help command tests + { + name: "nonadmin get backup help", + args: []string{"nonadmin", "get", "backup", "--help"}, + expectContains: []string{ + "Get one or more non-admin resources", + "backup", + }, + }, + { + name: "nonadmin create backup help", + args: []string{"nonadmin", "create", "backup", "--help"}, + expectContains: []string{ + "Create non-admin resources", + "backup", + "bsl", + }, + }, + { + name: "nonadmin delete backup help", + args: []string{"nonadmin", "delete", "backup", "--help"}, + expectContains: []string{ + "Delete non-admin resources", + "backup", + }, + }, + { + name: "nonadmin describe backup help", + args: []string{"nonadmin", "describe", "backup", "--help"}, + expectContains: []string{ + "Describe non-admin resources", + "backup", + }, + }, + { + name: "nonadmin logs backup help", + args: []string{"nonadmin", "logs", "backup", "--help"}, + expectContains: []string{ + "Get logs for non-admin resources", + "backup", + }, + }, + // Shorthand verb-noun order tests + { + name: "na get backup help", + args: []string{"na", "get", "backup", "--help"}, + expectContains: []string{ + "Get one or more non-admin resources", + "backup", + }, + }, + { + name: "na create backup help", + args: []string{"na", "create", "backup", "--help"}, + expectContains: []string{ + "Create non-admin resources", + "backup", + "bsl", + }, + }, + { + name: "na delete backup help", + args: []string{"na", "delete", "backup", "--help"}, + expectContains: []string{ + "Delete non-admin resources", + "backup", + }, + }, + { + name: "na describe backup help", + args: []string{"na", "describe", "backup", "--help"}, + expectContains: []string{ + "Describe non-admin resources", + "backup", + }, + }, + { + name: "na logs backup help", + args: []string{"na", "logs", "backup", "--help"}, + expectContains: []string{ + "Get logs for non-admin resources", + "backup", + }, + }, } for _, tt := range tests { @@ -125,6 +209,28 @@ func TestNonAdminBackupHelpFlags(t *testing.T) { {"nonadmin", "backup", "logs", "-h"}, {"na", "backup", "--help"}, {"na", "backup", "-h"}, + // Verb-noun order help flags + {"nonadmin", "get", "backup", "--help"}, + {"nonadmin", "get", "backup", "-h"}, + {"nonadmin", "create", "backup", "--help"}, + {"nonadmin", "create", "backup", "-h"}, + {"nonadmin", "delete", "backup", "--help"}, + {"nonadmin", "delete", "backup", "-h"}, + {"nonadmin", "describe", "backup", "--help"}, + {"nonadmin", "describe", "backup", "-h"}, + {"nonadmin", "logs", "backup", "--help"}, + {"nonadmin", "logs", "backup", "-h"}, + // Shorthand verb-noun order help flags + {"na", "get", "backup", "--help"}, + {"na", "get", "backup", "-h"}, + {"na", "create", "backup", "--help"}, + {"na", "create", "backup", "-h"}, + {"na", "delete", "backup", "--help"}, + {"na", "delete", "backup", "-h"}, + {"na", "describe", "backup", "--help"}, + {"na", "describe", "backup", "-h"}, + {"na", "logs", "backup", "--help"}, + {"na", "logs", "backup", "-h"}, } for _, cmd := range commands { @@ -215,6 +321,14 @@ func TestNonAdminBackupClientConfigIntegration(t *testing.T) { {"nonadmin", "backup", "delete", "--help"}, {"nonadmin", "backup", "logs", "--help"}, {"na", "backup", "get", "--help"}, + // Verb-noun order commands + {"nonadmin", "get", "backup", "--help"}, + {"nonadmin", "create", "backup", "--help"}, + {"nonadmin", "describe", "backup", "--help"}, + {"nonadmin", "delete", "backup", "--help"}, + {"nonadmin", "logs", "backup", "--help"}, + {"na", "get", "backup", "--help"}, + {"na", "create", "backup", "--help"}, } for _, cmd := range commands { @@ -259,3 +373,77 @@ func TestNonAdminBackupCommandStructure(t *testing.T) { } }) } + +// TestVerbNounOrderExamples tests that verb-noun order commands show proper examples +func TestVerbNounOrderExamples(t *testing.T) { + binaryPath := testutil.BuildCLIBinary(t) + + t.Run("verb commands show proper examples", func(t *testing.T) { + // Test that verb commands show examples with kubectl oadp prefix + expectedExamples := []string{ + "kubectl oadp nonadmin get backup", + "kubectl oadp nonadmin create backup", + "kubectl oadp nonadmin delete backup", + "kubectl oadp nonadmin describe backup", + "kubectl oadp nonadmin logs backup", + } + + commands := [][]string{ + {"nonadmin", "get", "--help"}, + {"nonadmin", "create", "--help"}, + {"nonadmin", "delete", "--help"}, + {"nonadmin", "describe", "--help"}, + {"nonadmin", "logs", "--help"}, + } + + for i, cmd := range commands { + testutil.TestHelpCommand(t, binaryPath, cmd, []string{expectedExamples[i]}) + } + }) + + t.Run("verb commands with specific resources show proper examples", func(t *testing.T) { + // Test that verb commands with specific resources show examples + 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", + } + + commands := [][]string{ + {"nonadmin", "get", "backup", "--help"}, + {"nonadmin", "create", "backup", "--help"}, + {"nonadmin", "delete", "backup", "--help"}, + {"nonadmin", "describe", "backup", "--help"}, + {"nonadmin", "logs", "backup", "--help"}, + } + + for i, cmd := range commands { + testutil.TestHelpCommand(t, binaryPath, cmd, []string{expectedExamples[i]}) + } + }) + + t.Run("shorthand verb commands show proper examples", func(t *testing.T) { + // Test that shorthand verb commands show examples + expectedExamples := []string{ + "kubectl oadp nonadmin get backup", + "kubectl oadp nonadmin create backup", + "kubectl oadp nonadmin delete backup", + "kubectl oadp nonadmin describe backup", + "kubectl oadp nonadmin logs backup", + } + + commands := [][]string{ + {"na", "get", "--help"}, + {"na", "create", "--help"}, + {"na", "delete", "--help"}, + {"na", "describe", "--help"}, + {"na", "logs", "--help"}, + } + + for i, cmd := range commands { + testutil.TestHelpCommand(t, binaryPath, cmd, []string{expectedExamples[i]}) + } + }) +} diff --git a/cmd/non-admin/nonadmin.go b/cmd/non-admin/nonadmin.go index 38dc65ea..7c5cb9b3 100644 --- a/cmd/non-admin/nonadmin.go +++ b/cmd/non-admin/nonadmin.go @@ -19,6 +19,7 @@ package nonadmin 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/verbs" "github.com/spf13/cobra" "github.com/vmware-tanzu/velero/pkg/client" ) @@ -38,5 +39,12 @@ func NewNonAdminCommand(f client.Factory) *cobra.Command { // Add backup storage location subcommand c.AddCommand(bsl.NewBSLCommand(f)) + // Add verb-based commands for compatibility with Velero CLI pattern + c.AddCommand(verbs.NewGetCommand(f)) + c.AddCommand(verbs.NewCreateCommand(f)) + c.AddCommand(verbs.NewDeleteCommand(f)) + c.AddCommand(verbs.NewDescribeCommand(f)) + c.AddCommand(verbs.NewLogsCommand(f)) + return c } diff --git a/cmd/non-admin/nonadmin_test.go b/cmd/non-admin/nonadmin_test.go index 8883fa10..d675abc3 100644 --- a/cmd/non-admin/nonadmin_test.go +++ b/cmd/non-admin/nonadmin_test.go @@ -56,6 +56,124 @@ func TestNonAdminCommands(t *testing.T) { "Create a non-admin backup", }, }, + { + name: "nonadmin backup create help", + args: []string{"nonadmin", "create", "backup", "--help"}, + expectContains: []string{ + "Create a non-admin backup", + }, + }, + // Verb-noun order help command tests + { + name: "nonadmin get help", + args: []string{"nonadmin", "get", "--help"}, + expectContains: []string{ + "Get one or more non-admin resources", + "backup", + }, + }, + { + name: "nonadmin create help", + args: []string{"nonadmin", "create", "--help"}, + expectContains: []string{ + "Create non-admin resources", + "backup", + "bsl", + }, + }, + { + name: "nonadmin delete help", + args: []string{"nonadmin", "delete", "--help"}, + expectContains: []string{ + "Delete non-admin resources", + "backup", + }, + }, + { + name: "nonadmin describe help", + args: []string{"nonadmin", "describe", "--help"}, + expectContains: []string{ + "Describe non-admin resources", + "backup", + }, + }, + { + name: "nonadmin logs help", + args: []string{"nonadmin", "logs", "--help"}, + expectContains: []string{ + "Get logs for non-admin resources", + "backup", + }, + }, + // Verb-noun order with specific resources + { + name: "nonadmin get backup help", + args: []string{"nonadmin", "get", "backup", "--help"}, + expectContains: []string{ + "Get one or more non-admin resources", + "backup", + }, + }, + { + name: "nonadmin create backup help", + args: []string{"nonadmin", "create", "backup", "--help"}, + expectContains: []string{ + "Create non-admin resources", + "backup", + "bsl", + }, + }, + { + name: "nonadmin delete backup help", + args: []string{"nonadmin", "delete", "backup", "--help"}, + expectContains: []string{ + "Delete non-admin resources", + "backup", + }, + }, + { + name: "nonadmin describe backup help", + args: []string{"nonadmin", "describe", "backup", "--help"}, + expectContains: []string{ + "Describe non-admin resources", + "backup", + }, + }, + { + name: "nonadmin logs backup help", + args: []string{"nonadmin", "logs", "backup", "--help"}, + expectContains: []string{ + "Get logs for non-admin resources", + "backup", + }, + }, + { + name: "nonadmin create bsl help", + args: []string{"nonadmin", "create", "bsl", "--help"}, + expectContains: []string{ + "Create non-admin resources", + "backup", + "bsl", + }, + }, + // Shorthand tests for verb-noun order + { + name: "na get help", + args: []string{"na", "get", "--help"}, + expectContains: []string{ + "Get one or more non-admin resources", + "backup", + }, + }, + { + name: "na create backup help", + args: []string{"na", "create", "backup", "--help"}, + expectContains: []string{ + "Create non-admin resources", + "backup", + "bsl", + }, + }, } for _, tt := range tests { @@ -76,6 +194,28 @@ func TestNonAdminHelpFlags(t *testing.T) { {"nonadmin", "backup", "-h"}, {"nonadmin", "bsl", "--help"}, {"nonadmin", "bsl", "-h"}, + // Verb-noun order help flags + {"nonadmin", "get", "--help"}, + {"nonadmin", "get", "-h"}, + {"nonadmin", "create", "--help"}, + {"nonadmin", "create", "-h"}, + {"nonadmin", "delete", "--help"}, + {"nonadmin", "delete", "-h"}, + {"nonadmin", "describe", "--help"}, + {"nonadmin", "describe", "-h"}, + {"nonadmin", "logs", "--help"}, + {"nonadmin", "logs", "-h"}, + {"nonadmin", "get", "backup", "--help"}, + {"nonadmin", "get", "backup", "-h"}, + {"nonadmin", "create", "backup", "--help"}, + {"nonadmin", "create", "backup", "-h"}, + {"nonadmin", "create", "bsl", "--help"}, + {"nonadmin", "create", "bsl", "-h"}, + // Shorthand verb-noun order help flags + {"na", "get", "--help"}, + {"na", "get", "-h"}, + {"na", "create", "--help"}, + {"na", "create", "-h"}, } for _, cmd := range commands { diff --git a/cmd/non-admin/verbs/README.md b/cmd/non-admin/verbs/README.md new file mode 100644 index 00000000..f59c65d9 --- /dev/null +++ b/cmd/non-admin/verbs/README.md @@ -0,0 +1,177 @@ +# Non-Admin Verb-Noun Command System + +This directory contains the verb-noun command system for non-admin resources in the OADP CLI. + +## Overview + +The non-admin verb system works identically to the main verb system but is specifically designed for non-admin resources like: +- **backup** - Non-admin backups +- **bsl** - Backup storage locations + +## Usage + +### Supported Commands + +```bash +# Get commands +kubectl oadp nonadmin get backup +kubectl oadp nonadmin get bsl # Error: BSL doesn't support get + +# Create commands +kubectl oadp nonadmin create backup my-backup +kubectl oadp nonadmin create bsl my-bsl + +# Delete commands +kubectl oadp nonadmin delete backup my-backup +kubectl oadp nonadmin delete bsl # Error: BSL doesn't support delete + +# Describe commands +kubectl oadp nonadmin describe backup my-backup +kubectl oadp nonadmin describe bsl # Error: BSL doesn't support describe + +# Logs commands +kubectl oadp nonadmin logs backup my-backup +kubectl oadp nonadmin logs bsl # Error: BSL doesn't support logs +``` + +## Architecture + +### Files + +- **`builder.go`** - `NonAdminVerbBuilder` for building verb commands +- **`registry.go`** - Resource registration for backup and bsl +- **`verbs.go`** - Verb command definitions (get, create, delete, describe, logs) + +### Key Differences from Main Verbs + +1. **Builder Type**: `NonAdminVerbBuilder` instead of `VerbBuilder` +2. **Config Type**: `NonAdminVerbConfig` instead of `VerbConfig` +3. **Handler Type**: `NonAdminResourceHandler` instead of `ResourceHandler` +4. **Single Factory**: Only uses one factory (non-admin factory) + +## Adding New Non-Admin Resources + +### Step 1: Create Resource Commands + +Ensure your resource follows the noun-verb pattern: + +``` +cmd/non-admin/your-resource/ +├── your-resource.go # Main command +├── get.go # get subcommand (if supported) +├── create.go # create subcommand (if supported) +└── describe.go # describe subcommand (if supported) +``` + +### Step 2: Add Resource Registration + +In `cmd/non-admin/verbs/registry.go`: + +```go +// RegisterYourResourceResources registers your-resource for a specific verb +func RegisterYourResourceResources(builder *NonAdminVerbBuilder, verb string) { + // Only register for supported verbs + supportedVerbs := []string{"get", "create", "describe"} + for _, supportedVerb := range supportedVerbs { + if verb == supportedVerb { + builder.RegisterResource("your-resource", NonAdminResourceHandler{ + GetCommandFunc: func(factory client.Factory) *cobra.Command { + return yourresource.NewCommand(factory) + }, + GetSubCommandFunc: func(resourceCmd *cobra.Command) *cobra.Command { + return getSubCommand(resourceCmd, verb) + }, + }) + break + } + } +} +``` + +### Step 3: Register in Verb Commands + +Update each verb function in `cmd/non-admin/verbs/verbs.go`: + +```go +func NewGetCommand(factory client.Factory) *cobra.Command { + builder := NewNonAdminVerbBuilder(factory) + RegisterBackupResources(builder, "get") + RegisterBSLResources(builder, "get") + RegisterYourResourceResources(builder, "get") // Add this line + + return builder.BuildVerbCommand(NonAdminVerbConfig{ + // ... existing config + }) +} +``` + +### Step 4: Update Examples + +Add your resource to the examples in each verb: + +```go +Example: ` # Get all non-admin backups + kubectl oadp nonadmin get backup + + # Get all your-resources + kubectl oadp nonadmin get your-resource`, +``` + +## Conditional Registration Example + +The BSL resource only supports `create`, so it uses conditional registration: + +```go +func RegisterBSLResources(builder *NonAdminVerbBuilder, verb string) { + if verb == "create" { + builder.RegisterResource("bsl", NonAdminResourceHandler{ + // ... registration logic + }) + } +} +``` + +## Testing + +### Build and Test +```bash +go build -o kubectl-oadp . + +# Test new resource +./kubectl-oadp nonadmin get your-resource +./kubectl-oadp nonadmin create your-resource test-name +``` + +### Verify Error Handling +```bash +# Should show "unknown resource type" for unsupported verbs +./kubectl-oadp nonadmin get bsl +./kubectl-oadp nonadmin describe bsl +``` + +## Current Resources + +### Backup +- **Supported Verbs**: get, create, delete, describe, logs +- **Command**: `backup.NewBackupCommand(factory)` + +### BSL (Backup Storage Location) +- **Supported Verbs**: create only +- **Command**: `bsl.NewBSLCommand(factory)` + +## Integration + +The non-admin verb commands are integrated into the main non-admin command in `cmd/non-admin/nonadmin.go`: + +```go +// Add verb-based commands for compatibility with Velero CLI pattern +c.AddCommand(verbs.NewGetCommand(f)) +c.AddCommand(verbs.NewCreateCommand(f)) +c.AddCommand(verbs.NewDeleteCommand(f)) +c.AddCommand(verbs.NewDescribeCommand(f)) +c.AddCommand(verbs.NewLogsCommand(f)) +``` + +This allows users to use either pattern: +- `oadp nonadmin backup get` (noun-verb) +- `oadp nonadmin get backup` (verb-noun) diff --git a/cmd/non-admin/verbs/builder.go b/cmd/non-admin/verbs/builder.go new file mode 100644 index 00000000..07a81c7c --- /dev/null +++ b/cmd/non-admin/verbs/builder.go @@ -0,0 +1,177 @@ +/* +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) + + 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 { + cmd.Flags().VisitAll(func(flag *pflag.Flag) { + if flag.Changed { + if flag.Value.Type() == "string" { + remainingArgs = append(remainingArgs, "--"+flag.Name, flag.Value.String()) + } else if flag.Value.Type() == "bool" { + if flag.Value.String() == "true" { + remainingArgs = append(remainingArgs, "--"+flag.Name) + } + } else if flag.Value.Type() == "stringArray" { + // Handle string array flags + remainingArgs = append(remainingArgs, "--"+flag.Name, flag.Value.String()) + } + } + }) + 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) { + 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 get" flags to "get" command) + // This ensures flags like -o are recognized at the verb level + for range []string{"get", "create", "delete", "describe", "logs"} { // Iterate over all possible verbs + 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/registry.go b/cmd/non-admin/verbs/registry.go new file mode 100644 index 00000000..4156a1a9 --- /dev/null +++ b/cmd/non-admin/verbs/registry.go @@ -0,0 +1,62 @@ +/* +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/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) + }, + }) +} + +// RegisterBSLResources registers bsl resource for a specific verb +// Note: BSL only supports create command, so we only register for create +func RegisterBSLResources(builder *NonAdminVerbBuilder, verb string) { + // Only register BSL for create command since it doesn't have get, delete, describe, or logs + if verb == "create" { + 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 new file mode 100644 index 00000000..167dc72b --- /dev/null +++ b/cmd/non-admin/verbs/verbs.go @@ -0,0 +1,103 @@ +/* +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/spf13/cobra" + "github.com/vmware-tanzu/velero/pkg/client" +) + +// NewGetCommand creates the "get" verb command that delegates to noun commands +func NewGetCommand(factory client.Factory) *cobra.Command { + builder := NewNonAdminVerbBuilder(factory) + RegisterBackupResources(builder, "get") + RegisterBSLResources(builder, "get") + + return builder.BuildVerbCommand(NonAdminVerbConfig{ + 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.", + Example: ` # Get all non-admin backups + kubectl oadp nonadmin get backup + + # Get a specific non-admin backup + kubectl oadp nonadmin get backup my-backup`, + }) +} + +// NewCreateCommand creates the "create" verb command that delegates to noun commands +func NewCreateCommand(factory client.Factory) *cobra.Command { + builder := NewNonAdminVerbBuilder(factory) + RegisterBackupResources(builder, "create") + RegisterBSLResources(builder, "create") + + return builder.BuildVerbCommand(NonAdminVerbConfig{ + 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.", + Example: ` # Create a non-admin backup + kubectl oadp nonadmin create backup my-backup + + # Create a backup storage location + kubectl oadp nonadmin create bsl my-bsl`, + }) +} + +// NewDeleteCommand creates the "delete" verb command that delegates to noun commands +func NewDeleteCommand(factory client.Factory) *cobra.Command { + builder := NewNonAdminVerbBuilder(factory) + RegisterBackupResources(builder, "delete") + RegisterBSLResources(builder, "delete") + + return builder.BuildVerbCommand(NonAdminVerbConfig{ + 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.", + Example: ` # Delete a non-admin backup + kubectl oadp nonadmin delete backup my-backup`, + }) +} + +// NewDescribeCommand creates the "describe" verb command that delegates to noun commands +func NewDescribeCommand(factory client.Factory) *cobra.Command { + builder := NewNonAdminVerbBuilder(factory) + RegisterBackupResources(builder, "describe") + RegisterBSLResources(builder, "describe") + + return builder.BuildVerbCommand(NonAdminVerbConfig{ + 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.", + Example: ` # Describe a non-admin backup + kubectl oadp nonadmin describe backup my-backup`, + }) +} + +// NewLogsCommand creates the "logs" verb command that delegates to noun commands +func NewLogsCommand(factory client.Factory) *cobra.Command { + builder := NewNonAdminVerbBuilder(factory) + RegisterBackupResources(builder, "logs") + RegisterBSLResources(builder, "logs") + + return builder.BuildVerbCommand(NonAdminVerbConfig{ + 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.", + Example: ` # Get logs for a non-admin backup + kubectl oadp nonadmin logs backup my-backup`, + }) +} diff --git a/cmd/root.go b/cmd/root.go index d955b660..df65cdac 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -24,6 +24,7 @@ import ( "github.com/migtools/oadp-cli/cmd/nabsl-request" nonadmin "github.com/migtools/oadp-cli/cmd/non-admin" + "github.com/migtools/oadp-cli/cmd/verbs" "github.com/spf13/cobra" "github.com/vmware-tanzu/velero/pkg/cmd/cli/backup" "github.com/vmware-tanzu/velero/pkg/cmd/cli/client" @@ -84,6 +85,13 @@ func NewVeleroRootCommand() *cobra.Command { rootCmd.AddCommand(restoreCmd) rootCmd.AddCommand(clientCmd) + // Add verb-based commands for compatibility with Velero CLI pattern + rootCmd.AddCommand(verbs.NewGetCommand(veleroFactory, nonAdminFactory)) + rootCmd.AddCommand(verbs.NewCreateCommand(veleroFactory, nonAdminFactory)) + rootCmd.AddCommand(verbs.NewDeleteCommand(veleroFactory, nonAdminFactory)) + rootCmd.AddCommand(verbs.NewDescribeCommand(veleroFactory, nonAdminFactory)) + rootCmd.AddCommand(verbs.NewLogsCommand(veleroFactory, nonAdminFactory)) + // Admin NABSL request commands - use Velero factory (admin namespace) rootCmd.AddCommand(nabsl.NewNABSLRequestCommand(veleroFactory)) diff --git a/cmd/root_test.go b/cmd/root_test.go index d388039f..442e82bc 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -73,6 +73,99 @@ func TestRootCommand(t *testing.T) { "Work with restores", }, }, + // Verb-noun order help command tests + { + name: "get help", + args: []string{"get", "--help"}, + expectContains: []string{ + "Get one or more resources", + "backup", + "restore", + }, + }, + { + name: "create help", + args: []string{"create", "--help"}, + expectContains: []string{ + "Create a resource", + "backup", + "restore", + }, + }, + { + name: "delete help", + args: []string{"delete", "--help"}, + expectContains: []string{ + "Delete a resource", + "backup", + "restore", + }, + }, + { + name: "describe help", + args: []string{"describe", "--help"}, + expectContains: []string{ + "Describe a resource", + "backup", + "restore", + }, + }, + { + name: "logs help", + args: []string{"logs", "--help"}, + expectContains: []string{ + "Get logs for a resource", + "backup", + "restore", + }, + }, + // Verb-noun order with specific resources + { + name: "get backup help", + args: []string{"get", "backup", "--help"}, + expectContains: []string{ + "Get one or more resources", + "backup", + "restore", + }, + }, + { + name: "create backup help", + args: []string{"create", "backup", "--help"}, + expectContains: []string{ + "Create a resource", + "backup", + "restore", + }, + }, + { + name: "delete backup help", + args: []string{"delete", "backup", "--help"}, + expectContains: []string{ + "Delete a resource", + "backup", + "restore", + }, + }, + { + name: "describe backup help", + args: []string{"describe", "backup", "--help"}, + expectContains: []string{ + "Describe a resource", + "backup", + "restore", + }, + }, + { + name: "logs backup help", + args: []string{"logs", "backup", "--help"}, + expectContains: []string{ + "Get logs for a resource", + "backup", + "restore", + "schedule", + }, + }, } for _, tt := range tests { @@ -95,6 +188,21 @@ func TestRootCommandHelpFlags(t *testing.T) { {"restore", "-h"}, {"version", "--help"}, {"version", "-h"}, + // Verb-noun order help flags + {"get", "--help"}, + {"get", "-h"}, + {"create", "--help"}, + {"create", "-h"}, + {"delete", "--help"}, + {"delete", "-h"}, + {"describe", "--help"}, + {"describe", "-h"}, + {"logs", "--help"}, + {"logs", "-h"}, + {"get", "backup", "--help"}, + {"get", "backup", "-h"}, + {"create", "backup", "--help"}, + {"create", "backup", "-h"}, } for _, cmd := range commands { @@ -114,6 +222,14 @@ func TestRootCommandSmoke(t *testing.T) { {"backup", "--help"}, {"restore", "--help"}, {"version", "--help"}, + // Verb-noun order smoke tests + {"get", "--help"}, + {"create", "--help"}, + {"delete", "--help"}, + {"describe", "--help"}, + {"logs", "--help"}, + {"get", "backup", "--help"}, + {"create", "backup", "--help"}, } for _, cmd := range smokeCommands { diff --git a/cmd/verbs/README.md b/cmd/verbs/README.md new file mode 100644 index 00000000..42ec89d9 --- /dev/null +++ b/cmd/verbs/README.md @@ -0,0 +1,275 @@ +# Verb-Noun Command System + +This document explains how to add new nouns (resource types) and verbs (actions) to the OADP CLI's verb-noun command system. + +## Overview + +The OADP CLI supports both command patterns: +- **Noun-Verb**: `oadp backup get` (traditional Velero style) +- **Verb-Noun**: `oadp get backup` (kubectl style) + +This system is implemented in two places: +- **Main commands**: `cmd/verbs/` - for admin-level resources +- **Non-admin commands**: `cmd/non-admin/verbs/` - for non-admin resources + +## Architecture + +### Core Components + +1. **Builder** (`builder.go`) - Generic command builder that handles delegation logic +2. **Registry** (`registry.go`) - Resource registration system +3. **Verbs** (`verbs.go`) - Individual verb command definitions + +### How It Works + +``` +User runs: oadp get backup + ↓ +Verb command (get) receives: ["backup"] + ↓ +Looks up "backup" in resource registry + ↓ +Gets backup.NewCommand() and finds "get" subcommand + ↓ +Delegates to: backup get (with all flags preserved) +``` + +## Adding New Verbs + +### Step 1: Add Verb Function + +In `cmd/verbs/verbs.go` (or `cmd/non-admin/verbs/verbs.go`): + +```go +// NewLogsCommand creates the "logs" verb command +func NewLogsCommand(veleroFactory, nonAdminFactory client.Factory) *cobra.Command { + builder := NewVerbBuilder(veleroFactory, nonAdminFactory) + RegisterBackupResources(builder, "logs") + RegisterRestoreResources(builder, "logs") + RegisterScheduleResources(builder, "logs") + + return builder.BuildVerbCommand(VerbConfig{ + Use: "logs", + Short: "Get logs for resources", + Long: "Get logs for resources. This is a verb-based command that delegates to the appropriate noun command.", + Example: ` # Get logs for a backup + kubectl oadp logs backup my-backup + + # Get logs for a restore + kubectl oadp logs restore my-restore`, + }) +} +``` + +### Step 2: Register in Root Command + +In `cmd/root.go` (or `cmd/non-admin/nonadmin.go`): + +```go +// Add verb-based commands +rootCmd.AddCommand(verbs.NewLogsCommand(veleroFactory, nonAdminFactory)) +``` + +### Step 3: Update Flag Collection (if needed) + +If your new verb has unique flags, update the `addFlagsFromResources` function in `builder.go`: + +```go +for _, verb := range []string{"get", "create", "delete", "describe", "logs", "your-new-verb"} { + // ... existing code +} +``` + +## Adding New Nouns (Resources) + +### Step 1: Create Resource Commands + +First, ensure your resource has the standard noun-verb structure: + +``` +cmd/your-resource/ +├── your-resource.go # Main command (e.g., "backup") +├── get.go # get subcommand +├── create.go # create subcommand +├── delete.go # delete subcommand +└── describe.go # describe subcommand +``` + +### Step 2: Add Resource Registration + +In `cmd/verbs/registry.go` (or `cmd/non-admin/verbs/registry.go`): + +```go +// RegisterYourResourceResources registers your-resource for a specific verb +func RegisterYourResourceResources(builder *VerbBuilder, verb string) { + builder.RegisterResource("your-resource", ResourceHandler{ + GetCommandFunc: func(factory client.Factory) *cobra.Command { + return yourresource.NewCommand(factory) + }, + GetSubCommandFunc: func(resourceCmd *cobra.Command) *cobra.Command { + return getSubCommand(resourceCmd, verb) + }, + }) +} +``` + +### Step 3: Register in All Verb Commands + +Update each verb function in `verbs.go`: + +```go +func NewGetCommand(veleroFactory, nonAdminFactory client.Factory) *cobra.Command { + builder := NewVerbBuilder(veleroFactory, nonAdminFactory) + RegisterBackupResources(builder, "get") + RegisterRestoreResources(builder, "get") + RegisterScheduleResources(builder, "get") + RegisterYourResourceResources(builder, "get") // Add this line + + return builder.BuildVerbCommand(VerbConfig{ + // ... existing config + }) +} +``` + +Repeat for all verbs: `create`, `delete`, `describe`, `logs`, etc. + +### Step 4: Update Examples + +Update the `Example` field in each verb to include your new resource: + +```go +Example: ` # Get all backups + kubectl oadp get backup + + # Get all your-resources + kubectl oadp get your-resource`, +``` + +## Conditional Resource Registration + +Some resources may not support all verbs. Use conditional registration: + +```go +// RegisterBSLResources - BSL only supports create +func RegisterBSLResources(builder *NonAdminVerbBuilder, verb string) { + if verb == "create" { + builder.RegisterResource("bsl", NonAdminResourceHandler{ + // ... registration logic + }) + } +} +``` + +## Non-Admin Resources + +For non-admin resources, follow the same pattern but use: + +- **Directory**: `cmd/non-admin/verbs/` +- **Builder**: `NonAdminVerbBuilder` +- **Config**: `NonAdminVerbConfig` +- **Handler**: `NonAdminResourceHandler` + +## Testing Your Changes + +### 1. Build the CLI +```bash +go build -o kubectl-oadp . +``` + +### 2. Test Verb-Noun Commands +```bash +# Test new verb +./kubectl-oadp your-verb --help + +# Test new noun +./kubectl-oadp get your-resource --help + +# Test actual delegation +./kubectl-oadp get your-resource +``` + +### 3. Test Flag Preservation +```bash +# Ensure flags work correctly +./kubectl-oadp get your-resource -o json +``` + +## Common Patterns + +### Resource with Limited Verbs + +If your resource only supports certain verbs: + +```go +func RegisterYourResourceResources(builder *VerbBuilder, verb string) { + supportedVerbs := []string{"get", "describe"} + for _, supportedVerb := range supportedVerbs { + if verb == supportedVerb { + builder.RegisterResource("your-resource", ResourceHandler{ + // ... registration + }) + break + } + } +} +``` + +### Resource with Custom Flags + +If your resource has unique flags, ensure they're properly copied: + +```go +// In builder.go, addFlagsFromResources function +// The system automatically collects flags from all registered resources +// No additional changes needed unless you have special flag handling +``` + +## Troubleshooting + +### "unknown resource type" Error +- Ensure the resource is registered in the verb's builder +- Check that the resource name matches exactly + +### "command not found" Error +- Verify the resource has the expected subcommand (get, create, etc.) +- Check the `getSubCommand` function in registry.go + +### Flags Not Working +- Ensure flags are added to the resource's subcommand +- Check that `addFlagsFromResources` includes your verb + +### Build Errors +- Verify all imports are correct +- Check that resource command functions exist and return `*cobra.Command` + +## Examples + +### Complete Example: Adding "schedule" Resource + +1. **Resource exists**: `cmd/schedule/` with `get.go`, `create.go`, etc. + +2. **Add to registry** (`cmd/verbs/registry.go`): +```go +func RegisterScheduleResources(builder *VerbBuilder, verb string) { + builder.RegisterResource("schedule", ResourceHandler{ + GetCommandFunc: func(factory client.Factory) *cobra.Command { + return schedule.NewCommand(factory) + }, + GetSubCommandFunc: func(resourceCmd *cobra.Command) *cobra.Command { + return getSubCommand(resourceCmd, verb) + }, + }) +} +``` + +3. **Update all verbs** (`cmd/verbs/verbs.go`): +```go +// In each verb function, add: +RegisterScheduleResources(builder, "get") // for NewGetCommand +RegisterScheduleResources(builder, "create") // for NewCreateCommand +// etc. +``` + +4. **Update examples** in each verb's `Example` field. + +That's it! The system will automatically handle delegation, flag preservation, and error handling. diff --git a/cmd/verbs/builder.go b/cmd/verbs/builder.go new file mode 100644 index 00000000..afccaf8a --- /dev/null +++ b/cmd/verbs/builder.go @@ -0,0 +1,188 @@ +/* +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" +) + +// ResourceHandler defines how to handle a specific resource type for a verb +type ResourceHandler struct { + // GetCommandFunc returns the command for this resource type + GetCommandFunc func(factory client.Factory) *cobra.Command + // GetSubCommandFunc returns the specific subcommand (e.g., "get", "create") from the resource command + GetSubCommandFunc func(resourceCmd *cobra.Command) *cobra.Command +} + +// VerbConfig defines the configuration for a verb command +type VerbConfig struct { + Use string + Short string + Long string + Example string +} + +// ResourceRegistry maps resource types to their handlers +type ResourceRegistry map[string]ResourceHandler + +// VerbBuilder creates extensible verb commands +type VerbBuilder struct { + veleroFactory client.Factory + nonAdminFactory client.Factory + resourceRegistry ResourceRegistry +} + +// NewVerbBuilder creates a new verb builder +func NewVerbBuilder(veleroFactory, nonAdminFactory client.Factory) *VerbBuilder { + return &VerbBuilder{ + veleroFactory: veleroFactory, + nonAdminFactory: nonAdminFactory, + resourceRegistry: make(ResourceRegistry), + } +} + +// RegisterResource registers a resource type with its handler +func (vb *VerbBuilder) RegisterResource(resourceType string, handler ResourceHandler) { + vb.resourceRegistry[resourceType] = handler +} + +// BuildVerbCommand creates a verb command that delegates to registered resources +func (vb *VerbBuilder) BuildVerbCommand(config VerbConfig) *cobra.Command { + verbCmd := &cobra.Command{ + Use: config.Use, + Short: config.Short, + Long: config.Long, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return vb.executeVerbCommand(cmd, args) + }, + Example: config.Example, + } + + // Add flags from all registered resources + vb.addFlagsFromResources(verbCmd) + + return verbCmd +} + +// executeVerbCommand handles the execution of a verb command +func (vb *VerbBuilder) executeVerbCommand(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return fmt.Errorf("resource type required") + } + + resourceType := args[0] + remainingArgs := args[1:] + + // Get the handler for this resource type + handler, exists := vb.resourceRegistry[resourceType] + if !exists { + return fmt.Errorf("unknown resource type: %s", resourceType) + } + + // Get the resource command + resourceCmd := handler.GetCommandFunc(vb.veleroFactory) + if resourceCmd == nil { + return fmt.Errorf("failed to get %s command", resourceType) + } + + // Get the specific subcommand + subCmd := handler.GetSubCommandFunc(resourceCmd) + if subCmd == nil { + return fmt.Errorf("%s %s command not found", resourceType, cmd.Name()) + } + + // 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 *VerbBuilder) addFlagsToArgs(cmd *cobra.Command, remainingArgs []string) []string { + cmd.Flags().VisitAll(func(flag *pflag.Flag) { + if flag.Changed { + if flag.Value.Type() == "string" { + remainingArgs = append(remainingArgs, "--"+flag.Name, flag.Value.String()) + } else if flag.Value.Type() == "bool" { + if flag.Value.String() == "true" { + remainingArgs = append(remainingArgs, "--"+flag.Name) + } + } else if flag.Value.Type() == "stringArray" { + // Handle string array flags + remainingArgs = append(remainingArgs, "--"+flag.Name, flag.Value.String()) + } + } + }) + return remainingArgs +} + +// createCommandInstance creates a new command instance to avoid argument inheritance +func (vb *VerbBuilder) 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 *VerbBuilder) addFlagsFromResources(verbCmd *cobra.Command) { + addedFlags := make(map[string]bool) + + for _, handler := range vb.resourceRegistry { + resourceCmd := handler.GetCommandFunc(vb.veleroFactory) + if resourceCmd == nil { + continue + } + + subCmd := handler.GetSubCommandFunc(resourceCmd) + if subCmd == nil { + continue + } + + // Add flags from this resource's subcommand + subCmd.Flags().VisitAll(func(flag *pflag.Flag) { + // Only add flag if it doesn't already exist + if !addedFlags[flag.Name] { + verbCmd.Flags().AddFlag(flag) + addedFlags[flag.Name] = true + } + }) + } +} diff --git a/cmd/verbs/registry.go b/cmd/verbs/registry.go new file mode 100644 index 00000000..d6adfe4f --- /dev/null +++ b/cmd/verbs/registry.go @@ -0,0 +1,94 @@ +/* +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/spf13/cobra" + "github.com/vmware-tanzu/velero/pkg/client" + "github.com/vmware-tanzu/velero/pkg/cmd/cli/backup" + "github.com/vmware-tanzu/velero/pkg/cmd/cli/restore" + "github.com/vmware-tanzu/velero/pkg/cmd/cli/schedule" +) + +// RegisterAllResources registers all available resource types with the verb builder +func RegisterAllResources(builder *VerbBuilder) { + // Register backup resource + builder.RegisterResource("backup", ResourceHandler{ + GetCommandFunc: func(factory client.Factory) *cobra.Command { + return backup.NewCommand(factory) + }, + GetSubCommandFunc: func(resourceCmd *cobra.Command) *cobra.Command { + return getSubCommand(resourceCmd, "get") + }, + }) + + // Register restore resource + builder.RegisterResource("restore", ResourceHandler{ + GetCommandFunc: func(factory client.Factory) *cobra.Command { + return restore.NewCommand(factory) + }, + GetSubCommandFunc: func(resourceCmd *cobra.Command) *cobra.Command { + return getSubCommand(resourceCmd, "get") + }, + }) +} + +// RegisterBackupResources registers backup resource for a specific verb +func RegisterBackupResources(builder *VerbBuilder, verb string) { + builder.RegisterResource("backup", ResourceHandler{ + GetCommandFunc: func(factory client.Factory) *cobra.Command { + return backup.NewCommand(factory) + }, + GetSubCommandFunc: func(resourceCmd *cobra.Command) *cobra.Command { + return getSubCommand(resourceCmd, verb) + }, + }) +} + +// RegisterRestoreResources registers restore resource for a specific verb +func RegisterRestoreResources(builder *VerbBuilder, verb string) { + builder.RegisterResource("restore", ResourceHandler{ + GetCommandFunc: func(factory client.Factory) *cobra.Command { + return restore.NewCommand(factory) + }, + GetSubCommandFunc: func(resourceCmd *cobra.Command) *cobra.Command { + return getSubCommand(resourceCmd, verb) + }, + }) +} + +// RegisterScheduleResources registers schedule resource for a specific verb +func RegisterScheduleResources(builder *VerbBuilder, verb string) { + builder.RegisterResource("schedule", ResourceHandler{ + GetCommandFunc: func(factory client.Factory) *cobra.Command { + return schedule.NewCommand(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/verbs/verbs.go b/cmd/verbs/verbs.go new file mode 100644 index 00000000..dd501811 --- /dev/null +++ b/cmd/verbs/verbs.go @@ -0,0 +1,124 @@ +/* +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/spf13/cobra" + "github.com/vmware-tanzu/velero/pkg/client" +) + +// NewGetCommand creates the "get" verb command that delegates to noun commands +func NewGetCommand(veleroFactory, nonAdminFactory client.Factory) *cobra.Command { + builder := NewVerbBuilder(veleroFactory, nonAdminFactory) + RegisterBackupResources(builder, "get") + RegisterRestoreResources(builder, "get") + RegisterScheduleResources(builder, "get") + + return builder.BuildVerbCommand(VerbConfig{ + Use: "get", + Short: "Get one or more resources", + Long: "Get one or more resources. This is a verb-based command that delegates to the appropriate noun command.", + Example: ` # Get all backups + kubectl oadp get backup + + # Get a specific backup + kubectl oadp get backup my-backup + + # Get all restores + kubectl oadp get restore`, + }) +} + +// NewCreateCommand creates the "create" verb command that delegates to noun commands +func NewCreateCommand(veleroFactory, nonAdminFactory client.Factory) *cobra.Command { + builder := NewVerbBuilder(veleroFactory, nonAdminFactory) + RegisterBackupResources(builder, "create") + RegisterRestoreResources(builder, "create") + RegisterScheduleResources(builder, "create") + + return builder.BuildVerbCommand(VerbConfig{ + Use: "create", + Short: "Create a resource", + Long: "Create a resource. This is a verb-based command that delegates to the appropriate noun command.", + Example: ` # Create a backup + kubectl oadp create backup my-backup + + # Create a restore + kubectl oadp create restore my-restore`, + }) +} + +// NewDeleteCommand creates the "delete" verb command that delegates to noun commands +func NewDeleteCommand(veleroFactory, nonAdminFactory client.Factory) *cobra.Command { + builder := NewVerbBuilder(veleroFactory, nonAdminFactory) + RegisterBackupResources(builder, "delete") + RegisterRestoreResources(builder, "delete") + RegisterScheduleResources(builder, "delete") + + return builder.BuildVerbCommand(VerbConfig{ + Use: "delete", + Short: "Delete a resource", + Long: "Delete a resource. This is a verb-based command that delegates to the appropriate noun command.", + Example: ` # Delete a backup + kubectl oadp delete backup my-backup + + # Delete a restore + kubectl oadp delete restore my-restore`, + }) +} + +// NewDescribeCommand creates the "describe" verb command that delegates to noun commands +func NewDescribeCommand(veleroFactory, nonAdminFactory client.Factory) *cobra.Command { + builder := NewVerbBuilder(veleroFactory, nonAdminFactory) + RegisterBackupResources(builder, "describe") + RegisterRestoreResources(builder, "describe") + RegisterScheduleResources(builder, "describe") + + return builder.BuildVerbCommand(VerbConfig{ + Use: "describe", + Short: "Describe a resource", + Long: "Describe a resource. This is a verb-based command that delegates to the appropriate noun command.", + Example: ` # Describe a backup + kubectl oadp describe backup my-backup + + # Describe a restore + kubectl oadp describe restore my-restore`, + }) +} + +// NewLogsCommand creates the "logs" verb command that delegates to noun commands +// This is an example of how easy it is to add new verbs! +func NewLogsCommand(veleroFactory, nonAdminFactory client.Factory) *cobra.Command { + builder := NewVerbBuilder(veleroFactory, nonAdminFactory) + RegisterBackupResources(builder, "logs") + RegisterRestoreResources(builder, "logs") + RegisterScheduleResources(builder, "logs") + + return builder.BuildVerbCommand(VerbConfig{ + Use: "logs", + Short: "Get logs for a resource", + Long: "Get logs for a resource. This is a verb-based command that delegates to the appropriate noun command.", + Example: ` # Get logs for a backup + kubectl oadp logs backup my-backup + + # Get logs for a restore + kubectl oadp logs restore my-restore + + # Get logs for a schedule + kubectl oadp logs schedule my-schedule`, + }) +}