github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/dashboard/dashboardexecute/executor.go (about)

     1  package dashboardexecute
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"os"
     8  	"strings"
     9  	"sync"
    10  	"time"
    11  
    12  	filehelpers "github.com/turbot/go-kit/files"
    13  	"github.com/turbot/steampipe/pkg/dashboard/dashboardevents"
    14  	"github.com/turbot/steampipe/pkg/dashboard/dashboardtypes"
    15  	"github.com/turbot/steampipe/pkg/db/db_common"
    16  	"github.com/turbot/steampipe/pkg/utils"
    17  	"github.com/turbot/steampipe/pkg/workspace"
    18  )
    19  
    20  type DashboardExecutor struct {
    21  	// map of executions, keyed by session id
    22  	executions    map[string]*DashboardExecutionTree
    23  	executionLock sync.Mutex
    24  	// is this an interactive execution
    25  	// i.e. inputs may be specified _after_ execution starts
    26  	// false when running a single dashboard in batch mode
    27  	interactive bool
    28  }
    29  
    30  func newDashboardExecutor() *DashboardExecutor {
    31  	return &DashboardExecutor{
    32  		executions: make(map[string]*DashboardExecutionTree),
    33  		// default to interactive execution
    34  		interactive: true,
    35  	}
    36  }
    37  
    38  var Executor = newDashboardExecutor()
    39  
    40  func (e *DashboardExecutor) ExecuteDashboard(ctx context.Context, sessionId, dashboardName string, inputs map[string]any, workspace *workspace.Workspace, client db_common.Client) (err error) {
    41  	var executionTree *DashboardExecutionTree
    42  	defer func() {
    43  		if err != nil && ctx.Err() != nil {
    44  			err = ctx.Err()
    45  		}
    46  		// if there was an error executing, send an ExecutionError event
    47  		if err != nil {
    48  			errorEvent := &dashboardevents.ExecutionError{
    49  				Error:     err,
    50  				Session:   sessionId,
    51  				Timestamp: time.Now(),
    52  			}
    53  			workspace.PublishDashboardEvent(ctx, errorEvent)
    54  		}
    55  	}()
    56  
    57  	// reset any existing executions for this session
    58  	e.CancelExecutionForSession(ctx, sessionId)
    59  
    60  	// now create a new execution
    61  	executionTree, err = NewDashboardExecutionTree(dashboardName, sessionId, client, workspace)
    62  	if err != nil {
    63  		return err
    64  	}
    65  
    66  	// if inputs must be provided before execution (i.e. this is a batch dashboard execution),
    67  	// verify all required inputs are provided
    68  	if err = e.validateInputs(executionTree, inputs); err != nil {
    69  		return err
    70  	}
    71  
    72  	// add to execution map
    73  	e.setExecution(sessionId, executionTree)
    74  
    75  	// if inputs have been passed, set them first
    76  	if len(inputs) > 0 {
    77  		executionTree.SetInputValues(inputs)
    78  	}
    79  
    80  	go executionTree.Execute(ctx)
    81  
    82  	return nil
    83  }
    84  
    85  // if inputs must be provided before execution (i.e. this is a batch dashboard execution),
    86  // verify all required inputs are provided
    87  func (e *DashboardExecutor) validateInputs(executionTree *DashboardExecutionTree, inputs map[string]any) error {
    88  	if e.interactive {
    89  		// interactive dashboard execution - no need to validate
    90  		return nil
    91  	}
    92  	var missingInputs []string
    93  	for _, inputName := range executionTree.InputRuntimeDependencies() {
    94  		if _, ok := inputs[inputName]; !ok {
    95  			missingInputs = append(missingInputs, inputName)
    96  		}
    97  	}
    98  	if missingCount := len(missingInputs); missingCount > 0 {
    99  		return fmt.Errorf("%s '%s' must be provided using '--dashboard-input name=value'", utils.Pluralize("input", missingCount), strings.Join(missingInputs, ","))
   100  	}
   101  
   102  	return nil
   103  }
   104  
   105  func (e *DashboardExecutor) LoadSnapshot(ctx context.Context, sessionId, snapshotName string, w *workspace.Workspace) (map[string]any, error) {
   106  	// find snapshot path in workspace
   107  	snapshotPath, ok := w.GetResourceMaps().Snapshots[snapshotName]
   108  	if !ok {
   109  		return nil, fmt.Errorf("snapshot %s not found in %s (%s)", snapshotName, w.Mod.Name(), w.Path)
   110  	}
   111  
   112  	if !filehelpers.FileExists(snapshotPath) {
   113  		return nil, fmt.Errorf("snapshot %s not does not exist", snapshotPath)
   114  	}
   115  
   116  	snapshotContent, err := os.ReadFile(snapshotPath)
   117  	if err != nil {
   118  		return nil, err
   119  	}
   120  
   121  	// deserialize the snapshot as an interface map
   122  	// we cannot deserialize into a SteampipeSnapshot struct
   123  	// (without custom derserialisation code) as the Panels property is an interface
   124  	snap := map[string]any{}
   125  
   126  	err = json.Unmarshal(snapshotContent, &snap)
   127  	if err != nil {
   128  		return nil, err
   129  	}
   130  
   131  	return snap, nil
   132  }
   133  
   134  func (e *DashboardExecutor) OnInputChanged(ctx context.Context, sessionId string, inputs map[string]any, changedInput string) error {
   135  	// find the execution
   136  	executionTree, found := e.executions[sessionId]
   137  	if !found {
   138  		return fmt.Errorf("no dashboard running for session %s", sessionId)
   139  	}
   140  
   141  	// get the previous value of this input
   142  	inputPrevValue := executionTree.inputValues[changedInput]
   143  	// first see if any other inputs rely on the one which was just changed
   144  	clearedInputs := e.clearDependentInputs(executionTree.Root, changedInput, inputs)
   145  	if len(clearedInputs) > 0 {
   146  		event := &dashboardevents.InputValuesCleared{
   147  			ClearedInputs: clearedInputs,
   148  			Session:       executionTree.sessionId,
   149  			ExecutionId:   executionTree.id,
   150  		}
   151  		executionTree.workspace.PublishDashboardEvent(ctx, event)
   152  	}
   153  	// if there are any dependent inputs, set their value to nil and send an event to the UI
   154  	// if the dashboard run is complete, just re-execute
   155  	if executionTree.GetRunStatus().IsFinished() || inputPrevValue != nil {
   156  		return e.ExecuteDashboard(
   157  			ctx,
   158  			sessionId,
   159  			executionTree.dashboardName,
   160  			inputs,
   161  			executionTree.workspace,
   162  			executionTree.client)
   163  	}
   164  
   165  	// set the inputs
   166  	executionTree.SetInputValues(inputs)
   167  
   168  	return nil
   169  }
   170  
   171  func (e *DashboardExecutor) clearDependentInputs(root dashboardtypes.DashboardTreeRun, changedInput string, inputs map[string]any) []string {
   172  	dependentInputs := root.GetInputsDependingOn(changedInput)
   173  	clearedInputs := dependentInputs
   174  	if len(dependentInputs) > 0 {
   175  		for _, inputName := range dependentInputs {
   176  			if inputs[inputName] != nil {
   177  				// clear the input value
   178  				inputs[inputName] = nil
   179  				childDependentInputs := e.clearDependentInputs(root, inputName, inputs)
   180  				clearedInputs = append(clearedInputs, childDependentInputs...)
   181  			}
   182  		}
   183  	}
   184  
   185  	return clearedInputs
   186  }
   187  
   188  func (e *DashboardExecutor) CancelExecutionForSession(_ context.Context, sessionId string) {
   189  	// find the execution
   190  	executionTree, found := e.getExecution(sessionId)
   191  	if !found {
   192  		// nothing to do
   193  		return
   194  	}
   195  
   196  	// cancel if in progress
   197  	executionTree.Cancel()
   198  	// remove from execution tree
   199  	e.removeExecution(sessionId)
   200  }
   201  
   202  // find the execution for the given session id
   203  func (e *DashboardExecutor) getExecution(sessionId string) (*DashboardExecutionTree, bool) {
   204  	e.executionLock.Lock()
   205  	defer e.executionLock.Unlock()
   206  
   207  	executionTree, found := e.executions[sessionId]
   208  	return executionTree, found
   209  }
   210  
   211  func (e *DashboardExecutor) setExecution(sessionId string, executionTree *DashboardExecutionTree) {
   212  	e.executionLock.Lock()
   213  	defer e.executionLock.Unlock()
   214  
   215  	e.executions[sessionId] = executionTree
   216  }
   217  
   218  func (e *DashboardExecutor) removeExecution(sessionId string) {
   219  	e.executionLock.Lock()
   220  	defer e.executionLock.Unlock()
   221  
   222  	delete(e.executions, sessionId)
   223  }