graphql-engine/cli/commands/plugins.go

191 lines
5.5 KiB
Go
Raw Normal View History

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 {
cmd.Root().PersistentPreRun(cmd, args)
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
}