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 }