github.com/dcarley/cf-cli@v6.24.1-0.20170220111324-4225ff346898+incompatible/cf/commands/plugin/install_plugin.go (about)

     1  package plugin
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"net/rpc"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  
    11  	"code.cloudfoundry.org/cli/cf/actors/plugininstaller"
    12  	"code.cloudfoundry.org/cli/cf/actors/pluginrepo"
    13  	"code.cloudfoundry.org/cli/cf/commandregistry"
    14  	"code.cloudfoundry.org/cli/cf/configuration/coreconfig"
    15  	"code.cloudfoundry.org/cli/cf/configuration/pluginconfig"
    16  	"code.cloudfoundry.org/cli/cf/flags"
    17  	. "code.cloudfoundry.org/cli/cf/i18n"
    18  	"code.cloudfoundry.org/cli/cf/requirements"
    19  	"code.cloudfoundry.org/cli/cf/terminal"
    20  	"code.cloudfoundry.org/cli/plugin"
    21  	"code.cloudfoundry.org/cli/util"
    22  	"code.cloudfoundry.org/cli/util/downloader"
    23  	"code.cloudfoundry.org/gofileutils/fileutils"
    24  
    25  	pluginRPCService "code.cloudfoundry.org/cli/plugin/rpc"
    26  )
    27  
    28  type PluginInstall struct {
    29  	ui           terminal.UI
    30  	config       coreconfig.Reader
    31  	pluginConfig pluginconfig.PluginConfiguration
    32  	pluginRepo   pluginrepo.PluginRepo
    33  	checksum     util.Sha1Checksum
    34  	rpcService   *pluginRPCService.CliRpcService
    35  }
    36  
    37  func init() {
    38  	commandregistry.Register(&PluginInstall{})
    39  }
    40  
    41  func (cmd *PluginInstall) MetaData() commandregistry.CommandMetadata {
    42  	fs := make(map[string]flags.FlagSet)
    43  	fs["r"] = &flags.StringFlag{ShortName: "r", Usage: T("Name of a registered repository where the specified plugin is located")}
    44  	fs["f"] = &flags.BoolFlag{ShortName: "f", Usage: T("Force install of plugin without confirmation")}
    45  
    46  	return commandregistry.CommandMetadata{
    47  		Name:        "install-plugin",
    48  		Description: T("Install CLI plugin"),
    49  		Usage: []string{
    50  			T(`CF_NAME install-plugin (LOCAL-PATH/TO/PLUGIN | URL | -r REPO_NAME PLUGIN_NAME) [-f]
    51  
    52     Prompts for confirmation unless '-f' is provided.`),
    53  		},
    54  		Examples: []string{
    55  			"CF_NAME install-plugin ~/Downloads/plugin-foobar",
    56  			"CF_NAME install-plugin https://example.com/plugin-foobar_linux_amd64",
    57  			"CF_NAME install-plugin -r My-Repo plugin-echo",
    58  		},
    59  		Flags:     fs,
    60  		TotalArgs: 1,
    61  	}
    62  }
    63  
    64  func (cmd *PluginInstall) Requirements(requirementsFactory requirements.Factory, fc flags.FlagContext) ([]requirements.Requirement, error) {
    65  	if len(fc.Args()) != 1 {
    66  		cmd.ui.Failed(T("Incorrect Usage. Requires an argument\n\n") + commandregistry.Commands.CommandUsage("install-plugin"))
    67  		return nil, fmt.Errorf("Incorrect usage: %d arguments of %d required", len(fc.Args()), 1)
    68  	}
    69  
    70  	reqs := []requirements.Requirement{}
    71  	return reqs, nil
    72  }
    73  
    74  func (cmd *PluginInstall) SetDependency(deps commandregistry.Dependency, pluginCall bool) commandregistry.Command {
    75  	cmd.ui = deps.UI
    76  	cmd.config = deps.Config
    77  	cmd.pluginConfig = deps.PluginConfig
    78  	cmd.pluginRepo = deps.PluginRepo
    79  	cmd.checksum = deps.ChecksumUtil
    80  
    81  	//reset rpc registration in case there is other running instance,
    82  	//each service can only be registered once
    83  	server := rpc.NewServer()
    84  
    85  	rpcService, err := pluginRPCService.NewRpcService(deps.TeePrinter, deps.TeePrinter, deps.Config, deps.RepoLocator, pluginRPCService.NewCommandRunner(), deps.Logger, cmd.ui.Writer(), server)
    86  	if err != nil {
    87  		cmd.ui.Failed("Error initializing RPC service: " + err.Error())
    88  	}
    89  
    90  	cmd.rpcService = rpcService
    91  
    92  	return cmd
    93  }
    94  
    95  func (cmd *PluginInstall) Execute(c flags.FlagContext) error {
    96  	if !cmd.confirmWithUser(
    97  		c,
    98  		T("**Attention: Plugins are binaries written by potentially untrusted authors. Install and use plugins at your own risk.**\n\nDo you want to install the plugin {{.Plugin}}?",
    99  			map[string]interface{}{
   100  				"Plugin": c.Args()[0],
   101  			}),
   102  	) {
   103  		return errors.New(T("Plugin installation cancelled"))
   104  	}
   105  
   106  	fileDownloader := downloader.NewDownloader(os.TempDir())
   107  
   108  	removeTmpFile := func() {
   109  		err := fileDownloader.RemoveFile()
   110  		if err != nil {
   111  			cmd.ui.Say(T("Problem removing downloaded binary in temp directory: ") + err.Error())
   112  		}
   113  	}
   114  	defer removeTmpFile()
   115  
   116  	deps := &plugininstaller.Context{
   117  		Checksummer:    cmd.checksum,
   118  		GetPluginRepos: cmd.config.PluginRepos,
   119  		FileDownloader: fileDownloader,
   120  		PluginRepo:     cmd.pluginRepo,
   121  		RepoName:       c.String("r"),
   122  		UI:             cmd.ui,
   123  	}
   124  	installer := plugininstaller.NewPluginInstaller(deps)
   125  	pluginSourceFilepath := installer.Install(c.Args()[0])
   126  
   127  	_, pluginExecutableName := filepath.Split(pluginSourceFilepath)
   128  
   129  	cmd.ui.Say(T(
   130  		"Installing plugin {{.PluginPath}}...",
   131  		map[string]interface{}{
   132  			"PluginPath": pluginExecutableName,
   133  		}),
   134  	)
   135  
   136  	pluginDestinationFilepath := filepath.Join(cmd.pluginConfig.GetPluginPath(), pluginExecutableName)
   137  
   138  	err := cmd.ensurePluginBinaryWithSameFileNameDoesNotAlreadyExist(pluginDestinationFilepath, pluginExecutableName)
   139  	if err != nil {
   140  		return err
   141  	}
   142  
   143  	pluginMetadata, err := cmd.runBinaryAndObtainPluginMetadata(pluginSourceFilepath)
   144  	if err != nil {
   145  		return err
   146  	}
   147  
   148  	err = cmd.ensurePluginIsSafeForInstallation(pluginMetadata, pluginDestinationFilepath, pluginSourceFilepath)
   149  	if err != nil {
   150  		return err
   151  	}
   152  
   153  	err = cmd.installPlugin(pluginMetadata, pluginDestinationFilepath, pluginSourceFilepath)
   154  	if err != nil {
   155  		return err
   156  	}
   157  
   158  	cmd.ui.Ok()
   159  	cmd.ui.Say(T(
   160  		"Plugin {{.PluginName}} v{{.Version}} successfully installed.",
   161  		map[string]interface{}{
   162  			"PluginName": pluginMetadata.Name,
   163  			"Version":    fmt.Sprintf("%d.%d.%d", pluginMetadata.Version.Major, pluginMetadata.Version.Minor, pluginMetadata.Version.Build),
   164  		}),
   165  	)
   166  	return nil
   167  }
   168  
   169  func (cmd *PluginInstall) confirmWithUser(c flags.FlagContext, prompt string) bool {
   170  	return c.Bool("f") || cmd.ui.Confirm(prompt)
   171  }
   172  
   173  func (cmd *PluginInstall) ensurePluginBinaryWithSameFileNameDoesNotAlreadyExist(pluginDestinationFilepath, pluginExecutableName string) error {
   174  	_, err := os.Stat(pluginDestinationFilepath)
   175  	if err == nil || os.IsExist(err) {
   176  		return errors.New(T(
   177  			"The file {{.PluginExecutableName}} already exists under the plugin directory.\n",
   178  			map[string]interface{}{
   179  				"PluginExecutableName": pluginExecutableName,
   180  			}),
   181  		)
   182  	} else if !os.IsNotExist(err) {
   183  		return errors.New(T(
   184  			"Unexpected error has occurred:\n{{.Error}}",
   185  			map[string]interface{}{
   186  				"Error": err.Error(),
   187  			}),
   188  		)
   189  	}
   190  	return nil
   191  }
   192  
   193  func (cmd *PluginInstall) ensurePluginIsSafeForInstallation(pluginMetadata *plugin.PluginMetadata, pluginDestinationFilepath string, pluginSourceFilepath string) error {
   194  	plugins := cmd.pluginConfig.Plugins()
   195  	if pluginMetadata.Name == "" {
   196  		return errors.New(T(
   197  			"Unable to obtain plugin name for executable {{.Executable}}",
   198  			map[string]interface{}{
   199  				"Executable": pluginSourceFilepath,
   200  			}),
   201  		)
   202  	}
   203  
   204  	if _, ok := plugins[pluginMetadata.Name]; ok {
   205  		return errors.New(T(
   206  			"Plugin name {{.PluginName}} is already taken",
   207  			map[string]interface{}{
   208  				"PluginName": pluginMetadata.Name,
   209  			}),
   210  		)
   211  	}
   212  
   213  	if pluginMetadata.Commands == nil {
   214  		return errors.New(T(
   215  			"Error getting command list from plugin {{.FilePath}}",
   216  			map[string]interface{}{
   217  				"FilePath": pluginSourceFilepath,
   218  			}),
   219  		)
   220  	}
   221  
   222  	for _, pluginCmd := range pluginMetadata.Commands {
   223  		//check for command conflicting core commands/alias
   224  		if pluginCmd.Name == "help" || commandregistry.Commands.CommandExists(pluginCmd.Name) {
   225  			return errors.New(T(
   226  				"Command `{{.Command}}` in the plugin being installed is a native CF command/alias.  Rename the `{{.Command}}` command in the plugin being installed in order to enable its installation and use.",
   227  				map[string]interface{}{
   228  					"Command": pluginCmd.Name,
   229  				}),
   230  			)
   231  		}
   232  
   233  		//check for alias conflicting core command/alias
   234  		if pluginCmd.Alias == "help" || commandregistry.Commands.CommandExists(pluginCmd.Alias) {
   235  			return errors.New(T(
   236  				"Alias `{{.Command}}` in the plugin being installed is a native CF command/alias.  Rename the `{{.Command}}` command in the plugin being installed in order to enable its installation and use.",
   237  				map[string]interface{}{
   238  					"Command": pluginCmd.Alias,
   239  				}),
   240  			)
   241  		}
   242  
   243  		for installedPluginName, installedPlugin := range plugins {
   244  			for _, installedPluginCmd := range installedPlugin.Commands {
   245  
   246  				//check for command conflicting other plugin commands/alias
   247  				if installedPluginCmd.Name == pluginCmd.Name || installedPluginCmd.Alias == pluginCmd.Name {
   248  					return errors.New(T(
   249  						"Command `{{.Command}}` is a command/alias in plugin '{{.PluginName}}'.  You could try uninstalling plugin '{{.PluginName}}' and then install this plugin in order to invoke the `{{.Command}}` command.  However, you should first fully understand the impact of uninstalling the existing '{{.PluginName}}' plugin.",
   250  						map[string]interface{}{
   251  							"Command":    pluginCmd.Name,
   252  							"PluginName": installedPluginName,
   253  						}),
   254  					)
   255  				}
   256  
   257  				//check for alias conflicting other plugin commands/alias
   258  				if pluginCmd.Alias != "" && (installedPluginCmd.Name == pluginCmd.Alias || installedPluginCmd.Alias == pluginCmd.Alias) {
   259  					return errors.New(T(
   260  						"Alias `{{.Command}}` is a command/alias in plugin '{{.PluginName}}'.  You could try uninstalling plugin '{{.PluginName}}' and then install this plugin in order to invoke the `{{.Command}}` command.  However, you should first fully understand the impact of uninstalling the existing '{{.PluginName}}' plugin.",
   261  						map[string]interface{}{
   262  							"Command":    pluginCmd.Alias,
   263  							"PluginName": installedPluginName,
   264  						}),
   265  					)
   266  				}
   267  			}
   268  		}
   269  	}
   270  	return nil
   271  }
   272  
   273  func (cmd *PluginInstall) installPlugin(pluginMetadata *plugin.PluginMetadata, pluginDestinationFilepath, pluginSourceFilepath string) error {
   274  	err := fileutils.CopyPathToPath(pluginSourceFilepath, pluginDestinationFilepath)
   275  	if err != nil {
   276  		return errors.New(T(
   277  			"Could not copy plugin binary: \n{{.Error}}",
   278  			map[string]interface{}{
   279  				"Error": err.Error(),
   280  			}),
   281  		)
   282  	}
   283  
   284  	configMetadata := pluginconfig.PluginMetadata{
   285  		Location: pluginDestinationFilepath,
   286  		Version:  pluginMetadata.Version,
   287  		Commands: pluginMetadata.Commands,
   288  	}
   289  
   290  	cmd.pluginConfig.SetPlugin(pluginMetadata.Name, configMetadata)
   291  	return nil
   292  }
   293  
   294  func (cmd *PluginInstall) runBinaryAndObtainPluginMetadata(pluginSourceFilepath string) (*plugin.PluginMetadata, error) {
   295  	err := cmd.rpcService.Start()
   296  	if err != nil {
   297  		return nil, err
   298  	}
   299  	defer cmd.rpcService.Stop()
   300  
   301  	err = cmd.runPluginBinary(pluginSourceFilepath, cmd.rpcService.Port())
   302  	if err != nil {
   303  		return nil, err
   304  	}
   305  
   306  	c := cmd.rpcService.RpcCmd
   307  	c.MetadataMutex.RLock()
   308  	defer c.MetadataMutex.RUnlock()
   309  	return c.PluginMetadata, nil
   310  }
   311  
   312  func (cmd *PluginInstall) runPluginBinary(location string, servicePort string) error {
   313  	pluginInvocation := exec.Command(location, servicePort, "SendMetadata")
   314  
   315  	err := pluginInvocation.Run()
   316  	if err != nil {
   317  		return err
   318  	}
   319  	return nil
   320  }