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