graphql-engine/cli/commands/plugins.go

183 lines
5.8 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"
"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
}