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