remove check cmd

This commit is contained in:
Puskar Basu
2024-05-29 15:31:44 +05:30
parent 8619e463f0
commit ce87e26ba7
3 changed files with 0 additions and 599 deletions

View File

@@ -1,417 +0,0 @@
package cmd
import (
"context"
"fmt"
"io"
"os"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/thediveo/enumflag/v2"
"github.com/turbot/go-kit/helpers"
"github.com/turbot/steampipe-plugin-sdk/v5/sperr"
"github.com/turbot/steampipe/pkg/cmdconfig"
"github.com/turbot/steampipe/pkg/constants"
"github.com/turbot/steampipe/pkg/contexthelpers"
"github.com/turbot/steampipe/pkg/control"
"github.com/turbot/steampipe/pkg/control/controldisplay"
"github.com/turbot/steampipe/pkg/control/controlexecute"
"github.com/turbot/steampipe/pkg/control/controlstatus"
"github.com/turbot/steampipe/pkg/display"
"github.com/turbot/steampipe/pkg/error_helpers"
"github.com/turbot/steampipe/pkg/statushooks"
"github.com/turbot/steampipe/pkg/steampipeconfig/modconfig"
"github.com/turbot/steampipe/pkg/utils"
"github.com/turbot/steampipe/pkg/workspace"
)
// variable used to assign the timing mode flag
var checkTimingMode = constants.CheckTimingModeOff
// variable used to assign the output mode flag
var checkOutputMode = constants.CheckOutputModeText
func checkCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "check [flags] [mod/benchmark/control/\"all\"]",
TraverseChildren: true,
Args: cobra.ArbitraryArgs,
Run: runCheckCmd,
Short: "Execute one or more controls",
Long: `Execute one or more Steampipe benchmarks and controls.
You may specify one or more benchmarks or controls to run (separated by a space), or run 'steampipe check all' to run all controls in the workspace.`,
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
ctx := cmd.Context()
workspaceResources, err := workspace.LoadResourceNames(ctx, viper.GetString(constants.ArgModLocation))
if err != nil {
return []string{}, cobra.ShellCompDirectiveError
}
completions := []string{}
for _, item := range workspaceResources.GetSortedBenchmarksAndControlNames() {
if strings.HasPrefix(item, toComplete) {
completions = append(completions, item)
}
}
return completions, cobra.ShellCompDirectiveNoFileComp
},
}
cmdconfig.
OnCmd(cmd).
AddCloudFlags().
AddWorkspaceDatabaseFlag().
AddModLocationFlag().
AddBoolFlag(constants.ArgHeader, true, "Include column headers for csv and table output").
AddBoolFlag(constants.ArgHelp, false, "Help for check", cmdconfig.FlagOptions.WithShortHand("h")).
AddStringFlag(constants.ArgSeparator, ",", "Separator string for csv output").
AddVarFlag(enumflag.New(&checkOutputMode, constants.ArgOutput, constants.CheckOutputModeIds, enumflag.EnumCaseInsensitive),
constants.ArgOutput,
fmt.Sprintf("Output format; one of: %s", strings.Join(constants.FlagValues(constants.CheckOutputModeIds), ", "))).
AddVarFlag(enumflag.New(&checkTimingMode, constants.ArgTiming, constants.CheckTimingModeIds, enumflag.EnumCaseInsensitive),
constants.ArgTiming,
fmt.Sprintf("Display timing information; one of: %s", strings.Join(constants.FlagValues(constants.CheckTimingModeIds), ", ")),
cmdconfig.FlagOptions.NoOptDefVal(constants.CheckTimingModeIds[checkTimingMode][0])).
AddStringSliceFlag(constants.ArgSearchPath, nil, "Set a custom search_path for the steampipe user for a check session (comma-separated)").
AddStringSliceFlag(constants.ArgSearchPathPrefix, nil, "Set a prefix to the current search path for a check session (comma-separated)").
AddStringFlag(constants.ArgTheme, "dark", "Set the output theme for 'text' output: light, dark or plain").
AddStringSliceFlag(constants.ArgExport, nil, "Export output to file, supported formats: csv, html, json, md, nunit3, sps (snapshot), asff").
AddBoolFlag(constants.ArgProgress, true, "Display control execution progress").
AddBoolFlag(constants.ArgDryRun, false, "Show which controls will be run without running them").
AddStringSliceFlag(constants.ArgTag, nil, "Filter controls based on their tag values ('--tag key=value')").
AddStringSliceFlag(constants.ArgVarFile, nil, "Specify an .spvar file containing variable values").
// NOTE: use StringArrayFlag for ArgVariable, not StringSliceFlag
// Cobra will interpret values passed to a StringSliceFlag as CSV,
// where args passed to StringArrayFlag are not parsed and used raw
AddStringArrayFlag(constants.ArgVariable, nil, "Specify the value of a variable").
AddStringFlag(constants.ArgWhere, "", "SQL 'where' clause, or named query, used to filter controls (cannot be used with '--tag')").
AddIntFlag(constants.ArgDatabaseQueryTimeout, constants.DatabaseDefaultCheckQueryTimeout, "The query timeout").
AddIntFlag(constants.ArgMaxParallel, constants.DefaultMaxConnections, "The maximum number of concurrent database connections to open").
AddBoolFlag(constants.ArgModInstall, true, "Specify whether to install mod dependencies before running the check").
AddBoolFlag(constants.ArgInput, true, "Enable interactive prompts").
AddBoolFlag(constants.ArgSnapshot, false, "Create snapshot in Turbot Pipes with the default (workspace) visibility").
AddBoolFlag(constants.ArgShare, false, "Create snapshot in Turbot Pipes with 'anyone_with_link' visibility").
AddStringArrayFlag(constants.ArgSnapshotTag, nil, "Specify tags to set on the snapshot").
AddStringFlag(constants.ArgSnapshotLocation, "", "The location to write snapshots - either a local file path or a Turbot Pipes workspace").
AddStringFlag(constants.ArgSnapshotTitle, "", "The title to give a snapshot")
cmd.AddCommand(getListSubCmd(listSubCmdOptions{parentCmd: cmd}))
return cmd
}
// exitCode=0 no runtime errors, no control alarms or errors
// exitCode=1 no runtime errors, 1 or more control alarms, no control errors
// exitCode=2 no runtime errors, 1 or more control errors
// exitCode=3+ runtime errors
func runCheckCmd(cmd *cobra.Command, args []string) {
utils.LogTime("runCheckCmd start")
// setup a cancel context and start cancel handler
ctx, cancel := context.WithCancel(cmd.Context())
contexthelpers.StartCancelHandler(cancel)
defer func() {
utils.LogTime("runCheckCmd end")
if r := recover(); r != nil {
error_helpers.ShowError(ctx, helpers.ToError(r))
exitCode = constants.ExitCodeUnknownErrorPanic
}
}()
// verify we have an argument
if !validateCheckArgs(ctx, cmd, args) {
exitCode = constants.ExitCodeInsufficientOrWrongInputs
return
}
// if diagnostic mode is set, print out config and return
if _, ok := os.LookupEnv(constants.EnvConfigDump); ok {
cmdconfig.DisplayConfig()
return
}
// verify that no other benchmarks/controls are given with an all
if helpers.StringSliceContains(args, "all") && len(args) > 1 {
error_helpers.FailOnError(sperr.New("cannot execute 'all' with other benchmarks/controls"))
}
// show the status spinner
statushooks.Show(ctx)
// initialise
statushooks.SetStatus(ctx, "Initializing...")
// disable status hooks in init - otherwise we will end up
// getting status updates all the way down from the service layer
initData := control.NewInitData(ctx)
if initData.Result.Error != nil {
exitCode = constants.ExitCodeInitializationFailed
error_helpers.ShowError(ctx, initData.Result.Error)
return
}
defer initData.Cleanup(ctx)
// hide the spinner so that warning messages can be shown
statushooks.Done(ctx)
// if there is a usage warning we display it
initData.Result.DisplayMessages()
// pull out useful properties
totalAlarms, totalErrors := 0, 0
// get the execution trees
// depending on the set of arguments and the export targets, we may get more than one
// example :
// "check benchmark.b1 benchmark.b2 --export check.json" would give one merged tree
// "check benchmark.b1 benchmark.b2 --export json" would give multiple trees
trees, err := getExecutionTrees(ctx, initData, args...)
error_helpers.FailOnError(err)
// execute controls synchronously (execute returns the number of alarms and errors)
for _, namedTree := range trees {
err = executeTree(ctx, namedTree.tree, initData)
if err != nil {
error_helpers.ShowError(ctx, err)
continue
}
// append the total number of alarms and errors for multiple runs
totalAlarms += namedTree.tree.Root.Summary.Status.Alarm
totalErrors += namedTree.tree.Root.Summary.Status.Error
err = publishSnapshot(ctx, namedTree.tree, viper.GetBool(constants.ArgShare), viper.GetBool(constants.ArgSnapshot))
if err != nil {
error_helpers.ShowError(ctx, err)
continue
}
printTiming(namedTree.tree)
err = exportExecutionTree(ctx, namedTree, initData, viper.GetStringSlice(constants.ArgExport))
if err != nil {
error_helpers.ShowError(ctx, err)
continue
}
}
// set the defined exit code after successful execution
exitCode = getExitCode(totalAlarms, totalErrors)
}
// exportExecutionTree relies on the fact that the given tree is already executed
func exportExecutionTree(ctx context.Context, namedTree *namedExecutionTree, initData *control.InitData, exportArgs []string) error {
statushooks.Show(ctx)
defer statushooks.Done(ctx)
if error_helpers.IsContextCanceled(ctx) {
return ctx.Err()
}
exportMsg, err := initData.ExportManager.DoExport(ctx, namedTree.name, namedTree.tree, exportArgs)
if err != nil {
return err
}
// print the location where the file is exported if progress=true
if len(exportMsg) > 0 && viper.GetBool(constants.ArgProgress) {
fmt.Printf("\n")
fmt.Println(strings.Join(exportMsg, "\n"))
fmt.Printf("\n")
}
return nil
}
// executeTree executes and displays the (table) results of an execution
func executeTree(ctx context.Context, tree *controlexecute.ExecutionTree, initData *control.InitData) error {
// create a context with check status hooks
checkCtx := createCheckContext(ctx)
err := tree.Execute(checkCtx)
if err != nil {
return err
}
err = displayControlResults(checkCtx, tree, initData.OutputFormatter)
if err != nil {
return err
}
return nil
}
func publishSnapshot(ctx context.Context, executionTree *controlexecute.ExecutionTree, shouldShare bool, shouldUpload bool) error {
if error_helpers.IsContextCanceled(ctx) {
return ctx.Err()
}
// if the share args are set, create a snapshot and share it
if shouldShare || shouldUpload {
statushooks.SetStatus(ctx, "Publishing snapshot")
return controldisplay.PublishSnapshot(ctx, executionTree, shouldShare)
}
return nil
}
// getExecutionTrees returns a list of execution trees with the names of their export targets
// if the --export flag has the name of a file, a single merged tree is generated from the positional arguments
// otherwise, one tree is generated for each argument
//
// this is necessary, since exporters can only export entire execution trees and when a file name is provided, we want to export the whole tree into one file
//
// example :
// "check benchmark.b1 benchmark.b2 --export check.json" would give one merged tree
// "check benchmark.b1 benchmark.b2 --export json" would give multiple trees
func getExecutionTrees(ctx context.Context, initData *control.InitData, args ...string) ([]*namedExecutionTree, error) {
var trees []*namedExecutionTree
if initData.ExportManager.HasNamedExport(viper.GetStringSlice(constants.ArgExport)) {
// create a single merged execution tree from all arguments
executionTree, err := controlexecute.NewExecutionTree(ctx, initData.Workspace, initData.Client, initData.ControlFilterWhereClause, args...)
if err != nil {
return nil, sperr.WrapWithMessage(err, "could not create merged execution tree")
}
name := fmt.Sprintf("check.%s", initData.Workspace.Mod.ShortName)
trees = append(trees, newNamedExecutionTree(name, executionTree))
} else {
for _, arg := range args {
if error_helpers.IsContextCanceled(ctx) {
return nil, ctx.Err()
}
executionTree, err := controlexecute.NewExecutionTree(ctx, initData.Workspace, initData.Client, initData.ControlFilterWhereClause, arg)
if err != nil {
return nil, sperr.WrapWithMessage(err, "could not create execution tree for %s", arg)
}
name, err := getExportName(arg, initData.Workspace.Mod.ShortName)
if err != nil {
return nil, sperr.WrapWithMessage(err, "could not evaluate export name for %s", arg)
}
trees = append(trees, newNamedExecutionTree(name, executionTree))
}
}
return trees, ctx.Err()
}
// getExportName resolves the base name of the target file
func getExportName(targetName string, modShortName string) (string, error) {
parsedName, _ := modconfig.ParseResourceName(targetName)
if targetName == "all" {
// there will be no block type = manually construct name
return fmt.Sprintf("%s.%s", modShortName, parsedName.Name), nil
}
// default to just converting to valid resource name
return parsedName.ToFullNameWithMod(modShortName)
}
// get the exit code for successful check run
func getExitCode(alarms int, errors int) int {
// 1 or more control errors, return exitCode=2
if errors > 0 {
return constants.ExitCodeControlsError
}
// 1 or more controls in alarm, return exitCode=1
if alarms > 0 {
return constants.ExitCodeControlsAlarm
}
// no controls in alarm/error
return constants.ExitCodeSuccessful
}
// create the context for the check run - add a control status renderer
func createCheckContext(ctx context.Context) context.Context {
return controlstatus.AddControlHooksToContext(ctx, controlstatus.NewStatusControlHooks())
}
func validateCheckArgs(ctx context.Context, cmd *cobra.Command, args []string) bool {
if len(args) == 0 {
fmt.Println()
error_helpers.ShowError(ctx, fmt.Errorf("you must provide at least one argument"))
fmt.Println()
//nolint:errcheck // cmd.Help always returns a nil error
cmd.Help()
fmt.Println()
return false
}
if err := cmdconfig.ValidateSnapshotArgs(ctx); err != nil {
error_helpers.ShowError(ctx, err)
return false
}
// only 1 character is allowed for '--separator'
if len(viper.GetString(constants.ArgSeparator)) > 1 {
error_helpers.ShowError(ctx, fmt.Errorf("'--%s' can be 1 character long at most", constants.ArgSeparator))
return false
}
// only 1 of 'share' and 'snapshot' may be set
if viper.GetBool(constants.ArgShare) && viper.GetBool(constants.ArgSnapshot) {
error_helpers.ShowError(ctx, fmt.Errorf("only 1 of '--%s' and '--%s' may be set", constants.ArgShare, constants.ArgSnapshot))
return false
}
// if both '--where' and '--tag' have been used, then it's an error
if viper.IsSet(constants.ArgWhere) && viper.IsSet(constants.ArgTag) {
error_helpers.ShowError(ctx, fmt.Errorf("only 1 of '--%s' and '--%s' may be set", constants.ArgWhere, constants.ArgTag))
return false
}
return true
}
func printTiming(tree *controlexecute.ExecutionTree) {
if !shouldPrintTiming() {
return
}
headers := []string{"", "Duration"}
var rows [][]string
for _, rg := range tree.Root.Groups {
if rg.GroupItem.GetUnqualifiedName() == "benchmark.root" {
// this is the created root benchmark
// adds the children
for _, g := range rg.Groups {
rows = append(rows, []string{g.GroupItem.GetUnqualifiedName(), rg.Duration.String()})
}
continue
}
rows = append(rows, []string{rg.GroupItem.GetUnqualifiedName(), rg.Duration.String()})
}
for _, c := range tree.Root.ControlRuns {
rows = append(rows, []string{c.Control.GetUnqualifiedName(), c.Duration.String()})
}
// blank line after renderer output
fmt.Println()
fmt.Println("Timing:")
display.ShowWrappedTable(headers, rows, &display.ShowWrappedTableOptions{AutoMerge: false})
}
func shouldPrintTiming() bool {
outputFormat := viper.GetString(constants.ArgOutput)
timingMode := viper.GetString(constants.ArgTiming)
return (timingMode != constants.ArgOff && !viper.GetBool(constants.ArgDryRun)) &&
(outputFormat == constants.OutputFormatText || outputFormat == constants.OutputFormatBrief)
}
func displayControlResults(ctx context.Context, executionTree *controlexecute.ExecutionTree, formatter controldisplay.Formatter) error {
reader, err := formatter.Format(ctx, executionTree)
if err != nil {
return err
}
_, err = io.Copy(os.Stdout, reader)
return err
}
type namedExecutionTree struct {
tree *controlexecute.ExecutionTree
name string
}
func newNamedExecutionTree(name string, tree *controlexecute.ExecutionTree) *namedExecutionTree {
return &namedExecutionTree{
tree: tree,
name: name,
}
}

