package commands /* Most of the plugin handler code is borrowed from the kubectl codebase. Wherever "courtesy: kubectl" is indicated, the copyright belongs to the respective authors with the following notice: Copyright 2014 The Kubernetes Authors. 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. */ import ( "fmt" "os" "os/exec" "path/filepath" "regexp" "runtime" "strings" "syscall" "unicode" "github.com/hasura/graphql-engine/cli" "github.com/pkg/errors" "github.com/spf13/cobra" ) var validPluginFilenamePrefixes = []string{"hasura"} // NewPluginsCmd returns the plugins command func NewPluginsCmd(ec *cli.ExecutionContext) *cobra.Command { pluginsCmd := &cobra.Command{ Use: "plugins", Aliases: []string{"plugin"}, Short: "Manage plugins for the cli", Long: `Plugins can be installed to extend the functionality of Hasura CLI An index for all available plugins can be found at https://github.com/hasura/cli-plugins-index Please open pull requests against this repo to add new plugins`, SilenceUsage: true, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { return ec.PluginsConfig.Repo.EnsureCloned() }, } pluginsCmd.AddCommand( newPluginsListCmd(ec), newPluginsInstallCmd(ec), newPluginsUnInstallCmd(ec), newPluginsUpgradeCmd(ec), ) return pluginsCmd } // PluginHandler is capable of parsing command line arguments // and performing executable filename lookups to search // for valid plugin files, and execute found plugins. // courtesy: kubectl // https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/cmd.go type PluginHandler interface { // exists at the given filename, or a boolean false. // Lookup will iterate over a list of given prefixes // in order to recognize valid plugin filenames. // The first filepath to match a prefix is returned. Lookup(filename string) (string, bool) // Execute receives an executable's filepath, a slice // of arguments, and a slice of environment variables // to relay to the executable. Execute(executablePath string, cmdArgs, environment []string) error } // DefaultPluginHandler implements PluginHandler type DefaultPluginHandler struct { ValidPrefixes []string } // NewDefaultPluginHandler instantiates the DefaultPluginHandler with a list of // given filename prefixes used to identify valid plugin filenames. func NewDefaultPluginHandler(validPrefixes []string) *DefaultPluginHandler { return &DefaultPluginHandler{ ValidPrefixes: validPrefixes, } } // Lookup implements PluginHandler func (h *DefaultPluginHandler) Lookup(filename string) (string, bool) { for _, prefix := range h.ValidPrefixes { filename := filepath.Join(ec.PluginsConfig.Paths.BinPath(), fmt.Sprintf("%s-%s", prefix, filename)) path, err := exec.LookPath(filename) if err != nil || len(path) == 0 { continue } return path, true } return "", false } // Execute implements PluginHandler func (h *DefaultPluginHandler) Execute(executablePath string, cmdArgs, environment []string) error { // Windows does not support exec syscall. if runtime.GOOS == "windows" { cmd := exec.Command(executablePath, cmdArgs...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin cmd.Env = environment err := cmd.Run() if err == nil { os.Exit(0) } return err } // invoke cmd binary relaying the environment and args given // append executablePath to cmdArgs, as execve will make first argument the "binary name". return syscall.Exec(executablePath, append([]string{executablePath}, cmdArgs...), environment) } // HandlePluginCommand receives a pluginHandler and command-line arguments and attempts to find // a plugin executable on the PATH that satisfies the given arguments. func HandlePluginCommand(pluginHandler PluginHandler, cmdArgs []string) error { remainingArgs := []string{} // all "non-flag" arguments for idx := range cmdArgs { if strings.HasPrefix(cmdArgs[idx], "-") { break } remainingArgs = append(remainingArgs, strings.Replace(cmdArgs[idx], "-", "_", -1)) } foundBinaryPath := "" // attempt to find binary, starting at longest possible name with given cmdArgs for len(remainingArgs) > 0 { path, found := pluginHandler.Lookup(strings.Join(remainingArgs, "-")) if !found { remainingArgs = remainingArgs[:len(remainingArgs)-1] continue } foundBinaryPath = path break } if len(foundBinaryPath) == 0 { return nil } // invoke cmd binary relaying the current environment and args given if err := pluginHandler.Execute(foundBinaryPath, cmdArgs[len(remainingArgs):], os.Environ()); err != nil { return err } return nil } func ensureDirs(paths ...string) error { for _, p := range paths { if err := os.MkdirAll(p, 0755); err != nil { return errors.Wrapf(err, "failed to ensure create directory %q", p) } } return nil } func indent(s string) string { out := "\\\n" s = strings.TrimRightFunc(s, unicode.IsSpace) out += regexp.MustCompile("(?m)^").ReplaceAllString(s, " | ") out += "\n/" return out } func limitString(s string, length int) string { if len(s) > length && length > 3 { s = s[:length-3] + "..." } return s }