github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/cmd/plugin.go (about) 1 package cmd 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "strings" 9 "sync" 10 "time" 11 12 "github.com/gosuri/uiprogress" 13 "github.com/spf13/cobra" 14 "github.com/spf13/viper" 15 "github.com/turbot/go-kit/helpers" 16 "github.com/turbot/steampipe-plugin-sdk/v5/sperr" 17 "github.com/turbot/steampipe/pkg/cmdconfig" 18 "github.com/turbot/steampipe/pkg/constants" 19 "github.com/turbot/steampipe/pkg/contexthelpers" 20 "github.com/turbot/steampipe/pkg/db/db_local" 21 "github.com/turbot/steampipe/pkg/display" 22 "github.com/turbot/steampipe/pkg/error_helpers" 23 "github.com/turbot/steampipe/pkg/installationstate" 24 "github.com/turbot/steampipe/pkg/ociinstaller" 25 "github.com/turbot/steampipe/pkg/ociinstaller/versionfile" 26 "github.com/turbot/steampipe/pkg/plugin" 27 "github.com/turbot/steampipe/pkg/statushooks" 28 "github.com/turbot/steampipe/pkg/steampipeconfig" 29 "github.com/turbot/steampipe/pkg/steampipeconfig/modconfig" 30 "github.com/turbot/steampipe/pkg/utils" 31 ) 32 33 type installedPlugin struct { 34 Name string `json:"name"` 35 Version string `json:"version"` 36 Connections []string `json:"connections"` 37 } 38 39 type failedPlugin struct { 40 Name string `json:"name"` 41 Reason string `json:"reason"` 42 Connections []string `json:"connections"` 43 } 44 45 type pluginJsonOutput struct { 46 Installed []installedPlugin `json:"installed"` 47 Failed []failedPlugin `json:"failed"` 48 Warnings []string `json:"warnings"` 49 } 50 51 // Plugin management commands 52 func pluginCmd() *cobra.Command { 53 var cmd = &cobra.Command{ 54 Use: "plugin [command]", 55 Args: cobra.NoArgs, 56 Short: "Steampipe plugin management", 57 Long: `Steampipe plugin management. 58 59 Plugins extend Steampipe to work with many different services and providers. 60 Find plugins using the public registry at https://hub.steampipe.io. 61 62 Examples: 63 64 # Install a plugin 65 steampipe plugin install aws 66 67 # Update a plugin 68 steampipe plugin update aws 69 70 # List installed plugins 71 steampipe plugin list 72 73 # Uninstall a plugin 74 steampipe plugin uninstall aws`, 75 PersistentPostRun: func(cmd *cobra.Command, args []string) { 76 utils.LogTime("cmd.plugin.PersistentPostRun start") 77 defer utils.LogTime("cmd.plugin.PersistentPostRun end") 78 plugin.CleanupOldTmpDirs(cmd.Context()) 79 }, 80 } 81 cmd.AddCommand(pluginInstallCmd()) 82 cmd.AddCommand(pluginListCmd()) 83 cmd.AddCommand(pluginUninstallCmd()) 84 cmd.AddCommand(pluginUpdateCmd()) 85 cmd.Flags().BoolP(constants.ArgHelp, "h", false, "Help for plugin") 86 87 return cmd 88 } 89 90 // Install a plugin 91 func pluginInstallCmd() *cobra.Command { 92 var cmd = &cobra.Command{ 93 Use: "install [flags] [registry/org/]name[@version]", 94 Args: cobra.ArbitraryArgs, 95 Run: runPluginInstallCmd, 96 Short: "Install one or more plugins", 97 Long: `Install one or more plugins. 98 99 Install a Steampipe plugin, making it available for queries and configuration. 100 The plugin name format is [registry/org/]name[@version]. The default 101 registry is hub.steampipe.io, default org is turbot and default version 102 is latest. The name is a required argument. 103 104 Examples: 105 106 # Install all missing plugins that are specified in configuration files 107 steampipe plugin install 108 109 # Install a common plugin (turbot/aws) 110 steampipe plugin install aws 111 112 # Install a specific plugin version 113 steampipe plugin install turbot/azure@0.1.0 114 115 # Hide progress bars during installation 116 steampipe plugin install --progress=false aws 117 118 # Skip creation of default plugin config file 119 steampipe plugin install --skip-config aws`, 120 } 121 122 cmdconfig. 123 OnCmd(cmd). 124 AddBoolFlag(constants.ArgProgress, true, "Display installation progress"). 125 AddBoolFlag(constants.ArgSkipConfig, false, "Skip creating the default config file for plugin"). 126 AddBoolFlag(constants.ArgHelp, false, "Help for plugin install", cmdconfig.FlagOptions.WithShortHand("h")) 127 return cmd 128 } 129 130 // Update plugins 131 func pluginUpdateCmd() *cobra.Command { 132 var cmd = &cobra.Command{ 133 Use: "update [flags] [registry/org/]name[@version]", 134 Args: cobra.ArbitraryArgs, 135 Run: runPluginUpdateCmd, 136 Short: "Update one or more plugins", 137 Long: `Update plugins. 138 139 Update one or more Steampipe plugins, making it available for queries and configuration. 140 The plugin name format is [registry/org/]name[@version]. The default 141 registry is hub.steampipe.io, default org is turbot and default version 142 is latest. The name is a required argument. 143 144 Examples: 145 146 # Update all plugins to their latest available version 147 steampipe plugin update --all 148 149 # Update a common plugin (turbot/aws) 150 steampipe plugin update aws 151 152 # Hide progress bars during update 153 steampipe plugin update --progress=false aws`, 154 } 155 156 cmdconfig. 157 OnCmd(cmd). 158 AddBoolFlag(constants.ArgAll, false, "Update all plugins to its latest available version"). 159 AddBoolFlag(constants.ArgProgress, true, "Display installation progress"). 160 AddBoolFlag(constants.ArgHelp, false, "Help for plugin update", cmdconfig.FlagOptions.WithShortHand("h")) 161 162 return cmd 163 } 164 165 // List plugins 166 func pluginListCmd() *cobra.Command { 167 var cmd = &cobra.Command{ 168 Use: "list", 169 Args: cobra.NoArgs, 170 Run: runPluginListCmd, 171 Short: "List currently installed plugins", 172 Long: `List currently installed plugins. 173 174 List all Steampipe plugins installed for this user. 175 176 Examples: 177 178 # List installed plugins 179 steampipe plugin list 180 181 # List plugins that have updates available 182 steampipe plugin list --outdated 183 184 # List plugins output in json 185 steampipe plugin list --output json`, 186 } 187 188 cmdconfig. 189 OnCmd(cmd). 190 AddBoolFlag("outdated", false, "Check each plugin in the list for updates"). 191 AddStringFlag(constants.ArgOutput, "table", "Output format: table or json"). 192 AddBoolFlag(constants.ArgHelp, false, "Help for plugin list", cmdconfig.FlagOptions.WithShortHand("h")) 193 return cmd 194 } 195 196 // Uninstall a plugin 197 func pluginUninstallCmd() *cobra.Command { 198 var cmd = &cobra.Command{ 199 Use: "uninstall [flags] [registry/org/]name", 200 Args: cobra.ArbitraryArgs, 201 Run: runPluginUninstallCmd, 202 Short: "Uninstall a plugin", 203 Long: `Uninstall a plugin. 204 205 Uninstall a Steampipe plugin, removing it from use. The plugin name format is 206 [registry/org/]name. (Version is not relevant in uninstall, since only one 207 version of a plugin can be installed at a time.) 208 209 Example: 210 211 # Uninstall a common plugin (turbot/aws) 212 steampipe plugin uninstall aws 213 214 `, 215 } 216 217 cmdconfig.OnCmd(cmd). 218 AddBoolFlag(constants.ArgHelp, false, "Help for plugin uninstall", cmdconfig.FlagOptions.WithShortHand("h")) 219 220 return cmd 221 } 222 223 var pluginInstallSteps = []string{ 224 "Downloading", 225 "Installing Plugin", 226 "Installing Docs", 227 "Installing Config", 228 "Updating Steampipe", 229 "Done", 230 } 231 232 func runPluginInstallCmd(cmd *cobra.Command, args []string) { 233 ctx := cmd.Context() 234 utils.LogTime("runPluginInstallCmd install") 235 defer func() { 236 utils.LogTime("runPluginInstallCmd end") 237 if r := recover(); r != nil { 238 error_helpers.ShowError(ctx, helpers.ToError(r)) 239 exitCode = constants.ExitCodeUnknownErrorPanic 240 } 241 }() 242 243 // args to 'plugin install' -- one or more plugins to install 244 // plugin names can be simple names for "standard" plugins, constraint suffixed names 245 // or full refs to the OCI image 246 // - aws 247 // - aws@0.118.0 248 // - aws@^0.118 249 // - ghcr.io/turbot/steampipe/plugins/turbot/aws:1.0.0 250 plugins := append([]string{}, args...) 251 showProgress := viper.GetBool(constants.ArgProgress) 252 installReports := make(display.PluginInstallReports, 0, len(plugins)) 253 254 if len(plugins) == 0 { 255 if len(steampipeconfig.GlobalConfig.Plugins) == 0 { 256 error_helpers.ShowError(ctx, sperr.New("No connections or plugins configured")) 257 exitCode = constants.ExitCodeInsufficientOrWrongInputs 258 return 259 } 260 261 // get the list of plugins to install 262 for imageRef := range steampipeconfig.GlobalConfig.Plugins { 263 ref := ociinstaller.NewSteampipeImageRef(imageRef) 264 plugins = append(plugins, ref.GetFriendlyName()) 265 } 266 } 267 268 state, err := installationstate.Load() 269 if err != nil { 270 error_helpers.ShowError(ctx, fmt.Errorf("could not load state")) 271 exitCode = constants.ExitCodePluginLoadingError 272 return 273 } 274 275 // a leading blank line - since we always output multiple lines 276 fmt.Println() 277 progressBars := uiprogress.New() 278 installWaitGroup := &sync.WaitGroup{} 279 reportChannel := make(chan *display.PluginInstallReport, len(plugins)) 280 281 if showProgress { 282 progressBars.Start() 283 } 284 for _, pluginName := range plugins { 285 installWaitGroup.Add(1) 286 bar := createProgressBar(pluginName, progressBars) 287 288 ref := ociinstaller.NewSteampipeImageRef(pluginName) 289 org, name, constraint := ref.GetOrgNameAndConstraint() 290 orgAndName := fmt.Sprintf("%s/%s", org, name) 291 var resolved plugin.ResolvedPluginVersion 292 if ref.IsFromSteampipeHub() { 293 rpv, err := plugin.GetLatestPluginVersionByConstraint(ctx, state.InstallationID, org, name, constraint) 294 if err != nil || rpv == nil { 295 report := &display.PluginInstallReport{ 296 Plugin: pluginName, 297 Skipped: true, 298 SkipReason: constants.InstallMessagePluginNotFound, 299 IsUpdateReport: false, 300 } 301 reportChannel <- report 302 installWaitGroup.Done() 303 continue 304 } 305 resolved = *rpv 306 } else { 307 resolved = plugin.NewResolvedPluginVersion(orgAndName, constraint, constraint) 308 } 309 310 go doPluginInstall(ctx, bar, pluginName, resolved, installWaitGroup, reportChannel) 311 } 312 go func() { 313 installWaitGroup.Wait() 314 close(reportChannel) 315 }() 316 installCount := 0 317 for report := range reportChannel { 318 installReports = append(installReports, report) 319 if !report.Skipped { 320 installCount++ 321 } else if !(report.Skipped && report.SkipReason == "Already installed") { 322 exitCode = constants.ExitCodePluginInstallFailure 323 } 324 } 325 if showProgress { 326 progressBars.Stop() 327 } 328 329 if installCount > 0 { 330 // TODO do we need to refresh connections here 331 332 // reload the config, since an installation should have created a new config file 333 var cmd = viper.Get(constants.ConfigKeyActiveCommand).(*cobra.Command) 334 config, errorsAndWarnings := steampipeconfig.LoadSteampipeConfig(ctx, viper.GetString(constants.ArgModLocation), cmd.Name()) 335 if errorsAndWarnings.GetError() != nil { 336 error_helpers.ShowWarning(fmt.Sprintf("Failed to reload config - install report may be incomplete (%s)", errorsAndWarnings.GetError())) 337 } else { 338 steampipeconfig.GlobalConfig = config 339 } 340 341 statushooks.Done(ctx) 342 } 343 display.PrintInstallReports(installReports, false) 344 345 // a concluding blank line - since we always output multiple lines 346 fmt.Println() 347 } 348 349 func doPluginInstall(ctx context.Context, bar *uiprogress.Bar, pluginName string, resolvedPlugin plugin.ResolvedPluginVersion, wg *sync.WaitGroup, returnChannel chan *display.PluginInstallReport) { 350 var report *display.PluginInstallReport 351 352 pluginAlreadyInstalled, _ := plugin.Exists(ctx, pluginName) 353 if pluginAlreadyInstalled { 354 // set the bar to MAX 355 //nolint:golint,errcheck // the error happens if we set this over the max value 356 bar.Set(len(pluginInstallSteps)) 357 // let the bar append itself with "Already Installed" 358 bar.AppendFunc(func(b *uiprogress.Bar) string { 359 return helpers.Resize(constants.InstallMessagePluginAlreadyInstalled, 20) 360 }) 361 report = &display.PluginInstallReport{ 362 Plugin: pluginName, 363 Skipped: true, 364 SkipReason: constants.InstallMessagePluginAlreadyInstalled, 365 IsUpdateReport: false, 366 } 367 } else { 368 // let the bar append itself with the current installation step 369 bar.AppendFunc(func(b *uiprogress.Bar) string { 370 if report != nil && report.SkipReason == constants.InstallMessagePluginNotFound { 371 return helpers.Resize(constants.InstallMessagePluginNotFound, 20) 372 } else { 373 if b.Current() == 0 { 374 // no install step to display yet 375 return "" 376 } 377 return helpers.Resize(pluginInstallSteps[b.Current()-1], 20) 378 } 379 }) 380 381 report = installPlugin(ctx, resolvedPlugin, false, bar) 382 } 383 returnChannel <- report 384 wg.Done() 385 } 386 387 func runPluginUpdateCmd(cmd *cobra.Command, args []string) { 388 ctx := cmd.Context() 389 utils.LogTime("runPluginUpdateCmd start") 390 defer func() { 391 utils.LogTime("runPluginUpdateCmd end") 392 if r := recover(); r != nil { 393 error_helpers.ShowError(ctx, helpers.ToError(r)) 394 exitCode = constants.ExitCodeUnknownErrorPanic 395 } 396 }() 397 398 // args to 'plugin update' -- one or more plugins to update 399 // These can be simple names for "standard" plugins, constraint suffixed names 400 // or full refs to the OCI image 401 // - aws 402 // - aws@0.118.0 403 // - aws@^0.118 404 // - ghcr.io/turbot/steampipe/plugins/turbot/aws:1.0.0 405 plugins, err := resolveUpdatePluginsFromArgs(args) 406 showProgress := viper.GetBool(constants.ArgProgress) 407 408 if err != nil { 409 fmt.Println() 410 error_helpers.ShowError(ctx, err) 411 fmt.Println() 412 cmd.Help() 413 fmt.Println() 414 exitCode = constants.ExitCodeInsufficientOrWrongInputs 415 return 416 } 417 418 if len(plugins) > 0 && !(cmdconfig.Viper().GetBool(constants.ArgAll)) && plugins[0] == constants.ArgAll { 419 // improve the response to wrong argument "steampipe plugin update all" 420 fmt.Println() 421 exitCode = constants.ExitCodeInsufficientOrWrongInputs 422 error_helpers.ShowError(ctx, fmt.Errorf("Did you mean %s?", constants.Bold("--all"))) 423 fmt.Println() 424 return 425 } 426 427 state, err := installationstate.Load() 428 if err != nil { 429 error_helpers.ShowError(ctx, fmt.Errorf("could not load state")) 430 exitCode = constants.ExitCodePluginLoadingError 431 return 432 } 433 434 // retrieve the plugin version data from steampipe config 435 pluginVersions := steampipeconfig.GlobalConfig.PluginVersions 436 437 var runUpdatesFor []*versionfile.InstalledVersion 438 updateResults := make(display.PluginInstallReports, 0, len(plugins)) 439 440 // a leading blank line - since we always output multiple lines 441 fmt.Println() 442 443 if cmdconfig.Viper().GetBool(constants.ArgAll) { 444 for k, v := range pluginVersions { 445 ref := ociinstaller.NewSteampipeImageRef(k) 446 org, name, constraint := ref.GetOrgNameAndConstraint() 447 key := fmt.Sprintf("%s/%s@%s", org, name, constraint) 448 449 plugins = append(plugins, key) 450 runUpdatesFor = append(runUpdatesFor, v) 451 } 452 } else { 453 // get the args and retrieve the installed versions 454 for _, p := range plugins { 455 ref := ociinstaller.NewSteampipeImageRef(p) 456 isExists, _ := plugin.Exists(ctx, p) 457 if isExists { 458 if strings.HasPrefix(ref.DisplayImageRef(), constants.SteampipeHubOCIBase) { 459 runUpdatesFor = append(runUpdatesFor, pluginVersions[ref.DisplayImageRef()]) 460 } else { 461 error_helpers.ShowError(ctx, fmt.Errorf("cannot check updates for plugins not distributed via hub.steampipe.io, you should uninstall then reinstall the plugin to get the latest version")) 462 exitCode = constants.ExitCodePluginLoadingError 463 return 464 } 465 } else { 466 exitCode = constants.ExitCodePluginNotFound 467 updateResults = append(updateResults, &display.PluginInstallReport{ 468 Skipped: true, 469 Plugin: p, 470 SkipReason: constants.InstallMessagePluginNotInstalled, 471 IsUpdateReport: true, 472 }) 473 } 474 } 475 } 476 477 if len(plugins) == len(updateResults) { 478 // we have report for all 479 // this may happen if all given plugins are 480 // not installed 481 display.PrintInstallReports(updateResults, true) 482 fmt.Println() 483 return 484 } 485 486 timeoutCtx, cancel := context.WithTimeout(ctx, 30*time.Second) 487 defer cancel() 488 489 statushooks.SetStatus(ctx, "Checking for available updates") 490 reports := plugin.GetUpdateReport(timeoutCtx, state.InstallationID, runUpdatesFor) 491 statushooks.Done(ctx) 492 if len(reports) == 0 { 493 // this happens if for some reason the update server could not be contacted, 494 // in which case we get back an empty map 495 error_helpers.ShowError(ctx, fmt.Errorf("there was an issue contacting the update server, please try later")) 496 exitCode = constants.ExitCodePluginLoadingError 497 return 498 } 499 500 updateWaitGroup := &sync.WaitGroup{} 501 reportChannel := make(chan *display.PluginInstallReport, len(reports)) 502 progressBars := uiprogress.New() 503 if showProgress { 504 progressBars.Start() 505 } 506 507 sorted := utils.SortedMapKeys(reports) 508 for _, key := range sorted { 509 report := reports[key] 510 updateWaitGroup.Add(1) 511 bar := createProgressBar(report.ShortNameWithConstraint(), progressBars) 512 go doPluginUpdate(ctx, bar, report, updateWaitGroup, reportChannel) 513 } 514 go func() { 515 updateWaitGroup.Wait() 516 close(reportChannel) 517 }() 518 installCount := 0 519 520 for updateResult := range reportChannel { 521 updateResults = append(updateResults, updateResult) 522 if !updateResult.Skipped { 523 installCount++ 524 } 525 } 526 if showProgress { 527 progressBars.Stop() 528 } 529 530 display.PrintInstallReports(updateResults, true) 531 532 // a concluding blank line - since we always output multiple lines 533 fmt.Println() 534 } 535 536 func doPluginUpdate(ctx context.Context, bar *uiprogress.Bar, pvr plugin.VersionCheckReport, wg *sync.WaitGroup, returnChannel chan *display.PluginInstallReport) { 537 var report *display.PluginInstallReport 538 539 if plugin.UpdateRequired(pvr) { 540 // update required, resolve version and install update 541 bar.AppendFunc(func(b *uiprogress.Bar) string { 542 // set the progress bar to append itself with the step underway 543 if b.Current() == 0 { 544 // no install step to display yet 545 return "" 546 } 547 return helpers.Resize(pluginInstallSteps[b.Current()-1], 20) 548 }) 549 rp := plugin.NewResolvedPluginVersion(pvr.ShortName(), pvr.CheckResponse.Version, pvr.CheckResponse.Constraint) 550 report = installPlugin(ctx, rp, true, bar) 551 } else { 552 // update NOT required, return already installed report 553 bar.AppendFunc(func(b *uiprogress.Bar) string { 554 // set the progress bar to append itself with "Already Installed" 555 return helpers.Resize(constants.InstallMessagePluginLatestAlreadyInstalled, 30) 556 }) 557 // set the progress bar to the maximum 558 bar.Set(len(pluginInstallSteps)) 559 report = &display.PluginInstallReport{ 560 Plugin: fmt.Sprintf("%s@%s", pvr.CheckResponse.Name, pvr.CheckResponse.Constraint), 561 Skipped: true, 562 SkipReason: constants.InstallMessagePluginLatestAlreadyInstalled, 563 IsUpdateReport: true, 564 } 565 } 566 567 returnChannel <- report 568 wg.Done() 569 } 570 571 func createProgressBar(plugin string, parentProgressBars *uiprogress.Progress) *uiprogress.Bar { 572 bar := parentProgressBars.AddBar(len(pluginInstallSteps)) 573 bar.PrependFunc(func(b *uiprogress.Bar) string { 574 return helpers.Resize(plugin, 30) 575 }) 576 return bar 577 } 578 579 func installPlugin(ctx context.Context, resolvedPlugin plugin.ResolvedPluginVersion, isUpdate bool, bar *uiprogress.Bar) *display.PluginInstallReport { 580 // start a channel for progress publications from plugin.Install 581 progress := make(chan struct{}, 5) 582 defer func() { 583 // close the progress channel 584 close(progress) 585 }() 586 go func() { 587 for { 588 // wait for a message on the progress channel 589 <-progress 590 // increment the progress bar 591 bar.Incr() 592 } 593 }() 594 595 image, err := plugin.Install(ctx, resolvedPlugin, progress, ociinstaller.WithSkipConfig(viper.GetBool(constants.ArgSkipConfig))) 596 if err != nil { 597 msg := "" 598 // used to build data for the plugin install report to be used for display purposes 599 _, name, constraint := ociinstaller.NewSteampipeImageRef(resolvedPlugin.GetVersionTag()).GetOrgNameAndConstraint() 600 if isPluginNotFoundErr(err) { 601 exitCode = constants.ExitCodePluginNotFound 602 msg = constants.InstallMessagePluginNotFound 603 } else { 604 msg = err.Error() 605 } 606 return &display.PluginInstallReport{ 607 Plugin: fmt.Sprintf("%s@%s", name, constraint), 608 Skipped: true, 609 SkipReason: msg, 610 IsUpdateReport: isUpdate, 611 } 612 } 613 614 // used to build data for the plugin install report to be used for display purposes 615 org, name, _ := image.ImageRef.GetOrgNameAndConstraint() 616 versionString := "" 617 if image.Config.Plugin.Version != "" { 618 versionString = " v" + image.Config.Plugin.Version 619 } 620 docURL := fmt.Sprintf("https://hub.steampipe.io/plugins/%s/%s", org, name) 621 if !image.ImageRef.IsFromSteampipeHub() { 622 docURL = fmt.Sprintf("https://%s/%s", org, name) 623 } 624 return &display.PluginInstallReport{ 625 Plugin: fmt.Sprintf("%s@%s", name, resolvedPlugin.Constraint), 626 Skipped: false, 627 Version: versionString, 628 DocURL: docURL, 629 IsUpdateReport: isUpdate, 630 } 631 } 632 633 func isPluginNotFoundErr(err error) bool { 634 return strings.HasSuffix(err.Error(), "not found") 635 } 636 637 func resolveUpdatePluginsFromArgs(args []string) ([]string, error) { 638 plugins := append([]string{}, args...) 639 640 if len(plugins) == 0 && !(cmdconfig.Viper().GetBool("all")) { 641 // either plugin name(s) or "all" must be provided 642 return nil, fmt.Errorf("you need to provide at least one plugin to update or use the %s flag", constants.Bold("--all")) 643 } 644 645 if len(plugins) > 0 && cmdconfig.Viper().GetBool(constants.ArgAll) { 646 // we can't allow update and install at the same time 647 return nil, fmt.Errorf("%s cannot be used when updating specific plugins", constants.Bold("`--all`")) 648 } 649 650 return plugins, nil 651 } 652 653 func runPluginListCmd(cmd *cobra.Command, _ []string) { 654 // setup a cancel context and start cancel handler 655 ctx, cancel := context.WithCancel(cmd.Context()) 656 contexthelpers.StartCancelHandler(cancel) 657 outputFormat := viper.GetString(constants.ArgOutput) 658 659 utils.LogTime("runPluginListCmd list") 660 defer func() { 661 utils.LogTime("runPluginListCmd end") 662 if r := recover(); r != nil { 663 error_helpers.ShowError(ctx, helpers.ToError(r)) 664 exitCode = constants.ExitCodeUnknownErrorPanic 665 } 666 }() 667 668 pluginList, failedPluginMap, missingPluginMap, res := getPluginList(ctx) 669 if res.Error != nil { 670 error_helpers.ShowErrorWithMessage(ctx, res.Error, "plugin listing failed") 671 exitCode = constants.ExitCodePluginListFailure 672 return 673 } 674 675 err := showPluginListOutput(pluginList, failedPluginMap, missingPluginMap, res, outputFormat) 676 if err != nil { 677 error_helpers.ShowError(ctx, err) 678 } 679 680 } 681 682 func showPluginListOutput(pluginList []plugin.PluginListItem, failedPluginMap, missingPluginMap map[string][]*modconfig.Connection, res error_helpers.ErrorAndWarnings, outputFormat string) error { 683 switch outputFormat { 684 case "table": 685 return showPluginListAsTable(pluginList, failedPluginMap, missingPluginMap, res) 686 case "json": 687 return showPluginListAsJSON(pluginList, failedPluginMap, missingPluginMap, res) 688 default: 689 return errors.New("invalid output format") 690 } 691 } 692 693 func showPluginListAsTable(pluginList []plugin.PluginListItem, failedPluginMap, missingPluginMap map[string][]*modconfig.Connection, res error_helpers.ErrorAndWarnings) error { 694 headers := []string{"Installed", "Version", "Connections"} 695 var rows [][]string 696 // List installed plugins in a table 697 if len(pluginList) != 0 { 698 for _, item := range pluginList { 699 rows = append(rows, []string{item.Name, item.Version.String(), strings.Join(item.Connections, ",")}) 700 } 701 } else { 702 rows = append(rows, []string{"", "", ""}) 703 } 704 display.ShowWrappedTable(headers, rows, &display.ShowWrappedTableOptions{AutoMerge: false}) 705 fmt.Printf("\n") 706 707 // List failed/missing plugins in a separate table 708 if len(failedPluginMap)+len(missingPluginMap) != 0 { 709 headers := []string{"Failed", "Connections", "Reason"} 710 var conns []string 711 var missingRows [][]string 712 713 // failed plugins 714 for p, item := range failedPluginMap { 715 for _, conn := range item { 716 conns = append(conns, conn.Name) 717 } 718 missingRows = append(missingRows, []string{p, strings.Join(conns, ","), constants.ConnectionErrorPluginFailedToStart}) 719 conns = []string{} 720 } 721 722 // missing plugins 723 for p, item := range missingPluginMap { 724 for _, conn := range item { 725 conns = append(conns, conn.Name) 726 } 727 missingRows = append(missingRows, []string{p, strings.Join(conns, ","), constants.InstallMessagePluginNotInstalled}) 728 conns = []string{} 729 } 730 731 display.ShowWrappedTable(headers, missingRows, &display.ShowWrappedTableOptions{AutoMerge: false}) 732 fmt.Println() 733 } 734 735 if len(res.Warnings) > 0 { 736 fmt.Println() 737 res.ShowWarnings() 738 fmt.Printf("\n") 739 } 740 return nil 741 } 742 743 func showPluginListAsJSON(pluginList []plugin.PluginListItem, failedPluginMap, missingPluginMap map[string][]*modconfig.Connection, res error_helpers.ErrorAndWarnings) error { 744 output := pluginJsonOutput{} 745 746 for _, item := range pluginList { 747 installed := installedPlugin{ 748 Name: item.Name, 749 Version: item.Version.String(), 750 Connections: item.Connections, 751 } 752 output.Installed = append(output.Installed, installed) 753 } 754 755 for p, item := range failedPluginMap { 756 connections := make([]string, len(item)) 757 for i, conn := range item { 758 connections[i] = conn.Name 759 } 760 failed := failedPlugin{ 761 Name: p, 762 Connections: connections, 763 Reason: constants.ConnectionErrorPluginFailedToStart, 764 } 765 output.Failed = append(output.Failed, failed) 766 } 767 768 for p, item := range missingPluginMap { 769 connections := make([]string, len(item)) 770 for i, conn := range item { 771 connections[i] = conn.Name 772 } 773 missing := failedPlugin{ 774 Name: p, 775 Connections: connections, 776 Reason: constants.InstallMessagePluginNotInstalled, 777 } 778 output.Failed = append(output.Failed, missing) 779 } 780 781 if len(res.Warnings) > 0 { 782 output.Warnings = res.Warnings 783 } 784 785 jsonOutput, err := json.MarshalIndent(output, "", " ") 786 if err != nil { 787 return err 788 } 789 fmt.Println(string(jsonOutput)) 790 fmt.Println() 791 return nil 792 } 793 794 func runPluginUninstallCmd(cmd *cobra.Command, args []string) { 795 // setup a cancel context and start cancel handler 796 ctx, cancel := context.WithCancel(cmd.Context()) 797 contexthelpers.StartCancelHandler(cancel) 798 799 utils.LogTime("runPluginUninstallCmd uninstall") 800 801 defer func() { 802 utils.LogTime("runPluginUninstallCmd end") 803 if r := recover(); r != nil { 804 error_helpers.ShowError(ctx, helpers.ToError(r)) 805 exitCode = constants.ExitCodeUnknownErrorPanic 806 } 807 }() 808 809 if len(args) == 0 { 810 fmt.Println() 811 error_helpers.ShowError(ctx, fmt.Errorf("you need to provide at least one plugin to uninstall")) 812 fmt.Println() 813 cmd.Help() 814 fmt.Println() 815 exitCode = constants.ExitCodeInsufficientOrWrongInputs 816 return 817 } 818 819 connectionMap, _, _, res := getPluginConnectionMap(ctx) 820 if res.Error != nil { 821 error_helpers.ShowError(ctx, res.Error) 822 exitCode = constants.ExitCodePluginListFailure 823 return 824 } 825 826 reports := steampipeconfig.PluginRemoveReports{} 827 statushooks.SetStatus(ctx, fmt.Sprintf("Uninstalling %s", utils.Pluralize("plugin", len(args)))) 828 for _, p := range args { 829 statushooks.SetStatus(ctx, fmt.Sprintf("Uninstalling %s", p)) 830 if report, err := plugin.Remove(ctx, p, connectionMap); err != nil { 831 if strings.Contains(err.Error(), "not found") { 832 exitCode = constants.ExitCodePluginNotFound 833 } 834 error_helpers.ShowErrorWithMessage(ctx, err, fmt.Sprintf("Failed to uninstall plugin '%s'", p)) 835 } else { 836 report.ShortName = p 837 reports = append(reports, *report) 838 } 839 } 840 statushooks.Done(ctx) 841 reports.Print() 842 } 843 844 func getPluginList(ctx context.Context) (pluginList []plugin.PluginListItem, failedPluginMap, missingPluginMap map[string][]*modconfig.Connection, res error_helpers.ErrorAndWarnings) { 845 statushooks.Show(ctx) 846 defer statushooks.Done(ctx) 847 848 // get the maps of available and failed/missing plugins 849 pluginConnectionMap, failedPluginMap, missingPluginMap, res := getPluginConnectionMap(ctx) 850 if res.Error != nil { 851 return nil, nil, nil, res 852 } 853 854 // TODO do we really need to look at installed plugins - can't we just use the plugin connection map 855 // get a list of the installed plugins by inspecting the install location 856 // pass pluginConnectionMap so we can populate the connections for each plugin 857 pluginList, err := plugin.List(ctx, pluginConnectionMap) 858 if err != nil { 859 res.Error = err 860 return nil, nil, nil, res 861 } 862 863 // remove the failed plugins from `list` since we don't want them in the installed table 864 for pluginName := range failedPluginMap { 865 for i := 0; i < len(pluginList); i++ { 866 if pluginList[i].Name == pluginName { 867 pluginList = append(pluginList[:i], pluginList[i+1:]...) 868 i-- // Decrement the loop index since we just removed an element 869 } 870 } 871 } 872 return pluginList, failedPluginMap, missingPluginMap, res 873 } 874 875 func getPluginConnectionMap(ctx context.Context) (pluginConnectionMap, failedPluginMap, missingPluginMap map[string][]*modconfig.Connection, res error_helpers.ErrorAndWarnings) { 876 utils.LogTime("cmd.getPluginConnectionMap start") 877 defer utils.LogTime("cmd.getPluginConnectionMap end") 878 879 statushooks.SetStatus(ctx, "Fetching connection map") 880 881 res = error_helpers.ErrorAndWarnings{} 882 883 connectionStateMap, stateRes := getConnectionState(ctx) 884 res.Merge(stateRes) 885 if res.Error != nil { 886 return nil, nil, nil, res 887 } 888 889 // create the map of failed/missing plugins and available/loaded plugins 890 failedPluginMap = map[string][]*modconfig.Connection{} 891 missingPluginMap = map[string][]*modconfig.Connection{} 892 pluginConnectionMap = make(map[string][]*modconfig.Connection) 893 894 for _, state := range connectionStateMap { 895 connection, ok := steampipeconfig.GlobalConfig.Connections[state.ConnectionName] 896 if !ok { 897 continue 898 } 899 900 if state.State == constants.ConnectionStateError && state.Error() == constants.ConnectionErrorPluginFailedToStart { 901 failedPluginMap[state.Plugin] = append(failedPluginMap[state.Plugin], connection) 902 } else if state.State == constants.ConnectionStateError && state.Error() == constants.ConnectionErrorPluginNotInstalled { 903 missingPluginMap[state.Plugin] = append(missingPluginMap[state.Plugin], connection) 904 } 905 906 pluginConnectionMap[state.Plugin] = append(pluginConnectionMap[state.Plugin], connection) 907 } 908 909 return pluginConnectionMap, failedPluginMap, missingPluginMap, res 910 } 911 912 // load the connection state, waiting until all connections are loaded 913 func getConnectionState(ctx context.Context) (steampipeconfig.ConnectionStateMap, error_helpers.ErrorAndWarnings) { 914 utils.LogTime("cmd.getConnectionState start") 915 defer utils.LogTime("cmd.getConnectionState end") 916 917 // start service 918 client, res := db_local.GetLocalClient(ctx, constants.InvokerPlugin, nil) 919 if res.Error != nil { 920 return nil, res 921 } 922 defer client.Close(ctx) 923 924 conn, err := client.AcquireManagementConnection(ctx) 925 if err != nil { 926 res.Error = err 927 return nil, res 928 } 929 defer conn.Release() 930 931 // load connection state 932 statushooks.SetStatus(ctx, "Loading connection state") 933 connectionStateMap, err := steampipeconfig.LoadConnectionState(ctx, conn.Conn(), steampipeconfig.WithWaitUntilReady()) 934 if err != nil { 935 res.Error = err 936 return nil, res 937 } 938 939 return connectionStateMap, res 940 }