package dashboardexecute import ( "context" "encoding/json" "fmt" "github.com/turbot/steampipe/pkg/utils" "os" "strings" "sync" "time" filehelpers "github.com/turbot/go-kit/files" "github.com/turbot/steampipe/pkg/dashboard/dashboardevents" "github.com/turbot/steampipe/pkg/dashboard/dashboardtypes" "github.com/turbot/steampipe/pkg/db/db_common" "github.com/turbot/steampipe/pkg/workspace" ) type DashboardExecutor struct { // map of executions, keyed by session id executions map[string]*DashboardExecutionTree executionLock sync.Mutex // is this an interactive execution // i.e. inputs may be specified _after_ execution starts // false when running a single dashboard in batch mode interactive bool } func newDashboardExecutor() *DashboardExecutor { return &DashboardExecutor{ executions: make(map[string]*DashboardExecutionTree), // default to interactive execution interactive: true, } } var Executor = newDashboardExecutor() func (e *DashboardExecutor) ExecuteDashboard(ctx context.Context, sessionId, dashboardName string, inputs map[string]any, workspace *workspace.Workspace, client db_common.Client) (err error) { var executionTree *DashboardExecutionTree defer func() { if err != nil && ctx.Err() != nil { err = ctx.Err() } // if there was an error executing, send an ExecutionError event if err != nil { errorEvent := &dashboardevents.ExecutionError{ Error: err, Session: sessionId, Timestamp: time.Now(), } workspace.PublishDashboardEvent(ctx, errorEvent) } }() // reset any existing executions for this session e.CancelExecutionForSession(ctx, sessionId) // now create a new execution executionTree, err = NewDashboardExecutionTree(dashboardName, sessionId, client, workspace) if err != nil { return err } // if inputs must be provided before execution (i.e. this is a batch dashboard execution), // verify all required inputs are provided if err = e.validateInputs(executionTree, inputs); err != nil { return err } // add to execution map e.setExecution(sessionId, executionTree) // if inputs have been passed, set them first if len(inputs) > 0 { executionTree.SetInputValues(inputs) } go executionTree.Execute(ctx) return nil } // if inputs must be provided before execution (i.e. this is a batch dashboard execution), // verify all required inputs are provided func (e *DashboardExecutor) validateInputs(executionTree *DashboardExecutionTree, inputs map[string]any) error { if e.interactive { // interactive dashboard execution - no need to validate return nil } var missingInputs []string for _, inputName := range executionTree.InputRuntimeDependencies() { if _, ok := inputs[inputName]; !ok { missingInputs = append(missingInputs, inputName) } } if missingCount := len(missingInputs); missingCount > 0 { return fmt.Errorf("%s '%s' must be provided using '--dashboard-input name=value'", utils.Pluralize("input", missingCount), strings.Join(missingInputs, ",")) } return nil } func (e *DashboardExecutor) LoadSnapshot(ctx context.Context, sessionId, snapshotName string, w *workspace.Workspace) (map[string]any, error) { // find snapshot path in workspace snapshotPath, ok := w.GetResourceMaps().Snapshots[snapshotName] if !ok { return nil, fmt.Errorf("snapshot %s not found in %s (%s)", snapshotName, w.Mod.Name(), w.Path) } if !filehelpers.FileExists(snapshotPath) { return nil, fmt.Errorf("snapshot %s not does not exist", snapshotPath) } snapshotContent, err := os.ReadFile(snapshotPath) if err != nil { return nil, err } // deserialize the snapshot as an interface map // we cannot deserialize into a SteampipeSnapshot struct // (without custom derserialisation code) as the Panels property is an interface snap := map[string]any{} err = json.Unmarshal(snapshotContent, &snap) if err != nil { return nil, err } return snap, nil } func (e *DashboardExecutor) OnInputChanged(ctx context.Context, sessionId string, inputs map[string]any, changedInput string) error { // find the execution executionTree, found := e.executions[sessionId] if !found { return fmt.Errorf("no dashboard running for session %s", sessionId) } // get the previous value of this input inputPrevValue := executionTree.inputValues[changedInput] // first see if any other inputs rely on the one which was just changed clearedInputs := e.clearDependentInputs(executionTree.Root, changedInput, inputs) if len(clearedInputs) > 0 { event := &dashboardevents.InputValuesCleared{ ClearedInputs: clearedInputs, Session: executionTree.sessionId, ExecutionId: executionTree.id, } executionTree.workspace.PublishDashboardEvent(ctx, event) } // if there are any dependent inputs, set their value to nil and send an event to the UI // if the dashboard run is complete, just re-execute if executionTree.GetRunStatus().IsFinished() || inputPrevValue != nil { return e.ExecuteDashboard( ctx, sessionId, executionTree.dashboardName, inputs, executionTree.workspace, executionTree.client) } // set the inputs executionTree.SetInputValues(inputs) return nil } func (e *DashboardExecutor) clearDependentInputs(root dashboardtypes.DashboardTreeRun, changedInput string, inputs map[string]any) []string { dependentInputs := root.GetInputsDependingOn(changedInput) clearedInputs := dependentInputs if len(dependentInputs) > 0 { for _, inputName := range dependentInputs { if inputs[inputName] != nil { // clear the input value inputs[inputName] = nil childDependentInputs := e.clearDependentInputs(root, inputName, inputs) clearedInputs = append(clearedInputs, childDependentInputs...) } } } return clearedInputs } func (e *DashboardExecutor) CancelExecutionForSession(_ context.Context, sessionId string) { // find the execution executionTree, found := e.getExecution(sessionId) if !found { // nothing to do return } // cancel if in progress executionTree.Cancel() // remove from execution tree e.removeExecution(sessionId) } // find the execution for the given session id func (e *DashboardExecutor) getExecution(sessionId string) (*DashboardExecutionTree, bool) { e.executionLock.Lock() defer e.executionLock.Unlock() executionTree, found := e.executions[sessionId] return executionTree, found } func (e *DashboardExecutor) setExecution(sessionId string, executionTree *DashboardExecutionTree) { e.executionLock.Lock() defer e.executionLock.Unlock() e.executions[sessionId] = executionTree } func (e *DashboardExecutor) removeExecution(sessionId string) { e.executionLock.Lock() defer e.executionLock.Unlock() delete(e.executions, sessionId) }