From 1c37f3e2fa5c856ee0c421da25baa8215e27b90a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20Pab=C3=B3n?= Date: Fri, 5 Feb 2021 07:57:26 -0800 Subject: [PATCH] Add components to the help screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Luis Pabón --- cmd/root.go | 44 +++++++ handler/plugin/list.go | 87 +++----------- handler/plugin/plugin.go | 9 +- pkg/plugin/list.go | 240 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 303 insertions(+), 77 deletions(-) create mode 100644 pkg/plugin/list.go diff --git a/cmd/root.go b/cmd/root.go index 072c08aa..626fc36b 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -21,6 +21,7 @@ import ( "github.com/portworx/pxc/pkg/commander" "github.com/portworx/pxc/pkg/config" "github.com/portworx/pxc/pkg/kubernetes" + pkgplugin "github.com/portworx/pxc/pkg/plugin" "github.com/portworx/pxc/pkg/util" "github.com/spf13/cobra" @@ -102,6 +103,24 @@ var _ = commander.RegisterCommandInit(func() { } rootCmd.Flags().BoolVar(&rootOptions.showOptions, "options", false, "Show global options for all commands") rootCmd.SetUsageTemplate(rootTmpl) + + // Custom Help + defaultHelpFunc := rootCmd.HelpFunc() + defaultUsageFunc := rootCmd.UsageFunc() + fromHelp := false + rootCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { + fromHelp = true + defaultHelpFunc(cmd, args) + rootComponentUsage(cmd) + }) + + rootCmd.SetUsageFunc(func(cmd *cobra.Command) error { + err := defaultUsageFunc(cmd) + if !fromHelp { + rootComponentUsage(cmd) + } + return err + }) }) func rootCmdExec(cmd *cobra.Command, args []string) error { @@ -112,6 +131,31 @@ func rootCmdExec(cmd *cobra.Command, args []string) error { return nil } +func rootComponentUsage(cmd *cobra.Command) { + arg0 := "pxc" + if util.InKubectlPluginMode() { + arg0 = "kubectl pxc" + } + + if cmd != rootCmd { + return + } + + lister := &pkgplugin.PluginLister{ + NameOnly: true, + } + lister.Complete(rootCmd) + components, _ := lister.GetSortedRootComponents() + + util.Printf("\nAvailable components:\n") + + // Add components as subcommands + for _, component := range components { + util.Printf(" %s\n", component) + } + util.Printf("\nUse \"%s [component] --help\" for more information about the component\n", arg0) +} + func rootPersistentPreRunE(cmd *cobra.Command, args []string) error { // Setup verbosity diff --git a/handler/plugin/list.go b/handler/plugin/list.go index 5438e1cf..a30c0e6a 100644 --- a/handler/plugin/list.go +++ b/handler/plugin/list.go @@ -19,20 +19,15 @@ limitations under the License. package plugin import ( - "bytes" "fmt" - "io/ioutil" "os" - "path" "path/filepath" "runtime" "strings" "github.com/portworx/pxc/pkg/commander" - "github.com/portworx/pxc/pkg/config" pluginpkg "github.com/portworx/pxc/pkg/plugin" "github.com/portworx/pxc/pkg/util" - "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -40,6 +35,7 @@ import ( type pluginListOptions struct { Verifier PathVerifier NameOnly bool + Lister pluginpkg.PluginLister PluginPaths []string } @@ -88,86 +84,31 @@ func ListAddCommand(cmd *cobra.Command) { } func listExec(cmd *cobra.Command, args []string) error { - listOptions.Complete(cmd) + listOptions.Lister.Complete(cmd) return listOptions.Run(cmd, args) } -func (o *pluginListOptions) Complete(cmd *cobra.Command) error { - o.Verifier = &CommandOverrideVerifier{ - root: cmd.Root(), - seenPlugins: make(map[string]string), - } - - o.PluginPaths = filepath.SplitList(os.Getenv("PATH")) - o.PluginPaths = append(o.PluginPaths, path.Join(config.CM().GetFlags().ConfigDir, "bin")) - return nil -} - func (o *pluginListOptions) Run(cmd *cobra.Command, args []string) error { - pluginsFound := false isFirstFile := true - pluginErrors := []error{} - pluginWarnings := 0 - - for _, dir := range uniquePathsList(o.PluginPaths) { - files, err := ioutil.ReadDir(dir) - if err != nil { - if _, ok := err.(*os.PathError); ok { - logrus.Warnf("Unable read directory %q from your PATH: %v. Skipping...", dir, err) - continue - } - - pluginErrors = append(pluginErrors, fmt.Errorf("error: unable to read directory %q in your PATH: %v", dir, err)) - continue - } - for _, f := range files { - if f.IsDir() { - continue - } - if !hasValidPrefix(f.Name(), pluginpkg.ValidPluginFilenamePrefixes) { - continue - } - - if isFirstFile { - util.Eprintf("The following compatible components are available:\n\n") - pluginsFound = true - isFirstFile = false - } - - pluginPath := f.Name() - if !o.NameOnly { - pluginPath = filepath.Join(dir, pluginPath) - } - - util.Printf("%s\n", pluginPath) - if errs := o.Verifier.Verify(filepath.Join(dir, f.Name())); len(errs) != 0 { - for _, err := range errs { - util.Eprintf(" - %s\n", err) - pluginWarnings++ - } - } - } + components, err := o.Lister.GetList() + if err != nil { + return err } - if !pluginsFound { - pluginErrors = append(pluginErrors, fmt.Errorf("error: unable to find any pxc components in your PATH")) + if len(components) == 0 { + return fmt.Errorf("Unable to find any pxc components in your PATH") } - if pluginWarnings > 0 { - if pluginWarnings == 1 { - pluginErrors = append(pluginErrors, fmt.Errorf("error: one component warning was found")) - } else { - pluginErrors = append(pluginErrors, fmt.Errorf("error: %v component warnings were found", pluginWarnings)) + for _, component := range components { + if isFirstFile { + util.Eprintf("The following compatible components are available:\n\n") + isFirstFile = false } - } - if len(pluginErrors) > 0 { - util.Eprintf("\n") - errs := bytes.NewBuffer(nil) - for _, e := range pluginErrors { - fmt.Fprintln(errs, e) + util.Printf("%s\n", component.Path) + for _, warning := range component.Warnings { + util.Eprintf(" - %s\n", warning) } - return fmt.Errorf("%s", errs.String()) } return nil diff --git a/handler/plugin/plugin.go b/handler/plugin/plugin.go index dfbcf662..bb6f336b 100644 --- a/handler/plugin/plugin.go +++ b/handler/plugin/plugin.go @@ -29,10 +29,11 @@ import ( var ( pluginLong = ` -Provides utilities for interacting with components. +Configure components -Plugins provide extended functionality that is not part of the major command-line distribution. -Please refer to the documentation and examples for more information about how write your own components.` +Components provide extended functionality that is not part of the major command-line +distribution. Please refer to the documentation and examples for more information +about how write your own components.` ) type pluginCmdFlags struct { @@ -48,7 +49,7 @@ var _ = commander.RegisterCommandVar(func() { pluginCmd = &cobra.Command{ Use: "component", DisableFlagsInUseLine: true, - Short: "Provides utilities for interacting with components", + Short: "Configure components", Long: pluginLong, Run: func(cmd *cobra.Command, args []string) { util.Printf("Please see pxc component --help for more information\n") diff --git a/pkg/plugin/list.go b/pkg/plugin/list.go new file mode 100644 index 00000000..5f8f9a9c --- /dev/null +++ b/pkg/plugin/list.go @@ -0,0 +1,240 @@ +/* +Copyright 2021 Pure Storage +Copyright 2017 The Kubernetes Authors. + +Originally from: +https://raw.githubusercontent.com/kubernetes/pxc/release-1.17/pkg/cmd/plugin/plugin.go + +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 plugin + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "path" + "path/filepath" + "runtime" + "sort" + "strings" + + "github.com/portworx/pxc/pkg/config" + "github.com/portworx/pxc/pkg/util" + "github.com/sirupsen/logrus" + + "github.com/spf13/cobra" +) + +type PluginLister struct { + Verifier PathVerifier + NameOnly bool + + PluginPaths []string +} + +type Component struct { + Path string + Warnings []string +} + +// PathVerifier receives a path and determines if it is valid or not +type PathVerifier interface { + // Verify determines if a given path is valid + Verify(path string) []error +} + +type CommandOverrideVerifier struct { + root *cobra.Command + seenPlugins map[string]string +} + +func (o *PluginLister) Complete(cmd *cobra.Command) error { + o.Verifier = &CommandOverrideVerifier{ + root: cmd.Root(), + seenPlugins: make(map[string]string), + } + + o.PluginPaths = filepath.SplitList(os.Getenv("PATH")) + o.PluginPaths = append(o.PluginPaths, path.Join(config.CM().GetFlags().ConfigDir, "bin")) + return nil +} + +func (o *PluginLister) GetList() ([]*Component, error) { + pluginErrors := []error{} + components := make([]*Component, 0) + + for _, dir := range uniquePathsList(o.PluginPaths) { + files, err := ioutil.ReadDir(dir) + if err != nil { + if _, ok := err.(*os.PathError); ok { + logrus.Warnf("Unable read directory %q from your PATH: %v. Skipping...", dir, err) + continue + } + + pluginErrors = append(pluginErrors, fmt.Errorf("error: unable to read directory %q in your PATH: %v", dir, err)) + continue + } + + for _, f := range files { + if f.IsDir() { + continue + } + if !hasValidPrefix(f.Name(), ValidPluginFilenamePrefixes) { + continue + } + + pluginPath := f.Name() + if !o.NameOnly { + pluginPath = filepath.Join(dir, pluginPath) + } + + component := &Component{ + Path: pluginPath, + Warnings: make([]string, 0), + } + + if errs := o.Verifier.Verify(filepath.Join(dir, f.Name())); len(errs) != 0 { + for _, err := range errs { + component.Warnings = append(component.Warnings, err.Error()) + } + } + components = append(components, component) + } + } + + if len(pluginErrors) > 0 { + util.Eprintf("\n") + errs := bytes.NewBuffer(nil) + for _, e := range pluginErrors { + fmt.Fprintln(errs, e) + } + return nil, fmt.Errorf("%s", errs.String()) + } + + return components, nil +} + +func (o *PluginLister) GetSortedRootComponents() ([]string, error) { + components, err := o.GetList() + if err != nil { + return nil, err + } + + list := make([]string, 0) + + // Add components as subcommands + for _, component := range components { + + // Remove the "pxc-" part of the component + c := strings.TrimPrefix(component.Path, "pxc-") + + // Only add top level component, no sub components + if strings.Contains(c, "-") { + continue + } + + list = append(list, c) + } + + sort.Strings(list) + return list, nil +} + +// Verify implements PathVerifier and determines if a given path +// is valid depending on whether or not it overwrites an existing +// pxc command path, or a previously seen plugin. +func (v *CommandOverrideVerifier) Verify(path string) []error { + if v.root == nil { + return []error{fmt.Errorf("unable to verify path with nil root")} + } + + // extract the plugin binary name + segs := strings.Split(path, "/") + binName := segs[len(segs)-1] + + cmdPath := strings.Split(binName, "-") + if len(cmdPath) > 1 { + // the first argument is always "pxc" for a plugin binary + cmdPath = cmdPath[1:] + } + + errors := []error{} + + if isExec, err := isExecutable(path); err == nil && !isExec { + errors = append(errors, fmt.Errorf("warning: %s identified as a pxc component, but it is not executable", path)) + } else if err != nil { + errors = append(errors, fmt.Errorf("error: unable to identify %s as an executable file: %v", path, err)) + } + + if existingPath, ok := v.seenPlugins[binName]; ok { + errors = append(errors, fmt.Errorf("warning: %s is overshadowed by a similarly named component: %s", path, existingPath)) + } else { + v.seenPlugins[binName] = path + } + + if cmd, _, err := v.root.Find(cmdPath); err == nil { + errors = append(errors, fmt.Errorf("warning: %s overwrites existing command: %q", binName, cmd.CommandPath())) + } + + return errors +} + +func isExecutable(fullPath string) (bool, error) { + info, err := os.Stat(fullPath) + if err != nil { + return false, err + } + + if runtime.GOOS == "windows" { + fileExt := strings.ToLower(filepath.Ext(fullPath)) + + switch fileExt { + case ".bat", ".cmd", ".com", ".exe", ".ps1": + return true, nil + } + return false, nil + } + + if m := info.Mode(); !m.IsDir() && m&0111 != 0 { + return true, nil + } + + return false, nil +} + +// uniquePathsList deduplicates a given slice of strings without +// sorting or otherwise altering its order in any way. +func uniquePathsList(paths []string) []string { + seen := map[string]bool{} + newPaths := []string{} + for _, p := range paths { + if seen[p] { + continue + } + seen[p] = true + newPaths = append(newPaths, p) + } + return newPaths +} + +func hasValidPrefix(filepath string, validPrefixes []string) bool { + for _, prefix := range validPrefixes { + if !strings.HasPrefix(filepath, prefix+"-") { + continue + } + return true + } + return false +}