github.com/cloudfoundry/cli@v7.1.0+incompatible/command/common/install_plugin_command.go (about)

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