github.com/jenkins-x/jx/v2@v2.1.155/pkg/cmd/cmd.go (about)

     1  /*
     2  Copyright 2018 The Kubernetes Authors & The Jenkins X Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package cmd
    18  
    19  import (
    20  	"fmt"
    21  	"io"
    22  	"io/ioutil"
    23  	"os"
    24  	"os/exec"
    25  	"path/filepath"
    26  	"runtime"
    27  	"strconv"
    28  	"strings"
    29  	"syscall"
    30  
    31  	"github.com/jenkins-x/jx/v2/pkg/cmd/deprecation"
    32  	"github.com/jenkins-x/jx/v2/pkg/cmd/experimental"
    33  	"github.com/jenkins-x/jx/v2/pkg/cmd/profile"
    34  	"github.com/jenkins-x/jx/v2/pkg/cmd/ui"
    35  
    36  	version2 "github.com/jenkins-x/jx/v2/pkg/cmd/version"
    37  
    38  	"github.com/spf13/viper"
    39  
    40  	"github.com/jenkins-x/jx/v2/pkg/cmd/boot"
    41  	"github.com/jenkins-x/jx/v2/pkg/cmd/compliance"
    42  	"github.com/jenkins-x/jx/v2/pkg/cmd/controller"
    43  	"github.com/jenkins-x/jx/v2/pkg/cmd/create"
    44  	"github.com/jenkins-x/jx/v2/pkg/cmd/deletecmd"
    45  	"github.com/jenkins-x/jx/v2/pkg/cmd/edit"
    46  	"github.com/jenkins-x/jx/v2/pkg/cmd/gc"
    47  	"github.com/jenkins-x/jx/v2/pkg/cmd/get"
    48  	"github.com/jenkins-x/jx/v2/pkg/cmd/importcmd"
    49  	"github.com/jenkins-x/jx/v2/pkg/cmd/initcmd"
    50  	"github.com/jenkins-x/jx/v2/pkg/cmd/preview"
    51  	"github.com/jenkins-x/jx/v2/pkg/cmd/rsh"
    52  	"github.com/jenkins-x/jx/v2/pkg/cmd/start"
    53  	"github.com/jenkins-x/jx/v2/pkg/cmd/stop"
    54  	"github.com/jenkins-x/jx/v2/pkg/cmd/sync"
    55  	"github.com/jenkins-x/jx/v2/pkg/cmd/uninstall"
    56  	"github.com/jenkins-x/jx/v2/pkg/cmd/update"
    57  	"github.com/jenkins-x/jx/v2/pkg/cmd/upgrade"
    58  
    59  	"github.com/jenkins-x/jx/v2/pkg/cmd/add"
    60  	"github.com/jenkins-x/jx/v2/pkg/cmd/namespace"
    61  	"github.com/jenkins-x/jx/v2/pkg/cmd/promote"
    62  
    63  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    64  
    65  	"github.com/jenkins-x/jx/v2/pkg/extensions"
    66  
    67  	"github.com/jenkins-x/jx-logging/pkg/log"
    68  	"github.com/jenkins-x/jx/v2/pkg/features"
    69  	"github.com/jenkins-x/jx/v2/pkg/util"
    70  
    71  	"github.com/jenkins-x/jx/v2/pkg/cmd/clients"
    72  	"github.com/jenkins-x/jx/v2/pkg/cmd/opts"
    73  	"github.com/jenkins-x/jx/v2/pkg/cmd/templates"
    74  	"github.com/jenkins-x/jx/v2/pkg/version"
    75  	"github.com/spf13/cobra"
    76  	"gopkg.in/AlecAivazis/survey.v1/terminal"
    77  )
    78  
    79  // NewJXCommand creates the `jx` command and its nested children.
    80  // args used to determine binary plugin to run can be overridden (does not affect compiled in commands).
    81  func NewJXCommand(f clients.Factory, in terminal.FileReader, out terminal.FileWriter,
    82  	err io.Writer, args []string) *cobra.Command {
    83  
    84  	configureViper()
    85  	rootCommand := &cobra.Command{
    86  		Use:              "jx",
    87  		Short:            "jx is a command line tool for working with Jenkins X",
    88  		PersistentPreRun: setLoggingLevel,
    89  		Run:              runHelp,
    90  	}
    91  
    92  	features.Init()
    93  
    94  	commonOpts := opts.NewCommonOptionsWithTerm(f, in, out, err)
    95  	commonOpts.AddBaseFlags(rootCommand)
    96  
    97  	addCommands := add.NewCmdAdd(commonOpts)
    98  	createCommands := create.NewCmdCreate(commonOpts)
    99  	deleteCommands := deletecmd.NewCmdDelete(commonOpts)
   100  
   101  	getCommands := get.NewCmdGet(commonOpts)
   102  	editCommands := edit.NewCmdEdit(commonOpts)
   103  	updateCommands := update.NewCmdUpdate(commonOpts)
   104  
   105  	installCommands := []*cobra.Command{
   106  		profile.NewCmdProfile(commonOpts),
   107  		boot.NewCmdBoot(commonOpts),
   108  		create.NewCmdInstall(commonOpts),
   109  		uninstall.NewCmdUninstall(commonOpts),
   110  		upgrade.NewCmdUpgrade(commonOpts),
   111  	}
   112  	installCommands = append(installCommands, findCommands("cluster", createCommands, deleteCommands)...)
   113  	installCommands = append(installCommands, findCommands("cluster", updateCommands)...)
   114  	installCommands = append(installCommands, findCommands("jenkins token", createCommands, deleteCommands)...)
   115  	installCommands = append(installCommands, initcmd.NewCmdInit(commonOpts))
   116  
   117  	addProjectCommands := []*cobra.Command{
   118  		importcmd.NewCmdImport(commonOpts),
   119  	}
   120  	addProjectCommands = append(addProjectCommands, findCommands("create spring", createCommands, deleteCommands)...)
   121  	addProjectCommands = append(addProjectCommands, findCommands("create quickstart", createCommands, deleteCommands)...)
   122  
   123  	gitCommands := []*cobra.Command{}
   124  	gitCommands = append(gitCommands, findCommands("git server", createCommands, deleteCommands)...)
   125  	gitCommands = append(gitCommands, findCommands("git token", createCommands, deleteCommands)...)
   126  	gitCommands = append(gitCommands, NewCmdRepo(commonOpts))
   127  
   128  	addonCommands := []*cobra.Command{}
   129  	addonCommands = append(addonCommands, findCommands("addon", createCommands, deleteCommands)...)
   130  	addonCommands = append(addonCommands, findCommands("app", createCommands, deleteCommands, addCommands)...)
   131  
   132  	environmentsCommands := []*cobra.Command{
   133  		preview.NewCmdPreview(commonOpts),
   134  		promote.NewCmdPromote(commonOpts),
   135  	}
   136  	environmentsCommands = append(environmentsCommands, findCommands("environment", createCommands, deleteCommands, editCommands, getCommands)...)
   137  
   138  	groups := templates.CommandGroups{
   139  		{
   140  			Message:  "Installing:",
   141  			Commands: installCommands,
   142  		},
   143  		{
   144  			Message:  "Adding Projects to Jenkins X:",
   145  			Commands: addProjectCommands,
   146  		},
   147  		{
   148  			Message:  "Apps:",
   149  			Commands: addonCommands,
   150  		},
   151  		{
   152  			Message:  "Git:",
   153  			Commands: gitCommands,
   154  		},
   155  		{
   156  			Message: "Working with Kubernetes:",
   157  			Commands: []*cobra.Command{
   158  				compliance.NewCompliance(commonOpts),
   159  				NewCmdCompletion(commonOpts),
   160  				NewCmdContext(commonOpts),
   161  				NewCmdEnvironment(commonOpts),
   162  				NewCmdTeam(commonOpts),
   163  				namespace.NewCmdNamespace(commonOpts),
   164  				NewCmdPrompt(commonOpts),
   165  				NewCmdScan(commonOpts),
   166  				NewCmdShell(commonOpts),
   167  				NewCmdStatus(commonOpts),
   168  			},
   169  		},
   170  		{
   171  			Message: "Working with Applications:",
   172  			Commands: []*cobra.Command{
   173  				NewCmdLogs(commonOpts),
   174  				NewCmdOpen(commonOpts),
   175  				rsh.NewCmdRsh(commonOpts),
   176  				sync.NewCmdSync(commonOpts),
   177  			},
   178  		},
   179  		{
   180  			Message:  "Working with Environments:",
   181  			Commands: environmentsCommands,
   182  		},
   183  		{
   184  			Message: "Working with Jenkins X resources:",
   185  			Commands: []*cobra.Command{
   186  				getCommands,
   187  				editCommands,
   188  				createCommands,
   189  				updateCommands,
   190  				deleteCommands,
   191  				addCommands,
   192  				start.NewCmdStart(commonOpts),
   193  				stop.NewCmdStop(commonOpts),
   194  			},
   195  		},
   196  		{
   197  			Message: "Jenkins X Pipeline Commands:",
   198  			Commands: []*cobra.Command{
   199  				NewCmdStep(commonOpts),
   200  			},
   201  		},
   202  		{
   203  			Message: "Jenkins X services:",
   204  			Commands: []*cobra.Command{
   205  				controller.NewCmdController(commonOpts),
   206  				gc.NewCmdGC(commonOpts),
   207  			},
   208  		},
   209  		{
   210  			Message: "Working with Jenkins X UI:",
   211  			Commands: []*cobra.Command{
   212  				ui.NewCmdUI(commonOpts),
   213  			},
   214  		},
   215  	}
   216  
   217  	groups.Add(rootCommand)
   218  
   219  	filters := []string{"options"}
   220  
   221  	getPluginCommandGroups := func() (templates.PluginCommandGroups, bool) {
   222  		verifier := &extensions.CommandOverrideVerifier{
   223  			Root:        rootCommand,
   224  			SeenPlugins: make(map[string]string, 0),
   225  		}
   226  		pluginCommandGroups, managedPluginsEnabled, err := commonOpts.GetPluginCommandGroups(verifier)
   227  		if err != nil {
   228  			log.Logger().Errorf("%v", err)
   229  		}
   230  		return pluginCommandGroups, managedPluginsEnabled
   231  	}
   232  	templates.ActsAsRootCommand(rootCommand, filters, getPluginCommandGroups, groups...)
   233  	rootCommand.AddCommand(NewCmdDocs(commonOpts))
   234  	rootCommand.AddCommand(version2.NewCmdVersion(commonOpts))
   235  	rootCommand.Version = version.GetVersion()
   236  	rootCommand.SetVersionTemplate("{{printf .Version}}\n Deprecated will be removed on July 1, 2020. Please use version instead\n")
   237  	rootCommand.AddCommand(NewCmdOptions(out))
   238  	rootCommand.AddCommand(NewCmdDiagnose(commonOpts))
   239  
   240  	// Mark the deprecated commands
   241  	deprecation.DeprecateCommands(rootCommand)
   242  
   243  	// Mark the experimental commands
   244  	experimental.AlphaCommands(rootCommand)
   245  	experimental.BetaCommands(rootCommand)
   246  
   247  	managedPlugins := &managedPluginHandler{
   248  		CommonOptions: commonOpts,
   249  	}
   250  	localPlugins := &localPluginHandler{}
   251  
   252  	if len(args) == 0 {
   253  		args = os.Args
   254  	}
   255  	if len(args) > 1 {
   256  		cmdPathPieces := args[1:]
   257  
   258  		// only look for suitable executables if
   259  		// the specified command does not already exist
   260  		if _, _, err := rootCommand.Find(cmdPathPieces); err != nil {
   261  			if _, managedPluginsEnabled := getPluginCommandGroups(); managedPluginsEnabled {
   262  				if err := handleEndpointExtensions(managedPlugins, cmdPathPieces); err != nil {
   263  					log.Logger().Errorf("%v", err)
   264  					os.Exit(1)
   265  				}
   266  			} else {
   267  				if err := handleEndpointExtensions(localPlugins, cmdPathPieces); err != nil {
   268  					log.Logger().Errorf("%v", err)
   269  					os.Exit(1)
   270  				}
   271  			}
   272  
   273  		}
   274  	}
   275  	return rootCommand
   276  }
   277  
   278  func configureViper() {
   279  	replacer := strings.NewReplacer("-", "_")
   280  	viper.SetEnvKeyReplacer(replacer)
   281  }
   282  
   283  func findCommands(subCommand string, commands ...*cobra.Command) []*cobra.Command {
   284  	answer := []*cobra.Command{}
   285  	for _, parent := range commands {
   286  		for _, c := range parent.Commands() {
   287  			if commandHasParentName(c, subCommand) {
   288  				answer = append(answer, c)
   289  			} else {
   290  				childCommands := findCommands(subCommand, c)
   291  				if len(childCommands) > 0 {
   292  					answer = append(answer, childCommands...)
   293  				}
   294  			}
   295  		}
   296  	}
   297  	return answer
   298  }
   299  
   300  func commandHasParentName(command *cobra.Command, name string) bool {
   301  	path := fullPath(command)
   302  	return strings.Contains(path, name)
   303  }
   304  
   305  func fullPath(command *cobra.Command) string {
   306  	name := command.Name()
   307  	parent := command.Parent()
   308  	if parent != nil {
   309  		return fullPath(parent) + " " + name
   310  	}
   311  	return name
   312  }
   313  
   314  func setLoggingLevel(cmd *cobra.Command, args []string) {
   315  	verbose, err := strconv.ParseBool(cmd.Flag(opts.OptionVerbose).Value.String())
   316  	if err != nil {
   317  		log.Logger().Errorf("Unable to check if the verbose flag is set")
   318  	}
   319  
   320  	level := os.Getenv("JX_LOG_LEVEL")
   321  	if level != "" {
   322  		if verbose {
   323  			log.Logger().Trace("The JX_LOG_LEVEL environment variable took precedence over the verbose flag")
   324  		}
   325  
   326  		err := log.SetLevel(level)
   327  		if err != nil {
   328  			log.Logger().Errorf("Unable to set log level to %s", level)
   329  		}
   330  	} else {
   331  		if verbose {
   332  			err := log.SetLevel("debug")
   333  			if err != nil {
   334  				log.Logger().Errorf("Unable to set log level to debug")
   335  			}
   336  		} else {
   337  			err := log.SetLevel("info")
   338  			if err != nil {
   339  				log.Logger().Errorf("Unable to set log level to info")
   340  			}
   341  		}
   342  	}
   343  }
   344  
   345  func runHelp(cmd *cobra.Command, args []string) {
   346  	cmd.Help() //nolint:errcheck
   347  }
   348  
   349  // PluginHandler is capable of parsing command line arguments
   350  // and performing executable filename lookups to search
   351  // for valid plugin files, and execute found plugins.
   352  type PluginHandler interface {
   353  	// Lookup receives a potential filename and returns
   354  	// a full or relative path to an executable, if one
   355  	// exists at the given filename, or an error.
   356  	Lookup(filename string) (string, error)
   357  	// Execute receives an executable's filepath, a slice
   358  	// of arguments, and a slice of environment variables
   359  	// to relay to the executable.
   360  	Execute(executablePath string, cmdArgs, environment []string) error
   361  }
   362  
   363  type managedPluginHandler struct {
   364  	*opts.CommonOptions
   365  	localPluginHandler
   366  }
   367  
   368  // Lookup implements PluginHandler
   369  func (h *managedPluginHandler) Lookup(filename string) (string, error) {
   370  	jxClient, ns, err := h.JXClientAndDevNamespace()
   371  	if err != nil {
   372  		return "", err
   373  	}
   374  
   375  	possibles, err := jxClient.JenkinsV1().Plugins(ns).List(metav1.ListOptions{
   376  		LabelSelector: fmt.Sprintf("%s=%s", extensions.PluginCommandLabel, filename),
   377  	})
   378  	if err != nil {
   379  		return "", err
   380  	}
   381  	if len(possibles.Items) > 0 {
   382  		found := possibles.Items[0]
   383  		if len(possibles.Items) > 1 {
   384  			// There is a warning about this when you install extensions as well
   385  			log.Logger().Warnf("More than one plugin installed for %s by apps. Selecting the one installed by %s at random.",
   386  				filename, found.Name)
   387  
   388  		}
   389  		return extensions.EnsurePluginInstalled(found)
   390  	}
   391  	return h.localPluginHandler.Lookup(filename)
   392  }
   393  
   394  // Execute implements PluginHandler
   395  func (h *managedPluginHandler) Execute(executablePath string, cmdArgs, environment []string) error {
   396  	return h.localPluginHandler.Execute(executablePath, cmdArgs, environment)
   397  }
   398  
   399  type localPluginHandler struct{}
   400  
   401  // Lookup implements PluginHandler
   402  func (h *localPluginHandler) Lookup(filename string) (string, error) {
   403  	// if on Windows, append the "exe" extension
   404  	// to the filename that we are looking up.
   405  	if runtime.GOOS == "windows" {
   406  		filename = filename + ".exe"
   407  	}
   408  
   409  	return exec.LookPath(filename)
   410  }
   411  
   412  // Execute implements PluginHandler
   413  func (h *localPluginHandler) Execute(executablePath string, cmdArgs, environment []string) error {
   414  	return syscall.Exec(executablePath, cmdArgs, environment)
   415  }
   416  
   417  func handleEndpointExtensions(pluginHandler PluginHandler, cmdArgs []string) error {
   418  	remainingArgs := []string{} // all "non-flag" arguments
   419  
   420  	for idx := range cmdArgs {
   421  		if strings.HasPrefix(cmdArgs[idx], "-") {
   422  			break
   423  		}
   424  		remainingArgs = append(remainingArgs, strings.Replace(cmdArgs[idx], "-", "_", -1))
   425  	}
   426  
   427  	foundBinaryPath := ""
   428  
   429  	pluginDir, err := util.PluginBinDir("jx")
   430  	if err != nil {
   431  		log.Logger().Debugf("failed to find plugin dir %s", err.Error())
   432  	}
   433  
   434  	// attempt to find binary, starting at longest possible name with given cmdArgs
   435  	for len(remainingArgs) > 0 {
   436  		commandName := fmt.Sprintf("jx-%s", strings.Join(remainingArgs, "-"))
   437  		path, err := pluginHandler.Lookup(commandName)
   438  		if err != nil || len(path) == 0 {
   439  			// lets see if we have previously downloaded this binary plugin
   440  			path = FindPluginBinary(pluginDir, commandName)
   441  			if path != "" {
   442  				foundBinaryPath = path
   443  				break
   444  			}
   445  
   446  			/* Usually "executable file not found in $PATH", spams output of jx help subcommand:
   447  			if err != nil {
   448  				log.Logger().Errorf("Error installing plugin for command %s. %v\n", remainingArgs, err)
   449  			}
   450  			*/
   451  			remainingArgs = remainingArgs[:len(remainingArgs)-1]
   452  			continue
   453  		}
   454  
   455  		foundBinaryPath = path
   456  		break
   457  	}
   458  
   459  	if len(foundBinaryPath) == 0 {
   460  		return nil
   461  	}
   462  
   463  	// invoke cmd binary relaying the current environment and args given
   464  	// remainingArgs will always have at least one element.
   465  	// execve will make remainingArgs[0] the "binary name".
   466  	if err := pluginHandler.Execute(foundBinaryPath, append([]string{foundBinaryPath}, cmdArgs[len(remainingArgs):]...), os.Environ()); err != nil {
   467  		return err
   468  	}
   469  
   470  	return nil
   471  }
   472  
   473  // FindPluginBinary tries to find the jx-foo binary plugin in the plugins dir `~/.jx/plugins/jx/bin` dir `
   474  func FindPluginBinary(pluginDir string, commandName string) string {
   475  	if pluginDir != "" {
   476  		files, err := ioutil.ReadDir(pluginDir)
   477  		if err != nil {
   478  			log.Logger().Debugf("failed to read plugin dir %s", err.Error())
   479  		} else {
   480  			prefix := commandName + "-"
   481  			for _, f := range files {
   482  				name := f.Name()
   483  				if strings.HasPrefix(name, prefix) {
   484  					path := filepath.Join(pluginDir, name)
   485  					log.Logger().Debugf("found plugin %s at %s", commandName, path)
   486  					return path
   487  				}
   488  			}
   489  		}
   490  	}
   491  	return ""
   492  }