Files
steampipe/pkg/dashboard/dashboardexecute/leaf_run.go

220 lines
6.2 KiB
Go

package dashboardexecute
import (
"context"
"github.com/turbot/steampipe/pkg/dashboard/dashboardtypes"
"github.com/turbot/steampipe/pkg/error_helpers"
"github.com/turbot/steampipe/pkg/query/queryresult"
"github.com/turbot/steampipe/pkg/statushooks"
"github.com/turbot/steampipe/pkg/steampipeconfig/modconfig"
"golang.org/x/exp/maps"
"log"
)
// LeafRun is a struct representing the execution of a leaf dashboard node
type LeafRun struct {
// all RuntimeDependencySubscribers are also publishers as they have args/params
RuntimeDependencySubscriberImpl
Resource modconfig.DashboardLeafNode `json:"properties,omitempty"`
Data *dashboardtypes.LeafData `json:"data,omitempty"`
TimingResult *queryresult.TimingResult `json:"-"`
// function called when the run is complete
// this property populated for 'with' runs
onComplete func()
}
func (r *LeafRun) AsTreeNode() *dashboardtypes.SnapshotTreeNode {
return &dashboardtypes.SnapshotTreeNode{
Name: r.Name,
NodeType: r.NodeType,
}
}
func NewLeafRun(resource modconfig.DashboardLeafNode, parent dashboardtypes.DashboardParent, executionTree *DashboardExecutionTree, opts ...LeafRunOption) (*LeafRun, error) {
r := &LeafRun{
Resource: resource,
}
// create RuntimeDependencySubscriberImpl- this handles 'with' run creation and resolving runtime dependency resolution
// (NOTE: we have to do this after creating run as we need to pass a ref to the run)
r.RuntimeDependencySubscriberImpl = *NewRuntimeDependencySubscriber(resource, parent, r, executionTree)
// apply options AFTER calling NewRuntimeDependencySubscriber
for _, opt := range opts {
opt(r)
}
err := r.initRuntimeDependencies(executionTree)
if err != nil {
return nil, err
}
r.NodeType = resource.BlockType()
// if the node has no runtime dependencies, resolve the sql
if !r.hasRuntimeDependencies() {
if err := r.resolveSQLAndArgs(); err != nil {
return nil, err
}
}
// add r into execution tree
executionTree.runs[r.Name] = r
// if we have children (nodes/edges), create runs for them
err = r.createChildRuns(executionTree)
if err != nil {
return nil, err
}
// create buffered channel for children to report their completion
r.createChildCompleteChan()
// populate the names of any withs we depend on
r.setRuntimeDependencies()
return r, nil
}
func (r *LeafRun) createChildRuns(executionTree *DashboardExecutionTree) error {
children := r.resource.GetChildren()
if len(children) == 0 {
return nil
}
r.children = make([]dashboardtypes.DashboardTreeRun, len(children))
var errors []error
for i, c := range children {
var opts []LeafRunOption
childRun, err := NewLeafRun(c.(modconfig.DashboardLeafNode), r, executionTree, opts...)
if err != nil {
errors = append(errors, err)
continue
}
r.children[i] = childRun
}
return error_helpers.CombineErrors(errors...)
}
// Execute implements DashboardTreeRun
func (r *LeafRun) Execute(ctx context.Context) {
defer func() {
// call our oncomplete is we have one
// (this is used to collect 'with' data and propagate errors)
if r.onComplete != nil {
r.onComplete()
}
}()
// if there is nothing to do, return
if r.Status.IsFinished() {
return
}
log.Printf("[TRACE] LeafRun '%s' Execute()", r.resource.Name())
// to get here, we must be a query provider
// if we have children and with runs, start them asyncronously (they may block waiting for our runtime dependencies)
r.executeChildrenAsync(ctx)
// start a goroutine to wait for children to complete
doneChan := r.waitForChildrenAsync(ctx)
if err := r.evaluateRuntimeDependencies(ctx); err != nil {
r.SetError(ctx, err)
return
}
// set status to running (this sends update event)
// (if we have blocked children, this will be changed to blocked)
r.setRunning(ctx)
// if we have sql to execute, do it now
// (if we are only performing a base execution, do not run the query)
if r.executeSQL != "" {
if err := r.executeQuery(ctx); err != nil {
r.SetError(ctx, err)
return
}
}
// wait for all children and withs
err := <-doneChan
if err == nil {
log.Printf("[TRACE] %s children complete", r.resource.Name())
// aggregate our child data
r.combineChildData()
// set complete status on dashboard
r.SetComplete(ctx)
} else {
log.Printf("[TRACE] %s children complete with error: %s", r.resource.Name(), err.Error())
r.SetError(ctx, err)
}
}
// SetError implements DashboardTreeRun (override to set snapshothook status)
func (r *LeafRun) SetError(ctx context.Context, err error) {
// increment error count for snapshot hook
statushooks.SnapshotError(ctx)
r.DashboardTreeRunImpl.SetError(ctx, err)
}
// SetComplete implements DashboardTreeRun (override to set snapshothook status
func (r *LeafRun) SetComplete(ctx context.Context) {
// call snapshot hooks with progress
statushooks.UpdateSnapshotProgress(ctx, 1)
r.DashboardTreeRunImpl.SetComplete(ctx)
}
// IsSnapshotPanel implements SnapshotPanel
func (*LeafRun) IsSnapshotPanel() {}
// if this leaf run has a query or sql, execute it now
func (r *LeafRun) executeQuery(ctx context.Context) error {
log.Printf("[TRACE] LeafRun '%s' SQL resolved, executing", r.resource.Name())
queryResult, err := r.executionTree.client.ExecuteSync(ctx, r.executeSQL, r.Args...)
if err != nil {
log.Printf("[TRACE] LeafRun '%s' query failed: %s", r.resource.Name(), err.Error())
return err
}
log.Printf("[TRACE] LeafRun '%s' complete", r.resource.Name())
r.Data = dashboardtypes.NewLeafData(queryResult)
r.TimingResult = queryResult.TimingResult
return nil
}
func (r *LeafRun) combineChildData() {
// we either have children OR a query
// if there are no children, do nothing
if len(r.children) == 0 {
return
}
// create empty data to populate
r.Data = &dashboardtypes.LeafData{}
// build map of columns for the schema
schemaMap := make(map[string]*queryresult.ColumnDef)
for _, c := range r.children {
childLeafRun := c.(*LeafRun)
data := childLeafRun.Data
// if there is no data or this is a 'with', skip
if data == nil || childLeafRun.resource.BlockType() == modconfig.BlockTypeWith {
continue
}
for _, s := range data.Columns {
if _, ok := schemaMap[s.Name]; !ok {
schemaMap[s.Name] = s
}
}
r.Data.Rows = append(r.Data.Rows, data.Rows...)
}
r.Data.Columns = maps.Values(schemaMap)
}