github.com/jenspinney/cli@v6.42.1-0.20190207184520-7450c600020e+incompatible/command/common/install_plugin_command.go (about)

     1  package common
     2  
     3  import (
     4  	"io/ioutil"
     5  	"os"
     6  	"runtime"
     7  	"strings"
     8  
     9  	"code.cloudfoundry.org/cli/actor/actionerror"
    10  	"code.cloudfoundry.org/cli/actor/pluginaction"
    11  	"code.cloudfoundry.org/cli/api/plugin"
    12  	"code.cloudfoundry.org/cli/api/plugin/pluginerror"
    13  	"code.cloudfoundry.org/cli/command"
    14  	"code.cloudfoundry.org/cli/command/flag"
    15  	"code.cloudfoundry.org/cli/command/plugin/shared"
    16  	"code.cloudfoundry.org/cli/command/translatableerror"
    17  	"code.cloudfoundry.org/cli/util"
    18  	"code.cloudfoundry.org/cli/util/configv3"
    19  	log "github.com/sirupsen/logrus"
    20  )
    21  
    22  //go:generate counterfeiter . InstallPluginActor
    23  
    24  type InstallPluginActor interface {
    25  	CreateExecutableCopy(path string, tempPluginDir string) (string, error)
    26  	DownloadExecutableBinaryFromURL(url string, tempPluginDir string, proxyReader plugin.ProxyReader) (string, error)
    27  	FileExists(path string) bool
    28  	GetAndValidatePlugin(metadata pluginaction.PluginMetadata, commands pluginaction.CommandList, path string) (configv3.Plugin, error)
    29  	GetPlatformString(runtimeGOOS string, runtimeGOARCH string) string
    30  	GetPluginInfoFromRepositoriesForPlatform(pluginName string, pluginRepos []configv3.PluginRepository, platform string) (pluginaction.PluginInfo, []string, error)
    31  	GetPluginRepository(repositoryName string) (configv3.PluginRepository, error)
    32  	InstallPluginFromPath(path string, plugin configv3.Plugin) error
    33  	UninstallPlugin(uninstaller pluginaction.PluginUninstaller, name string) error
    34  	ValidateFileChecksum(path string, checksum string) bool
    35  }
    36  
    37  const installConfirmationPrompt = "Do you want to install the plugin {{.Path}}?"
    38  
    39  type cancelInstall struct {
    40  }
    41  
    42  func (cancelInstall) Error() string {
    43  	return "Nobody should see this error. If you do, report it!"
    44  }
    45  
    46  type PluginSource int
    47  
    48  const (
    49  	PluginFromRepository PluginSource = iota
    50  	PluginFromLocalFile
    51  	PluginFromURL
    52  )
    53  
    54  type InstallPluginCommand struct {
    55  	OptionalArgs         flag.InstallPluginArgs `positional-args:"yes"`
    56  	SkipSSLValidation    bool                   `short:"k" hidden:"true" description:"Skip SSL certificate validation"`
    57  	Force                bool                   `short:"f" description:"Force install of plugin without confirmation"`
    58  	RegisteredRepository string                 `short:"r" description:"Restrict search for plugin to this registered repository"`
    59  	usage                interface{}            `usage:"CF_NAME install-plugin PLUGIN_NAME [-r REPO_NAME] [-f]\n   CF_NAME install-plugin LOCAL-PATH/TO/PLUGIN | URL [-f]\n\nWARNING:\n   Plugins are binaries written by potentially untrusted authors.\n   Install and use plugins at your own risk.\n\nEXAMPLES:\n   CF_NAME install-plugin ~/Downloads/plugin-foobar\n   CF_NAME install-plugin https://example.com/plugin-foobar_linux_amd64\n   CF_NAME install-plugin -r My-Repo plugin-echo"`
    60  	relatedCommands      interface{}            `related_commands:"add-plugin-repo, list-plugin-repos, plugins"`
    61  	UI                   command.UI
    62  	Config               command.Config
    63  	Actor                InstallPluginActor
    64  	ProgressBar          plugin.ProxyReader
    65  }
    66  
    67  func (cmd *InstallPluginCommand) Setup(config command.Config, ui command.UI) error {
    68  	cmd.UI = ui
    69  	cmd.Config = config
    70  	cmd.Actor = pluginaction.NewActor(config, shared.NewClient(config, ui, cmd.SkipSSLValidation))
    71  
    72  	cmd.ProgressBar = shared.NewProgressBarProxyReader(cmd.UI.Writer())
    73  
    74  	return nil
    75  }
    76  
    77  func (cmd InstallPluginCommand) Execute([]string) error {
    78  	log.WithField("PluginHome", cmd.Config.PluginHome()).Info("making plugin dir")
    79  
    80  	tempPluginDir, err := ioutil.TempDir(cmd.Config.PluginHome(), "temp")
    81  	log.WithField("tempPluginDir", tempPluginDir).Debug("making tempPluginDir dir")
    82  	defer os.RemoveAll(tempPluginDir)
    83  
    84  	if err != nil {
    85  		return err
    86  	}
    87  
    88  	tempPluginPath, pluginSource, err := cmd.getPluginBinaryAndSource(tempPluginDir)
    89  	if _, ok := err.(cancelInstall); ok {
    90  		cmd.UI.DisplayText("Plugin installation cancelled.")
    91  		return nil
    92  	} else if err != nil {
    93  		return err
    94  	}
    95  	log.WithFields(log.Fields{"tempPluginPath": tempPluginPath, "pluginSource": pluginSource}).Debug("getPluginBinaryAndSource")
    96  
    97  	// copy twice when downloading from a URL to keep Windows specific code
    98  	// isolated to CreateExecutableCopy
    99  	executablePath, err := cmd.Actor.CreateExecutableCopy(tempPluginPath, tempPluginDir)
   100  	if err != nil {
   101  		return err
   102  	}
   103  	log.WithField("executablePath", executablePath).Debug("created executable copy")
   104  
   105  	rpcService, err := shared.NewRPCService(cmd.Config, cmd.UI)
   106  	if err != nil {
   107  		return err
   108  	}
   109  	log.Info("started RPC server")
   110  
   111  	plugin, err := cmd.Actor.GetAndValidatePlugin(rpcService, Commands, executablePath)
   112  	if err != nil {
   113  		return err
   114  	}
   115  	log.Info("validated plugin")
   116  
   117  	if installedPlugin, installed := cmd.Config.GetPluginCaseInsensitive(plugin.Name); installed {
   118  		log.WithField("version", installedPlugin.Version).Debug("uninstall plugin")
   119  
   120  		if !cmd.Force && pluginSource != PluginFromRepository {
   121  			return translatableerror.PluginAlreadyInstalledError{
   122  				BinaryName: cmd.Config.BinaryName(),
   123  				Name:       plugin.Name,
   124  				Version:    plugin.Version.String(),
   125  			}
   126  		}
   127  
   128  		err = cmd.uninstallPlugin(installedPlugin, rpcService)
   129  		if err != nil {
   130  			return err
   131  		}
   132  	}
   133  
   134  	log.Info("install plugin")
   135  	return cmd.installPlugin(plugin, executablePath)
   136  }
   137  
   138  func (cmd InstallPluginCommand) installPlugin(plugin configv3.Plugin, pluginPath string) error {
   139  	cmd.UI.DisplayTextWithFlavor("Installing plugin {{.Name}}...", map[string]interface{}{
   140  		"Name": plugin.Name,
   141  	})
   142  
   143  	installErr := cmd.Actor.InstallPluginFromPath(pluginPath, plugin)
   144  	if installErr != nil {
   145  		return installErr
   146  	}
   147  
   148  	cmd.UI.DisplayOK()
   149  	cmd.UI.DisplayText("Plugin {{.Name}} {{.Version}} successfully installed.", map[string]interface{}{
   150  		"Name":    plugin.Name,
   151  		"Version": plugin.Version.String(),
   152  	})
   153  	return nil
   154  }
   155  
   156  func (cmd InstallPluginCommand) uninstallPlugin(plugin configv3.Plugin, rpcService *shared.RPCService) error {
   157  	cmd.UI.DisplayText("Plugin {{.Name}} {{.Version}} is already installed. Uninstalling existing plugin...", map[string]interface{}{
   158  		"Name":    plugin.Name,
   159  		"Version": plugin.Version.String(),
   160  	})
   161  
   162  	uninstallErr := cmd.Actor.UninstallPlugin(rpcService, plugin.Name)
   163  	if uninstallErr != nil {
   164  		return uninstallErr
   165  	}
   166  
   167  	cmd.UI.DisplayOK()
   168  	cmd.UI.DisplayText("Plugin {{.Name}} successfully uninstalled.", map[string]interface{}{
   169  		"Name": plugin.Name,
   170  	})
   171  
   172  	return nil
   173  }
   174  
   175  func (cmd InstallPluginCommand) getPluginBinaryAndSource(tempPluginDir string) (string, PluginSource, error) {
   176  	pluginNameOrLocation := cmd.OptionalArgs.PluginNameOrLocation.String()
   177  
   178  	switch {
   179  	case cmd.RegisteredRepository != "":
   180  		log.WithField("RegisteredRepository", cmd.RegisteredRepository).Info("installing from specified repository")
   181  		pluginRepository, err := cmd.Actor.GetPluginRepository(cmd.RegisteredRepository)
   182  		if err != nil {
   183  			return "", 0, err
   184  		}
   185  		path, pluginSource, err := cmd.getPluginFromRepositories(pluginNameOrLocation, []configv3.PluginRepository{pluginRepository}, tempPluginDir)
   186  
   187  		if err != nil {
   188  			switch pluginErr := err.(type) {
   189  			case actionerror.PluginNotFoundInAnyRepositoryError:
   190  				return "", 0, translatableerror.PluginNotFoundInRepositoryError{
   191  					BinaryName:     cmd.Config.BinaryName(),
   192  					PluginName:     pluginNameOrLocation,
   193  					RepositoryName: cmd.RegisteredRepository,
   194  				}
   195  
   196  			case actionerror.FetchingPluginInfoFromRepositoryError:
   197  				// The error wrapped inside pluginErr is handled differently in the case of
   198  				// a specified repo from that of searching through all repos.  pluginErr.Err
   199  				// is then processed by shared.HandleError by this function's caller.
   200  				return "", 0, pluginErr.Err
   201  
   202  			default:
   203  				return "", 0, err
   204  			}
   205  		}
   206  		return path, pluginSource, nil
   207  
   208  	case cmd.Actor.FileExists(pluginNameOrLocation):
   209  		log.WithField("pluginNameOrLocation", pluginNameOrLocation).Info("installing from specified file")
   210  		return cmd.getPluginFromLocalFile(pluginNameOrLocation)
   211  
   212  	case util.IsHTTPScheme(pluginNameOrLocation):
   213  		log.WithField("pluginNameOrLocation", pluginNameOrLocation).Info("installing from specified URL")
   214  		return cmd.getPluginFromURL(pluginNameOrLocation, tempPluginDir)
   215  
   216  	case util.IsUnsupportedURLScheme(pluginNameOrLocation):
   217  		log.WithField("pluginNameOrLocation", pluginNameOrLocation).Error("Unsupported URL")
   218  		return "", 0, translatableerror.UnsupportedURLSchemeError{UnsupportedURL: pluginNameOrLocation}
   219  
   220  	default:
   221  		log.Info("installing from first repository with plugin")
   222  		repos := cmd.Config.PluginRepositories()
   223  		if len(repos) == 0 {
   224  			return "", 0, translatableerror.PluginNotFoundOnDiskOrInAnyRepositoryError{PluginName: pluginNameOrLocation, BinaryName: cmd.Config.BinaryName()}
   225  		}
   226  
   227  		path, pluginSource, err := cmd.getPluginFromRepositories(pluginNameOrLocation, repos, tempPluginDir)
   228  		if err != nil {
   229  			switch pluginErr := err.(type) {
   230  			case actionerror.PluginNotFoundInAnyRepositoryError:
   231  				return "", 0, translatableerror.PluginNotFoundOnDiskOrInAnyRepositoryError{PluginName: pluginNameOrLocation, BinaryName: cmd.Config.BinaryName()}
   232  
   233  			case actionerror.FetchingPluginInfoFromRepositoryError:
   234  				return "", 0, cmd.handleFetchingPluginInfoFromRepositoriesError(pluginErr)
   235  
   236  			default:
   237  				return "", 0, err
   238  			}
   239  		}
   240  		return path, pluginSource, nil
   241  	}
   242  }
   243  
   244  // These are specific errors that we output to the user in the context of
   245  // installing from any repository.
   246  func (InstallPluginCommand) handleFetchingPluginInfoFromRepositoriesError(fetchErr actionerror.FetchingPluginInfoFromRepositoryError) error {
   247  	switch clientErr := fetchErr.Err.(type) {
   248  	case pluginerror.RawHTTPStatusError:
   249  		return translatableerror.FetchingPluginInfoFromRepositoriesError{
   250  			Message:        clientErr.Status,
   251  			RepositoryName: fetchErr.RepositoryName,
   252  		}
   253  
   254  	case pluginerror.SSLValidationHostnameError:
   255  		return translatableerror.FetchingPluginInfoFromRepositoriesError{
   256  			Message:        clientErr.Error(),
   257  			RepositoryName: fetchErr.RepositoryName,
   258  		}
   259  
   260  	case pluginerror.UnverifiedServerError:
   261  		return translatableerror.FetchingPluginInfoFromRepositoriesError{
   262  			Message:        clientErr.Error(),
   263  			RepositoryName: fetchErr.RepositoryName,
   264  		}
   265  
   266  	default:
   267  		return clientErr
   268  	}
   269  }
   270  
   271  func (cmd InstallPluginCommand) getPluginFromLocalFile(pluginLocation string) (string, PluginSource, error) {
   272  	err := cmd.installPluginPrompt(installConfirmationPrompt, map[string]interface{}{
   273  		"Path": pluginLocation,
   274  	})
   275  	if err != nil {
   276  		return "", 0, err
   277  	}
   278  
   279  	return pluginLocation, PluginFromLocalFile, err
   280  }
   281  
   282  func (cmd InstallPluginCommand) getPluginFromURL(pluginLocation string, tempPluginDir string) (string, PluginSource, error) {
   283  	err := cmd.installPluginPrompt(installConfirmationPrompt, map[string]interface{}{
   284  		"Path": pluginLocation,
   285  	})
   286  	if err != nil {
   287  		return "", 0, err
   288  	}
   289  
   290  	cmd.UI.DisplayText("Starting download of plugin binary from URL...")
   291  
   292  	tempPath, err := cmd.Actor.DownloadExecutableBinaryFromURL(pluginLocation, tempPluginDir, cmd.ProgressBar)
   293  	if err != nil {
   294  		return "", 0, err
   295  	}
   296  
   297  	return tempPath, PluginFromURL, err
   298  }
   299  
   300  func (cmd InstallPluginCommand) getPluginFromRepositories(pluginName string, repos []configv3.PluginRepository, tempPluginDir string) (string, PluginSource, error) {
   301  	var repoNames []string
   302  	for _, repo := range repos {
   303  		repoNames = append(repoNames, repo.Name)
   304  	}
   305  
   306  	cmd.UI.DisplayTextWithFlavor("Searching {{.RepositoryName}} for plugin {{.PluginName}}...", map[string]interface{}{
   307  		"RepositoryName": strings.Join(repoNames, ", "),
   308  		"PluginName":     pluginName,
   309  	})
   310  
   311  	currentPlatform := cmd.Actor.GetPlatformString(runtime.GOOS, runtime.GOARCH)
   312  	pluginInfo, repoList, err := cmd.Actor.GetPluginInfoFromRepositoriesForPlatform(pluginName, repos, currentPlatform)
   313  	if err != nil {
   314  		return "", 0, err
   315  	}
   316  
   317  	cmd.UI.DisplayText("Plugin {{.PluginName}} {{.PluginVersion}} found in: {{.RepositoryName}}", map[string]interface{}{
   318  		"PluginName":     pluginName,
   319  		"PluginVersion":  pluginInfo.Version,
   320  		"RepositoryName": strings.Join(repoList, ", "),
   321  	})
   322  
   323  	installedPlugin, exist := cmd.Config.GetPlugin(pluginName)
   324  	if exist {
   325  		cmd.UI.DisplayText("Plugin {{.PluginName}} {{.PluginVersion}} is already installed.", map[string]interface{}{
   326  			"PluginName":    installedPlugin.Name,
   327  			"PluginVersion": installedPlugin.Version.String(),
   328  		})
   329  
   330  		err = cmd.installPluginPrompt("Do you want to uninstall the existing plugin and install {{.Path}} {{.PluginVersion}}?", map[string]interface{}{
   331  			"Path":          pluginName,
   332  			"PluginVersion": pluginInfo.Version,
   333  		})
   334  	} else {
   335  		err = cmd.installPluginPrompt(installConfirmationPrompt, map[string]interface{}{
   336  			"Path": pluginName,
   337  		})
   338  	}
   339  
   340  	if err != nil {
   341  		return "", 0, err
   342  	}
   343  
   344  	cmd.UI.DisplayText("Starting download of plugin binary from repository {{.RepositoryName}}...", map[string]interface{}{
   345  		"RepositoryName": repoList[0],
   346  	})
   347  
   348  	tempPath, err := cmd.Actor.DownloadExecutableBinaryFromURL(pluginInfo.URL, tempPluginDir, cmd.ProgressBar)
   349  	if err != nil {
   350  		return "", 0, err
   351  	}
   352  
   353  	if !cmd.Actor.ValidateFileChecksum(tempPath, pluginInfo.Checksum) {
   354  		return "", 0, translatableerror.InvalidChecksumError{}
   355  	}
   356  
   357  	return tempPath, PluginFromRepository, err
   358  }
   359  
   360  func (cmd InstallPluginCommand) installPluginPrompt(template string, templateValues ...map[string]interface{}) error {
   361  	cmd.UI.DisplayHeader("Attention: Plugins are binaries written by potentially untrusted authors.")
   362  	cmd.UI.DisplayHeader("Install and use plugins at your own risk.")
   363  
   364  	if cmd.Force {
   365  		return nil
   366  	}
   367  
   368  	var (
   369  		really    bool
   370  		promptErr error
   371  	)
   372  
   373  	really, promptErr = cmd.UI.DisplayBoolPrompt(false, template, templateValues...)
   374  
   375  	if promptErr != nil {
   376  		return promptErr
   377  	}
   378  
   379  	if !really {
   380  		log.Debug("plugin confirmation - 'no' inputed")
   381  		return cancelInstall{}
   382  	}
   383  
   384  	return nil
   385  }