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