Files
steampipe/cmd/query.go

197 lines
7.5 KiB
Go

package cmd
import (
"bufio"
"context"
"fmt"
"os"
"slices"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/thediveo/enumflag/v2"
"github.com/turbot/go-kit/helpers"
pconstants "github.com/turbot/pipe-fittings/v2/constants"
"github.com/turbot/pipe-fittings/v2/contexthelpers"
"github.com/turbot/pipe-fittings/v2/utils"
"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/error_helpers"
"github.com/turbot/steampipe/v2/pkg/query"
"github.com/turbot/steampipe/v2/pkg/query/queryexecute"
"github.com/turbot/steampipe/v2/pkg/statushooks"
)
// variable used to assign the timing mode flag
var queryTimingMode = constants.QueryTimingModeOff
// variable used to assign the output mode flag
var queryOutputMode = constants.QueryOutputModeTable
func queryCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "query",
TraverseChildren: true,
Args: cobra.ArbitraryArgs,
Run: runQueryCmd,
Short: "Execute SQL queries interactively or by argument",
Long: `Execute SQL queries interactively, or by a query argument.
Open a interactive SQL query console to Steampipe to explore your data and run
multiple queries. If QUERY is passed on the command line then it will be run
immediately and the command will exit.
Examples:
# Open an interactive query console
steampipe query
# Run a specific query directly
steampipe query "select * from cloud"`,
}
// Notes:
// * In the future we may add --csv and --json flags as shortcuts for --output
cmdconfig.
OnCmd(cmd).
AddCloudFlags().
AddWorkspaceDatabaseFlag().
AddBoolFlag(pconstants.ArgHelp, false, "Help for query", cmdconfig.FlagOptions.WithShortHand("h")).
AddBoolFlag(pconstants.ArgHeader, true, "Include column headers csv and table output").
AddStringFlag(pconstants.ArgSeparator, ",", "Separator string for csv output").
AddVarFlag(enumflag.New(&queryOutputMode, pconstants.ArgOutput, constants.QueryOutputModeIds, enumflag.EnumCaseInsensitive),
pconstants.ArgOutput,
fmt.Sprintf("Output format; one of: %s", strings.Join(constants.FlagValues(constants.QueryOutputModeIds), ", "))).
AddVarFlag(enumflag.New(&queryTimingMode, pconstants.ArgTiming, constants.QueryTimingModeIds, enumflag.EnumCaseInsensitive),
pconstants.ArgTiming,
fmt.Sprintf("Display query timing; one of: %s", strings.Join(constants.FlagValues(constants.QueryTimingModeIds), ", ")),
cmdconfig.FlagOptions.NoOptDefVal(pconstants.ArgOn)).
AddStringSliceFlag(pconstants.ArgSearchPath, nil, "Set a custom search_path for the steampipe user for a query session (comma-separated)").
AddStringSliceFlag(pconstants.ArgSearchPathPrefix, nil, "Set a prefix to the current search path for a query session (comma-separated)").
AddBoolFlag(pconstants.ArgInput, true, "Enable interactive prompts").
AddBoolFlag(pconstants.ArgSnapshot, false, "Create snapshot in Turbot Pipes with the default (workspace) visibility").
AddBoolFlag(pconstants.ArgShare, false, "Create snapshot in Turbot Pipes with 'anyone_with_link' visibility").
AddStringArrayFlag(pconstants.ArgSnapshotTag, nil, "Specify tags to set on the snapshot").
AddStringFlag(pconstants.ArgSnapshotTitle, "", "The title to give a snapshot").
AddIntFlag(pconstants.ArgDatabaseQueryTimeout, 0, "The query timeout").
AddStringSliceFlag(pconstants.ArgExport, nil, "Export output to file, supported format: sps (snapshot)").
AddStringFlag(pconstants.ArgSnapshotLocation, "", "The location to write snapshots - either a local file path or a Turbot Pipes workspace").
AddBoolFlag(pconstants.ArgProgress, true, "Display snapshot upload status")
return cmd
}
func runQueryCmd(cmd *cobra.Command, args []string) {
ctx := cmd.Context()
utils.LogTime("cmd.runQueryCmd start")
defer func() {
utils.LogTime("cmd.runQueryCmd end")
if r := recover(); r != nil {
error_helpers.ShowError(ctx, helpers.ToError(r))
}
}()
// validate args
err := validateQueryArgs(ctx, args)
error_helpers.FailOnError(err)
// if diagnostic mode is set, print out config and return
if _, ok := os.LookupEnv(constants.EnvConfigDump); ok {
cmdconfig.DisplayConfig()
return
}
if len(args) == 0 {
// no positional arguments - check if there's anything on stdin
if stdinData := getPipedStdinData(); len(stdinData) > 0 {
// we have data - treat this as an argument
args = append(args, stdinData)
}
}
// enable paging only in interactive mode
interactiveMode := len(args) == 0
// set config to indicate whether we are running an interactive query
viper.Set(constants.ConfigKeyInteractive, interactiveMode)
// initialize the cancel handler - for context cancellation
initCtx, cancel := context.WithCancel(ctx)
contexthelpers.StartCancelHandler(cancel)
// start the initializer
initData := query.NewInitData(initCtx, args)
if initData.Result.Error != nil {
exitCode = constants.ExitCodeInitializationFailed
error_helpers.ShowError(ctx, initData.Result.Error)
return
}
defer initData.Cleanup(ctx)
var failures int
switch {
case interactiveMode:
err = queryexecute.RunInteractiveSession(ctx, initData)
default:
// NOTE: disable any status updates - we do not want 'loading' output from any queries
ctx = statushooks.DisableStatusHooks(ctx)
// fall through to running a batch query
failures, err = queryexecute.RunBatchSession(ctx, initData)
}
// check for err and set the exit code else set the exit code if some queries failed or some rows returned an error
if err != nil {
exitCode = constants.ExitCodeInitializationFailed
error_helpers.ShowError(ctx, err)
} else if failures > 0 {
exitCode = constants.ExitCodeQueryExecutionFailed
}
}
func validateQueryArgs(ctx context.Context, args []string) error {
interactiveMode := len(args) == 0
if interactiveMode && (viper.IsSet(pconstants.ArgSnapshot) || viper.IsSet(pconstants.ArgShare)) {
exitCode = constants.ExitCodeInsufficientOrWrongInputs
return sperr.New("cannot share snapshots in interactive mode")
}
if interactiveMode && len(viper.GetStringSlice(pconstants.ArgExport)) > 0 {
exitCode = constants.ExitCodeInsufficientOrWrongInputs
return sperr.New("cannot export query results in interactive mode")
}
// if share or snapshot args are set, there must be a query specified
err := cmdconfig.ValidateSnapshotArgs(ctx)
if err != nil {
exitCode = constants.ExitCodeInsufficientOrWrongInputs
return err
}
validOutputFormats := []string{constants.OutputFormatLine, constants.OutputFormatCSV, constants.OutputFormatTable, constants.OutputFormatJSON, constants.OutputFormatSnapshot, constants.OutputFormatSnapshotShort, constants.OutputFormatNone}
output := viper.GetString(pconstants.ArgOutput)
if !slices.Contains(validOutputFormats, output) {
exitCode = constants.ExitCodeInsufficientOrWrongInputs
return sperr.New("invalid output format: '%s', must be one of [%s]", output, strings.Join(validOutputFormats, ", "))
}
return nil
}
// getPipedStdinData reads the Standard Input and returns the available data as a string
// if and only if the data was piped to the process
func getPipedStdinData() string {
fi, err := os.Stdin.Stat()
if err != nil {
error_helpers.ShowWarning("could not fetch information about STDIN")
return ""
}
stdinData := ""
if (fi.Mode()&os.ModeCharDevice) == 0 && fi.Size() > 0 {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
stdinData = fmt.Sprintf("%s%s", stdinData, scanner.Text())
}
}
return stdinData
}