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 }