github.com/sleungcy-sap/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 }