View File

@@ -90,7 +90,6 @@ func AddCommands() {
rootCmd.AddCommand(
pluginCmd(),
queryCmd(),
checkCmd(),
serviceCmd(),
modCmd(),
generateCompletionScriptsCmd(),

View File

@@ -1,181 +0,0 @@
package control
import (
"context"
"fmt"
"net/url"
"strings"
"github.com/spf13/viper"
"github.com/turbot/steampipe/pkg/constants"
"github.com/turbot/steampipe/pkg/control/controldisplay"
"github.com/turbot/steampipe/pkg/error_helpers"
"github.com/turbot/steampipe/pkg/initialisation"
"github.com/turbot/steampipe/pkg/statushooks"
"github.com/turbot/steampipe/pkg/workspace"
)
type InitData struct {
initialisation.InitData
OutputFormatter controldisplay.Formatter
ControlFilterWhereClause string
}
// NewInitData returns a new InitData object
// It also starts an asynchronous population of the object
// InitData.Done closes after asynchronous initialization completes
func NewInitData(ctx context.Context) *InitData {
// create InitData, but do not initialize yet, since 'viper' is not completely setup
i := &InitData{
InitData: *initialisation.NewInitData(),
}
statushooks.SetStatus(ctx, "Loading workspace")
// load the workspace
w, errAndWarnings := workspace.LoadWorkspacePromptingForVariables(ctx)
if errAndWarnings.GetError() != nil {
return &InitData{
InitData: *initialisation.NewErrorInitData(fmt.Errorf("failed to load workspace: %s", error_helpers.HandleCancelError(errAndWarnings.GetError()).Error())),
}
}
statushooks.SetStatus(ctx, "Initialising...")
// disable status hooks for any subsequent initialisation
ctx = statushooks.DisableStatusHooks(ctx)
i.Workspace = w
i.Result.AddWarnings(errAndWarnings.Warnings...)
if !w.ModfileExists() {
i.Result.Error = workspace.ErrorNoModDefinition
}
if viper.GetString(constants.ArgOutput) == constants.OutputFormatNone {
// set progress to false
viper.Set(constants.ArgProgress, false)
}
// set color schema
err := initialiseCheckColorScheme()
if err != nil {
i.Result.Error = err
return i
}
if len(w.GetResourceMaps().Controls)+len(w.GetResourceMaps().Benchmarks) == 0 {
i.Result.AddWarnings("no controls or benchmarks found in current workspace")
}
if err := controldisplay.EnsureTemplates(); err != nil {
i.Result.Error = err
return i
}
if len(viper.GetStringSlice(constants.ArgExport)) > 0 {
i.registerCheckExporters(ctx)
// validate required export formats
if err := i.ExportManager.ValidateExportFormat(viper.GetStringSlice(constants.ArgExport)); err != nil {
i.Result.Error = err
return i
}
}
output := viper.GetString(constants.ArgOutput)
formatter, err := parseOutputArg(ctx, output)
if err != nil {
i.Result.Error = err
return i
}
i.OutputFormatter = formatter
i.setControlFilterClause()
// initialize
i.InitData.Init(ctx, constants.InvokerCheck)
return i
}
func (i *InitData) setControlFilterClause() {
if viper.IsSet(constants.ArgTag) {
// if '--tag' args were used, derive the whereClause from them
tags := viper.GetStringSlice(constants.ArgTag)
i.ControlFilterWhereClause = generateWhereClauseFromTags(tags)
} else if viper.IsSet(constants.ArgWhere) {
// if a 'where' arg was used, execute this sql to get a list of control names
// use this list to build a name map used to determine whether to run a particular control
i.ControlFilterWhereClause = viper.GetString(constants.ArgWhere)
}
// if we derived or were passed a where clause, run the filter
if len(i.ControlFilterWhereClause) > 0 {
// if we have a control filter where clause, we must create the control introspection tables
viper.Set(constants.ArgIntrospection, constants.IntrospectionControl)
}
}
func generateWhereClauseFromTags(tags []string) string {
whereMap := map[string][]string{}
// 'tags' should be KV Pairs of the form: 'benchmark=pic' or 'cis_level=1'
for _, tag := range tags {
value, _ := url.ParseQuery(tag)
for k, v := range value {
if _, found := whereMap[k]; !found {
whereMap[k] = []string{}
}
whereMap[k] = append(whereMap[k], v...)
}
}
whereComponents := []string{}
for key, values := range whereMap {
thisComponent := []string{}
for _, x := range values {
if len(x) == 0 {
// ignore
continue
}
thisComponent = append(thisComponent, fmt.Sprintf("tags->>'%s'='%s'", key, x))
}
whereComponents = append(whereComponents, fmt.Sprintf("(%s)", strings.Join(thisComponent, " OR ")))
}
return strings.Join(whereComponents, " AND ")
}
// register exporters for each of the supported check formats
func (i *InitData) registerCheckExporters(ctx context.Context) {
exporters, err := controldisplay.GetExporters(ctx)
error_helpers.FailOnErrorWithMessage(err, "failed to load exporters")
// register all exporters
i.RegisterExporters(exporters...)
}
// parseOutputArg parses the --output flag value and returns the Formatter that can format the data
func parseOutputArg(ctx context.Context, arg string) (formatter controldisplay.Formatter, err error) {
formatResolver, err := controldisplay.NewFormatResolver(ctx)
if err != nil {
return nil, err
}
return formatResolver.GetFormatter(arg)
}
func initialiseCheckColorScheme() error {
theme := viper.GetString(constants.ArgTheme)
if !viper.GetBool(constants.ConfigKeyIsTerminalTTY) {
// enforce plain output for non-terminals
theme = "plain"
}
themeDef, ok := controldisplay.ColorSchemes[theme]
if !ok {
return fmt.Errorf("invalid theme '%s'", theme)
}
scheme, err := controldisplay.NewControlColorScheme(themeDef)
if err != nil {
return err
}
controldisplay.ControlColors = scheme
return nil
}