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  }