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 }