diff --git a/pkg/cli/plugin_cmd.go b/pkg/cli/plugin_cmd.go index 32507c46a..9e53a804e 100644 --- a/pkg/cli/plugin_cmd.go +++ b/pkg/cli/plugin_cmd.go @@ -198,6 +198,10 @@ func getCmdForPluginEx(p *PluginInfo, cmdName string, mapEntry *plugin.CommandMa "scope": p.Scope, "type": common.CommandTypePlugin, "pluginInstallationPath": p.InstallationPath, + // Telemetry uses the below annotation to identify the source + // of the command within the plugin so that it can determine how + // to invoke this command directly from the plugin binary. + common.AnnotationForCmdSrcPath: strings.Join(srcHierarchy, " "), }, Hidden: hidden, Aliases: aliases, diff --git a/pkg/cli/plugin_cmd_test.go b/pkg/cli/plugin_cmd_test.go index 5cbb466c7..308883222 100644 --- a/pkg/cli/plugin_cmd_test.go +++ b/pkg/cli/plugin_cmd_test.go @@ -12,6 +12,7 @@ import ( "github.com/stretchr/testify/assert" + "github.com/vmware-tanzu/tanzu-cli/pkg/common" "github.com/vmware-tanzu/tanzu-plugin-runtime/plugin" ) @@ -43,10 +44,72 @@ func TestGetCmdForPlugin(t *testing.T) { cmd := GetCmdForPlugin(pi) err = cmd.Execute() - assert.Equal(cmd.Name(), pi.Name) - assert.Equal(cmd.Short, pi.Description) - assert.Equal(cmd.Aliases, pi.Aliases) assert.Nil(err) + + assert.Equal(pi.Name, cmd.Name()) + assert.Equal(pi.Description, cmd.Short) + assert.Equal(pi.Aliases, cmd.Aliases) + + annotations := cmd.Annotations + assert.Equal(5, len(annotations)) + assert.Equal(string(pi.Group), annotations["group"]) + assert.Equal(pi.Scope, annotations["scope"]) + assert.Equal(common.CommandTypePlugin, annotations["type"]) + assert.Equal(pi.InstallationPath, annotations["pluginInstallationPath"]) + // No remapping in this test + assert.Equal("", annotations[common.AnnotationForCmdSrcPath]) +} + +func TestGetCmdForRemappedPlugin(t *testing.T) { + assert := assert.New(t) + + dir, err := os.MkdirTemp("", "tanzu-cli-getcmd") + assert.Nil(err) + defer os.RemoveAll(dir) + + path, err := setupFakePlugin(dir, "fakefoo", "") + assert.Nil(err) + + const ( + originalCmdName = "fakefoo" + renamedCmdName = "fakefoo2" + ) + + pi := &PluginInfo{ + Name: originalCmdName, + Description: "Fake foo", + Group: plugin.SystemCmdGroup, + Aliases: []string{"ff"}, + InstallationPath: path, + Hidden: true, + } + + remapping := &plugin.CommandMapEntry{ + SourceCommandPath: originalCmdName, + DestinationCommandPath: renamedCmdName, + Description: "Other desc", + Aliases: []string{"ff2"}, + } + + cmd := getCmdForPluginEx(pi, renamedCmdName, remapping) + + err = cmd.Execute() + assert.Nil(err) + + assert.Equal(renamedCmdName, cmd.Name()) + assert.Equal(remapping.Description, cmd.Short) + assert.Equal(remapping.Aliases, cmd.Aliases) + // A remapped command should not be hidden even if the original command is hidden + assert.False(cmd.Hidden) + + annotations := cmd.Annotations + assert.Equal(5, len(annotations)) + assert.Equal(string(pi.Group), annotations["group"]) + assert.Equal(pi.Scope, annotations["scope"]) + assert.Equal(common.CommandTypePlugin, annotations["type"]) + assert.Equal(pi.InstallationPath, annotations["pluginInstallationPath"]) + // We should see the remapped command name in the annotations + assert.Equal(remapping.SourceCommandPath, annotations[common.AnnotationForCmdSrcPath]) } func TestEnvForPlugin(t *testing.T) { diff --git a/pkg/common/constants.go b/pkg/common/constants.go index f4da82461..2d2e9d898 100644 --- a/pkg/common/constants.go +++ b/pkg/common/constants.go @@ -37,3 +37,8 @@ const CoreName = "core" // CommandTypePlugin represents the command type is plugin const CommandTypePlugin = "plugin" + +// Command Annotations +const ( + AnnotationForCmdSrcPath = "cmdSrcPath" +) diff --git a/pkg/fakes/plugin_cmd_tree_cache_fake.go b/pkg/fakes/plugin_cmd_tree_cache_fake.go index ef322df44..d8950fae9 100644 --- a/pkg/fakes/plugin_cmd_tree_cache_fake.go +++ b/pkg/fakes/plugin_cmd_tree_cache_fake.go @@ -4,26 +4,27 @@ package fakes import ( "sync" + "github.com/spf13/cobra" + "github.com/vmware-tanzu/tanzu-cli/pkg/cli" "github.com/vmware-tanzu/tanzu-cli/pkg/plugincmdtree" ) type CommandTreeCache struct { - ConstructAndAddTreeStub func(*cli.PluginInfo) error - constructAndAddTreeMutex sync.RWMutex - constructAndAddTreeArgsForCall []struct { + DeletePluginTreeStub func(*cli.PluginInfo) error + deletePluginTreeMutex sync.RWMutex + deletePluginTreeArgsForCall []struct { arg1 *cli.PluginInfo } - constructAndAddTreeReturns struct { + deletePluginTreeReturns struct { result1 error } - constructAndAddTreeReturnsOnCall map[int]struct { + deletePluginTreeReturnsOnCall map[int]struct { result1 error } - DeleteTreeStub func(*cli.PluginInfo) error + DeleteTreeStub func() error deleteTreeMutex sync.RWMutex deleteTreeArgsForCall []struct { - arg1 *cli.PluginInfo } deleteTreeReturns struct { result1 error @@ -31,16 +32,17 @@ type CommandTreeCache struct { deleteTreeReturnsOnCall map[int]struct { result1 error } - GetTreeStub func(*cli.PluginInfo) (*plugincmdtree.CommandNode, error) - getTreeMutex sync.RWMutex - getTreeArgsForCall []struct { - arg1 *cli.PluginInfo + GetPluginTreeStub func(*cobra.Command, *cli.PluginInfo) (*plugincmdtree.CommandNode, error) + getPluginTreeMutex sync.RWMutex + getPluginTreeArgsForCall []struct { + arg1 *cobra.Command + arg2 *cli.PluginInfo } - getTreeReturns struct { + getPluginTreeReturns struct { result1 *plugincmdtree.CommandNode result2 error } - getTreeReturnsOnCall map[int]struct { + getPluginTreeReturnsOnCall map[int]struct { result1 *plugincmdtree.CommandNode result2 error } @@ -48,16 +50,16 @@ type CommandTreeCache struct { invocationsMutex sync.RWMutex } -func (fake *CommandTreeCache) ConstructAndAddTree(arg1 *cli.PluginInfo) error { - fake.constructAndAddTreeMutex.Lock() - ret, specificReturn := fake.constructAndAddTreeReturnsOnCall[len(fake.constructAndAddTreeArgsForCall)] - fake.constructAndAddTreeArgsForCall = append(fake.constructAndAddTreeArgsForCall, struct { +func (fake *CommandTreeCache) DeletePluginTree(arg1 *cli.PluginInfo) error { + fake.deletePluginTreeMutex.Lock() + ret, specificReturn := fake.deletePluginTreeReturnsOnCall[len(fake.deletePluginTreeArgsForCall)] + fake.deletePluginTreeArgsForCall = append(fake.deletePluginTreeArgsForCall, struct { arg1 *cli.PluginInfo }{arg1}) - stub := fake.ConstructAndAddTreeStub - fakeReturns := fake.constructAndAddTreeReturns - fake.recordInvocation("ConstructAndAddTree", []interface{}{arg1}) - fake.constructAndAddTreeMutex.Unlock() + stub := fake.DeletePluginTreeStub + fakeReturns := fake.deletePluginTreeReturns + fake.recordInvocation("DeletePluginTree", []interface{}{arg1}) + fake.deletePluginTreeMutex.Unlock() if stub != nil { return stub(arg1) } @@ -67,60 +69,59 @@ func (fake *CommandTreeCache) ConstructAndAddTree(arg1 *cli.PluginInfo) error { return fakeReturns.result1 } -func (fake *CommandTreeCache) ConstructAndAddTreeCallCount() int { - fake.constructAndAddTreeMutex.RLock() - defer fake.constructAndAddTreeMutex.RUnlock() - return len(fake.constructAndAddTreeArgsForCall) +func (fake *CommandTreeCache) DeletePluginTreeCallCount() int { + fake.deletePluginTreeMutex.RLock() + defer fake.deletePluginTreeMutex.RUnlock() + return len(fake.deletePluginTreeArgsForCall) } -func (fake *CommandTreeCache) ConstructAndAddTreeCalls(stub func(*cli.PluginInfo) error) { - fake.constructAndAddTreeMutex.Lock() - defer fake.constructAndAddTreeMutex.Unlock() - fake.ConstructAndAddTreeStub = stub +func (fake *CommandTreeCache) DeletePluginTreeCalls(stub func(*cli.PluginInfo) error) { + fake.deletePluginTreeMutex.Lock() + defer fake.deletePluginTreeMutex.Unlock() + fake.DeletePluginTreeStub = stub } -func (fake *CommandTreeCache) ConstructAndAddTreeArgsForCall(i int) *cli.PluginInfo { - fake.constructAndAddTreeMutex.RLock() - defer fake.constructAndAddTreeMutex.RUnlock() - argsForCall := fake.constructAndAddTreeArgsForCall[i] +func (fake *CommandTreeCache) DeletePluginTreeArgsForCall(i int) *cli.PluginInfo { + fake.deletePluginTreeMutex.RLock() + defer fake.deletePluginTreeMutex.RUnlock() + argsForCall := fake.deletePluginTreeArgsForCall[i] return argsForCall.arg1 } -func (fake *CommandTreeCache) ConstructAndAddTreeReturns(result1 error) { - fake.constructAndAddTreeMutex.Lock() - defer fake.constructAndAddTreeMutex.Unlock() - fake.ConstructAndAddTreeStub = nil - fake.constructAndAddTreeReturns = struct { +func (fake *CommandTreeCache) DeletePluginTreeReturns(result1 error) { + fake.deletePluginTreeMutex.Lock() + defer fake.deletePluginTreeMutex.Unlock() + fake.DeletePluginTreeStub = nil + fake.deletePluginTreeReturns = struct { result1 error }{result1} } -func (fake *CommandTreeCache) ConstructAndAddTreeReturnsOnCall(i int, result1 error) { - fake.constructAndAddTreeMutex.Lock() - defer fake.constructAndAddTreeMutex.Unlock() - fake.ConstructAndAddTreeStub = nil - if fake.constructAndAddTreeReturnsOnCall == nil { - fake.constructAndAddTreeReturnsOnCall = make(map[int]struct { +func (fake *CommandTreeCache) DeletePluginTreeReturnsOnCall(i int, result1 error) { + fake.deletePluginTreeMutex.Lock() + defer fake.deletePluginTreeMutex.Unlock() + fake.DeletePluginTreeStub = nil + if fake.deletePluginTreeReturnsOnCall == nil { + fake.deletePluginTreeReturnsOnCall = make(map[int]struct { result1 error }) } - fake.constructAndAddTreeReturnsOnCall[i] = struct { + fake.deletePluginTreeReturnsOnCall[i] = struct { result1 error }{result1} } -func (fake *CommandTreeCache) DeleteTree(arg1 *cli.PluginInfo) error { +func (fake *CommandTreeCache) DeleteTree() error { fake.deleteTreeMutex.Lock() ret, specificReturn := fake.deleteTreeReturnsOnCall[len(fake.deleteTreeArgsForCall)] fake.deleteTreeArgsForCall = append(fake.deleteTreeArgsForCall, struct { - arg1 *cli.PluginInfo - }{arg1}) + }{}) stub := fake.DeleteTreeStub fakeReturns := fake.deleteTreeReturns - fake.recordInvocation("DeleteTree", []interface{}{arg1}) + fake.recordInvocation("DeleteTree", []interface{}{}) fake.deleteTreeMutex.Unlock() if stub != nil { - return stub(arg1) + return stub() } if specificReturn { return ret.result1 @@ -134,19 +135,12 @@ func (fake *CommandTreeCache) DeleteTreeCallCount() int { return len(fake.deleteTreeArgsForCall) } -func (fake *CommandTreeCache) DeleteTreeCalls(stub func(*cli.PluginInfo) error) { +func (fake *CommandTreeCache) DeleteTreeCalls(stub func() error) { fake.deleteTreeMutex.Lock() defer fake.deleteTreeMutex.Unlock() fake.DeleteTreeStub = stub } -func (fake *CommandTreeCache) DeleteTreeArgsForCall(i int) *cli.PluginInfo { - fake.deleteTreeMutex.RLock() - defer fake.deleteTreeMutex.RUnlock() - argsForCall := fake.deleteTreeArgsForCall[i] - return argsForCall.arg1 -} - func (fake *CommandTreeCache) DeleteTreeReturns(result1 error) { fake.deleteTreeMutex.Lock() defer fake.deleteTreeMutex.Unlock() @@ -170,18 +164,19 @@ func (fake *CommandTreeCache) DeleteTreeReturnsOnCall(i int, result1 error) { }{result1} } -func (fake *CommandTreeCache) GetTree(arg1 *cli.PluginInfo) (*plugincmdtree.CommandNode, error) { - fake.getTreeMutex.Lock() - ret, specificReturn := fake.getTreeReturnsOnCall[len(fake.getTreeArgsForCall)] - fake.getTreeArgsForCall = append(fake.getTreeArgsForCall, struct { - arg1 *cli.PluginInfo - }{arg1}) - stub := fake.GetTreeStub - fakeReturns := fake.getTreeReturns - fake.recordInvocation("GetTree", []interface{}{arg1}) - fake.getTreeMutex.Unlock() +func (fake *CommandTreeCache) GetPluginTree(arg1 *cobra.Command, arg2 *cli.PluginInfo) (*plugincmdtree.CommandNode, error) { + fake.getPluginTreeMutex.Lock() + ret, specificReturn := fake.getPluginTreeReturnsOnCall[len(fake.getPluginTreeArgsForCall)] + fake.getPluginTreeArgsForCall = append(fake.getPluginTreeArgsForCall, struct { + arg1 *cobra.Command + arg2 *cli.PluginInfo + }{arg1, arg2}) + stub := fake.GetPluginTreeStub + fakeReturns := fake.getPluginTreeReturns + fake.recordInvocation("GetPluginTree", []interface{}{arg1, arg2}) + fake.getPluginTreeMutex.Unlock() if stub != nil { - return stub(arg1) + return stub(arg1, arg2) } if specificReturn { return ret.result1, ret.result2 @@ -189,46 +184,46 @@ func (fake *CommandTreeCache) GetTree(arg1 *cli.PluginInfo) (*plugincmdtree.Comm return fakeReturns.result1, fakeReturns.result2 } -func (fake *CommandTreeCache) GetTreeCallCount() int { - fake.getTreeMutex.RLock() - defer fake.getTreeMutex.RUnlock() - return len(fake.getTreeArgsForCall) +func (fake *CommandTreeCache) GetPluginTreeCallCount() int { + fake.getPluginTreeMutex.RLock() + defer fake.getPluginTreeMutex.RUnlock() + return len(fake.getPluginTreeArgsForCall) } -func (fake *CommandTreeCache) GetTreeCalls(stub func(*cli.PluginInfo) (*plugincmdtree.CommandNode, error)) { - fake.getTreeMutex.Lock() - defer fake.getTreeMutex.Unlock() - fake.GetTreeStub = stub +func (fake *CommandTreeCache) GetPluginTreeCalls(stub func(*cobra.Command, *cli.PluginInfo) (*plugincmdtree.CommandNode, error)) { + fake.getPluginTreeMutex.Lock() + defer fake.getPluginTreeMutex.Unlock() + fake.GetPluginTreeStub = stub } -func (fake *CommandTreeCache) GetTreeArgsForCall(i int) *cli.PluginInfo { - fake.getTreeMutex.RLock() - defer fake.getTreeMutex.RUnlock() - argsForCall := fake.getTreeArgsForCall[i] - return argsForCall.arg1 +func (fake *CommandTreeCache) GetPluginTreeArgsForCall(i int) (*cobra.Command, *cli.PluginInfo) { + fake.getPluginTreeMutex.RLock() + defer fake.getPluginTreeMutex.RUnlock() + argsForCall := fake.getPluginTreeArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 } -func (fake *CommandTreeCache) GetTreeReturns(result1 *plugincmdtree.CommandNode, result2 error) { - fake.getTreeMutex.Lock() - defer fake.getTreeMutex.Unlock() - fake.GetTreeStub = nil - fake.getTreeReturns = struct { +func (fake *CommandTreeCache) GetPluginTreeReturns(result1 *plugincmdtree.CommandNode, result2 error) { + fake.getPluginTreeMutex.Lock() + defer fake.getPluginTreeMutex.Unlock() + fake.GetPluginTreeStub = nil + fake.getPluginTreeReturns = struct { result1 *plugincmdtree.CommandNode result2 error }{result1, result2} } -func (fake *CommandTreeCache) GetTreeReturnsOnCall(i int, result1 *plugincmdtree.CommandNode, result2 error) { - fake.getTreeMutex.Lock() - defer fake.getTreeMutex.Unlock() - fake.GetTreeStub = nil - if fake.getTreeReturnsOnCall == nil { - fake.getTreeReturnsOnCall = make(map[int]struct { +func (fake *CommandTreeCache) GetPluginTreeReturnsOnCall(i int, result1 *plugincmdtree.CommandNode, result2 error) { + fake.getPluginTreeMutex.Lock() + defer fake.getPluginTreeMutex.Unlock() + fake.GetPluginTreeStub = nil + if fake.getPluginTreeReturnsOnCall == nil { + fake.getPluginTreeReturnsOnCall = make(map[int]struct { result1 *plugincmdtree.CommandNode result2 error }) } - fake.getTreeReturnsOnCall[i] = struct { + fake.getPluginTreeReturnsOnCall[i] = struct { result1 *plugincmdtree.CommandNode result2 error }{result1, result2} @@ -237,12 +232,12 @@ func (fake *CommandTreeCache) GetTreeReturnsOnCall(i int, result1 *plugincmdtree func (fake *CommandTreeCache) Invocations() map[string][][]interface{} { fake.invocationsMutex.RLock() defer fake.invocationsMutex.RUnlock() - fake.constructAndAddTreeMutex.RLock() - defer fake.constructAndAddTreeMutex.RUnlock() + fake.deletePluginTreeMutex.RLock() + defer fake.deletePluginTreeMutex.RUnlock() fake.deleteTreeMutex.RLock() defer fake.deleteTreeMutex.RUnlock() - fake.getTreeMutex.RLock() - defer fake.getTreeMutex.RUnlock() + fake.getPluginTreeMutex.RLock() + defer fake.getPluginTreeMutex.RUnlock() copiedInvocations := map[string][][]interface{}{} for key, value := range fake.invocations { copiedInvocations[key] = value diff --git a/pkg/plugincmdtree/cache.go b/pkg/plugincmdtree/cache.go index 462a7e498..dd4bdfdae 100644 --- a/pkg/plugincmdtree/cache.go +++ b/pkg/plugincmdtree/cache.go @@ -4,22 +4,25 @@ // Package plugincmdtree provides functionality for constructing and maintaining the plugin command trees package plugincmdtree -import "github.com/vmware-tanzu/tanzu-cli/pkg/cli" +import ( + "github.com/spf13/cobra" + + "github.com/vmware-tanzu/tanzu-cli/pkg/cli" +) // Cache is the local cache for storing and accessing // command trees of different plugins // //go:generate counterfeiter -o ../fakes/plugin_cmd_tree_cache_fake.go --fake-name CommandTreeCache . Cache type Cache interface { - // GetTree returns the plugin command tree + // GetPluginTree returns the plugin command tree // If the plugin command tree doesn't exist, it constructs and adds the command tree to the cache - // and then returns the plugin command tree, otherwise it returns error - GetTree(plugin *cli.PluginInfo) (*CommandNode, error) - // ConstructAndAddTree constructs and adds the plugin command tree to the cache - // If the plugin command tree already exists, it returns success immediately - ConstructAndAddTree(plugin *cli.PluginInfo) error - // DeleteTree deletes the plugin command tree from the cache - DeleteTree(plugin *cli.PluginInfo) error + // and then returns the plugin command tree, otherwise it returns an error + GetPluginTree(rootCmd *cobra.Command, plugin *cli.PluginInfo) (*CommandNode, error) + // DeletePluginTree deletes the plugin command tree from the cache + DeletePluginTree(plugin *cli.PluginInfo) error + // DeleteTree deletes the entire command tree from the cache + DeleteTree() error } type CommandNode struct { diff --git a/pkg/plugincmdtree/plugins_cache.go b/pkg/plugincmdtree/plugins_cache.go index 4a78d194c..146c7331b 100644 --- a/pkg/plugincmdtree/plugins_cache.go +++ b/pkg/plugincmdtree/plugins_cache.go @@ -12,9 +12,13 @@ import ( "strings" "github.com/pkg/errors" + "github.com/spf13/cobra" "golang.org/x/sync/errgroup" "gopkg.in/yaml.v3" + "github.com/vmware-tanzu/tanzu-plugin-runtime/config/types" + "github.com/vmware-tanzu/tanzu-plugin-runtime/log" + "github.com/vmware-tanzu/tanzu-cli/pkg/cli" "github.com/vmware-tanzu/tanzu-cli/pkg/common" ) @@ -34,7 +38,7 @@ func getPluginsCommandTreeCacheDir() string { return filepath.Join(common.DefaultCacheDir, pluginsCommandTreeDir) } func GetPluginsCommandTreeCachePath() string { - return filepath.Join(getPluginsCommandTreeCacheDir(), "command_tree.yaml") + return filepath.Join(getPluginsCommandTreeCacheDir(), "command_tree_v2.yaml") } func getPluginsDocsCachePath() string { @@ -46,6 +50,8 @@ type cacheImpl struct { pluginDocsGenerator func(plugin *cli.PluginInfo) error } +var _ Cache = &cacheImpl{} + // NewCache create a cache for plugin command tree func NewCache() (Cache, error) { pct, err := getPluginCommandTree() @@ -65,33 +71,32 @@ func NewCache() (Cache, error) { }, nil } -func (c *cacheImpl) GetTree(plugin *cli.PluginInfo) (*CommandNode, error) { - // This is just a safety net to generate the command tree if we missed/failed to generate the plugin command tree during plugin install - // If the plugin command tree exists, then ConstructAndAddTree is a no-op - if err := c.ConstructAndAddTree(plugin); err != nil { +func (c *cacheImpl) GetPluginTree(rootCmd *cobra.Command, plugin *cli.PluginInfo) (*CommandNode, error) { + // If the tree does not already exist, we construct it, if it does exist constructAndAddTree is a no-op + if err := c.constructAndAddTree(rootCmd, plugin); err != nil { return nil, err } pluginCmdTree, exists := c.pluginCommands.CommandTree[plugin.InstallationPath] if !exists { - return nil, fmt.Errorf("failed to get the command tree for plugin '%v:%v' with target %v installled at %v", plugin.Name, plugin.Version, plugin.Target, plugin.InstallationPath) + return nil, fmt.Errorf("failed to get the command tree for plugin '%v:%v' with target %v installed at %v", plugin.Name, plugin.Version, plugin.Target, plugin.InstallationPath) } return pluginCmdTree, nil } -// ConstructAndAddTree uses the 'generate_docs' (default command that plugins support) to get the complete command chains supported. +// constructAndAddTree uses the 'generate_docs' (default command that plugins support) to get the complete command chains supported. // However, the plugin docs generated doesn't provide the information regarding the aliases of the command/sub-commands. -// So, it would use the help command for each sub-command to extract the aliases supported and finally construct -// the plugin command tree and adds it to cache so that CLI can extract the command chain by parsing the user input +// So, this function uses the help command for each sub-command to extract the aliases supported and finally constructs +// the plugin command tree and adds it to cache so that the CLI can extract the command chain by parsing the user input // against the plugin command tree. -func (c *cacheImpl) ConstructAndAddTree(plugin *cli.PluginInfo) error { +func (c *cacheImpl) constructAndAddTree(rootCmd *cobra.Command, plugin *cli.PluginInfo) error { _, exists := c.pluginCommands.CommandTree[plugin.InstallationPath] if exists { return nil } - pluginCmdTree, err := c.constructPluginCommandTree(plugin) + pluginCmdTree, err := c.constructPluginCommandTree(rootCmd, plugin) if err != nil { return errors.Wrapf(err, "failed to generate command tree for plugin %q", plugin.Name) } @@ -99,7 +104,8 @@ func (c *cacheImpl) ConstructAndAddTree(plugin *cli.PluginInfo) error { return c.savePluginCommandTree() } -func (c *cacheImpl) DeleteTree(plugin *cli.PluginInfo) error { + +func (c *cacheImpl) DeletePluginTree(plugin *cli.PluginInfo) error { _, exists := c.pluginCommands.CommandTree[plugin.InstallationPath] if !exists { return nil @@ -110,6 +116,11 @@ func (c *cacheImpl) DeleteTree(plugin *cli.PluginInfo) error { return c.savePluginCommandTree() } +func (c *cacheImpl) DeleteTree() error { + c.pluginCommands.CommandTree = make(map[string]*CommandNode) + return os.RemoveAll(GetPluginsCommandTreeCachePath()) +} + func (c *cacheImpl) savePluginCommandTree() error { data, err := yaml.Marshal(c.pluginCommands) if err != nil { @@ -122,7 +133,8 @@ func (c *cacheImpl) savePluginCommandTree() error { return nil } -func (c *cacheImpl) constructPluginCommandTree(plugin *cli.PluginInfo) (*CommandNode, error) { +//nolint:gocyclo // This function is complex +func (c *cacheImpl) constructPluginCommandTree(rootCmd *cobra.Command, plugin *cli.PluginInfo) (*CommandNode, error) { if err := c.pluginDocsGenerator(plugin); err != nil { return nil, errors.Wrapf(err, "failed to generate docs for the plugin %q", plugin.Name) } @@ -134,7 +146,14 @@ func (c *cacheImpl) constructPluginCommandTree(plugin *cli.PluginInfo) (*Command if err != nil { return nil, errors.Wrapf(err, "error while reading local plugin command tree directory") } + var aliasErrGroup errgroup.Group + numTargets := 1 + if plugin.Target == types.TargetK8s { + // For k8s plugin, we need to generate the command tree for both the k8s level and the root level + numTargets = 2 + } + for _, file := range files { if file.IsDir() { continue @@ -145,33 +164,62 @@ func (c *cacheImpl) constructPluginCommandTree(plugin *cli.PluginInfo) (*Command continue } - filename := strings.TrimSuffix(file.Name(), ".md") - cmdNames := strings.Split(filename, "_") - - var aliasArgs []string - current := cmdTreeRoot - for _, cmdName := range cmdNames { - if current.Subcommands[cmdName] == nil { - current.Subcommands[cmdName] = NewCommandNode() + // Loop a second time for k8s targets since they are both at the root + // level and under the k8s target + for i := 0; i < numTargets; i++ { + filename := strings.TrimSuffix(file.Name(), ".md") + cmdNames := strings.Split(filename, "_") + if i == 0 { + // Only add the target when on the first loop. + // If there is a second loop, it is for the root level of the k8s target + cmdNames = adjustCmdNamesForPluginTarget(cmdNames, plugin) } - current = current.Subcommands[cmdName] - if cmdName != "tanzu" && cmdName != plugin.Name { - aliasArgs = append(aliasArgs, cmdName) - aliasArgsCopy := make([]string, len(aliasArgs)) - copy(aliasArgsCopy, aliasArgs) - currentCopy := current - if !currentCopy.AliasProcessed { - // kickoff the goroutine to add the alias to the command - aliasErrGroup.Go(func() error { - cmdAlias, aliasErr := getPluginCommandAlias(plugin, aliasArgsCopy) - if aliasErr != nil { - return aliasErr + var aliasArgs []string + current := cmdTreeRoot + for _, cmdName := range cmdNames { + if current.Subcommands[cmdName] == nil { + current.Subcommands[cmdName] = NewCommandNode() + } + + current = current.Subcommands[cmdName] + if cmdName != "tanzu" { + // The aliasArgs are used to construct the command we will use to get the help text + // so we can extract the aliases of command. + aliasArgs = append(aliasArgs, cmdName) + + if cmdName == string(plugin.Target) { + if !current.AliasProcessed { + current.Aliases = getTargetAliases(plugin.Target) + current.AliasProcessed = true + } + continue + } + + if !current.AliasProcessed { + // kickoff the goroutine to add the alias to the command + + // Find the command that the CLI has created + // so that we can read its annotations. + cmd, _, err := rootCmd.Find(aliasArgs) + if err != nil { + return nil, err } - currentCopy.Aliases = cmdAlias - return nil - }) - currentCopy.AliasProcessed = true + + aliasArgsCopy := make([]string, len(aliasArgs)) + copy(aliasArgsCopy, aliasArgs) + currentCopy := current + + aliasErrGroup.Go(func() error { + cmdAlias, aliasErr := getPluginCommandAlias(plugin, cmd, aliasArgsCopy) + if aliasErr != nil { + return aliasErr + } + currentCopy.Aliases = cmdAlias + return nil + }) + currentCopy.AliasProcessed = true + } } } } @@ -180,13 +228,78 @@ func (c *cacheImpl) constructPluginCommandTree(plugin *cli.PluginInfo) (*Command if err := aliasErrGroup.Wait(); err != nil { return nil, errors.Wrap(err, "failed to generate command alias") } - if cmdTreeRoot.Subcommands["tanzu"] != nil && cmdTreeRoot.Subcommands["tanzu"].Subcommands[plugin.Name] != nil { - return cmdTreeRoot.Subcommands["tanzu"].Subcommands[plugin.Name], nil + if cmdTreeRoot.Subcommands["tanzu"] != nil { + return cmdTreeRoot.Subcommands["tanzu"], nil } return nil, nil } +func getTargetAliases(target types.Target) map[string]struct{} { + switch target { + case types.TargetK8s: + return map[string]struct{}{ + "k8s": {}, + "kubernetes": {}, + } + case types.TargetTMC: + return map[string]struct{}{ + "tmc": {}, + "mission-control": {}, + } + case types.TargetOperations: + return map[string]struct{}{ + "ops": {}, + "operations": {}, + } + default: + log.V(5).Warning("Unexpected target", target) + return nil + } +} + +// adjustCmdNamesForPluginTarget adjusts the command names to insert the plugin target +// when appropriate. The cmdNames parameter is the list of command names that were +// extracted from one of the generated docs file; it does not contain the target yet. +func adjustCmdNamesForPluginTarget(cmdNames []string, plugin *cli.PluginInfo) []string { + // Just the "tanzu" command + if len(cmdNames) < 2 { + return cmdNames + } + + // No changes required since the global target means the plugin + // is directly at the root level + if plugin.Target == types.TargetGlobal { + return cmdNames + } + + // For remapped commands, we don't add the target + for _, cmdMap := range plugin.CommandMap { + // If the cmdNames (excluding the "tanzu" command) starts with the destination command path + // it means we are dealing with a remapped command and we should not add the target + index := 1 // Start at 1 to skip the "tanzu" command + isRemapped := true + for _, destCmd := range strings.Split(cmdMap.DestinationCommandPath, " ") { + if strings.TrimSpace(destCmd) == "" { + continue + } + + if len(cmdNames) < index+1 || cmdNames[index] != destCmd { + // Not a remapped command + isRemapped = false + break + } + index++ + } + if isRemapped { + return cmdNames + } + } + + // Insert the target as the second element (after "tanzu") in the command names + return append([]string{cmdNames[0], string(plugin.Target)}, cmdNames[1:]...) +} + func getPluginCommandTree() (*pluginCommandTree, error) { b, err := os.ReadFile(GetPluginsCommandTreeCachePath()) if err != nil { @@ -219,11 +332,31 @@ func generatePluginDocs(plugin *cli.PluginInfo) error { return nil } -func getPluginCommandAlias(plugin *cli.PluginInfo, aliasArgs []string) (map[string]struct{}, error) { +func getPluginCommandAlias(plugin *cli.PluginInfo, cmd *cobra.Command, aliasArgs []string) (map[string]struct{}, error) { + // Drop the the target if there is one since it is not needed when calling the plugin directly + if len(aliasArgs) > 0 && types.IsValidTarget(aliasArgs[0], false, false) { + aliasArgs = aliasArgs[1:] + } + + // Drop the next element of aliasArgs since it is either: + // - the plugin name, which is not needed when calling the plugin directly + // - the remapped command name, which will be added back through the command source path annotation + if len(aliasArgs) > 0 { + aliasArgs = aliasArgs[1:] + } + + // Handle any remapped commands by adding the command source path before + // calling the plugin directly. + cmdSrcPath := cmd.Annotations[common.AnnotationForCmdSrcPath] + if cmdSrcPath != "" { + aliasArgs = append(strings.Split(cmdSrcPath, " "), aliasArgs...) + } aliasArgs = append(aliasArgs, "-h") + runner := cli.NewRunner(plugin.Name, plugin.InstallationPath, aliasArgs) ctx := context.Background() stdout, _, err := runner.RunOutput(ctx) + if err != nil { return nil, err } diff --git a/pkg/plugincmdtree/plugins_cache_test.go b/pkg/plugincmdtree/plugins_cache_test.go index feee80088..ebcfd591d 100644 --- a/pkg/plugincmdtree/plugins_cache_test.go +++ b/pkg/plugincmdtree/plugins_cache_test.go @@ -11,10 +11,13 @@ import ( "gopkg.in/yaml.v3" + "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/vmware-tanzu/tanzu-cli/pkg/cli" + "github.com/vmware-tanzu/tanzu-cli/pkg/common" configtypes "github.com/vmware-tanzu/tanzu-plugin-runtime/config/types" + plugintypes "github.com/vmware-tanzu/tanzu-plugin-runtime/plugin" ) const samplePluginToGenerateAliasWithHelpCommand string = `#!/bin/bash @@ -24,7 +27,7 @@ const samplePluginToGenerateAliasWithHelpCommand string = `#!/bin/bash if [ "$1" = "-h" ]; then echo "fake plugin help" echo "Aliases:" - echo " sp" + echo " %[1]s, %[2]s" elif [ "$1" = "foo1" ] && [ "$2" = "-h" ]; then echo "fake foo1 command with aliases" echo "Aliases:" @@ -35,38 +38,233 @@ elif [ "$1" = "foo1" ] && [ "$2" = "foo2" ] && [ "$3" = "-h" ]; then echo "fake foo2 command with aliases" echo "Aliases:" echo " foo2, f2" +elif [ "$1" = "cluster" ] && [ "$2" = "-h" ]; then + echo "fake cluster command with aliases" + echo "Aliases:" + echo " cluster, cl" else echo "Invalid command." fi ` -const expectedPluginCmdTree string = ` -commandTree: +const remappedCmdName = "cluster" + +var pluginName = map[configtypes.Target]string{ + configtypes.TargetGlobal: "cluster-plugin-global", + configtypes.TargetK8s: "cluster-plugin-k8s", + configtypes.TargetOperations: "cluster-plugin-ops", // Use the remapped command as a prefix to make sure it doesn't affect the command tree +} + +var pluginAlias = map[configtypes.Target]string{ + configtypes.TargetGlobal: "pg", + configtypes.TargetK8s: "pk", + configtypes.TargetOperations: "po", +} + +var expectedPluginTree = map[configtypes.Target]string{ + configtypes.TargetGlobal: ` ? %s : subcommands: - bar1: + cluster-plugin-global: + subcommands: + bar1: + subcommands: {} + aliases: {} + foo1: + subcommands: + foo2: + subcommands: {} + aliases: + f2: {} + foo2: {} + aliases: + f1: {} + foo1: {} + aliases: + pg: {} + cluster-plugin-global: {} + cluster: subcommands: {} - aliases: {} - foo1: + aliases: + cl: {} + cluster: {} + aliases: {} +`, + configtypes.TargetK8s: ` + ? %s + : subcommands: + cluster-plugin-k8s: subcommands: - foo2: + bar1: subcommands: {} + aliases: {} + foo1: + subcommands: + foo2: + subcommands: {} + aliases: + f2: {} + foo2: {} + aliases: + f1: {} + foo1: {} + aliases: + pk: {} + cluster-plugin-k8s: {} + kubernetes: + subcommands: + cluster-plugin-k8s: + subcommands: + bar1: + subcommands: {} + aliases: {} + foo1: + subcommands: + foo2: + subcommands: {} + aliases: + f2: {} + foo2: {} + aliases: + f1: {} + foo1: {} aliases: - f2: {} - foo2: {} + pk: {} + cluster-plugin-k8s: {} + aliases: + k8s: {} + kubernetes: {} + cluster: + subcommands: {} aliases: - f1: {} - foo1: {} + cl: {} + cluster: {} aliases: {} -` +`, + configtypes.TargetOperations: ` + ? %s + : subcommands: + operations: + subcommands: + cluster-plugin-ops: + subcommands: + bar1: + subcommands: {} + aliases: {} + foo1: + subcommands: + foo2: + subcommands: {} + aliases: + f2: {} + foo2: {} + aliases: + f1: {} + foo1: {} + aliases: + po: {} + cluster-plugin-ops: {} + aliases: + ops: {} + operations: {} + cluster: + subcommands: {} + aliases: + cl: {} + cluster: {} + aliases: {} +`, +} + +// Create a test root command which contains different plugin commands we want to test: +// - tanzu +// - tanzu cluster-plugin-global +// - tanzu cluster-plugin-k8s +// - tanzu operations cluster-plugin-ops +// - tanzu kubernetes cluster-plugin-k8s +// - tanzu remapped +var rootCmd = func() *cobra.Command { + cmd := &cobra.Command{ + Use: "tanzu", + } + // Add the two targets we will test + opsTargetCmd := &cobra.Command{Use: "operations"} + k8sTargetCmd := &cobra.Command{Use: "kubernetes"} + cmd.AddCommand(opsTargetCmd, k8sTargetCmd) + + // Add the plugins + // tanzu sample-plugin + cmd.AddCommand(&cobra.Command{ + Use: pluginName[configtypes.TargetGlobal], + RunE: func(cmd *cobra.Command, args []string) error { + return nil + }, + Annotations: map[string]string{ + common.AnnotationForCmdSrcPath: "", + }, + }) + // tanzu operations ops-plugin + opsTargetCmd.AddCommand(&cobra.Command{ + Use: pluginName[configtypes.TargetOperations], + RunE: func(cmd *cobra.Command, args []string) error { + return nil + }, + Annotations: map[string]string{ + common.AnnotationForCmdSrcPath: "", + }, + }) + // tanzu kuberntes k8s-plugin + k8sTargetCmd.AddCommand(&cobra.Command{ + Use: pluginName[configtypes.TargetK8s], + RunE: func(cmd *cobra.Command, args []string) error { + return nil + }, + Annotations: map[string]string{ + common.AnnotationForCmdSrcPath: "", + }, + }) + // k8s plugin are also at the root level + // tanzu k8s-plugin + cmd.AddCommand(&cobra.Command{ + Use: pluginName[configtypes.TargetK8s], + RunE: func(cmd *cobra.Command, args []string) error { + return nil + }, + Annotations: map[string]string{ + common.AnnotationForCmdSrcPath: "", + }, + }) + // And a command from the plugin remapped to the root + // tanzu remapped + cmd.AddCommand(&cobra.Command{ + Use: remappedCmdName, + RunE: func(cmd *cobra.Command, args []string) error { + return nil + }, + Annotations: map[string]string{ + // This is the source command for the remapped command + common.AnnotationForCmdSrcPath: remappedCmdName, + }, + }) + + return cmd +}() func Test_RepeatConstructAndAddTree(t *testing.T) { for i := 0; i < 10; i++ { - testConstructAndAddTree(t) + testConstructAndAddTreeForPlugin(t, configtypes.TargetGlobal) } } -func testConstructAndAddTree(t *testing.T) { +func Test_ConstructAndAddTreeK8s(t *testing.T) { + testConstructAndAddTreeForPlugin(t, configtypes.TargetK8s) +} + +func Test_ConstructAndAddTreeOps(t *testing.T) { + testConstructAndAddTreeForPlugin(t, configtypes.TargetOperations) +} + +func testConstructAndAddTreeForPlugin(t *testing.T, target configtypes.Target) { // create the command docs tmpCacheDir, err := os.MkdirTemp("", "cache") assert.NoError(t, err) @@ -78,8 +276,7 @@ func testConstructAndAddTree(t *testing.T) { defer os.RemoveAll(tmpCMDDocsDir) // pre-generate the plugin docs for the dummy plugin - docsFiles := []string{"tanzu.md", "tanzu_sample-plugin.md", "tanzu_sample-plugin_foo1.md", "tanzu_sample-plugin_bar1.md", "tanzu_sample-plugin_foo1_foo2.md"} - err = createPluginDocs(tmpCMDDocsDir, docsFiles) + err = createPluginDocs(tmpCMDDocsDir, target) assert.NoError(t, err, "failed to create command docs for testing") os.Setenv("TEST_CUSTOM_PLUGIN_COMMAND_TREE_CACHE_DIR", tmpCacheDir) @@ -87,10 +284,6 @@ func testConstructAndAddTree(t *testing.T) { os.Unsetenv("TEST_CUSTOM_PLUGIN_COMMAND_TREE_CACHE_DIR") }() - // setup the plugin - samplePluginName := "sample-plugin" - setupDummyPlugin(t, tmpCacheDir, samplePluginName) - // Initialize the cache pct, err := getPluginCommandTree() assert.NoError(t, err) @@ -102,22 +295,31 @@ func testConstructAndAddTree(t *testing.T) { }, } - // Create a sample plugin - plugin := &cli.PluginInfo{ - Name: "sample-plugin", - InstallationPath: filepath.Join(tmpCacheDir, samplePluginName), - Target: configtypes.TargetK8s, + // Create a sample plugin with the specified target and a remapped command + samplePlugin := &cli.PluginInfo{ + Name: pluginName[target], + InstallationPath: filepath.Join(tmpCacheDir, pluginName[target]), + Target: target, Version: "1.0.0", + CommandMap: []plugintypes.CommandMapEntry{ + { + // This command will be "tanzu remapped" + SourceCommandPath: remappedCmdName, + DestinationCommandPath: remappedCmdName, + }, + }, } + // setup the plugin + setupDummyPlugin(t, tmpCacheDir, pluginName[target], pluginAlias[target]) // Test constructing the plugin command tree with valid plugin binary path, // it should generate the command tree and alias correctly - err = cache.ConstructAndAddTree(plugin) + err = cache.constructAndAddTree(rootCmd, samplePlugin) assert.NoError(t, err) assert.Equal(t, 1, len(cache.pluginCommands.CommandTree)) - validatePluginCommandTree(t, cache.pluginCommands, fmt.Sprintf(expectedPluginCmdTree, plugin.InstallationPath)) + validatePluginCommandTree(t, cache.pluginCommands, samplePlugin.InstallationPath, fmt.Sprintf(expectedPluginTree[target], samplePlugin.InstallationPath)) // Test getting the command tree for a non-existing plugin nonExistingPlugin := &cli.PluginInfo{ @@ -126,14 +328,14 @@ func testConstructAndAddTree(t *testing.T) { Target: configtypes.TargetK8s, Version: "1.0.0", } - err = cache.ConstructAndAddTree(nonExistingPlugin) + err = cache.constructAndAddTree(rootCmd, nonExistingPlugin) assert.Error(t, err) } func TestCache_GetTree(t *testing.T) { // Create a sample plugin plugin := &cli.PluginInfo{ - Name: "sample-plugin", + Name: pluginName[configtypes.TargetGlobal], InstallationPath: "/path/to/sample-plugin", Target: configtypes.TargetK8s, Version: "1.0.0", @@ -142,7 +344,7 @@ func TestCache_GetTree(t *testing.T) { expectedCMDTree := cache.pluginCommands.CommandTree[plugin.InstallationPath] // Test getting the command tree - commandTree, err := cache.GetTree(plugin) + commandTree, err := cache.GetPluginTree(rootCmd, plugin) assert.NoError(t, err) assert.NotNil(t, commandTree) assert.Equal(t, expectedCMDTree, commandTree) @@ -156,15 +358,24 @@ func TestCache_GetTree(t *testing.T) { Target: configtypes.TargetK8s, Version: "1.0.0", } - nonExistingCommandTree, err := cache.GetTree(nonExistingPlugin) + nonExistingCommandTree, err := cache.GetPluginTree(rootCmd, nonExistingPlugin) assert.Error(t, err) assert.Nil(t, nonExistingCommandTree) } -func TestCache_DeleteTree(t *testing.T) { +func TestCache_DeletePluginTree(t *testing.T) { + tmpCacheDir, err := os.MkdirTemp("", "cache") + assert.NoError(t, err) + defer os.RemoveAll(tmpCacheDir) + + os.Setenv("TEST_CUSTOM_PLUGIN_COMMAND_TREE_CACHE_DIR", tmpCacheDir) + defer func() { + os.Unsetenv("TEST_CUSTOM_PLUGIN_COMMAND_TREE_CACHE_DIR") + }() + // Create a sample plugin plugin := &cli.PluginInfo{ - Name: "sample-plugin", + Name: pluginName[configtypes.TargetGlobal], InstallationPath: "/path/to/sample-plugin", Target: configtypes.TargetK8s, Version: "1.0.0", @@ -172,8 +383,9 @@ func TestCache_DeleteTree(t *testing.T) { cache := getCacheWithSamplePluginCommandTree(plugin.Name, plugin.InstallationPath) // Test deleting the command tree - err := cache.DeleteTree(plugin) + err = cache.DeletePluginTree(plugin) assert.NoError(t, err) + // Make sure the cache was updated in memory assert.Equal(t, 0, len(cache.pluginCommands.CommandTree)) // Test getting the command tree for a non-existing plugin @@ -183,14 +395,45 @@ func TestCache_DeleteTree(t *testing.T) { Target: configtypes.TargetK8s, Version: "1.0.0", } - err = cache.DeleteTree(nonExistingPlugin) + err = cache.DeletePluginTree(nonExistingPlugin) + assert.NoError(t, err) +} + +func TestCache_DeleteTree(t *testing.T) { + tmpCacheDir, err := os.MkdirTemp("", "cache") + assert.NoError(t, err) + defer os.RemoveAll(tmpCacheDir) + + os.Setenv("TEST_CUSTOM_PLUGIN_COMMAND_TREE_CACHE_DIR", tmpCacheDir) + defer func() { + os.Unsetenv("TEST_CUSTOM_PLUGIN_COMMAND_TREE_CACHE_DIR") + }() + + // Create a cache file on disk to make sure it will later be removed + err = os.WriteFile(GetPluginsCommandTreeCachePath(), []byte("commandTree: {}"), 0644) + assert.NoError(t, err) + + // Create a sample plugin + plugin := &cli.PluginInfo{ + Name: pluginName[configtypes.TargetGlobal], + InstallationPath: "/path/to/sample-plugin", + } + cache := getCacheWithSamplePluginCommandTree(plugin.Name, plugin.InstallationPath) + + // Test deleting the entire command tree + err = cache.DeleteTree() assert.NoError(t, err) + // Make sure the cache was updated in memory + assert.Equal(t, 0, len(cache.pluginCommands.CommandTree)) + // Make sure the cache file was removed + _, err = os.Stat(GetPluginsCommandTreeCachePath()) + assert.Error(t, err, "expected the cache file to be removed") } func getCacheWithSamplePluginCommandTree(_, pluginInstallationPath string) *cacheImpl { pluginCMDTree := &CommandNode{ Subcommands: map[string]*CommandNode{ - "plugin-subcmd1": &CommandNode{ + "plugin-subcmd1": { Subcommands: map[string]*CommandNode{ "plugin-subcmd2": NewCommandNode(), }, @@ -214,7 +457,27 @@ func getCacheWithSamplePluginCommandTree(_, pluginInstallationPath string) *cach return cache } -func createPluginDocs(docsDir string, docNames []string) error { +func createPluginDocs(docsDir string, target configtypes.Target) error { + var targetStr string + switch target { + case configtypes.TargetGlobal: + targetStr = "global" + case configtypes.TargetK8s: + targetStr = "k8s" + case configtypes.TargetOperations: + targetStr = "ops" + } + + docNames := []string{ + "tanzu.md", + "tanzu_cluster-plugin-" + targetStr + ".md", + "tanzu_cluster-plugin-" + targetStr + "_foo1.md", + "tanzu_cluster-plugin-" + targetStr + "_bar1.md", + "tanzu_cluster-plugin-" + targetStr + "_foo1_foo2.md", + // A remapped command + "tanzu_" + remappedCmdName + ".md", + } + for _, doc := range docNames { docPath := filepath.Join(docsDir, doc) file, err := os.Create(docPath) @@ -226,23 +489,25 @@ func createPluginDocs(docsDir string, docNames []string) error { return nil } -func setupDummyPlugin(t *testing.T, dirName, pluginName string) { +func setupDummyPlugin(t *testing.T, dirName, pluginName, pluginAlias string) { pluginExeFile, err := os.OpenFile(filepath.Join(dirName, pluginName), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0755) assert.NoError(t, err) defer pluginExeFile.Close() - fmt.Fprint(pluginExeFile, samplePluginToGenerateAliasWithHelpCommand) + fmt.Fprintf(pluginExeFile, samplePluginToGenerateAliasWithHelpCommand, pluginName, pluginAlias) } -func validatePluginCommandTree(t *testing.T, gotPluginCommandTree *pluginCommandTree, expectedPluginCommandTreeYaml string) { + +func validatePluginCommandTree(t *testing.T, gotPluginCommandTree *pluginCommandTree, pluginInstallationPath, expectedPluginCommandTreeYaml string) { // the yaml representation should marshaled and unmarshal to get rid of the "AliasProcessed" field for comparison - gotPluginCommandTreeBytes, err := yaml.Marshal(gotPluginCommandTree) + gotPluginCommandTreeBytes, err := yaml.Marshal(gotPluginCommandTree.CommandTree[pluginInstallationPath]) assert.NoError(t, err) - getPluginCommandTreeUnmarshaled := &pluginCommandTree{} - err = yaml.Unmarshal(gotPluginCommandTreeBytes, getPluginCommandTreeUnmarshaled) + gotPluginCommandTreeUnmarshaled := &CommandNode{} + err = yaml.Unmarshal(gotPluginCommandTreeBytes, gotPluginCommandTreeUnmarshaled) assert.NoError(t, err) + expectedPluginCommandTreeYaml = fmt.Sprintf("commandTree:\n%s", expectedPluginCommandTreeYaml) expPluginCmdTree := &pluginCommandTree{} err = yaml.Unmarshal([]byte(expectedPluginCommandTreeYaml), expPluginCmdTree) assert.NoError(t, err, "failed to unmarshal the expected plugin command tree") - assert.Equal(t, expPluginCmdTree, getPluginCommandTreeUnmarshaled, "the plugin command tree doesn't match with the expected ") + assert.Equal(t, expPluginCmdTree.CommandTree[pluginInstallationPath], gotPluginCommandTreeUnmarshaled, "the plugin command tree doesn't match with the expected ") } diff --git a/pkg/pluginmanager/manager.go b/pkg/pluginmanager/manager.go index 8bbf18744..60136b4d7 100644 --- a/pkg/pluginmanager/manager.go +++ b/pkg/pluginmanager/manager.go @@ -908,24 +908,8 @@ func updatePluginInfoAndInitializePlugin(p *discovery.Discovered, plugin *cli.Pl if err := configlib.ConfigureFeatureFlags(plugin.DefaultFeatureFlags, configlib.SkipIfExists()); err != nil { log.Infof("could not configure default featureflags for the plugin: %v", err.Error()) } - // add plugin to the plugin command tree cache for telemetry to consume later for plugin command chain parsing - addPluginToCommandTreeCache(plugin) - return nil -} -// addPluginToCommandTreeCache would construct and add the plugin command tree to the command tree cache -// which would be consumed by telemetry for plugin command chain parsing -func addPluginToCommandTreeCache(plugin *cli.PluginInfo) { - // update the plugin command tree cache - ctr, err := plugincmdtree.NewCache() - if err != nil { - telemetry.LogError(err, "") - return - } - err = ctr.ConstructAndAddTree(plugin) - if err != nil { - telemetry.LogError(err, "") - } + return nil } // deletePluginFromCommandTreeCache deletes the plugin command tree from the command tree cache @@ -937,7 +921,7 @@ func deletePluginFromCommandTreeCache(plugin *cli.PluginInfo) { telemetry.LogError(err, "") return } - err = ctr.DeleteTree(plugin) + err = ctr.DeletePluginTree(plugin) if err != nil { telemetry.LogError(err, "") } diff --git a/pkg/telemetry/client.go b/pkg/telemetry/client.go index ecf7f0628..7f1efd4f2 100644 --- a/pkg/telemetry/client.go +++ b/pkg/telemetry/client.go @@ -238,21 +238,21 @@ func (tc *telemetryClient) updateMetricsForPlugin(cmd *cobra.Command, args []str tc.currentOperationMetrics.PluginVersion = plugin.Version tc.currentOperationMetrics.Target = string(plugin.Target) tc.currentOperationMetrics.Endpoint = getEndpointSHAWithCtxTypePrefix(plugin) - // for plugins, cobra can only parse the command upto the plugin name, + // for plugins, cobra can only parse the command up to the plugin name, // and the rest of the subcommands and args would be captured as args // ex: tanzu cluster kubeconfig get testCluster --export-file /path/to/file // the above command after parsing cobra will provide the below // ==> cmd.CommandPath() would return "tanzu cluster" // args = ["kubeconfig","get","testCluster","--export-file","/path/to/file"] // So, use the plugin command parser to figure out(best-effort) the command path using command tree as reference - cobraParsedCMDPath := strings.Join(strings.Split(cmd.CommandPath(), " ")[1:], " ") - cmdPath, err := tc.parsePluginCommandPath(plugin, args) + cobraParsedCMDPath := strings.Split(cmd.CommandPath(), " ")[1:] + cmdPath, err := tc.parsePluginCommandPath(cmd.Root(), plugin, append(cobraParsedCMDPath, args...)) if err != nil { LogError(err, "") // assign the default plugin path - tc.currentOperationMetrics.CommandName = cobraParsedCMDPath + tc.currentOperationMetrics.CommandName = strings.Join(cobraParsedCMDPath, " ") } else { - tc.currentOperationMetrics.CommandName = cobraParsedCMDPath + cmdPath + tc.currentOperationMetrics.CommandName = cmdPath } } @@ -274,15 +274,16 @@ func (tc *telemetryClient) pluginInfoFromCommand(cmd *cobra.Command) *cli.Plugin // parsePluginCommandPath parses the args provided by the cobra and uses the best-effort strategy to // map to the plugin command tree and would return the command path -func (tc *telemetryClient) parsePluginCommandPath(plugin *cli.PluginInfo, args []string) (string, error) { +func (tc *telemetryClient) parsePluginCommandPath(rootCmd *cobra.Command, plugin *cli.PluginInfo, args []string) (string, error) { pctCache, err := tc.cmdTreeCacheGetter() if err != nil { return "", err } - pct, err := pctCache.GetTree(plugin) + pct, err := pctCache.GetPluginTree(rootCmd, plugin) if err != nil { return "", err } + cmdPath := "" current := pct for _, arg := range args { @@ -298,7 +299,10 @@ func (tc *telemetryClient) parsePluginCommandPath(plugin *cli.PluginInfo, args [ continue default: if subCMD := subCommandMatchingArg(current, arg); subCMD != nil { - cmdPath = cmdPath + " " + arg + if cmdPath != "" { + cmdPath += " " + } + cmdPath += arg current = subCMD } } diff --git a/pkg/telemetry/client_test.go b/pkg/telemetry/client_test.go index 4a406a207..a87714251 100644 --- a/pkg/telemetry/client_test.go +++ b/pkg/telemetry/client_test.go @@ -235,8 +235,9 @@ var _ = Describe("Unit tests for UpdateCmdPreRunMetrics()", func() { return nil }, Annotations: map[string]string{ - "type": common.CommandTypePlugin, - "pluginInstallationPath": "/path/to/plugin1", + "type": common.CommandTypePlugin, + "pluginInstallationPath": "/path/to/plugin1", + common.AnnotationForCmdSrcPath: "", }, } rootCmd.AddCommand(globalPluginCmd) @@ -247,7 +248,7 @@ var _ = Describe("Unit tests for UpdateCmdPreRunMetrics()", func() { InstallationPath: "/path/to/plugin1", }}) - cmdTreeCache.GetTreeReturns(nil, errors.New("fake-get-command-tree-error")) + cmdTreeCache.GetPluginTreeReturns(nil, errors.New("fake-get-command-tree-error")) // command: tanzu plugin1 arg1 err = tc.UpdateCmdPreRunMetrics(globalPluginCmd, []string{"arg1"}) @@ -280,8 +281,9 @@ var _ = Describe("Unit tests for UpdateCmdPreRunMetrics()", func() { return nil }, Annotations: map[string]string{ - "type": common.CommandTypePlugin, - "pluginInstallationPath": "/path/to/k8s-plugin1", + "type": common.CommandTypePlugin, + "pluginInstallationPath": "/path/to/k8s-plugin1", + common.AnnotationForCmdSrcPath: "", }, } k8sTargetCmd.AddCommand(k8sPlugincmd) @@ -294,7 +296,7 @@ var _ = Describe("Unit tests for UpdateCmdPreRunMetrics()", func() { InstallationPath: "/path/to/k8s-plugin1", }}) - cmdTreeCache.GetTreeReturns(nil, errors.New("fake-get-command-tree-error")) + cmdTreeCache.GetPluginTreeReturns(nil, errors.New("fake-get-command-tree-error")) // command : tanzu kubernetes k8s-plugin1 plugin-subcmd1 -v 6 plugin-subcmd2 -ab --flag1=value1 --flag2 value2 -- --arg1 --arg2 err = tc.UpdateCmdPreRunMetrics(k8sPlugincmd, []string{"plugin-subcmd1", "-v", "6", "plugin-subcmd2", "-ab", "--flag1=value1", "--flag2", "value2", "--", "--arg1", "--arg2"}) @@ -346,8 +348,9 @@ var _ = Describe("Unit tests for UpdateCmdPreRunMetrics()", func() { return nil }, Annotations: map[string]string{ - "type": common.CommandTypePlugin, - "pluginInstallationPath": "/path/to/k8s-plugin1", + "type": common.CommandTypePlugin, + "pluginInstallationPath": "/path/to/k8s-plugin1", + common.AnnotationForCmdSrcPath: "", }, } k8sTargetCmd.AddCommand(k8sPlugincmd) @@ -362,12 +365,38 @@ var _ = Describe("Unit tests for UpdateCmdPreRunMetrics()", func() { // cmd tree for "k8s-plugin1" plugin that would be used by parser for parsing the command args and return command path pluginCMDTree = &plugincmdtree.CommandNode{ Subcommands: map[string]*plugincmdtree.CommandNode{ - "plugin-subcmd1": &plugincmdtree.CommandNode{ + "k8s-plugin1": { Subcommands: map[string]*plugincmdtree.CommandNode{ - "plugin-subcmd2": plugincmdtree.NewCommandNode(), + "plugin-subcmd1": { + Subcommands: map[string]*plugincmdtree.CommandNode{ + "plugin-subcmd2": plugincmdtree.NewCommandNode(), + }, + Aliases: map[string]struct{}{ + "pscmd1-alias": {}, + }, + }, + }, + Aliases: map[string]struct{}{}, + }, + "kubernetes": { + Subcommands: map[string]*plugincmdtree.CommandNode{ + "k8s-plugin1": { + Subcommands: map[string]*plugincmdtree.CommandNode{ + "plugin-subcmd1": { + Subcommands: map[string]*plugincmdtree.CommandNode{ + "plugin-subcmd2": plugincmdtree.NewCommandNode(), + }, + Aliases: map[string]struct{}{ + "pscmd1-alias": {}, + }, + }, + }, + Aliases: map[string]struct{}{}, + }, }, Aliases: map[string]struct{}{ - "pscmd1-alias": {}, + "k8s": {}, + "kubernetes": {}, }, }, }, @@ -376,7 +405,7 @@ var _ = Describe("Unit tests for UpdateCmdPreRunMetrics()", func() { Context("When the user command string matches accurately with plugin command tree ", func() { It("should return success and the metrics should have the command path updated", func() { - cmdTreeCache.GetTreeReturns(pluginCMDTree, nil) + cmdTreeCache.GetPluginTreeReturns(pluginCMDTree, nil) // command : tanzu kubernetes k8s-plugin1 plugin-subcmd1 -v 6 plugin-subcmd2 -ab --flag1=value1 --flag2 value2 -- --arg1 --arg2 err = tc.UpdateCmdPreRunMetrics(k8sPlugincmd, []string{"plugin-subcmd1", "-v", "6", "plugin-subcmd2", "-ab", "--flag1=value1", "--flag2", "value2", "--", "--arg1", "--arg2"}) @@ -411,7 +440,7 @@ var _ = Describe("Unit tests for UpdateCmdPreRunMetrics()", func() { Context("When the user command string having command alias matches with plugin command tree ", func() { It("should return success and the metrics should have the command path updated correctly", func() { - cmdTreeCache.GetTreeReturns(pluginCMDTree, nil) + cmdTreeCache.GetPluginTreeReturns(pluginCMDTree, nil) // command : tanzu kubernetes k8s-plugin1 plugin-subcmd1 -v 6 plugin-subcmd2 -ab --flag1=value1 --flag2 value2 -- --arg1 --arg2 err = tc.UpdateCmdPreRunMetrics(k8sPlugincmd, []string{"pscmd1-alias", "-v", "6", "plugin-subcmd2", "-ab", "--flag1=value1", "--flag2", "value2", "--", "--arg1", "--arg2"}) @@ -445,7 +474,7 @@ var _ = Describe("Unit tests for UpdateCmdPreRunMetrics()", func() { }) Context("When the user command string partially matches with plugin command tree ", func() { It("should return success and the metrics should have command path updated upto the point of command match(best-effort)", func() { - cmdTreeCache.GetTreeReturns(pluginCMDTree, nil) + cmdTreeCache.GetPluginTreeReturns(pluginCMDTree, nil) // command : tanzu kubernetes k8s-plugin1 plugin-subcmd1 -v 6 plugin-subcmd2 -ab --flag1=value1 --flag2 value2 -- --arg1 --arg2 err = tc.UpdateCmdPreRunMetrics(k8sPlugincmd, []string{"plugin-subcmd1", "-v", "6", "plugin-subcmd-notmatched", "-ab", "--flag1=value1", "--flag2", "value2", "--", "--arg1", "--arg2"})