github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/dashboard/dashboardexecute/dashboard_execution_tree.go (about) 1 package dashboardexecute 2 3 import ( 4 "context" 5 "fmt" 6 "log" 7 "sync" 8 "time" 9 10 "github.com/turbot/steampipe/pkg/connection_sync" 11 "github.com/turbot/steampipe/pkg/dashboard/dashboardevents" 12 "github.com/turbot/steampipe/pkg/dashboard/dashboardtypes" 13 "github.com/turbot/steampipe/pkg/db/db_common" 14 "github.com/turbot/steampipe/pkg/steampipeconfig/modconfig" 15 "github.com/turbot/steampipe/pkg/utils" 16 "github.com/turbot/steampipe/pkg/workspace" 17 "golang.org/x/exp/maps" 18 ) 19 20 // DashboardExecutionTree is a structure representing the control result hierarchy 21 type DashboardExecutionTree struct { 22 Root dashboardtypes.DashboardTreeRun 23 24 dashboardName string 25 sessionId string 26 client db_common.Client 27 // map of executing runs, keyed by full name 28 runs map[string]dashboardtypes.DashboardTreeRun 29 workspace *workspace.Workspace 30 runComplete chan dashboardtypes.DashboardTreeRun 31 32 // map of subscribers to notify when an input value changes 33 cancel context.CancelFunc 34 inputLock sync.Mutex 35 inputValues map[string]any 36 id string 37 } 38 39 func NewDashboardExecutionTree(rootName string, sessionId string, client db_common.Client, workspace *workspace.Workspace) (*DashboardExecutionTree, error) { 40 // now populate the DashboardExecutionTree 41 executionTree := &DashboardExecutionTree{ 42 dashboardName: rootName, 43 sessionId: sessionId, 44 client: client, 45 runs: make(map[string]dashboardtypes.DashboardTreeRun), 46 workspace: workspace, 47 runComplete: make(chan dashboardtypes.DashboardTreeRun, 1), 48 inputValues: make(map[string]any), 49 } 50 executionTree.id = fmt.Sprintf("%p", executionTree) 51 52 // create the root run node (either a report run or a counter run) 53 root, err := executionTree.createRootItem(rootName) 54 if err != nil { 55 return nil, err 56 } 57 58 executionTree.Root = root 59 return executionTree, nil 60 } 61 62 func (e *DashboardExecutionTree) createRootItem(rootName string) (dashboardtypes.DashboardTreeRun, error) { 63 parsedName, err := modconfig.ParseResourceName(rootName) 64 if err != nil { 65 return nil, err 66 } 67 fullName, err := parsedName.ToFullName() 68 if err != nil { 69 return nil, err 70 } 71 if parsedName.ItemType == "" { 72 return nil, fmt.Errorf("root item is not valid named resource") 73 } 74 // if no mod is specified, assume the workspace mod 75 if parsedName.Mod == "" { 76 parsedName.Mod = e.workspace.Mod.ShortName 77 rootName = fullName 78 } 79 switch parsedName.ItemType { 80 case modconfig.BlockTypeDashboard: 81 dashboard, ok := e.workspace.GetResourceMaps().Dashboards[rootName] 82 if !ok { 83 return nil, fmt.Errorf("dashboard '%s' does not exist in workspace", rootName) 84 } 85 return NewDashboardRun(dashboard, e, e) 86 case modconfig.BlockTypeBenchmark: 87 benchmark, ok := e.workspace.GetResourceMaps().Benchmarks[rootName] 88 if !ok { 89 return nil, fmt.Errorf("benchmark '%s' does not exist in workspace", rootName) 90 } 91 return NewCheckRun(benchmark, e, e) 92 case modconfig.BlockTypeQuery: 93 // wrap in a table 94 query, ok := e.workspace.GetResourceMaps().Queries[rootName] 95 if !ok { 96 return nil, fmt.Errorf("query '%s' does not exist in workspace", rootName) 97 } 98 // wrap this in a chart and a dashboard 99 dashboard, err := modconfig.NewQueryDashboard(query) 100 // TACTICAL - set the execution tree dashboard name from the query dashboard 101 e.dashboardName = dashboard.Name() 102 if err != nil { 103 return nil, err 104 } 105 return NewDashboardRun(dashboard, e, e) 106 case modconfig.BlockTypeControl: 107 // wrap in a table 108 control, ok := e.workspace.GetResourceMaps().Controls[rootName] 109 if !ok { 110 return nil, fmt.Errorf("query '%s' does not exist in workspace", rootName) 111 } 112 // wrap this in a chart and a dashboard 113 dashboard, err := modconfig.NewQueryDashboard(control) 114 if err != nil { 115 return nil, err 116 } 117 return NewDashboardRun(dashboard, e, e) 118 default: 119 return nil, fmt.Errorf("reporting type %s cannot be executed as dashboard", parsedName.ItemType) 120 } 121 } 122 123 func (e *DashboardExecutionTree) Execute(ctx context.Context) { 124 startTime := time.Now() 125 126 searchPath := e.client.GetRequiredSessionSearchPath() 127 128 // store context 129 cancelCtx, cancel := context.WithCancel(ctx) 130 e.cancel = cancel 131 workspace := e.workspace 132 133 // perform any necessary initialisation 134 // (e.g. check run creates the control execution tree) 135 e.Root.Initialise(cancelCtx) 136 if e.Root.GetError() != nil { 137 return 138 } 139 140 // TODO should we always wait even with non custom search path? 141 // if there is a custom search path, wait until the first connection of each plugin has loaded 142 if customSearchPath := e.client.GetCustomSearchPath(); customSearchPath != nil { 143 if err := connection_sync.WaitForSearchPathSchemas(ctx, e.client, customSearchPath); err != nil { 144 e.Root.SetError(ctx, err) 145 return 146 } 147 } 148 149 panels := e.BuildSnapshotPanels() 150 // build map of those variables referenced by the dashboard run 151 referencedVariables := GetReferencedVariables(e.Root, e.workspace) 152 153 immutablePanels, err := utils.JsonCloneToMap(panels) 154 if err != nil { 155 e.SetError(ctx, err) 156 return 157 } 158 workspace.PublishDashboardEvent(ctx, &dashboardevents.ExecutionStarted{ 159 Root: e.Root, 160 Session: e.sessionId, 161 ExecutionId: e.id, 162 Panels: immutablePanels, 163 Inputs: e.inputValues, 164 Variables: referencedVariables, 165 StartTime: startTime, 166 }) 167 defer func() { 168 169 e := &dashboardevents.ExecutionComplete{ 170 Root: e.Root, 171 Session: e.sessionId, 172 ExecutionId: e.id, 173 Panels: panels, 174 Inputs: e.inputValues, 175 Variables: referencedVariables, 176 // search path elements are quoted (for consumption by postgres) 177 // unquote them 178 SearchPath: utils.UnquoteStringArray(searchPath), 179 StartTime: startTime, 180 EndTime: time.Now(), 181 } 182 workspace.PublishDashboardEvent(ctx, e) 183 }() 184 185 log.Println("[TRACE]", "begin DashboardExecutionTree.Execute") 186 defer log.Println("[TRACE]", "end DashboardExecutionTree.Execute") 187 188 if e.GetRunStatus().IsFinished() { 189 // there must be no nodes to execute 190 log.Println("[TRACE]", "execution tree already complete") 191 return 192 } 193 194 // execute synchronously 195 e.Root.Execute(cancelCtx) 196 } 197 198 // GetRunStatus returns the stats of the Root run 199 func (e *DashboardExecutionTree) GetRunStatus() dashboardtypes.RunStatus { 200 return e.Root.GetRunStatus() 201 } 202 203 // SetError sets the error on the Root run 204 func (e *DashboardExecutionTree) SetError(ctx context.Context, err error) { 205 e.Root.SetError(ctx, err) 206 } 207 208 // GetName implements DashboardParent 209 // use mod short name - this will be the root name for all child runs 210 func (e *DashboardExecutionTree) GetName() string { 211 return e.workspace.Mod.ShortName 212 } 213 214 // GetParent implements DashboardTreeRun 215 func (e *DashboardExecutionTree) GetParent() dashboardtypes.DashboardParent { 216 return nil 217 } 218 219 // GetNodeType implements DashboardTreeRun 220 func (*DashboardExecutionTree) GetNodeType() string { 221 panic("should never call for DashboardExecutionTree") 222 } 223 224 func (e *DashboardExecutionTree) SetInputValues(inputValues map[string]any) { 225 log.Printf("[TRACE] SetInputValues") 226 e.inputLock.Lock() 227 defer e.inputLock.Unlock() 228 229 // we only support inputs if root is a dashboard (NOT a benchmark) 230 runtimeDependencyPublisher, ok := e.Root.(RuntimeDependencyPublisher) 231 if !ok { 232 // should never happen 233 log.Printf("[WARN] SetInputValues called but root Dashboard run is not a RuntimeDependencyPublisher: %s", e.Root.GetName()) 234 return 235 } 236 237 for name, value := range inputValues { 238 log.Printf("[TRACE] DashboardExecutionTree SetInput %s = %v", name, value) 239 e.inputValues[name] = value 240 // publish runtime dependency 241 runtimeDependencyPublisher.PublishRuntimeDependencyValue(name, &dashboardtypes.ResolvedRuntimeDependencyValue{Value: value}) 242 } 243 } 244 245 // ChildCompleteChan implements DashboardParent 246 func (e *DashboardExecutionTree) ChildCompleteChan() chan dashboardtypes.DashboardTreeRun { 247 return e.runComplete 248 } 249 250 // ChildStatusChanged implements DashboardParent 251 func (*DashboardExecutionTree) ChildStatusChanged(context.Context) {} 252 253 func (e *DashboardExecutionTree) Cancel() { 254 // if we have not completed, and already have a cancel function - cancel 255 if e.GetRunStatus().IsFinished() || e.cancel == nil { 256 log.Printf("[TRACE] DashboardExecutionTree Cancel NOT cancelling status %s cancel func %p", e.GetRunStatus(), e.cancel) 257 return 258 } 259 260 log.Printf("[TRACE] DashboardExecutionTree Cancel - calling cancel") 261 e.cancel() 262 263 // if there are any children, wait for the execution to complete 264 if !e.Root.RunComplete() { 265 <-e.runComplete 266 } 267 268 log.Printf("[TRACE] DashboardExecutionTree Cancel - all children complete") 269 } 270 271 func (e *DashboardExecutionTree) BuildSnapshotPanels() map[string]dashboardtypes.SnapshotPanel { 272 // just build from e.runs 273 res := map[string]dashboardtypes.SnapshotPanel{} 274 275 for name, run := range e.runs { 276 res[name] = run.(dashboardtypes.SnapshotPanel) 277 // special case handling for check runs 278 if checkRun, ok := run.(*CheckRun); ok { 279 checkRunChildren := checkRun.BuildSnapshotPanels(res) 280 for k, v := range checkRunChildren { 281 res[k] = v 282 } 283 } 284 } 285 return res 286 } 287 288 // InputRuntimeDependencies returns the names of all inputs which are runtime dependencies 289 func (e *DashboardExecutionTree) InputRuntimeDependencies() []string { 290 var deps = map[string]struct{}{} 291 for _, r := range e.runs { 292 if leafRun, ok := r.(*LeafRun); ok { 293 for _, r := range leafRun.runtimeDependencies { 294 if r.Dependency.PropertyPath.ItemType == modconfig.BlockTypeInput { 295 deps[r.Dependency.SourceResourceName()] = struct{}{} 296 } 297 } 298 } 299 } 300 return maps.Keys(deps) 301 } 302 303 // GetChildren implements DashboardParent 304 func (e *DashboardExecutionTree) GetChildren() []dashboardtypes.DashboardTreeRun { 305 return []dashboardtypes.DashboardTreeRun{e.Root} 306 } 307 308 // ChildrenComplete implements DashboardParent 309 func (e *DashboardExecutionTree) ChildrenComplete() bool { 310 return e.Root.RunComplete() 311 } 312 313 // Tactical: Empty implementations of DashboardParent functions 314 // TODO remove need for this 315 316 func (e *DashboardExecutionTree) Initialise(ctx context.Context) { 317 panic("should never call for DashboardExecutionTree") 318 } 319 320 func (e *DashboardExecutionTree) GetTitle() string { 321 panic("should never call for DashboardExecutionTree") 322 } 323 324 func (e *DashboardExecutionTree) GetError() error { 325 panic("should never call for DashboardExecutionTree") 326 } 327 328 func (e *DashboardExecutionTree) SetComplete(ctx context.Context) { 329 panic("should never call for DashboardExecutionTree") 330 } 331 332 func (e *DashboardExecutionTree) RunComplete() bool { 333 panic("should never call for DashboardExecutionTree") 334 } 335 336 func (e *DashboardExecutionTree) GetInputsDependingOn(s string) []string { 337 panic("should never call for DashboardExecutionTree") 338 } 339 340 func (*DashboardExecutionTree) AsTreeNode() *dashboardtypes.SnapshotTreeNode { 341 panic("should never call for DashboardExecutionTree") 342 } 343 344 func (*DashboardExecutionTree) GetResource() modconfig.DashboardLeafNode { 345 panic("should never call for DashboardExecutionTree") 346 }