github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/dashboard/dashboardexecute/runtime_dependency_publisher_impl.go (about) 1 package dashboardexecute 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "log" 8 "strconv" 9 "sync" 10 11 "github.com/turbot/steampipe/pkg/dashboard/dashboardtypes" 12 "github.com/turbot/steampipe/pkg/steampipeconfig/modconfig" 13 "github.com/turbot/steampipe/pkg/utils" 14 ) 15 16 type runtimeDependencyPublisherImpl struct { 17 DashboardParentImpl 18 Args []any `json:"args,omitempty"` 19 Params []*modconfig.ParamDef `json:"params,omitempty"` 20 subscriptions map[string][]*RuntimeDependencyPublishTarget 21 withValueMutex *sync.Mutex 22 withRuns map[string]*LeafRun 23 inputs map[string]*modconfig.DashboardInput 24 } 25 26 func newRuntimeDependencyPublisherImpl(resource modconfig.DashboardLeafNode, parent dashboardtypes.DashboardParent, run dashboardtypes.DashboardTreeRun, executionTree *DashboardExecutionTree) runtimeDependencyPublisherImpl { 27 b := runtimeDependencyPublisherImpl{ 28 DashboardParentImpl: newDashboardParentImpl(resource, parent, run, executionTree), 29 subscriptions: make(map[string][]*RuntimeDependencyPublishTarget), 30 inputs: make(map[string]*modconfig.DashboardInput), 31 withRuns: make(map[string]*LeafRun), 32 withValueMutex: new(sync.Mutex), 33 } 34 // if the resource is a query provider, get params and set status 35 if queryProvider, ok := resource.(modconfig.QueryProvider); ok { 36 // get params 37 b.Params = queryProvider.GetParams() 38 if queryProvider.RequiresExecution(queryProvider) || len(queryProvider.GetChildren()) > 0 { 39 b.Status = dashboardtypes.RunInitialized 40 } 41 } 42 43 return b 44 } 45 46 func (p *runtimeDependencyPublisherImpl) Initialise(context.Context) {} 47 48 func (p *runtimeDependencyPublisherImpl) Execute(context.Context) { 49 panic("must be implemented by child struct") 50 } 51 52 func (p *runtimeDependencyPublisherImpl) AsTreeNode() *dashboardtypes.SnapshotTreeNode { 53 panic("must be implemented by child struct") 54 } 55 56 func (p *runtimeDependencyPublisherImpl) GetName() string { 57 return p.Name 58 } 59 60 func (p *runtimeDependencyPublisherImpl) ProvidesRuntimeDependency(dependency *modconfig.RuntimeDependency) bool { 61 resourceName := dependency.SourceResourceName() 62 switch dependency.PropertyPath.ItemType { 63 case modconfig.BlockTypeWith: 64 // we cannot use withRuns here as if withs have dependencies on each other, 65 // this function may be called before all runs have been added 66 // instead, look directly at the underlying resource withs 67 if wp, ok := p.resource.(modconfig.WithProvider); ok { 68 for _, w := range wp.GetWiths() { 69 if w.UnqualifiedName == resourceName { 70 return true 71 } 72 } 73 } 74 return false 75 case modconfig.BlockTypeInput: 76 return p.inputs[resourceName] != nil 77 case modconfig.BlockTypeParam: 78 for _, p := range p.Params { 79 // check short name not resource name (which is unqualified name) 80 if p.ShortName == dependency.PropertyPath.Name { 81 return true 82 } 83 } 84 } 85 return false 86 } 87 88 func (p *runtimeDependencyPublisherImpl) SubscribeToRuntimeDependency(name string, opts ...RuntimeDependencyPublishOption) chan *dashboardtypes.ResolvedRuntimeDependencyValue { 89 target := &RuntimeDependencyPublishTarget{ 90 // make a channel (buffer to avoid potential sync issues) 91 channel: make(chan *dashboardtypes.ResolvedRuntimeDependencyValue, 1), 92 } 93 for _, o := range opts { 94 o(target) 95 } 96 log.Printf("[TRACE] SubscribeToRuntimeDependency %s", name) 97 98 // subscribe, passing a function which invokes getWithValue to resolve the required with value 99 p.subscriptions[name] = append(p.subscriptions[name], target) 100 return target.channel 101 } 102 103 func (p *runtimeDependencyPublisherImpl) PublishRuntimeDependencyValue(name string, result *dashboardtypes.ResolvedRuntimeDependencyValue) { 104 for _, target := range p.subscriptions[name] { 105 if target.transform != nil { 106 // careful not to mutate result which may be reused 107 target.channel <- target.transform(result) 108 } else { 109 target.channel <- result 110 } 111 close(target.channel) 112 } 113 // clear subscriptions 114 delete(p.subscriptions, name) 115 } 116 117 func (p *runtimeDependencyPublisherImpl) GetWithRuns() map[string]*LeafRun { 118 return p.withRuns 119 } 120 121 func (p *runtimeDependencyPublisherImpl) initWiths() error { 122 // if the resource is a runtime dependency provider, create with runs and resolve dependencies 123 wp, ok := p.resource.(modconfig.WithProvider) 124 if !ok { 125 return nil 126 } 127 // if we have with blocks, create runs for them 128 // BEFORE creating child runs, and before adding runtime dependencies 129 err := p.createWithRuns(wp.GetWiths(), p.executionTree) 130 if err != nil { 131 return err 132 } 133 134 return nil 135 } 136 137 // getWithValue accepts the raw with result (dashboardtypes.LeafData) and the property path, and extracts the appropriate data 138 func (p *runtimeDependencyPublisherImpl) getWithValue(name string, result *dashboardtypes.LeafData, path *modconfig.ParsedPropertyPath) (any, error) { 139 // get the set of rows which will be used ot generate the return value 140 rows := result.Rows 141 /* 142 You can 143 reference the whole table with: 144 with.stuff1 145 this is equivalent to: 146 with.stuff1.rows 147 and 148 with.stuff1.rows[*] 149 150 Rows is a list, and you can index it to get a single row: 151 with.stuff1.rows[0] 152 or splat it to get all rows: 153 with.stuff1.rows[*] 154 Each row, in turn, contains all the columns, so you can get a single column of a single row: 155 with.stuff1.rows[0].a 156 if you splat the row, then you can get an array of a single column from all rows. This would be passed to sql as an array: 157 with.stuff1.rows[*].a 158 */ 159 160 // with.stuff1 -> PropertyPath will be "" 161 // with.stuff1.rows -> PropertyPath will be "rows" 162 // with.stuff1.rows[*] -> PropertyPath will be "rows.*" 163 // with.stuff1.rows[0] -> PropertyPath will be "rows.0" 164 // with.stuff1.rows[0].a -> PropertyPath will be "rows.0.a" 165 const rowsSegment = 0 166 const rowsIdxSegment = 1 167 const columnSegment = 2 168 169 // second path section MUST be "rows" 170 if len(path.PropertyPath) > rowsSegment && path.PropertyPath[rowsSegment] != "rows" || len(path.PropertyPath) > (columnSegment+1) { 171 return nil, fmt.Errorf("reference to with '%s' has invalid property path '%s'", name, path.Original) 172 } 173 174 // if no row is specified assume all 175 rowIdxStr := "*" 176 if len(path.PropertyPath) > rowsIdxSegment { 177 // so there is 3rd part - this will be the row idx (or '*') 178 rowIdxStr = path.PropertyPath[rowsIdxSegment] 179 } 180 var column string 181 182 // is a column specified? 183 if len(path.PropertyPath) > columnSegment { 184 column = path.PropertyPath[columnSegment] 185 } else { 186 if len(result.Columns) > 1 { 187 // we do not support returning all columns (yet 188 return nil, fmt.Errorf("reference to with '%s' is returning more than one column - not supported", name) 189 } 190 column = result.Columns[0].Name 191 } 192 193 if rowIdxStr == "*" { 194 return columnValuesFromRows(column, rows) 195 } 196 197 rowIdx, err := strconv.Atoi(rowIdxStr) 198 if err != nil { 199 return nil, fmt.Errorf("reference to with '%s' has invalid property path '%s' - cannot parse row idx '%s'", name, path.Original, rowIdxStr) 200 } 201 202 // do we have the requested row 203 if rowCount := len(rows); rowIdx >= rowCount { 204 return nil, fmt.Errorf("reference to with '%s' has invalid row index '%d' - %d %s were returned", name, rowIdx, rowCount, utils.Pluralize("row", rowCount)) 205 } 206 // so we are returning a single row 207 row := rows[rowIdx] 208 return row[column], nil 209 } 210 211 func columnValuesFromRows(column string, rows []map[string]any) (any, error) { 212 if column == "" { 213 return nil, fmt.Errorf("columnValuesFromRows failed - no column specified") 214 } 215 var res = make([]any, len(rows)) 216 for i, row := range rows { 217 var ok bool 218 res[i], ok = row[column] 219 if !ok { 220 return nil, fmt.Errorf("column %s does not exist", column) 221 } 222 } 223 return res, nil 224 } 225 226 func (p *runtimeDependencyPublisherImpl) setWithValue(w *LeafRun) { 227 p.withValueMutex.Lock() 228 defer p.withValueMutex.Unlock() 229 230 name := w.resource.GetUnqualifiedName() 231 // if there was an error, w.Data will be nil and w.error will be non-nil 232 result := &dashboardtypes.ResolvedRuntimeDependencyValue{Error: w.err} 233 234 if w.err == nil { 235 populateData(w.Data, result) 236 } 237 p.PublishRuntimeDependencyValue(name, result) 238 } 239 240 func populateData(withData *dashboardtypes.LeafData, result *dashboardtypes.ResolvedRuntimeDependencyValue) { 241 result.Value = withData 242 // TACTICAL - is there are any JSON columns convert them back to a JSON string 243 var jsonColumns []string 244 for _, c := range withData.Columns { 245 if c.DataType == "JSONB" || c.DataType == "JSON" { 246 jsonColumns = append(jsonColumns, c.Name) 247 } 248 } 249 // now convert any json values into a json string 250 for _, c := range jsonColumns { 251 for _, row := range withData.Rows { 252 jsonBytes, err := json.Marshal(row[c]) 253 if err != nil { 254 // publish result with the error 255 result.Error = err 256 result.Value = nil 257 return 258 } 259 row[c] = string(jsonBytes) 260 } 261 } 262 } 263 264 func (p *runtimeDependencyPublisherImpl) createWithRuns(withs []*modconfig.DashboardWith, executionTree *DashboardExecutionTree) error { 265 for _, w := range withs { 266 // NOTE: set the name of the run to be the scoped name 267 withRunName := fmt.Sprintf("%s.%s", p.GetName(), w.UnqualifiedName) 268 withRun, err := NewLeafRun(w, p, executionTree, setName(withRunName)) 269 if err != nil { 270 return err 271 } 272 // set an onComplete function to populate 'with' data 273 withRun.onComplete = func() { p.setWithValue(withRun) } 274 275 p.withRuns[w.UnqualifiedName] = withRun 276 p.children = append(p.children, withRun) 277 } 278 return nil 279 } 280 281 // called when the args are resolved - if anyone is subscribing to the args value, publish 282 func (p *runtimeDependencyPublisherImpl) argsResolved(args []any) { 283 // use params to get param names for each arg and then look of subscriber 284 for i, param := range p.Params { 285 if i == len(args) { 286 return 287 } 288 // do we have a subscription for this param 289 if _, ok := p.subscriptions[param.UnqualifiedName]; ok { 290 p.PublishRuntimeDependencyValue(param.UnqualifiedName, &dashboardtypes.ResolvedRuntimeDependencyValue{Value: args[i]}) 291 } 292 } 293 log.Printf("[TRACE] %s: argsResolved", p.Name) 294 }