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 }