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" "runtime" "strings" "syscall" "github.com/hasura/graphql-engine/cli/v2" "github.com/hasura/graphql-engine/cli/v2/internal/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: "The functionality of the CLI can be extended by using plugins. For a list of all available plugins, run ``hasura plugins list``, or visit this repository: https://github.com/hasura/cli-plugins-index.\n\nIf you're interested in contributing, please open a PR against this repo to add new plugin.", SilenceUsage: true, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { op := genOpName(cmd, "PersistentPreRunE") cmd.Root().PersistentPreRun(cmd, args) // setup plugins path err := ec.SetupPlugins() if err != nil { return errors.E(op, fmt.Errorf("setting up plugins path failed: %w", err)) } if err := ec.PluginsConfig.Repo.EnsureCloned(); err != nil { return errors.E(op, fmt.Errorf("pulling latest plugins list from internet failed: %w", err)) } return nil }, } 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 { var op errors.Op = "commands.DefaultPluginHandler.Execute" // 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 errors.E(op, err) } // invoke cmd binary relaying the environment and args given // append executablePath to cmdArgs, as execve will make first argument the "binary name". if err := syscall.Exec(executablePath, append([]string{executablePath}, cmdArgs...), environment); err != nil { return errors.E(op, err) } return nil } // 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 { var op errors.Op = "commands.HandlePluginCommand" 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 errors.E(op, err) } return nil } func limitString(s string, length int) string { if len(s) > length && length > 3 { s = s[:length-3] + "..." } return s }