github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/control/controlexecute/execution_tree.go (about)

     1  package controlexecute
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"log"
     7  	"sort"
     8  	"time"
     9  
    10  	"github.com/spf13/viper"
    11  	"github.com/turbot/go-kit/helpers"
    12  	"github.com/turbot/steampipe-plugin-sdk/v5/sperr"
    13  	"github.com/turbot/steampipe/pkg/connection_sync"
    14  	"github.com/turbot/steampipe/pkg/constants"
    15  	"github.com/turbot/steampipe/pkg/control/controlstatus"
    16  	"github.com/turbot/steampipe/pkg/db/db_common"
    17  	"github.com/turbot/steampipe/pkg/query/queryresult"
    18  	"github.com/turbot/steampipe/pkg/statushooks"
    19  	"github.com/turbot/steampipe/pkg/steampipeconfig/modconfig"
    20  	"github.com/turbot/steampipe/pkg/utils"
    21  	"github.com/turbot/steampipe/pkg/workspace"
    22  	"golang.org/x/sync/semaphore"
    23  )
    24  
    25  // ExecutionTree is a structure representing the control execution hierarchy
    26  type ExecutionTree struct {
    27  	Root *ResultGroup `json:"root"`
    28  	// flat list of all control runs
    29  	ControlRuns []*ControlRun                  `json:"-"`
    30  	StartTime   time.Time                      `json:"start_time"`
    31  	EndTime     time.Time                      `json:"end_time"`
    32  	Progress    *controlstatus.ControlProgress `json:"progress"`
    33  	// map of dimension property name to property value to color map
    34  	DimensionColorGenerator *DimensionColorGenerator `json:"-"`
    35  	// the current session search path
    36  	SearchPath []string             `json:"-"`
    37  	Workspace  *workspace.Workspace `json:"-"`
    38  	client     db_common.Client
    39  	// an optional map of control names used to filter the controls which are run
    40  	controlNameFilterMap map[string]bool
    41  }
    42  
    43  func NewExecutionTree(ctx context.Context, workspace *workspace.Workspace, client db_common.Client, controlFilterWhereClause string, args ...string) (*ExecutionTree, error) {
    44  	if len(args) < 1 {
    45  		return nil, sperr.New("need at least one argument to create a check execution tree")
    46  	}
    47  
    48  	searchPath := client.GetRequiredSessionSearchPath()
    49  
    50  	// now populate the ExecutionTree
    51  	executionTree := &ExecutionTree{
    52  		Workspace:  workspace,
    53  		client:     client,
    54  		SearchPath: utils.UnquoteStringArray(searchPath),
    55  	}
    56  	// if a "--where" or "--tag" parameter was passed, build a map of control names used to filter the controls to run
    57  	// create a context with status hooks disabled
    58  	noStatusCtx := statushooks.DisableStatusHooks(ctx)
    59  	err := executionTree.populateControlFilterMap(noStatusCtx, controlFilterWhereClause)
    60  	if err != nil {
    61  		return nil, err
    62  	}
    63  
    64  	var resolvedItem modconfig.ModTreeItem
    65  
    66  	// if only one argument is provided, add this as execution root
    67  	if len(args) == 1 {
    68  		resolvedItem, err = executionTree.getExecutionRootFromArg(args[0])
    69  		if err != nil {
    70  			return nil, err
    71  		}
    72  	} else {
    73  		// for multiple items, use a root benchmark as the parent of the items
    74  		// this root benchmark will be converted to a ResultGroup that can be worked with
    75  		// this is necessary because snapshots only support a single tree item as the child of the root
    76  		items := []modconfig.ModTreeItem{}
    77  		for _, arg := range args {
    78  			item, err := executionTree.getExecutionRootFromArg(arg)
    79  			if err != nil {
    80  				return nil, err
    81  			}
    82  			items = append(items, item)
    83  		}
    84  
    85  		// create a root benchmark with `items` as it's children
    86  		resolvedItem = modconfig.NewRootBenchmarkWithChildren(workspace.Mod, items).(modconfig.ModTreeItem)
    87  	}
    88  	// build tree of result groups, starting with a synthetic 'root' node
    89  	executionTree.Root = NewRootResultGroup(ctx, executionTree, resolvedItem)
    90  
    91  	// after tree has built, ControlCount will be set - create progress rendered
    92  	executionTree.Progress = controlstatus.NewControlProgress(len(executionTree.ControlRuns))
    93  
    94  	return executionTree, nil
    95  }
    96  
    97  // IsExportSourceData implements ExportSourceData
    98  func (*ExecutionTree) IsExportSourceData() {}
    99  
   100  // AddControl checks whether control should be included in the tree
   101  // if so, creates a ControlRun, which is added to the parent group
   102  func (e *ExecutionTree) AddControl(ctx context.Context, control *modconfig.Control, group *ResultGroup) {
   103  	// note we use short name to determine whether to include a control
   104  	if e.ShouldIncludeControl(control.ShortName) {
   105  		// create new ControlRun with treeItem as the parent
   106  		controlRun := NewControlRun(control, group, e)
   107  		// add it into the group
   108  		group.addControl(controlRun)
   109  
   110  		// also add it into the execution tree control run list
   111  		e.ControlRuns = append(e.ControlRuns, controlRun)
   112  	}
   113  }
   114  
   115  func (e *ExecutionTree) Execute(ctx context.Context) error {
   116  	log.Println("[TRACE]", "begin ExecutionTree.Execute")
   117  	defer log.Println("[TRACE]", "end ExecutionTree.Execute")
   118  	e.StartTime = time.Now()
   119  	e.Progress.Start(ctx)
   120  
   121  	defer func() {
   122  		e.EndTime = time.Now()
   123  		e.Progress.Finish(ctx)
   124  	}()
   125  
   126  	// TODO should we always wait even with non custom search path?
   127  	// if there is a custom search path, wait until the first connection of each plugin has loaded
   128  	if customSearchPath := e.client.GetCustomSearchPath(); customSearchPath != nil {
   129  		if err := connection_sync.WaitForSearchPathSchemas(ctx, e.client, customSearchPath); err != nil {
   130  			return err
   131  		}
   132  	}
   133  
   134  	// the number of goroutines parallel to start
   135  	var maxParallelGoRoutines int64 = constants.DefaultMaxConnections
   136  	if viper.IsSet(constants.ArgMaxParallel) {
   137  		maxParallelGoRoutines = viper.GetInt64(constants.ArgMaxParallel)
   138  	}
   139  
   140  	// to limit the number of parallel controls go routines started
   141  	parallelismLock := semaphore.NewWeighted(maxParallelGoRoutines)
   142  
   143  	// just execute the root - it will traverse the tree
   144  	e.Root.execute(ctx, e.client, parallelismLock)
   145  
   146  	if err := e.waitForActiveRunsToComplete(ctx, parallelismLock, maxParallelGoRoutines); err != nil {
   147  		log.Printf("[WARN] timed out waiting for active runs to complete")
   148  	}
   149  
   150  	// now build map of dimension property name to property value to color map
   151  	e.DimensionColorGenerator, _ = NewDimensionColorGenerator(4, 27)
   152  	e.DimensionColorGenerator.populate(e)
   153  
   154  	return nil
   155  }
   156  
   157  func (e *ExecutionTree) waitForActiveRunsToComplete(ctx context.Context, parallelismLock *semaphore.Weighted, maxParallelGoRoutines int64) error {
   158  	waitCtx := ctx
   159  	// if the context was already cancelled, we must creat ea new one to use  when waiting to acquire the lock
   160  	if ctx.Err() != nil {
   161  		// use a Background context - since the original context has been cancelled
   162  		// this lets us wait for the active control queries to cancel
   163  		c, cancel := context.WithTimeout(context.Background(), constants.ControlQueryCancellationTimeoutSecs*time.Second)
   164  		waitCtx = c
   165  		defer cancel()
   166  	}
   167  	// wait till we can acquire all semaphores - meaning that all active runs have finished
   168  	return parallelismLock.Acquire(waitCtx, maxParallelGoRoutines)
   169  }
   170  
   171  func (e *ExecutionTree) populateControlFilterMap(ctx context.Context, controlFilterWhereClause string) error {
   172  	// if we derived or were passed a where clause, run the filter
   173  	if len(controlFilterWhereClause) > 0 {
   174  		log.Println("[TRACE]", "filtering controls with", controlFilterWhereClause)
   175  		var err error
   176  		e.controlNameFilterMap, err = e.getControlMapFromWhereClause(ctx, controlFilterWhereClause)
   177  		if err != nil {
   178  			return err
   179  		}
   180  	}
   181  
   182  	return nil
   183  }
   184  
   185  func (e *ExecutionTree) ShouldIncludeControl(controlName string) bool {
   186  	if e.controlNameFilterMap == nil {
   187  		return true
   188  	}
   189  	_, ok := e.controlNameFilterMap[controlName]
   190  	return ok
   191  }
   192  
   193  // getExecutionRootFromArg resolves the arg into the execution root
   194  // - if the arg is a control name, the root will be the Control with that name
   195  // - if the arg is a benchmark name, the root will be the Benchmark with that name
   196  // - if the arg is a mod name, the root will be the Mod with that name
   197  // - if the arg is 'all' the root will be a node with all Mods as children
   198  func (e *ExecutionTree) getExecutionRootFromArg(arg string) (modconfig.ModTreeItem, error) {
   199  	// special case handling for the string "all"
   200  	if arg == "all" {
   201  		// if the arg is "all", we want to execute all _direct_ children of the Mod
   202  		// but NOT children which come from dependency mods
   203  
   204  		// to achieve this, use a  DirectChildrenModDecorator
   205  		return &DirectChildrenModDecorator{Mod: e.Workspace.Mod}, nil
   206  	}
   207  
   208  	// if the arg is the name of one of the workspace dependendencies, wrap it in DirectChildrenModDecorator
   209  	// so we only execute _its_ direct children
   210  	for _, mod := range e.Workspace.Mods {
   211  		if mod.ShortName == arg {
   212  			return &DirectChildrenModDecorator{Mod: mod}, nil
   213  		}
   214  	}
   215  
   216  	// what resource type is arg?
   217  	parsedName, err := modconfig.ParseResourceName(arg)
   218  	if err != nil {
   219  		// just log error
   220  		return nil, fmt.Errorf("failed to parse check argument '%s': %v", arg, err)
   221  	}
   222  
   223  	resource, found := e.Workspace.GetResource(parsedName)
   224  
   225  	root, ok := resource.(modconfig.ModTreeItem)
   226  	if !found || !ok {
   227  		return nil, fmt.Errorf("no resources found matching argument '%s'", arg)
   228  	}
   229  	// root item must be either a benchmark or a control
   230  	if !helpers.StringSliceContains([]string{modconfig.BlockTypeControl, modconfig.BlockTypeBenchmark}, root.BlockType()) {
   231  		return nil, fmt.Errorf("cannot execute '%s' using check, only controls and benchmarks may be run", resource.Name())
   232  	}
   233  	return root, nil
   234  }
   235  
   236  // Get a map of control names from the introspection table steampipe_control
   237  // This is used to implement the 'where' control filtering
   238  func (e *ExecutionTree) getControlMapFromWhereClause(ctx context.Context, whereClause string) (map[string]bool, error) {
   239  	// query may either be a 'where' clause, or a named query
   240  	resolvedQuery, _, err := e.Workspace.ResolveQueryAndArgsFromSQLString(whereClause)
   241  	if err != nil {
   242  		return nil, err
   243  	}
   244  	// did we in fact resolve a named query, or just return the 'name' as the query
   245  	isNamedQuery := resolvedQuery.ExecuteSQL != whereClause
   246  
   247  	// if the query is NOT a named query, we need to construct a full query by adding a select
   248  	if !isNamedQuery {
   249  		resolvedQuery.ExecuteSQL = fmt.Sprintf("select resource_name from %s where %s", constants.IntrospectionTableControl, whereClause)
   250  	}
   251  
   252  	res, err := e.client.ExecuteSync(ctx, resolvedQuery.ExecuteSQL, resolvedQuery.Args...)
   253  	if err != nil {
   254  		return nil, err
   255  	}
   256  
   257  	//
   258  	// find the "resource_name" column index
   259  	resourceNameColumnIndex := -1
   260  
   261  	for i, c := range res.Cols {
   262  		if c.Name == "resource_name" {
   263  			resourceNameColumnIndex = i
   264  		}
   265  	}
   266  	if resourceNameColumnIndex == -1 {
   267  		return nil, fmt.Errorf("the named query passed in the 'where' argument must return the 'resource_name' column")
   268  	}
   269  
   270  	var controlNames = make(map[string]bool)
   271  	for _, row := range res.Rows {
   272  		rowResult := row.(*queryresult.RowResult)
   273  		controlName := rowResult.Data[resourceNameColumnIndex].(string)
   274  		controlNames[controlName] = true
   275  	}
   276  	return controlNames, nil
   277  }
   278  
   279  func (e *ExecutionTree) GetAllTags() []string {
   280  	// map keep track which tags have been added as columns
   281  	tagColumnMap := make(map[string]bool)
   282  	var tagColumns []string
   283  	for _, r := range e.ControlRuns {
   284  		if r.Control.Tags != nil {
   285  			for tag := range r.Control.Tags {
   286  				if !tagColumnMap[tag] {
   287  					tagColumns = append(tagColumns, tag)
   288  					tagColumnMap[tag] = true
   289  				}
   290  			}
   291  		}
   292  	}
   293  	sort.Strings(tagColumns)
   294  	return tagColumns
   295  }