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  }