mirror of
https://github.com/turbot/steampipe.git
synced 2025-12-19 09:58:53 -05:00
952 lines
30 KiB
Go
952 lines
30 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gosuri/uiprogress"
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/viper"
|
|
"github.com/turbot/go-kit/helpers"
|
|
pconstants "github.com/turbot/pipe-fittings/v2/constants"
|
|
"github.com/turbot/pipe-fittings/v2/contexthelpers"
|
|
perror_helpers "github.com/turbot/pipe-fittings/v2/error_helpers"
|
|
putils "github.com/turbot/pipe-fittings/v2/ociinstaller"
|
|
pplugin "github.com/turbot/pipe-fittings/v2/plugin"
|
|
"github.com/turbot/pipe-fittings/v2/querydisplay"
|
|
"github.com/turbot/pipe-fittings/v2/utils"
|
|
"github.com/turbot/pipe-fittings/v2/versionfile"
|
|
"github.com/turbot/steampipe-plugin-sdk/v5/sperr"
|
|
"github.com/turbot/steampipe/v2/pkg/cmdconfig"
|
|
"github.com/turbot/steampipe/v2/pkg/constants"
|
|
"github.com/turbot/steampipe/v2/pkg/db/db_local"
|
|
"github.com/turbot/steampipe/v2/pkg/error_helpers"
|
|
"github.com/turbot/steampipe/v2/pkg/installationstate"
|
|
"github.com/turbot/steampipe/v2/pkg/ociinstaller"
|
|
"github.com/turbot/steampipe/v2/pkg/plugin"
|
|
"github.com/turbot/steampipe/v2/pkg/statushooks"
|
|
"github.com/turbot/steampipe/v2/pkg/steampipeconfig"
|
|
)
|
|
|
|
type installedPlugin struct {
|
|
Name string `json:"name"`
|
|
Version string `json:"version"`
|
|
Connections []string `json:"connections"`
|
|
}
|
|
|
|
type failedPlugin struct {
|
|
Name string `json:"name"`
|
|
Reason string `json:"reason"`
|
|
Connections []string `json:"connections"`
|
|
}
|
|
|
|
type pluginJsonOutput struct {
|
|
Installed []installedPlugin `json:"installed"`
|
|
Failed []failedPlugin `json:"failed"`
|
|
Warnings []string `json:"warnings"`
|
|
}
|
|
|
|
// Plugin management commands
|
|
func pluginCmd() *cobra.Command {
|
|
var cmd = &cobra.Command{
|
|
Use: "plugin [command]",
|
|
Args: cobra.NoArgs,
|
|
Short: "Steampipe plugin management",
|
|
Long: `Steampipe plugin management.
|
|
|
|
Plugins extend Steampipe to work with many different services and providers.
|
|
Find plugins using the public registry at https://hub.steampipe.io.
|
|
|
|
Examples:
|
|
|
|
# Install a plugin
|
|
steampipe plugin install aws
|
|
|
|
# Update a plugin
|
|
steampipe plugin update aws
|
|
|
|
# List installed plugins
|
|
steampipe plugin list
|
|
|
|
# Uninstall a plugin
|
|
steampipe plugin uninstall aws`,
|
|
PersistentPostRun: func(cmd *cobra.Command, args []string) {
|
|
utils.LogTime("cmd.plugin.PersistentPostRun start")
|
|
defer utils.LogTime("cmd.plugin.PersistentPostRun end")
|
|
pplugin.CleanupOldTmpDirs(cmd.Context())
|
|
},
|
|
}
|
|
cmd.AddCommand(pluginInstallCmd())
|
|
cmd.AddCommand(pluginListCmd())
|
|
cmd.AddCommand(pluginUninstallCmd())
|
|
cmd.AddCommand(pluginUpdateCmd())
|
|
cmd.Flags().BoolP(pconstants.ArgHelp, "h", false, "Help for plugin")
|
|
|
|
return cmd
|
|
}
|
|
|
|
// Install a plugin
|
|
func pluginInstallCmd() *cobra.Command {
|
|
var cmd = &cobra.Command{
|
|
Use: "install [flags] [registry/org/]name[@version]",
|
|
Args: cobra.ArbitraryArgs,
|
|
Run: runPluginInstallCmd,
|
|
Short: "Install one or more plugins",
|
|
Long: `Install one or more plugins.
|
|
|
|
Install a Steampipe plugin, making it available for queries and configuration.
|
|
The plugin name format is [registry/org/]name[@version]. The default
|
|
registry is hub.steampipe.io, default org is turbot and default version
|
|
is latest. The name is a required argument.
|
|
|
|
Examples:
|
|
|
|
# Install all missing plugins that are specified in configuration files
|
|
steampipe plugin install
|
|
|
|
# Install a common plugin (turbot/aws)
|
|
steampipe plugin install aws
|
|
|
|
# Install a specific plugin version
|
|
steampipe plugin install turbot/azure@0.1.0
|
|
|
|
# Hide progress bars during installation
|
|
steampipe plugin install --progress=false aws
|
|
|
|
# Skip creation of default plugin config file
|
|
steampipe plugin install --skip-config aws`,
|
|
}
|
|
|
|
cmdconfig.
|
|
OnCmd(cmd).
|
|
AddBoolFlag(pconstants.ArgProgress, true, "Display installation progress").
|
|
AddBoolFlag(pconstants.ArgSkipConfig, false, "Skip creating the default config file for plugin").
|
|
AddBoolFlag(pconstants.ArgHelp, false, "Help for plugin install", cmdconfig.FlagOptions.WithShortHand("h"))
|
|
return cmd
|
|
}
|
|
|
|
// Update plugins
|
|
func pluginUpdateCmd() *cobra.Command {
|
|
var cmd = &cobra.Command{
|
|
Use: "update [flags] [registry/org/]name[@version]",
|
|
Args: cobra.ArbitraryArgs,
|
|
Run: runPluginUpdateCmd,
|
|
Short: "Update one or more plugins",
|
|
Long: `Update plugins.
|
|
|
|
Update one or more Steampipe plugins, making it available for queries and configuration.
|
|
The plugin name format is [registry/org/]name[@version]. The default
|
|
registry is hub.steampipe.io, default org is turbot and default version
|
|
is latest. The name is a required argument.
|
|
|
|
Examples:
|
|
|
|
# Update all plugins to their latest available version
|
|
steampipe plugin update --all
|
|
|
|
# Update a common plugin (turbot/aws)
|
|
steampipe plugin update aws
|
|
|
|
# Hide progress bars during update
|
|
steampipe plugin update --progress=false aws`,
|
|
}
|
|
|
|
cmdconfig.
|
|
OnCmd(cmd).
|
|
AddBoolFlag(pconstants.ArgAll, false, "Update all plugins to its latest available version").
|
|
AddBoolFlag(pconstants.ArgProgress, true, "Display installation progress").
|
|
AddBoolFlag(pconstants.ArgHelp, false, "Help for plugin update", cmdconfig.FlagOptions.WithShortHand("h"))
|
|
return cmd
|
|
}
|
|
|
|
// List plugins
|
|
func pluginListCmd() *cobra.Command {
|
|
var cmd = &cobra.Command{
|
|
Use: "list",
|
|
Args: cobra.NoArgs,
|
|
Run: runPluginListCmd,
|
|
Short: "List currently installed plugins",
|
|
Long: `List currently installed plugins.
|
|
|
|
List all Steampipe plugins installed for this user.
|
|
|
|
Examples:
|
|
|
|
# List installed plugins
|
|
steampipe plugin list
|
|
|
|
# List plugins that have updates available
|
|
steampipe plugin list --outdated
|
|
|
|
# List plugins output in json
|
|
steampipe plugin list --output json`,
|
|
}
|
|
|
|
cmdconfig.
|
|
OnCmd(cmd).
|
|
AddBoolFlag("outdated", false, "Check each plugin in the list for updates").
|
|
AddStringFlag(pconstants.ArgOutput, "table", "Output format: table or json").
|
|
AddBoolFlag(pconstants.ArgHelp, false, "Help for plugin list", cmdconfig.FlagOptions.WithShortHand("h"))
|
|
return cmd
|
|
}
|
|
|
|
// Uninstall a plugin
|
|
func pluginUninstallCmd() *cobra.Command {
|
|
var cmd = &cobra.Command{
|
|
Use: "uninstall [flags] [registry/org/]name",
|
|
Args: cobra.ArbitraryArgs,
|
|
Run: runPluginUninstallCmd,
|
|
Short: "Uninstall a plugin",
|
|
Long: `Uninstall a plugin.
|
|
|
|
Uninstall a Steampipe plugin, removing it from use. The plugin name format is
|
|
[registry/org/]name. (Version is not relevant in uninstall, since only one
|
|
version of a plugin can be installed at a time.)
|
|
|
|
Example:
|
|
|
|
# Uninstall a common plugin (turbot/aws)
|
|
steampipe plugin uninstall aws
|
|
|
|
`,
|
|
}
|
|
|
|
cmdconfig.OnCmd(cmd).
|
|
AddBoolFlag(pconstants.ArgHelp, false, "Help for plugin uninstall", cmdconfig.FlagOptions.WithShortHand("h"))
|
|
|
|
return cmd
|
|
}
|
|
|
|
var pluginInstallSteps = []string{
|
|
"Downloading",
|
|
"Installing Plugin",
|
|
"Installing Docs",
|
|
"Installing Config",
|
|
"Updating Steampipe",
|
|
"Done",
|
|
}
|
|
|
|
func runPluginInstallCmd(cmd *cobra.Command, args []string) {
|
|
ctx := cmd.Context()
|
|
utils.LogTime("runPluginInstallCmd install")
|
|
defer func() {
|
|
utils.LogTime("runPluginInstallCmd end")
|
|
if r := recover(); r != nil {
|
|
error_helpers.ShowError(ctx, helpers.ToError(r))
|
|
exitCode = constants.ExitCodeUnknownErrorPanic
|
|
}
|
|
}()
|
|
|
|
// args to 'plugin install' -- one or more plugins to install
|
|
// plugin names can be simple names for "standard" plugins, constraint suffixed names
|
|
// or full refs to the OCI image
|
|
// - aws
|
|
// - aws@0.118.0
|
|
// - aws@^0.118
|
|
// - ghcr.io/turbot/steampipe/plugins/turbot/aws:1.0.0
|
|
plugins := append([]string{}, args...)
|
|
showProgress := viper.GetBool(pconstants.ArgProgress)
|
|
installReports := make(pplugin.PluginInstallReports, 0, len(plugins))
|
|
|
|
if len(plugins) == 0 {
|
|
if len(steampipeconfig.GlobalConfig.Plugins) == 0 {
|
|
error_helpers.ShowError(ctx, sperr.New("No connections or plugins configured"))
|
|
exitCode = constants.ExitCodeInsufficientOrWrongInputs
|
|
return
|
|
}
|
|
|
|
// get the list of plugins to install
|
|
for imageRef := range steampipeconfig.GlobalConfig.Plugins {
|
|
ref := putils.NewImageRef(imageRef)
|
|
plugins = append(plugins, ref.GetFriendlyName())
|
|
}
|
|
}
|
|
|
|
state, err := installationstate.Load()
|
|
if err != nil {
|
|
error_helpers.ShowError(ctx, fmt.Errorf("could not load state"))
|
|
exitCode = constants.ExitCodePluginLoadingError
|
|
return
|
|
}
|
|
|
|
// a leading blank line - since we always output multiple lines
|
|
fmt.Println()
|
|
progressBars := uiprogress.New()
|
|
installWaitGroup := &sync.WaitGroup{}
|
|
reportChannel := make(chan *pplugin.PluginInstallReport, len(plugins))
|
|
|
|
if showProgress {
|
|
progressBars.Start()
|
|
}
|
|
for _, pluginName := range plugins {
|
|
installWaitGroup.Add(1)
|
|
bar := createProgressBar(pluginName, progressBars)
|
|
|
|
ref := putils.NewImageRef(pluginName)
|
|
org, name, constraint := ref.GetOrgNameAndStream()
|
|
orgAndName := fmt.Sprintf("%s/%s", org, name)
|
|
var resolved pplugin.ResolvedPluginVersion
|
|
if ref.IsFromTurbotHub() {
|
|
rpv, err := pplugin.GetLatestPluginVersionByConstraint(ctx, state.InstallationID, org, name, constraint)
|
|
if err != nil || rpv == nil {
|
|
report := &pplugin.PluginInstallReport{
|
|
Plugin: pluginName,
|
|
Skipped: true,
|
|
SkipReason: pconstants.InstallMessagePluginNotFound,
|
|
IsUpdateReport: false,
|
|
}
|
|
reportChannel <- report
|
|
installWaitGroup.Done()
|
|
continue
|
|
}
|
|
resolved = *rpv
|
|
} else {
|
|
resolved = pplugin.NewResolvedPluginVersion(orgAndName, constraint, constraint)
|
|
}
|
|
|
|
go doPluginInstall(ctx, bar, pluginName, resolved, installWaitGroup, reportChannel)
|
|
}
|
|
go func() {
|
|
installWaitGroup.Wait()
|
|
close(reportChannel)
|
|
}()
|
|
installCount := 0
|
|
for report := range reportChannel {
|
|
installReports = append(installReports, report)
|
|
if !report.Skipped {
|
|
installCount++
|
|
} else if !(report.Skipped && report.SkipReason == "Already installed") {
|
|
exitCode = constants.ExitCodePluginInstallFailure
|
|
}
|
|
}
|
|
if showProgress {
|
|
progressBars.Stop()
|
|
}
|
|
|
|
if installCount > 0 {
|
|
// TODO do we need to refresh connections here
|
|
|
|
// reload the config, since an installation should have created a new config file
|
|
var cmd = viper.Get(constants.ConfigKeyActiveCommand).(*cobra.Command)
|
|
config, errorsAndWarnings := steampipeconfig.LoadSteampipeConfig(ctx, viper.GetString(pconstants.ArgModLocation), cmd.Name())
|
|
if errorsAndWarnings.GetError() != nil {
|
|
error_helpers.ShowWarning(fmt.Sprintf("Failed to reload config - install report may be incomplete (%s)", errorsAndWarnings.GetError()))
|
|
} else {
|
|
steampipeconfig.GlobalConfig = config
|
|
}
|
|
|
|
statushooks.Done(ctx)
|
|
}
|
|
pplugin.PrintInstallReports(installReports, false)
|
|
|
|
// a concluding blank line - since we always output multiple lines
|
|
fmt.Println()
|
|
}
|
|
|
|
func doPluginInstall(ctx context.Context, bar *uiprogress.Bar, pluginName string, resolvedPlugin pplugin.ResolvedPluginVersion, wg *sync.WaitGroup, returnChannel chan *pplugin.PluginInstallReport) {
|
|
var report *pplugin.PluginInstallReport
|
|
|
|
pluginAlreadyInstalled, _ := pplugin.Exists(ctx, pluginName)
|
|
if pluginAlreadyInstalled {
|
|
// set the bar to MAX
|
|
//nolint:golint,errcheck // the error happens if we set this over the max value
|
|
bar.Set(len(pluginInstallSteps))
|
|
// let the bar append itself with "Already Installed"
|
|
bar.AppendFunc(func(b *uiprogress.Bar) string {
|
|
return helpers.Resize(pconstants.InstallMessagePluginAlreadyInstalled, 20)
|
|
})
|
|
report = &pplugin.PluginInstallReport{
|
|
Plugin: pluginName,
|
|
Skipped: true,
|
|
SkipReason: pconstants.InstallMessagePluginAlreadyInstalled,
|
|
IsUpdateReport: false,
|
|
}
|
|
} else {
|
|
// let the bar append itself with the current installation step
|
|
bar.AppendFunc(func(b *uiprogress.Bar) string {
|
|
if report != nil && report.SkipReason == pconstants.InstallMessagePluginNotFound {
|
|
return helpers.Resize(pconstants.InstallMessagePluginNotFound, 20)
|
|
} else {
|
|
if b.Current() == 0 {
|
|
// no install step to display yet
|
|
return ""
|
|
}
|
|
return helpers.Resize(pluginInstallSteps[b.Current()-1], 20)
|
|
}
|
|
})
|
|
|
|
report = installPlugin(ctx, resolvedPlugin, false, bar)
|
|
}
|
|
returnChannel <- report
|
|
wg.Done()
|
|
}
|
|
|
|
func runPluginUpdateCmd(cmd *cobra.Command, args []string) {
|
|
ctx := cmd.Context()
|
|
utils.LogTime("runPluginUpdateCmd start")
|
|
defer func() {
|
|
utils.LogTime("runPluginUpdateCmd end")
|
|
if r := recover(); r != nil {
|
|
error_helpers.ShowError(ctx, helpers.ToError(r))
|
|
exitCode = constants.ExitCodeUnknownErrorPanic
|
|
}
|
|
}()
|
|
|
|
// args to 'plugin update' -- one or more plugins to update
|
|
// These can be simple names for "standard" plugins, constraint suffixed names
|
|
// or full refs to the OCI image
|
|
// - aws
|
|
// - aws@0.118.0
|
|
// - aws@^0.118
|
|
// - ghcr.io/turbot/steampipe/plugins/turbot/aws:1.0.0
|
|
plugins, err := resolveUpdatePluginsFromArgs(args)
|
|
showProgress := viper.GetBool(pconstants.ArgProgress)
|
|
|
|
if err != nil {
|
|
fmt.Println()
|
|
error_helpers.ShowError(ctx, err)
|
|
fmt.Println()
|
|
cmd.Help()
|
|
fmt.Println()
|
|
exitCode = constants.ExitCodeInsufficientOrWrongInputs
|
|
return
|
|
}
|
|
|
|
if len(plugins) > 0 && !(cmdconfig.Viper().GetBool(pconstants.ArgAll)) && plugins[0] == pconstants.ArgAll {
|
|
// improve the response to wrong argument "steampipe plugin update all"
|
|
fmt.Println()
|
|
exitCode = constants.ExitCodeInsufficientOrWrongInputs
|
|
error_helpers.ShowError(ctx, fmt.Errorf("Did you mean %s?", pconstants.Bold("--all")))
|
|
fmt.Println()
|
|
return
|
|
}
|
|
|
|
state, err := installationstate.Load()
|
|
if err != nil {
|
|
error_helpers.ShowError(ctx, fmt.Errorf("could not load state"))
|
|
exitCode = constants.ExitCodePluginLoadingError
|
|
return
|
|
}
|
|
|
|
// retrieve the plugin version data from steampipe config
|
|
pluginVersions := steampipeconfig.GlobalConfig.PluginVersions
|
|
|
|
var runUpdatesFor []*versionfile.InstalledVersion
|
|
updateResults := make(pplugin.PluginInstallReports, 0, len(plugins))
|
|
|
|
// a leading blank line - since we always output multiple lines
|
|
fmt.Println()
|
|
|
|
if cmdconfig.Viper().GetBool(pconstants.ArgAll) {
|
|
for k, v := range pluginVersions {
|
|
ref := putils.NewImageRef(k)
|
|
org, name, constraint := ref.GetOrgNameAndStream()
|
|
key := fmt.Sprintf("%s/%s@%s", org, name, constraint)
|
|
|
|
plugins = append(plugins, key)
|
|
runUpdatesFor = append(runUpdatesFor, v)
|
|
}
|
|
} else {
|
|
// get the args and retrieve the installed versions
|
|
for _, p := range plugins {
|
|
ref := putils.NewImageRef(p)
|
|
isExists, _ := pplugin.Exists(ctx, p)
|
|
if isExists {
|
|
if strings.HasPrefix(ref.DisplayImageRef(), constants.SteampipeHubOCIBase) {
|
|
runUpdatesFor = append(runUpdatesFor, pluginVersions[ref.DisplayImageRef()])
|
|
} else {
|
|
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"))
|
|
exitCode = constants.ExitCodePluginLoadingError
|
|
return
|
|
}
|
|
} else {
|
|
exitCode = constants.ExitCodePluginNotFound
|
|
updateResults = append(updateResults, &pplugin.PluginInstallReport{
|
|
Skipped: true,
|
|
Plugin: p,
|
|
SkipReason: pconstants.InstallMessagePluginNotInstalled,
|
|
IsUpdateReport: true,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(plugins) == len(updateResults) {
|
|
// we have report for all
|
|
// this may happen if all given plugins are
|
|
// not installed
|
|
pplugin.PrintInstallReports(updateResults, true)
|
|
fmt.Println()
|
|
return
|
|
}
|
|
|
|
timeoutCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
|
defer cancel()
|
|
|
|
statushooks.SetStatus(ctx, "Checking for available updates")
|
|
reports := pplugin.GetUpdateReport(timeoutCtx, state.InstallationID, runUpdatesFor)
|
|
statushooks.Done(ctx)
|
|
if len(reports) == 0 {
|
|
// this happens if for some reason the update server could not be contacted,
|
|
// in which case we get back an empty map
|
|
error_helpers.ShowError(ctx, fmt.Errorf("there was an issue contacting the update server, please try later"))
|
|
exitCode = constants.ExitCodePluginLoadingError
|
|
return
|
|
}
|
|
|
|
updateWaitGroup := &sync.WaitGroup{}
|
|
reportChannel := make(chan *pplugin.PluginInstallReport, len(reports))
|
|
progressBars := uiprogress.New()
|
|
if showProgress {
|
|
progressBars.Start()
|
|
}
|
|
|
|
sorted := utils.SortedMapKeys(reports)
|
|
for _, key := range sorted {
|
|
report := reports[key]
|
|
updateWaitGroup.Add(1)
|
|
bar := createProgressBar(report.ShortNameWithConstraint(), progressBars)
|
|
go doPluginUpdate(ctx, bar, report, updateWaitGroup, reportChannel)
|
|
}
|
|
go func() {
|
|
updateWaitGroup.Wait()
|
|
close(reportChannel)
|
|
}()
|
|
installCount := 0
|
|
|
|
for updateResult := range reportChannel {
|
|
updateResults = append(updateResults, updateResult)
|
|
if !updateResult.Skipped {
|
|
installCount++
|
|
}
|
|
}
|
|
if showProgress {
|
|
progressBars.Stop()
|
|
}
|
|
|
|
pplugin.PrintInstallReports(updateResults, true)
|
|
|
|
// a concluding blank line - since we always output multiple lines
|
|
fmt.Println()
|
|
}
|
|
|
|
func doPluginUpdate(ctx context.Context, bar *uiprogress.Bar, pvr pplugin.PluginVersionCheckReport, wg *sync.WaitGroup, returnChannel chan *pplugin.PluginInstallReport) {
|
|
var report *pplugin.PluginInstallReport
|
|
|
|
if pplugin.UpdateRequired(pvr) {
|
|
// update required, resolve version and install update
|
|
bar.AppendFunc(func(b *uiprogress.Bar) string {
|
|
// set the progress bar to append itself with the step underway
|
|
if b.Current() == 0 {
|
|
// no install step to display yet
|
|
return ""
|
|
}
|
|
return helpers.Resize(pluginInstallSteps[b.Current()-1], 20)
|
|
})
|
|
rp := pplugin.NewResolvedPluginVersion(pvr.ShortName(), pvr.CheckResponse.Version, pvr.CheckResponse.Constraint)
|
|
report = installPlugin(ctx, rp, true, bar)
|
|
} else {
|
|
// update NOT required, return already installed report
|
|
bar.AppendFunc(func(b *uiprogress.Bar) string {
|
|
// set the progress bar to append itself with "Already Installed"
|
|
return helpers.Resize(pconstants.InstallMessagePluginLatestAlreadyInstalled, 30)
|
|
})
|
|
// set the progress bar to the maximum
|
|
bar.Set(len(pluginInstallSteps))
|
|
report = &pplugin.PluginInstallReport{
|
|
Plugin: fmt.Sprintf("%s@%s", pvr.CheckResponse.Name, pvr.CheckResponse.Constraint),
|
|
Skipped: true,
|
|
SkipReason: pconstants.InstallMessagePluginLatestAlreadyInstalled,
|
|
IsUpdateReport: true,
|
|
}
|
|
}
|
|
|
|
returnChannel <- report
|
|
wg.Done()
|
|
}
|
|
|
|
func createProgressBar(plugin string, parentProgressBars *uiprogress.Progress) *uiprogress.Bar {
|
|
bar := parentProgressBars.AddBar(len(pluginInstallSteps))
|
|
bar.PrependFunc(func(b *uiprogress.Bar) string {
|
|
return helpers.Resize(plugin, 30)
|
|
})
|
|
return bar
|
|
}
|
|
|
|
func installPlugin(ctx context.Context, resolvedPlugin pplugin.ResolvedPluginVersion, isUpdate bool, bar *uiprogress.Bar) *pplugin.PluginInstallReport {
|
|
// start a channel for progress publications from plugin.Install
|
|
progress := make(chan struct{}, 5)
|
|
defer func() {
|
|
// close the progress channel
|
|
close(progress)
|
|
}()
|
|
go func() {
|
|
for {
|
|
// wait for a message on the progress channel
|
|
<-progress
|
|
// increment the progress bar
|
|
bar.Incr()
|
|
}
|
|
}()
|
|
|
|
skipConfig := viper.GetBool(pconstants.ArgSkipConfig)
|
|
// we should never install the config file for plugin updates; config files should only be installed during plugin install
|
|
if isUpdate {
|
|
skipConfig = true
|
|
}
|
|
|
|
image, err := plugin.Install(ctx, resolvedPlugin, progress, constants.BaseImageRef, ociinstaller.SteampipeMediaTypeProvider{}, putils.WithSkipConfig(skipConfig))
|
|
if err != nil {
|
|
msg := ""
|
|
// used to build data for the plugin install report to be used for display purposes
|
|
_, name, constraint := putils.NewImageRef(resolvedPlugin.GetVersionTag()).GetOrgNameAndStream()
|
|
if isPluginNotFoundErr(err) {
|
|
exitCode = constants.ExitCodePluginNotFound
|
|
msg = pconstants.InstallMessagePluginNotFound
|
|
} else {
|
|
msg = err.Error()
|
|
}
|
|
return &pplugin.PluginInstallReport{
|
|
Plugin: fmt.Sprintf("%s@%s", name, constraint),
|
|
Skipped: true,
|
|
SkipReason: msg,
|
|
IsUpdateReport: isUpdate,
|
|
}
|
|
}
|
|
|
|
// used to build data for the plugin install report to be used for display purposes
|
|
org, name, _ := image.ImageRef.GetOrgNameAndStream()
|
|
versionString := ""
|
|
if image.Config.Plugin.Version != "" {
|
|
versionString = " v" + image.Config.Plugin.Version
|
|
}
|
|
docURL := fmt.Sprintf("https://hub.steampipe.io/plugins/%s/%s", org, name)
|
|
if !image.ImageRef.IsFromTurbotHub() {
|
|
docURL = fmt.Sprintf("https://%s/%s", org, name)
|
|
}
|
|
return &pplugin.PluginInstallReport{
|
|
Plugin: fmt.Sprintf("%s@%s", name, resolvedPlugin.Constraint),
|
|
Skipped: false,
|
|
Version: versionString,
|
|
DocURL: docURL,
|
|
IsUpdateReport: isUpdate,
|
|
}
|
|
}
|
|
|
|
func isPluginNotFoundErr(err error) bool {
|
|
return strings.HasSuffix(err.Error(), "not found")
|
|
}
|
|
|
|
func resolveUpdatePluginsFromArgs(args []string) ([]string, error) {
|
|
plugins := append([]string{}, args...)
|
|
|
|
if len(plugins) == 0 && !(cmdconfig.Viper().GetBool("all")) {
|
|
// either plugin name(s) or "all" must be provided
|
|
return nil, fmt.Errorf("you need to provide at least one plugin to update or use the %s flag", pconstants.Bold("--all"))
|
|
}
|
|
|
|
if len(plugins) > 0 && cmdconfig.Viper().GetBool(pconstants.ArgAll) {
|
|
// we can't allow update and install at the same time
|
|
return nil, fmt.Errorf("%s cannot be used when updating specific plugins", pconstants.Bold("`--all`"))
|
|
}
|
|
|
|
return plugins, nil
|
|
}
|
|
|
|
func runPluginListCmd(cmd *cobra.Command, _ []string) {
|
|
// setup a cancel context and start cancel handler
|
|
ctx, cancel := context.WithCancel(cmd.Context())
|
|
contexthelpers.StartCancelHandler(cancel)
|
|
outputFormat := viper.GetString(pconstants.ArgOutput)
|
|
|
|
utils.LogTime("runPluginListCmd list")
|
|
defer func() {
|
|
utils.LogTime("runPluginListCmd end")
|
|
if r := recover(); r != nil {
|
|
error_helpers.ShowError(ctx, helpers.ToError(r))
|
|
exitCode = constants.ExitCodeUnknownErrorPanic
|
|
}
|
|
}()
|
|
|
|
pluginList, failedPluginMap, missingPluginMap, res := getPluginList(ctx)
|
|
if res.Error != nil {
|
|
error_helpers.ShowErrorWithMessage(ctx, res.Error, "plugin listing failed")
|
|
exitCode = constants.ExitCodePluginListFailure
|
|
return
|
|
}
|
|
|
|
err := showPluginListOutput(pluginList, failedPluginMap, missingPluginMap, res, outputFormat)
|
|
if err != nil {
|
|
error_helpers.ShowError(ctx, err)
|
|
}
|
|
|
|
}
|
|
|
|
func showPluginListOutput(pluginList []plugin.PluginListItem, failedPluginMap, missingPluginMap map[string][]plugin.PluginConnection, res perror_helpers.ErrorAndWarnings, outputFormat string) error {
|
|
switch outputFormat {
|
|
case "table":
|
|
return showPluginListAsTable(pluginList, failedPluginMap, missingPluginMap, res)
|
|
case "json":
|
|
return showPluginListAsJSON(pluginList, failedPluginMap, missingPluginMap, res)
|
|
default:
|
|
return errors.New("invalid output format")
|
|
}
|
|
}
|
|
|
|
func showPluginListAsTable(pluginList []plugin.PluginListItem, failedPluginMap, missingPluginMap map[string][]plugin.PluginConnection, res perror_helpers.ErrorAndWarnings) error {
|
|
headers := []string{"Installed", "Version", "Connections"}
|
|
var rows [][]string
|
|
// List installed plugins in a table
|
|
if len(pluginList) != 0 {
|
|
for _, item := range pluginList {
|
|
rows = append(rows, []string{item.Name, item.Version.String(), strings.Join(item.Connections, ",")})
|
|
}
|
|
} else {
|
|
rows = append(rows, []string{"", "", ""})
|
|
}
|
|
querydisplay.ShowWrappedTable(headers, rows, &querydisplay.ShowWrappedTableOptions{AutoMerge: false})
|
|
fmt.Printf("\n")
|
|
|
|
// List failed/missing plugins in a separate table
|
|
if len(failedPluginMap)+len(missingPluginMap) != 0 {
|
|
headers := []string{"Failed", "Connections", "Reason"}
|
|
var conns []string
|
|
var missingRows [][]string
|
|
|
|
// failed plugins
|
|
for p, item := range failedPluginMap {
|
|
for _, conn := range item {
|
|
conns = append(conns, conn.GetName())
|
|
}
|
|
missingRows = append(missingRows, []string{p, strings.Join(conns, ","), pconstants.ConnectionErrorPluginFailedToStart})
|
|
conns = []string{}
|
|
}
|
|
|
|
// missing plugins
|
|
for p, item := range missingPluginMap {
|
|
for _, conn := range item {
|
|
conns = append(conns, conn.GetName())
|
|
}
|
|
missingRows = append(missingRows, []string{p, strings.Join(conns, ","), pconstants.InstallMessagePluginNotInstalled})
|
|
conns = []string{}
|
|
}
|
|
|
|
querydisplay.ShowWrappedTable(headers, missingRows, &querydisplay.ShowWrappedTableOptions{AutoMerge: false})
|
|
fmt.Println()
|
|
}
|
|
|
|
if len(res.Warnings) > 0 {
|
|
fmt.Println()
|
|
res.ShowWarnings()
|
|
fmt.Printf("\n")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func showPluginListAsJSON(pluginList []plugin.PluginListItem, failedPluginMap, missingPluginMap map[string][]plugin.PluginConnection, res perror_helpers.ErrorAndWarnings) error {
|
|
output := pluginJsonOutput{}
|
|
|
|
for _, item := range pluginList {
|
|
installed := installedPlugin{
|
|
Name: item.Name,
|
|
Version: item.Version.String(),
|
|
Connections: item.Connections,
|
|
}
|
|
output.Installed = append(output.Installed, installed)
|
|
}
|
|
|
|
for p, item := range failedPluginMap {
|
|
connections := make([]string, len(item))
|
|
for i, conn := range item {
|
|
connections[i] = conn.GetName()
|
|
}
|
|
failed := failedPlugin{
|
|
Name: p,
|
|
Connections: connections,
|
|
Reason: pconstants.ConnectionErrorPluginFailedToStart,
|
|
}
|
|
output.Failed = append(output.Failed, failed)
|
|
}
|
|
|
|
for p, item := range missingPluginMap {
|
|
connections := make([]string, len(item))
|
|
for i, conn := range item {
|
|
connections[i] = conn.GetName()
|
|
}
|
|
missing := failedPlugin{
|
|
Name: p,
|
|
Connections: connections,
|
|
Reason: pconstants.InstallMessagePluginNotInstalled,
|
|
}
|
|
output.Failed = append(output.Failed, missing)
|
|
}
|
|
|
|
if len(res.Warnings) > 0 {
|
|
output.Warnings = res.Warnings
|
|
}
|
|
|
|
jsonOutput, err := json.MarshalIndent(output, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Println(string(jsonOutput))
|
|
fmt.Println()
|
|
return nil
|
|
}
|
|
|
|
func runPluginUninstallCmd(cmd *cobra.Command, args []string) {
|
|
// setup a cancel context and start cancel handler
|
|
ctx, cancel := context.WithCancel(cmd.Context())
|
|
contexthelpers.StartCancelHandler(cancel)
|
|
|
|
utils.LogTime("runPluginUninstallCmd uninstall")
|
|
|
|
defer func() {
|
|
utils.LogTime("runPluginUninstallCmd end")
|
|
if r := recover(); r != nil {
|
|
error_helpers.ShowError(ctx, helpers.ToError(r))
|
|
exitCode = constants.ExitCodeUnknownErrorPanic
|
|
}
|
|
}()
|
|
|
|
if len(args) == 0 {
|
|
fmt.Println()
|
|
error_helpers.ShowError(ctx, fmt.Errorf("you need to provide at least one plugin to uninstall"))
|
|
fmt.Println()
|
|
cmd.Help()
|
|
fmt.Println()
|
|
exitCode = constants.ExitCodeInsufficientOrWrongInputs
|
|
return
|
|
}
|
|
|
|
connectionMap, _, _, res := getPluginConnectionMap(ctx)
|
|
if res.Error != nil {
|
|
error_helpers.ShowError(ctx, res.Error)
|
|
exitCode = constants.ExitCodePluginListFailure
|
|
return
|
|
}
|
|
|
|
reports := plugin.PluginRemoveReports{}
|
|
statushooks.SetStatus(ctx, fmt.Sprintf("Uninstalling %s", utils.Pluralize("plugin", len(args))))
|
|
for _, p := range args {
|
|
statushooks.SetStatus(ctx, fmt.Sprintf("Uninstalling %s", p))
|
|
if report, err := plugin.Remove(ctx, p, connectionMap); err != nil {
|
|
if strings.Contains(err.Error(), "not found") {
|
|
exitCode = constants.ExitCodePluginNotFound
|
|
}
|
|
error_helpers.ShowErrorWithMessage(ctx, err, fmt.Sprintf("Failed to uninstall plugin '%s'", p))
|
|
} else {
|
|
report.ShortName = p
|
|
reports = append(reports, *report)
|
|
}
|
|
}
|
|
statushooks.Done(ctx)
|
|
reports.Print()
|
|
}
|
|
|
|
func getPluginList(ctx context.Context) (pluginList []plugin.PluginListItem, failedPluginMap, missingPluginMap map[string][]plugin.PluginConnection, res perror_helpers.ErrorAndWarnings) {
|
|
statushooks.Show(ctx)
|
|
defer statushooks.Done(ctx)
|
|
|
|
// get the maps of available and failed/missing plugins
|
|
pluginConnectionMap, failedPluginMap, missingPluginMap, res := getPluginConnectionMap(ctx)
|
|
if res.Error != nil {
|
|
return nil, nil, nil, res
|
|
}
|
|
|
|
// retrieve the plugin version data from steampipe config
|
|
pluginVersions := steampipeconfig.GlobalConfig.PluginVersions
|
|
|
|
// TODO do we really need to look at installed plugins - can't we just use the plugin connection map
|
|
// get a list of the installed plugins by inspecting the install location
|
|
// pass pluginConnectionMap so we can populate the connections for each plugin
|
|
pluginList, err := plugin.List(ctx, pluginConnectionMap, pluginVersions)
|
|
if err != nil {
|
|
res.Error = err
|
|
return nil, nil, nil, res
|
|
}
|
|
|
|
// remove the failed plugins from `list` since we don't want them in the installed table
|
|
for pluginName := range failedPluginMap {
|
|
for i := 0; i < len(pluginList); i++ {
|
|
if pluginList[i].Name == pluginName {
|
|
pluginList = append(pluginList[:i], pluginList[i+1:]...)
|
|
i-- // Decrement the loop index since we just removed an element
|
|
}
|
|
}
|
|
}
|
|
return pluginList, failedPluginMap, missingPluginMap, res
|
|
}
|
|
|
|
func getPluginConnectionMap(ctx context.Context) (pluginConnectionMap, failedPluginMap, missingPluginMap map[string][]plugin.PluginConnection, res perror_helpers.ErrorAndWarnings) {
|
|
utils.LogTime("cmd.getPluginConnectionMap start")
|
|
defer utils.LogTime("cmd.getPluginConnectionMap end")
|
|
|
|
statushooks.SetStatus(ctx, "Fetching connection map")
|
|
|
|
res = perror_helpers.ErrorAndWarnings{}
|
|
|
|
connectionStateMap, stateRes := getConnectionState(ctx)
|
|
res.Merge(stateRes)
|
|
if res.Error != nil {
|
|
return nil, nil, nil, res
|
|
}
|
|
|
|
// create the map of failed/missing plugins and available/loaded plugins
|
|
failedPluginMap = map[string][]plugin.PluginConnection{}
|
|
missingPluginMap = map[string][]plugin.PluginConnection{}
|
|
pluginConnectionMap = make(map[string][]plugin.PluginConnection)
|
|
|
|
for _, state := range connectionStateMap {
|
|
connection, ok := steampipeconfig.GlobalConfig.Connections[state.ConnectionName]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
if state.State == constants.ConnectionStateError && state.Error() == pconstants.ConnectionErrorPluginFailedToStart {
|
|
failedPluginMap[state.Plugin] = append(failedPluginMap[state.Plugin], connection)
|
|
} else if state.State == constants.ConnectionStateError && state.Error() == pconstants.ConnectionErrorPluginNotInstalled {
|
|
missingPluginMap[state.Plugin] = append(missingPluginMap[state.Plugin], connection)
|
|
}
|
|
|
|
pluginConnectionMap[state.Plugin] = append(pluginConnectionMap[state.Plugin], connection)
|
|
}
|
|
|
|
return pluginConnectionMap, failedPluginMap, missingPluginMap, res
|
|
}
|
|
|
|
// load the connection state, waiting until all connections are loaded
|
|
func getConnectionState(ctx context.Context) (steampipeconfig.ConnectionStateMap, perror_helpers.ErrorAndWarnings) {
|
|
utils.LogTime("cmd.getConnectionState start")
|
|
defer utils.LogTime("cmd.getConnectionState end")
|
|
|
|
// start service
|
|
client, res := db_local.GetLocalClient(ctx, constants.InvokerPlugin)
|
|
if res.Error != nil {
|
|
return nil, res
|
|
}
|
|
defer client.Close(ctx)
|
|
|
|
conn, err := client.AcquireManagementConnection(ctx)
|
|
if err != nil {
|
|
res.Error = err
|
|
return nil, res
|
|
}
|
|
defer conn.Release()
|
|
|
|
// load connection state
|
|
statushooks.SetStatus(ctx, "Loading connection state")
|
|
connectionStateMap, err := steampipeconfig.LoadConnectionState(ctx, conn.Conn(), steampipeconfig.WithWaitUntilReady())
|
|
if err != nil {
|
|
res.Error = err
|
|
return nil, res
|
|
}
|
|
|
|
return connectionStateMap, res
|
|
}
|