github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/dashboard/dashboardexecute/leaf_run.go (about) 1 package dashboardexecute 2 3 import ( 4 "context" 5 "log" 6 7 "github.com/turbot/steampipe/pkg/dashboard/dashboardtypes" 8 "github.com/turbot/steampipe/pkg/error_helpers" 9 "github.com/turbot/steampipe/pkg/query/queryresult" 10 "github.com/turbot/steampipe/pkg/statushooks" 11 "github.com/turbot/steampipe/pkg/steampipeconfig/modconfig" 12 "golang.org/x/exp/maps" 13 ) 14 15 // LeafRun is a struct representing the execution of a leaf dashboard node 16 type LeafRun struct { 17 // all RuntimeDependencySubscribers are also publishers as they have args/params 18 RuntimeDependencySubscriberImpl 19 Resource modconfig.DashboardLeafNode `json:"properties,omitempty"` 20 21 Data *dashboardtypes.LeafData `json:"data,omitempty"` 22 TimingResult *queryresult.TimingResult `json:"-"` 23 // function called when the run is complete 24 // this property populated for 'with' runs 25 onComplete func() 26 } 27 28 func (r *LeafRun) AsTreeNode() *dashboardtypes.SnapshotTreeNode { 29 return &dashboardtypes.SnapshotTreeNode{ 30 Name: r.Name, 31 NodeType: r.NodeType, 32 } 33 } 34 35 func NewLeafRun(resource modconfig.DashboardLeafNode, parent dashboardtypes.DashboardParent, executionTree *DashboardExecutionTree, opts ...LeafRunOption) (*LeafRun, error) { 36 r := &LeafRun{ 37 Resource: resource, 38 } 39 40 // create RuntimeDependencySubscriberImpl- this handles 'with' run creation and resolving runtime dependency resolution 41 // (NOTE: we have to do this after creating run as we need to pass a ref to the run) 42 r.RuntimeDependencySubscriberImpl = *NewRuntimeDependencySubscriber(resource, parent, r, executionTree) 43 44 // apply options AFTER calling NewRuntimeDependencySubscriber 45 for _, opt := range opts { 46 opt(r) 47 } 48 49 err := r.initRuntimeDependencies(executionTree) 50 if err != nil { 51 return nil, err 52 } 53 54 r.NodeType = resource.BlockType() 55 56 // if the node has no runtime dependencies, resolve the sql 57 if !r.hasRuntimeDependencies() { 58 if err := r.resolveSQLAndArgs(); err != nil { 59 return nil, err 60 } 61 } 62 // add r into execution tree 63 executionTree.runs[r.Name] = r 64 65 // if we have children (nodes/edges), create runs for them 66 err = r.createChildRuns(executionTree) 67 if err != nil { 68 return nil, err 69 } 70 71 // create buffered channel for children to report their completion 72 r.createChildCompleteChan() 73 74 // populate the names of any withs we depend on 75 r.setRuntimeDependencies() 76 77 return r, nil 78 } 79 80 func (r *LeafRun) createChildRuns(executionTree *DashboardExecutionTree) error { 81 children := r.resource.GetChildren() 82 if len(children) == 0 { 83 return nil 84 } 85 86 r.children = make([]dashboardtypes.DashboardTreeRun, len(children)) 87 var errors []error 88 89 for i, c := range children { 90 var opts []LeafRunOption 91 childRun, err := NewLeafRun(c.(modconfig.DashboardLeafNode), r, executionTree, opts...) 92 if err != nil { 93 errors = append(errors, err) 94 continue 95 } 96 97 r.children[i] = childRun 98 } 99 return error_helpers.CombineErrors(errors...) 100 } 101 102 // Execute implements DashboardTreeRun 103 func (r *LeafRun) Execute(ctx context.Context) { 104 defer func() { 105 // call our oncomplete is we have one 106 // (this is used to collect 'with' data and propagate errors) 107 if r.onComplete != nil { 108 r.onComplete() 109 } 110 }() 111 112 // if there is nothing to do, return 113 if r.Status.IsFinished() { 114 return 115 } 116 117 log.Printf("[TRACE] LeafRun '%s' Execute()", r.resource.Name()) 118 119 // to get here, we must be a query provider 120 121 // if we have children and with runs, start them asyncronously (they may block waiting for our runtime dependencies) 122 r.executeChildrenAsync(ctx) 123 124 // start a goroutine to wait for children to complete 125 doneChan := r.waitForChildrenAsync(ctx) 126 127 if err := r.evaluateRuntimeDependencies(ctx); err != nil { 128 r.SetError(ctx, err) 129 return 130 } 131 132 // set status to running (this sends update event) 133 // (if we have blocked children, this will be changed to blocked) 134 r.setRunning(ctx) 135 136 // if we have sql to execute, do it now 137 // (if we are only performing a base execution, do not run the query) 138 if r.executeSQL != "" { 139 if err := r.executeQuery(ctx); err != nil { 140 r.SetError(ctx, err) 141 return 142 } 143 } 144 145 // wait for all children and withs 146 err := <-doneChan 147 if err == nil { 148 log.Printf("[TRACE] %s children complete", r.resource.Name()) 149 150 // aggregate our child data 151 r.combineChildData() 152 // set complete status on dashboard 153 r.SetComplete(ctx) 154 } else { 155 log.Printf("[TRACE] %s children complete with error: %s", r.resource.Name(), err.Error()) 156 r.SetError(ctx, err) 157 } 158 } 159 160 // SetError implements DashboardTreeRun (override to set snapshothook status) 161 func (r *LeafRun) SetError(ctx context.Context, err error) { 162 // increment error count for snapshot hook 163 statushooks.SnapshotError(ctx) 164 r.DashboardTreeRunImpl.SetError(ctx, err) 165 } 166 167 // SetComplete implements DashboardTreeRun (override to set snapshothook status 168 func (r *LeafRun) SetComplete(ctx context.Context) { 169 // call snapshot hooks with progress 170 statushooks.UpdateSnapshotProgress(ctx, 1) 171 172 r.DashboardTreeRunImpl.SetComplete(ctx) 173 } 174 175 // IsSnapshotPanel implements SnapshotPanel 176 func (*LeafRun) IsSnapshotPanel() {} 177 178 // if this leaf run has a query or sql, execute it now 179 func (r *LeafRun) executeQuery(ctx context.Context) error { 180 log.Printf("[TRACE] LeafRun '%s' SQL resolved, executing", r.resource.Name()) 181 182 queryResult, err := r.executionTree.client.ExecuteSync(ctx, r.executeSQL, r.Args...) 183 if err != nil { 184 log.Printf("[TRACE] LeafRun '%s' query failed: %s", r.resource.Name(), err.Error()) 185 return err 186 187 } 188 log.Printf("[TRACE] LeafRun '%s' complete", r.resource.Name()) 189 190 r.Data = dashboardtypes.NewLeafData(queryResult) 191 r.TimingResult = queryResult.TimingResult 192 return nil 193 } 194 195 func (r *LeafRun) combineChildData() { 196 // we either have children OR a query 197 // if there are no children, do nothing 198 if len(r.children) == 0 { 199 return 200 } 201 // create empty data to populate 202 r.Data = &dashboardtypes.LeafData{} 203 // build map of columns for the schema 204 schemaMap := make(map[string]*queryresult.ColumnDef) 205 for _, c := range r.children { 206 childLeafRun := c.(*LeafRun) 207 data := childLeafRun.Data 208 // if there is no data or this is a 'with', skip 209 if data == nil || childLeafRun.resource.BlockType() == modconfig.BlockTypeWith { 210 continue 211 } 212 for _, s := range data.Columns { 213 if _, ok := schemaMap[s.Name]; !ok { 214 schemaMap[s.Name] = s 215 } 216 } 217 r.Data.Rows = append(r.Data.Rows, data.Rows...) 218 } 219 r.Data.Columns = maps.Values(schemaMap) 220 }