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

     1  package controlexecute
     2  
     3  import (
     4  	"context"
     5  	"log"
     6  	"sort"
     7  	"sync"
     8  	"sync/atomic"
     9  	"time"
    10  
    11  	"github.com/spf13/viper"
    12  	"github.com/turbot/go-kit/helpers"
    13  	"github.com/turbot/steampipe/pkg/constants"
    14  	"github.com/turbot/steampipe/pkg/control/controlstatus"
    15  	"github.com/turbot/steampipe/pkg/dashboard/dashboardtypes"
    16  	"github.com/turbot/steampipe/pkg/db/db_common"
    17  	"github.com/turbot/steampipe/pkg/error_helpers"
    18  	"github.com/turbot/steampipe/pkg/steampipeconfig/modconfig"
    19  	"golang.org/x/sync/semaphore"
    20  )
    21  
    22  const RootResultGroupName = "root_result_group"
    23  
    24  // ResultGroup is a struct representing a grouping of control results
    25  // It may correspond to a Benchmark, or some other arbitrary grouping
    26  type ResultGroup struct {
    27  	GroupId       string            `json:"name" csv:"group_id"`
    28  	Title         string            `json:"title,omitempty" csv:"title"`
    29  	Description   string            `json:"description,omitempty" csv:"description"`
    30  	Tags          map[string]string `json:"tags,omitempty"`
    31  	Documentation string            `json:"documentation,omitempty"`
    32  	Display       string            `json:"display,omitempty"`
    33  	Type          string            `json:"type,omitempty"`
    34  
    35  	// the overall summary of the group
    36  	Summary *GroupSummary `json:"summary"`
    37  	// child result groups
    38  	Groups []*ResultGroup `json:"-"`
    39  	// child control runs
    40  	ControlRuns []*ControlRun `json:"-"`
    41  	// list of children stored as controlexecute.ExecutionTreeNode
    42  	Children []ExecutionTreeNode                    `json:"-"`
    43  	Severity map[string]controlstatus.StatusSummary `json:"-"`
    44  	// "benchmark"
    45  	NodeType string `json:"panel_type"`
    46  	// the control tree item associated with this group(i.e. a mod/benchmark)
    47  	GroupItem modconfig.ModTreeItem `json:"-"`
    48  	Parent    *ResultGroup          `json:"-"`
    49  	Duration  time.Duration         `json:"-"`
    50  
    51  	// a list of distinct dimension keys from descendant controls
    52  	DimensionKeys []string `json:"-"`
    53  
    54  	childrenComplete   uint32
    55  	executionStartTime time.Time
    56  	// lock to prevent multiple control_runs updating this
    57  	updateLock *sync.Mutex
    58  }
    59  
    60  type GroupSummary struct {
    61  	Status   controlstatus.StatusSummary            `json:"status"`
    62  	Severity map[string]controlstatus.StatusSummary `json:"-"`
    63  }
    64  
    65  func NewGroupSummary() *GroupSummary {
    66  	return &GroupSummary{Severity: make(map[string]controlstatus.StatusSummary)}
    67  }
    68  
    69  // NewRootResultGroup creates a ResultGroup to act as the root node of a control execution tree
    70  func NewRootResultGroup(ctx context.Context, executionTree *ExecutionTree, rootItem modconfig.ModTreeItem) *ResultGroup {
    71  	root := &ResultGroup{
    72  		GroupId:    RootResultGroupName,
    73  		Groups:     []*ResultGroup{},
    74  		Tags:       make(map[string]string),
    75  		Summary:    NewGroupSummary(),
    76  		Severity:   make(map[string]controlstatus.StatusSummary),
    77  		updateLock: new(sync.Mutex),
    78  		NodeType:   modconfig.BlockTypeBenchmark,
    79  		Title:      rootItem.GetTitle(),
    80  	}
    81  
    82  	// if root item is a benchmark, create new result group with root as parent
    83  	if control, ok := rootItem.(*modconfig.Control); ok {
    84  		// if root item is a control, add control run
    85  		executionTree.AddControl(ctx, control, root)
    86  	} else {
    87  		// create a result group for this item
    88  		itemGroup := NewResultGroup(ctx, executionTree, rootItem, root)
    89  		root.addResultGroup(itemGroup)
    90  	}
    91  
    92  	return root
    93  }
    94  
    95  // NewResultGroup creates a result group from a ModTreeItem
    96  func NewResultGroup(ctx context.Context, executionTree *ExecutionTree, treeItem modconfig.ModTreeItem, parent *ResultGroup) *ResultGroup {
    97  	group := &ResultGroup{
    98  		GroupId:     treeItem.Name(),
    99  		Title:       treeItem.GetTitle(),
   100  		Description: treeItem.GetDescription(),
   101  		Tags:        treeItem.GetTags(),
   102  		GroupItem:   treeItem,
   103  		Parent:      parent,
   104  		Groups:      []*ResultGroup{},
   105  		Summary:     NewGroupSummary(),
   106  		Severity:    make(map[string]controlstatus.StatusSummary),
   107  		updateLock:  new(sync.Mutex),
   108  		NodeType:    modconfig.BlockTypeBenchmark,
   109  	}
   110  
   111  	// populate additional properties (this avoids adding GetDocumentation, GetDisplay and GetType to all ModTreeItems)
   112  	switch t := treeItem.(type) {
   113  	case *modconfig.Benchmark:
   114  		group.Documentation = t.GetDocumentation()
   115  		group.Display = t.GetDisplay()
   116  		group.Type = t.GetType()
   117  	case *modconfig.Control:
   118  		group.Documentation = t.GetDocumentation()
   119  		group.Display = t.GetDisplay()
   120  		group.Type = t.GetType()
   121  	}
   122  	// add child groups for children which are benchmarks
   123  	for _, c := range treeItem.GetChildren() {
   124  		if benchmark, ok := c.(*modconfig.Benchmark); ok {
   125  			// create a result group for this item
   126  			benchmarkGroup := NewResultGroup(ctx, executionTree, benchmark, group)
   127  			// if the group has any control runs, add to tree
   128  			if benchmarkGroup.ControlRunCount() > 0 {
   129  				// create a new result group with 'group' as the parent
   130  				group.addResultGroup(benchmarkGroup)
   131  			}
   132  		}
   133  		if control, ok := c.(*modconfig.Control); ok {
   134  			executionTree.AddControl(ctx, control, group)
   135  		}
   136  	}
   137  
   138  	return group
   139  }
   140  
   141  func (r *ResultGroup) AllTagKeys() []string {
   142  	tags := []string{}
   143  	for k := range r.Tags {
   144  		tags = append(tags, k)
   145  	}
   146  	for _, child := range r.Groups {
   147  		tags = append(tags, child.AllTagKeys()...)
   148  	}
   149  	for _, run := range r.ControlRuns {
   150  		for k := range run.Control.Tags {
   151  			tags = append(tags, k)
   152  		}
   153  	}
   154  	tags = helpers.StringSliceDistinct(tags)
   155  	sort.Strings(tags)
   156  	return tags
   157  }
   158  
   159  // GetGroupByName finds an immediate child ResultGroup with a specific name
   160  func (r *ResultGroup) GetGroupByName(name string) *ResultGroup {
   161  	for _, group := range r.Groups {
   162  		if group.GroupId == name {
   163  			return group
   164  		}
   165  	}
   166  	return nil
   167  }
   168  
   169  // GetChildGroupByName finds a nested child ResultGroup with a specific name
   170  func (r *ResultGroup) GetChildGroupByName(name string) *ResultGroup {
   171  	for _, group := range r.Groups {
   172  		if group.GroupId == name {
   173  			return group
   174  		}
   175  		if child := group.GetChildGroupByName(name); child != nil {
   176  			return child
   177  		}
   178  	}
   179  	return nil
   180  }
   181  
   182  // GetControlRunByName finds a child ControlRun with a specific control name
   183  func (r *ResultGroup) GetControlRunByName(name string) *ControlRun {
   184  	for _, run := range r.ControlRuns {
   185  		if run.Control.Name() == name {
   186  			return run
   187  		}
   188  	}
   189  	return nil
   190  }
   191  
   192  func (r *ResultGroup) ControlRunCount() int {
   193  	count := len(r.ControlRuns)
   194  	for _, g := range r.Groups {
   195  		count += g.ControlRunCount()
   196  	}
   197  	return count
   198  }
   199  
   200  // IsSnapshotPanel implements SnapshotPanel
   201  func (*ResultGroup) IsSnapshotPanel() {}
   202  
   203  // IsExecutionTreeNode implements ExecutionTreeNode
   204  func (*ResultGroup) IsExecutionTreeNode() {}
   205  
   206  // GetChildren implements ExecutionTreeNode
   207  func (r *ResultGroup) GetChildren() []ExecutionTreeNode { return r.Children }
   208  
   209  // GetName implements ExecutionTreeNode
   210  func (r *ResultGroup) GetName() string { return r.GroupId }
   211  
   212  // AsTreeNode implements ExecutionTreeNode
   213  func (r *ResultGroup) AsTreeNode() *dashboardtypes.SnapshotTreeNode {
   214  	res := &dashboardtypes.SnapshotTreeNode{
   215  		Name:     r.GroupId,
   216  		Children: make([]*dashboardtypes.SnapshotTreeNode, len(r.Children)),
   217  		NodeType: r.NodeType,
   218  	}
   219  	for i, c := range r.Children {
   220  		res.Children[i] = c.AsTreeNode()
   221  	}
   222  	return res
   223  }
   224  
   225  // add result group into our list, and also add a tree node into our child list
   226  func (r *ResultGroup) addResultGroup(group *ResultGroup) {
   227  	r.Groups = append(r.Groups, group)
   228  	r.Children = append(r.Children, group)
   229  }
   230  
   231  // add control into our list, and also add a tree node into our child list
   232  func (r *ResultGroup) addControl(controlRun *ControlRun) {
   233  	r.ControlRuns = append(r.ControlRuns, controlRun)
   234  	r.Children = append(r.Children, controlRun)
   235  }
   236  
   237  func (r *ResultGroup) addDimensionKeys(keys ...string) {
   238  	r.updateLock.Lock()
   239  	defer r.updateLock.Unlock()
   240  	r.DimensionKeys = append(r.DimensionKeys, keys...)
   241  	if r.Parent != nil {
   242  		r.Parent.addDimensionKeys(keys...)
   243  	}
   244  	r.DimensionKeys = helpers.StringSliceDistinct(r.DimensionKeys)
   245  	sort.Strings(r.DimensionKeys)
   246  }
   247  
   248  // onChildDone is a callback that gets called from the children of this result group when they are done
   249  func (r *ResultGroup) onChildDone() {
   250  	newCount := atomic.AddUint32(&r.childrenComplete, 1)
   251  	totalCount := uint32(len(r.ControlRuns) + len(r.Groups))
   252  	if newCount < totalCount {
   253  		// all children haven't finished execution yet
   254  		return
   255  	}
   256  
   257  	// all children are done
   258  	r.Duration = time.Since(r.executionStartTime)
   259  	if r.Parent != nil {
   260  		r.Parent.onChildDone()
   261  	}
   262  }
   263  
   264  func (r *ResultGroup) updateSummary(summary *controlstatus.StatusSummary) {
   265  	r.updateLock.Lock()
   266  	defer r.updateLock.Unlock()
   267  
   268  	r.Summary.Status.Skip += summary.Skip
   269  	r.Summary.Status.Alarm += summary.Alarm
   270  	r.Summary.Status.Info += summary.Info
   271  	r.Summary.Status.Ok += summary.Ok
   272  	r.Summary.Status.Error += summary.Error
   273  
   274  	if r.Parent != nil {
   275  		r.Parent.updateSummary(summary)
   276  	}
   277  }
   278  
   279  func (r *ResultGroup) updateSeverityCounts(severity string, summary *controlstatus.StatusSummary) {
   280  	r.updateLock.Lock()
   281  	defer r.updateLock.Unlock()
   282  
   283  	val, exists := r.Severity[severity]
   284  	if !exists {
   285  		val = controlstatus.StatusSummary{}
   286  	}
   287  	val.Alarm += summary.Alarm
   288  	val.Error += summary.Error
   289  	val.Info += summary.Info
   290  	val.Ok += summary.Ok
   291  	val.Skip += summary.Skip
   292  
   293  	r.Summary.Severity[severity] = val
   294  	if r.Parent != nil {
   295  		r.Parent.updateSeverityCounts(severity, summary)
   296  	}
   297  }
   298  
   299  func (r *ResultGroup) execute(ctx context.Context, client db_common.Client, parallelismLock *semaphore.Weighted) {
   300  	log.Printf("[TRACE] begin ResultGroup.Execute: %s\n", r.GroupId)
   301  	defer log.Printf("[TRACE] end ResultGroup.Execute: %s\n", r.GroupId)
   302  
   303  	r.executionStartTime = time.Now()
   304  
   305  	for _, controlRun := range r.ControlRuns {
   306  		if error_helpers.IsContextCanceled(ctx) {
   307  			controlRun.setError(ctx, ctx.Err())
   308  			continue
   309  		}
   310  
   311  		if viper.GetBool(constants.ArgDryRun) {
   312  			controlRun.skip(ctx)
   313  			continue
   314  		}
   315  
   316  		err := parallelismLock.Acquire(ctx, 1)
   317  		if err != nil {
   318  			controlRun.setError(ctx, err)
   319  			continue
   320  		}
   321  
   322  		go executeRun(ctx, controlRun, parallelismLock, client)
   323  	}
   324  	for _, child := range r.Groups {
   325  		child.execute(ctx, client, parallelismLock)
   326  	}
   327  }
   328  
   329  func executeRun(ctx context.Context, run *ControlRun, parallelismLock *semaphore.Weighted, client db_common.Client) {
   330  	defer func() {
   331  		if r := recover(); r != nil {
   332  			// if the Execute panic'ed, set it as an error
   333  			run.setError(ctx, helpers.ToError(r))
   334  		}
   335  		// Release in defer, so that we don't retain the lock even if there's a panic inside
   336  		parallelismLock.Release(1)
   337  	}()
   338  
   339  	run.execute(ctx, client)
   340  }