github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/control/controlexecute/control_run.go (about) 1 package controlexecute 2 3 import ( 4 "context" 5 "fmt" 6 "log" 7 "sync" 8 "time" 9 10 typehelpers "github.com/turbot/go-kit/types" 11 "github.com/turbot/steampipe-plugin-sdk/v5/grpc" 12 "github.com/turbot/steampipe/pkg/constants" 13 "github.com/turbot/steampipe/pkg/control/controlstatus" 14 "github.com/turbot/steampipe/pkg/dashboard/dashboardtypes" 15 "github.com/turbot/steampipe/pkg/db/db_common" 16 "github.com/turbot/steampipe/pkg/error_helpers" 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 ) 22 23 // ControlRun is a struct representing the execution of a control run. It will contain one or more result items (i.e. for one or more resources). 24 type ControlRun struct { 25 // properties from control 26 ControlId string `json:"-"` 27 FullName string `json:"name"` 28 Title string `json:"title,omitempty"` 29 Description string `json:"description,omitempty"` 30 Documentation string `json:"documentation,omitempty"` 31 Tags map[string]string `json:"tags,omitempty"` 32 Display string `json:"display,omitempty"` 33 Type string `json:"display_type,omitempty"` 34 35 // this will be serialised under 'properties' 36 Severity string `json:"-"` 37 38 // "control" 39 NodeType string `json:"panel_type"` 40 41 // the control being run 42 Control *modconfig.Control `json:"properties,omitempty"` 43 // control summary 44 Summary *controlstatus.StatusSummary `json:"summary"` 45 RunStatus dashboardtypes.RunStatus `json:"status"` 46 // result rows 47 Rows ResultRows `json:"-"` 48 49 // the results in snapshot format 50 Data *dashboardtypes.LeafData `json:"data"` 51 52 // a list of distinct dimension keys from the results of this control 53 DimensionKeys []string `json:"-"` 54 55 // execution duration 56 Duration time.Duration `json:"-"` 57 // parent result group 58 Group *ResultGroup `json:"-"` 59 // execution tree 60 Tree *ExecutionTree `json:"-"` 61 // save run error as string for JSON export 62 RunErrorString string `json:"error,omitempty"` 63 runError error 64 // the query result stream 65 queryResult *queryresult.Result 66 rowMap map[string]ResultRows 67 stateLock sync.Mutex 68 doneChan chan bool 69 attempts int 70 } 71 72 func NewControlRun(control *modconfig.Control, group *ResultGroup, executionTree *ExecutionTree) *ControlRun { 73 controlId := control.Name() 74 75 // only show qualified control names for controls from dependent mods 76 if control.Mod.Name() == executionTree.Workspace.Mod.Name() { 77 controlId = control.UnqualifiedName 78 } 79 80 res := &ControlRun{ 81 Control: control, 82 ControlId: controlId, 83 FullName: control.Name(), 84 Description: control.GetDescription(), 85 Documentation: control.GetDocumentation(), 86 Tags: control.GetTags(), 87 Display: control.GetDisplay(), 88 Type: control.GetType(), 89 90 Severity: typehelpers.SafeString(control.Severity), 91 Title: typehelpers.SafeString(control.Title), 92 rowMap: make(map[string]ResultRows), 93 Summary: &controlstatus.StatusSummary{}, 94 Tree: executionTree, 95 RunStatus: dashboardtypes.RunInitialized, 96 97 Group: group, 98 NodeType: modconfig.BlockTypeControl, 99 doneChan: make(chan bool, 1), 100 } 101 return res 102 } 103 104 // GetControlId implements ControlRunStatusProvider 105 func (r *ControlRun) GetControlId() string { 106 r.stateLock.Lock() 107 defer r.stateLock.Unlock() 108 return r.ControlId 109 } 110 111 // GetRunStatus implements ControlRunStatusProvider 112 func (r *ControlRun) GetRunStatus() dashboardtypes.RunStatus { 113 r.stateLock.Lock() 114 defer r.stateLock.Unlock() 115 return r.RunStatus 116 } 117 118 // GetStatusSummary implements ControlRunStatusProvider 119 func (r *ControlRun) GetStatusSummary() *controlstatus.StatusSummary { 120 r.stateLock.Lock() 121 defer r.stateLock.Unlock() 122 return r.Summary 123 } 124 125 func (r *ControlRun) Finished() bool { 126 return r.GetRunStatus().IsFinished() 127 } 128 129 // MatchTag returns the value corresponding to the input key. Returns 'false' if not found 130 func (r *ControlRun) MatchTag(key string, value string) bool { 131 val, found := r.Control.GetTags()[key] 132 return found && (val == value) 133 } 134 135 func (r *ControlRun) GetError() error { 136 return r.runError 137 } 138 139 // IsSnapshotPanel implements SnapshotPanel 140 func (*ControlRun) IsSnapshotPanel() {} 141 142 // IsExecutionTreeNode implements ExecutionTreeNode 143 func (*ControlRun) IsExecutionTreeNode() {} 144 145 // GetChildren implements ExecutionTreeNode 146 func (*ControlRun) GetChildren() []ExecutionTreeNode { return nil } 147 148 // GetName implements ExecutionTreeNode 149 func (r *ControlRun) GetName() string { return r.Control.Name() } 150 151 // AsTreeNode implements ExecutionTreeNode 152 func (r *ControlRun) AsTreeNode() *dashboardtypes.SnapshotTreeNode { 153 res := &dashboardtypes.SnapshotTreeNode{ 154 Name: r.Control.Name(), 155 NodeType: r.NodeType, 156 } 157 return res 158 } 159 160 func (r *ControlRun) setError(ctx context.Context, err error) { 161 if err == nil { 162 return 163 } 164 if r.runError == context.DeadlineExceeded { 165 r.runError = fmt.Errorf("control execution timed out") 166 } else { 167 r.runError = error_helpers.TransformErrorToSteampipe(err) 168 } 169 r.RunErrorString = r.runError.Error() 170 // update error count 171 r.Summary.Error++ 172 if error_helpers.IsContextCancelledError(err) { 173 r.setRunStatus(ctx, dashboardtypes.RunCanceled) 174 } else { 175 r.setRunStatus(ctx, dashboardtypes.RunError) 176 } 177 } 178 179 func (r *ControlRun) skip(ctx context.Context) { 180 r.setRunStatus(ctx, dashboardtypes.RunComplete) 181 } 182 183 func (r *ControlRun) execute(ctx context.Context, client db_common.Client) { 184 utils.LogTime("ControlRun.execute start") 185 defer utils.LogTime("ControlRun.execute end") 186 187 log.Printf("[TRACE] begin ControlRun.Start: %s\n", r.Control.Name()) 188 defer log.Printf("[TRACE] end ControlRun.Start: %s\n", r.Control.Name()) 189 190 control := r.Control 191 192 startTime := time.Now() 193 194 // function to cleanup and update status after control run completion 195 defer func() { 196 // update the result group status with our status - this will be passed all the way up the execution tree 197 r.Group.updateSummary(r.Summary) 198 if len(r.Severity) != 0 { 199 r.Group.updateSeverityCounts(r.Severity, r.Summary) 200 } 201 r.Duration = time.Since(startTime) 202 if r.Group != nil { 203 r.Group.onChildDone() 204 } 205 log.Printf("[TRACE] finishing with concurrency, %s, , %d\n", r.Control.Name(), r.Tree.Progress.Executing) 206 }() 207 208 // get a db connection 209 sessionResult := r.acquireSession(ctx, client) 210 if sessionResult.Error != nil { 211 if !error_helpers.IsCancelledError(sessionResult.Error) { 212 log.Printf("[TRACE] controlRun %s execute failed to acquire session: %s", r.ControlId, sessionResult.Error) 213 sessionResult.Error = fmt.Errorf("error acquiring database connection, %s", sessionResult.Error.Error()) 214 r.setError(ctx, sessionResult.Error) 215 } 216 return 217 } 218 219 dbSession := sessionResult.Session 220 defer func() { 221 // do this in a closure, otherwise the argument will not get evaluated during calltime 222 dbSession.Close(error_helpers.IsContextCanceled(ctx)) 223 }() 224 225 // set our status 226 r.RunStatus = dashboardtypes.RunRunning 227 228 // update the current running control in the Progress renderer 229 r.Tree.Progress.OnControlStart(ctx, r) 230 defer func() { 231 // update Progress 232 if r.GetRunStatus() == dashboardtypes.RunError { 233 r.Tree.Progress.OnControlError(ctx, r) 234 } else { 235 r.Tree.Progress.OnControlComplete(ctx, r) 236 } 237 }() 238 239 // resolve the control query 240 resolvedQuery, err := r.resolveControlQuery(control) 241 if err != nil { 242 r.setError(ctx, err) 243 return 244 } 245 246 controlExecutionCtx := r.getControlQueryContext(ctx) 247 248 // execute the control query 249 // NOTE no need to pass an OnComplete callback - we are already closing our session after waiting for results 250 log.Printf("[TRACE] execute start for, %s\n", control.Name()) 251 queryResult, err := client.ExecuteInSession(controlExecutionCtx, dbSession, nil, resolvedQuery.ExecuteSQL, resolvedQuery.Args...) 252 log.Printf("[TRACE] execute finish for, %s\n", control.Name()) 253 254 if err != nil { 255 r.attempts++ 256 257 // is this an rpc EOF error - meaning that the plugin somehow crashed 258 if grpc.IsGRPCConnectivityError(err) { 259 if r.attempts < constants.MaxControlRunAttempts { 260 log.Printf("[TRACE] control %s query failed with plugin connectivity error %s - retrying…", r.Control.Name(), err) 261 // recurse into this function to retry using the original context - which Execute will use to create it's own timeout context 262 r.execute(ctx, client) 263 return 264 } else { 265 log.Printf("[TRACE] control %s query failed again with plugin connectivity error %s - NOT retrying…", r.Control.Name(), err) 266 } 267 } 268 r.setError(ctx, err) 269 return 270 } 271 272 r.queryResult = queryResult 273 274 // now wait for control completion 275 log.Printf("[TRACE] wait result for, %s\n", control.Name()) 276 r.waitForResults(ctx) 277 log.Printf("[TRACE] finish result for, %s\n", control.Name()) 278 } 279 280 // try to acquire a database session - retry up to 4 times if there is an error 281 func (r *ControlRun) acquireSession(ctx context.Context, client db_common.Client) *db_common.AcquireSessionResult { 282 var sessionResult *db_common.AcquireSessionResult 283 for attempt := 0; attempt < 4; attempt++ { 284 sessionResult = client.AcquireSession(ctx) 285 if sessionResult.Error == nil || error_helpers.IsCancelledError(sessionResult.Error) { 286 break 287 } 288 289 log.Printf("[TRACE] controlRun %s acquireSession failed with error: %s - retrying", r.ControlId, sessionResult.Error) 290 } 291 292 return sessionResult 293 } 294 295 // create a context with status updates disabled (we do not want to show 'loading' results) 296 func (r *ControlRun) getControlQueryContext(ctx context.Context) context.Context { 297 // disable the status spinner to hide 'loading' results) 298 newCtx := statushooks.DisableStatusHooks(ctx) 299 300 return newCtx 301 } 302 303 func (r *ControlRun) resolveControlQuery(control *modconfig.Control) (*modconfig.ResolvedQuery, error) { 304 resolvedQuery, err := r.Tree.Workspace.ResolveQueryFromQueryProvider(control, nil) 305 if err != nil { 306 return nil, fmt.Errorf(`cannot run %s - failed to resolve query "%s": %s`, control.Name(), typehelpers.SafeString(control.SQL), err.Error()) 307 } 308 return resolvedQuery, nil 309 } 310 311 func (r *ControlRun) waitForResults(ctx context.Context) { 312 defer func() { 313 dimensionsSchema := r.getDimensionSchema() 314 // convert the data to snapshot format 315 r.Data = r.Rows.ToLeafData(dimensionsSchema) 316 }() 317 318 for { 319 select { 320 case <-ctx.Done(): 321 r.setError(ctx, ctx.Err()) 322 return 323 case row := <-*r.queryResult.RowChan: 324 // nil row means control run is complete 325 if row == nil { 326 // nil row means we are done 327 r.setRunStatus(ctx, dashboardtypes.RunComplete) 328 r.createdOrderedResultRows() 329 return 330 } 331 // if the row is in error then we terminate the run 332 if row.Error != nil { 333 // set error status (parent summary will be set from parent defer) 334 r.setError(ctx, row.Error) 335 return 336 } 337 338 // so all is ok - create another result row 339 result, err := NewResultRow(r, row, r.queryResult.Cols) 340 if err != nil { 341 r.setError(ctx, err) 342 return 343 } 344 r.addResultRow(result) 345 case <-r.doneChan: 346 return 347 } 348 } 349 } 350 351 func (r *ControlRun) getDimensionSchema() map[string]*queryresult.ColumnDef { 352 var dimensionsSchema = make(map[string]*queryresult.ColumnDef) 353 354 for _, row := range r.Rows { 355 for _, dim := range row.Dimensions { 356 if _, ok := dimensionsSchema[dim.Key]; !ok { 357 // add to map 358 dimensionsSchema[dim.Key] = &queryresult.ColumnDef{ 359 Name: dim.Key, 360 DataType: dim.SqlType, 361 } 362 // also add to DimensionKeys 363 r.DimensionKeys = append(r.DimensionKeys, dim.Key) 364 } 365 } 366 } 367 // add keys to group 368 r.Group.addDimensionKeys(r.DimensionKeys...) 369 return dimensionsSchema 370 } 371 372 // add the result row to our results and update the summary with the row status 373 func (r *ControlRun) addResultRow(row *ResultRow) { 374 // update results 375 r.rowMap[row.Status] = append(r.rowMap[row.Status], row) 376 377 // update summary 378 switch row.Status { 379 case constants.ControlOk: 380 r.Summary.Ok++ 381 case constants.ControlAlarm: 382 r.Summary.Alarm++ 383 case constants.ControlSkip: 384 r.Summary.Skip++ 385 case constants.ControlInfo: 386 r.Summary.Info++ 387 case constants.ControlError: 388 r.Summary.Error++ 389 } 390 } 391 392 // populate ordered list of rows 393 func (r *ControlRun) createdOrderedResultRows() { 394 statusOrder := []string{constants.ControlError, constants.ControlAlarm, constants.ControlInfo, constants.ControlOk, constants.ControlSkip} 395 for _, status := range statusOrder { 396 r.Rows = append(r.Rows, r.rowMap[status]...) 397 } 398 } 399 400 func (r *ControlRun) setRunStatus(ctx context.Context, status dashboardtypes.RunStatus) { 401 r.stateLock.Lock() 402 r.RunStatus = status 403 r.stateLock.Unlock() 404 405 if r.Finished() { 406 // close the doneChan - we don't need it anymore 407 close(r.doneChan) 408 } 409 }