github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/dashboard/dashboardexecute/runtime_dependency_subscriber_impl.go (about) 1 package dashboardexecute 2 3 import ( 4 "context" 5 "fmt" 6 "log" 7 "sync" 8 9 "github.com/turbot/go-kit/helpers" 10 typehelpers "github.com/turbot/go-kit/types" 11 "github.com/turbot/steampipe/pkg/dashboard/dashboardtypes" 12 "github.com/turbot/steampipe/pkg/error_helpers" 13 "github.com/turbot/steampipe/pkg/steampipeconfig/modconfig" 14 "golang.org/x/exp/maps" 15 ) 16 17 type RuntimeDependencySubscriberImpl struct { 18 // all RuntimeDependencySubscribers are also publishers as they have args/params 19 runtimeDependencyPublisherImpl 20 // if the underlying resource has a base resource, create a RuntimeDependencySubscriberImpl instance to handle 21 // generation and publication of runtime depdencies from the base resource 22 baseDependencySubscriber *RuntimeDependencySubscriberImpl 23 // map of runtime dependencies, keyed by dependency long name 24 runtimeDependencies map[string]*dashboardtypes.ResolvedRuntimeDependency 25 RawSQL string `json:"sql,omitempty"` 26 executeSQL string 27 // a list of the (scoped) names of any runtime dependencies that we rely on 28 RuntimeDependencyNames []string `json:"dependencies,omitempty"` 29 } 30 31 func NewRuntimeDependencySubscriber(resource modconfig.DashboardLeafNode, parent dashboardtypes.DashboardParent, run dashboardtypes.DashboardTreeRun, executionTree *DashboardExecutionTree) *RuntimeDependencySubscriberImpl { 32 b := &RuntimeDependencySubscriberImpl{ 33 runtimeDependencies: make(map[string]*dashboardtypes.ResolvedRuntimeDependency), 34 } 35 36 // create RuntimeDependencyPublisherImpl 37 // (we must create after creating the run as iut requires a ref to the run) 38 b.runtimeDependencyPublisherImpl = newRuntimeDependencyPublisherImpl(resource, parent, run, executionTree) 39 40 return b 41 } 42 43 // GetBaseDependencySubscriber implements RuntimeDependencySubscriber 44 func (s *RuntimeDependencySubscriberImpl) GetBaseDependencySubscriber() RuntimeDependencySubscriber { 45 return s.baseDependencySubscriber 46 } 47 48 // if the resource is a runtime dependency provider, create with runs and resolve dependencies 49 func (s *RuntimeDependencySubscriberImpl) initRuntimeDependencies(executionTree *DashboardExecutionTree) error { 50 if _, ok := s.resource.(modconfig.RuntimeDependencyProvider); !ok { 51 return nil 52 } 53 54 // if our underlying resource has a base which has runtime dependencies, 55 // create a RuntimeDependencySubscriberImpl for it 56 if err := s.initBaseRuntimeDependencySubscriber(executionTree); err != nil { 57 return err 58 } 59 60 // call into publisher to start any with runs 61 if err := s.runtimeDependencyPublisherImpl.initWiths(); err != nil { 62 return err 63 } 64 // resolve any runtime dependencies 65 return s.resolveRuntimeDependencies() 66 } 67 68 func (s *RuntimeDependencySubscriberImpl) initBaseRuntimeDependencySubscriber(executionTree *DashboardExecutionTree) error { 69 if base := s.resource.(modconfig.HclResource).GetBase(); base != nil { 70 if _, ok := base.(modconfig.RuntimeDependencyProvider); ok { 71 // create base dependency subscriber 72 // pass ourselves as 'run' 73 // - this is only used when sending update events, which will not happen for the baseDependencySubscriber 74 s.baseDependencySubscriber = NewRuntimeDependencySubscriber(base.(modconfig.DashboardLeafNode), nil, s, executionTree) 75 err := s.baseDependencySubscriber.initRuntimeDependencies(executionTree) 76 if err != nil { 77 return err 78 } 79 // create buffered channel for base with to report their completion 80 s.baseDependencySubscriber.createChildCompleteChan() 81 } 82 } 83 return nil 84 } 85 86 // if this node has runtime dependencies, find the publisher of the dependency and create a dashboardtypes.ResolvedRuntimeDependency 87 // which we use to resolve the values 88 func (s *RuntimeDependencySubscriberImpl) resolveRuntimeDependencies() error { 89 rdp, ok := s.resource.(modconfig.RuntimeDependencyProvider) 90 if !ok { 91 return nil 92 } 93 94 runtimeDependencies := rdp.GetRuntimeDependencies() 95 96 for n, d := range runtimeDependencies { 97 // find a runtime dependency publisher who can provider this runtime dependency 98 publisher := s.findRuntimeDependencyPublisher(d) 99 if publisher == nil { 100 // should never happen as validation should have caught this 101 return fmt.Errorf("cannot resolve runtime dependency %s", d.String()) 102 } 103 104 // read name and dep into local loop vars to ensure correct value used when transform func is invoked 105 name := n 106 dep := d 107 108 // determine the function to use to retrieve the runtime dependency value 109 var opts []RuntimeDependencyPublishOption 110 111 switch dep.PropertyPath.ItemType { 112 case modconfig.BlockTypeWith: 113 // set a transform function to extract the requested with data 114 opts = append(opts, WithTransform(func(resolvedVal *dashboardtypes.ResolvedRuntimeDependencyValue) *dashboardtypes.ResolvedRuntimeDependencyValue { 115 transformedResolvedVal := &dashboardtypes.ResolvedRuntimeDependencyValue{Error: resolvedVal.Error} 116 if resolvedVal.Error == nil { 117 // the runtime dependency value for a 'with' is *dashboardtypes.LeafData 118 withValue, err := s.getWithValue(name, resolvedVal.Value.(*dashboardtypes.LeafData), dep.PropertyPath) 119 if err != nil { 120 transformedResolvedVal.Error = fmt.Errorf("failed to resolve with value '%s' for %s: %s", dep.PropertyPath.Original, name, err.Error()) 121 } else { 122 transformedResolvedVal.Value = withValue 123 } 124 } 125 return transformedResolvedVal 126 })) 127 } 128 // subscribe, passing a function which invokes getWithValue to resolve the required with value 129 valueChannel := publisher.SubscribeToRuntimeDependency(d.SourceResourceName(), opts...) 130 131 publisherName := publisher.GetName() 132 s.runtimeDependencies[name] = dashboardtypes.NewResolvedRuntimeDependency(dep, valueChannel, publisherName) 133 } 134 135 return nil 136 } 137 138 func (s *RuntimeDependencySubscriberImpl) findRuntimeDependencyPublisher(runtimeDependency *modconfig.RuntimeDependency) RuntimeDependencyPublisher { 139 // the runtime dependency publisher is either the root dashboard run, 140 // or if this resource (or in case of a node/edge, the resource parent) has a base, 141 // the baseDependencySubscriber for that base 142 var subscriber RuntimeDependencySubscriber = s 143 if s.NodeType == modconfig.BlockTypeNode || s.NodeType == modconfig.BlockTypeEdge { 144 subscriber = s.parent.(RuntimeDependencySubscriber) 145 } 146 baseSubscriber := subscriber.GetBaseDependencySubscriber() 147 148 // "if I have a base with runtime dependencies, those dependencies must be provided BY THE BASE" 149 // check the provider property on the runtime dependency 150 // - if the matches the underlying resource for the baseDependencySubscriber, 151 // then baseDependencySubscriber _should_ be the dependency publisher 152 if !helpers.IsNil(baseSubscriber) && runtimeDependency.Provider == baseSubscriber.GetResource() { 153 if baseSubscriber.ProvidesRuntimeDependency(runtimeDependency) { 154 return baseSubscriber 155 } 156 157 // unexpected 158 log.Printf("[WARN] dependency %s has a dependency provider matching the base resource %s but the BaseDependencySubscriber does not provider the runtime dependency", 159 runtimeDependency.String(), baseSubscriber.GetName()) 160 return nil 161 } 162 163 // "if I am a base resource with runtime dependencies, I provide my own dependencies" 164 // see if we can satisfy the dependency (this would occur when initialising the baseDependencySubscriber) 165 if s.ProvidesRuntimeDependency(runtimeDependency) { 166 return s 167 } 168 169 // "if I am a nested resource, my dashboard provides my dependencies" 170 // otherwise the dashboard run must be the publisher 171 dashboardRun := s.executionTree.runs[s.DashboardName].(RuntimeDependencyPublisher) 172 if dashboardRun.ProvidesRuntimeDependency(runtimeDependency) { 173 return dashboardRun 174 } 175 176 return nil 177 } 178 179 func (s *RuntimeDependencySubscriberImpl) evaluateRuntimeDependencies(ctx context.Context) error { 180 log.Printf("[TRACE] %s: evaluateRuntimeDependencies", s.Name) 181 // now wait for any runtime dependencies then resolve args and params 182 // (it is possible to have params but no sql) 183 if s.hasRuntimeDependencies() { 184 // if there are any unresolved runtime dependencies, wait for them 185 if err := s.waitForRuntimeDependencies(ctx); err != nil { 186 return err 187 } 188 log.Printf("[TRACE] %s: runtime dependencies availablem resolving sql and args", s.Name) 189 190 // ok now we have runtime dependencies, we can resolve the query 191 if err := s.resolveSQLAndArgs(); err != nil { 192 return err 193 } 194 // call the argsResolved callback in case anyone is waiting for the args 195 s.argsResolved(s.Args) 196 } 197 return nil 198 } 199 200 func (s *RuntimeDependencySubscriberImpl) waitForRuntimeDependencies(ctx context.Context) error { 201 log.Printf("[TRACE] %s: waitForRuntimeDependencies", s.Name) 202 203 if !s.hasRuntimeDependencies() { 204 log.Printf("[TRACE] %s: no runtime dependencies", s.Name) 205 return nil 206 } 207 208 // wait for base dependencies if we have any 209 if s.baseDependencySubscriber != nil { 210 log.Printf("[TRACE] %s: calling baseDependencySubscriber.waitForRuntimeDependencies", s.Name) 211 if err := s.baseDependencySubscriber.waitForRuntimeDependencies(ctx); err != nil { 212 return err 213 } 214 } 215 216 log.Printf("[TRACE] %s: checking whether all depdencies are resolved", s.Name) 217 218 allRuntimeDepsResolved := true 219 for _, dep := range s.runtimeDependencies { 220 if !dep.IsResolved() { 221 allRuntimeDepsResolved = false 222 log.Printf("[TRACE] %s: dependency %s is NOT resolved", s.Name, dep.Dependency.String()) 223 } 224 } 225 if allRuntimeDepsResolved { 226 return nil 227 } 228 229 log.Printf("[TRACE] %s: BLOCKED", s.Name) 230 // set status to blocked 231 s.setStatus(ctx, dashboardtypes.RunBlocked) 232 233 var wg sync.WaitGroup 234 var errChan = make(chan error) 235 var doneChan = make(chan struct{}) 236 for _, r := range s.runtimeDependencies { 237 if !r.IsResolved() { 238 // make copy of loop var for goroutine 239 resolvedDependency := r 240 log.Printf("[TRACE] %s: wait for %s", s.Name, resolvedDependency.Dependency.String()) 241 wg.Add(1) 242 go func() { 243 defer wg.Done() 244 // block until the dependency is available 245 err := resolvedDependency.Resolve() 246 log.Printf("[TRACE] %s: Resolve returned for %s", s.Name, resolvedDependency.Dependency.String()) 247 if err != nil { 248 log.Printf("[TRACE] %s: Resolve for %s returned error:L %s", s.Name, resolvedDependency.Dependency.String(), err.Error()) 249 errChan <- err 250 } 251 }() 252 } 253 } 254 go func() { 255 log.Printf("[TRACE] %s: goroutine waiting for all runtime deps to be available", s.Name) 256 wg.Wait() 257 close(doneChan) 258 }() 259 260 var errors []error 261 262 wait_loop: 263 for { 264 select { 265 case err := <-errChan: 266 errors = append(errors, err) 267 case <-doneChan: 268 break wait_loop 269 case <-ctx.Done(): 270 errors = append(errors, ctx.Err()) 271 break wait_loop 272 } 273 } 274 275 log.Printf("[TRACE] %s: all runtime dependencies ready", s.resource.Name()) 276 return error_helpers.CombineErrors(errors...) 277 } 278 279 func (s *RuntimeDependencySubscriberImpl) findRuntimeDependenciesForParentProperty(parentProperty string) []*dashboardtypes.ResolvedRuntimeDependency { 280 var res []*dashboardtypes.ResolvedRuntimeDependency 281 for _, dep := range s.runtimeDependencies { 282 if dep.Dependency.ParentPropertyName == parentProperty { 283 res = append(res, dep) 284 } 285 } 286 // also look at base subscriber 287 if s.baseDependencySubscriber != nil { 288 for _, dep := range s.baseDependencySubscriber.runtimeDependencies { 289 if dep.Dependency.ParentPropertyName == parentProperty { 290 res = append(res, dep) 291 } 292 } 293 } 294 return res 295 } 296 297 func (s *RuntimeDependencySubscriberImpl) findRuntimeDependencyForParentProperty(parentProperty string) *dashboardtypes.ResolvedRuntimeDependency { 298 res := s.findRuntimeDependenciesForParentProperty(parentProperty) 299 if len(res) > 1 { 300 panic(fmt.Sprintf("findRuntimeDependencyForParentProperty for %s, parent property %s, returned more that 1 result", s.Name, parentProperty)) 301 } 302 if res == nil { 303 return nil 304 } 305 // return first result 306 return res[0] 307 } 308 309 // resolve the sql for this leaf run into the source sql and resolved args 310 func (s *RuntimeDependencySubscriberImpl) resolveSQLAndArgs() error { 311 log.Printf("[TRACE] %s: resolveSQLAndArgs", s.resource.Name()) 312 queryProvider, ok := s.resource.(modconfig.QueryProvider) 313 if !ok { 314 // not a query provider - nothing to do 315 return nil 316 } 317 318 // convert arg runtime dependencies into arg map 319 runtimeArgs, err := s.buildRuntimeDependencyArgs() 320 if err != nil { 321 log.Printf("[TRACE] %s: buildRuntimeDependencyArgs failed: %s", s.resource.Name(), err.Error()) 322 return err 323 } 324 325 // now if any param defaults had runtime dependencies, populate them 326 s.populateParamDefaults(queryProvider) 327 328 log.Printf("[TRACE] %s: built runtime args: %v", s.resource.Name(), runtimeArgs) 329 330 // does this leaf run have any SQL to execute? 331 if queryProvider.RequiresExecution(queryProvider) { 332 log.Printf("[TRACE] ResolveArgsFromQueryProvider for %s", queryProvider.Name()) 333 resolvedQuery, err := s.executionTree.workspace.ResolveQueryFromQueryProvider(queryProvider, runtimeArgs) 334 if err != nil { 335 return err 336 } 337 s.RawSQL = resolvedQuery.RawSQL 338 s.executeSQL = resolvedQuery.ExecuteSQL 339 s.Args = resolvedQuery.Args 340 } else { 341 // otherwise just resolve the args 342 343 // merge the base args with the runtime args 344 runtimeArgs, err = modconfig.MergeArgs(queryProvider, runtimeArgs) 345 if err != nil { 346 return err 347 } 348 349 args, err := modconfig.ResolveArgs(queryProvider, runtimeArgs) 350 if err != nil { 351 return err 352 } 353 s.Args = args 354 } 355 return nil 356 } 357 358 func (s *RuntimeDependencySubscriberImpl) populateParamDefaults(provider modconfig.QueryProvider) { 359 paramDefs := provider.GetParams() 360 for _, paramDef := range paramDefs { 361 if dep := s.findRuntimeDependencyForParentProperty(paramDef.UnqualifiedName); dep != nil { 362 // assuming the default property is the target, set the default 363 if typehelpers.SafeString(dep.Dependency.TargetPropertyName) == "default" { 364 //nolint:errcheck // the only reason where SetDefault could fail is if `dep.Value` cannot be marshalled as a JSON string 365 paramDef.SetDefault(dep.Value) 366 } 367 } 368 } 369 } 370 371 // convert runtime dependencies into arg map 372 func (s *RuntimeDependencySubscriberImpl) buildRuntimeDependencyArgs() (*modconfig.QueryArgs, error) { 373 res := modconfig.NewQueryArgs() 374 375 log.Printf("[TRACE] %s: buildRuntimeDependencyArgs - %d runtime dependencies", s.resource.Name(), len(s.runtimeDependencies)) 376 377 // if the runtime dependencies use position args, get the max index and ensure the args array is large enough 378 maxArgIndex := -1 379 // build list of all args runtime dependencies 380 argRuntimeDependencies := s.findRuntimeDependenciesForParentProperty(modconfig.AttributeArgs) 381 382 for _, dep := range argRuntimeDependencies { 383 if dep.Dependency.TargetPropertyIndex != nil && *dep.Dependency.TargetPropertyIndex > maxArgIndex { 384 maxArgIndex = *dep.Dependency.TargetPropertyIndex 385 } 386 } 387 if maxArgIndex != -1 { 388 res.ArgList = make([]*string, maxArgIndex+1) 389 } 390 391 // now set the arg values 392 for _, dep := range argRuntimeDependencies { 393 if dep.Dependency.TargetPropertyName != nil { 394 err := res.SetNamedArgVal(dep.Value, *dep.Dependency.TargetPropertyName) 395 if err != nil { 396 return nil, err 397 } 398 399 } else { 400 if dep.Dependency.TargetPropertyIndex == nil { 401 return nil, fmt.Errorf("invalid runtime dependency - both ArgName and ArgIndex are nil ") 402 } 403 err := res.SetPositionalArgVal(dep.Value, *dep.Dependency.TargetPropertyIndex) 404 if err != nil { 405 return nil, err 406 } 407 } 408 } 409 return res, nil 410 } 411 412 // populate the list of runtime dependencies that this run depends on 413 func (s *RuntimeDependencySubscriberImpl) setRuntimeDependencies() { 414 names := make(map[string]struct{}, len(s.runtimeDependencies)) 415 for _, d := range s.runtimeDependencies { 416 // add to DependencyWiths using ScopedName, i.e. <parent FullName>.<with UnqualifiedName>. 417 // we do this as there may be a with from a base resource with a clashing with name 418 // NOTE: this must be consistent with the naming in RuntimeDependencyPublisherImpl.createWithRuns 419 names[d.ScopedName()] = struct{}{} 420 } 421 422 // get base runtime dependencies (if any) 423 if s.baseDependencySubscriber != nil { 424 s.baseDependencySubscriber.setRuntimeDependencies() 425 s.RuntimeDependencyNames = append(s.RuntimeDependencyNames, s.baseDependencySubscriber.RuntimeDependencyNames...) 426 } 427 428 s.RuntimeDependencyNames = maps.Keys(names) 429 } 430 431 func (s *RuntimeDependencySubscriberImpl) hasRuntimeDependencies() bool { 432 return len(s.runtimeDependencies)+len(s.baseRuntimeDependencies()) > 0 433 } 434 435 func (s *RuntimeDependencySubscriberImpl) baseRuntimeDependencies() map[string]*dashboardtypes.ResolvedRuntimeDependency { 436 if s.baseDependencySubscriber == nil { 437 return map[string]*dashboardtypes.ResolvedRuntimeDependency{} 438 } 439 return s.baseDependencySubscriber.runtimeDependencies 440 } 441 442 // override DashboardParentImpl.executeChildrenAsync to also execute 'withs' of our baseRun 443 func (s *RuntimeDependencySubscriberImpl) executeChildrenAsync(ctx context.Context) { 444 // if we have a baseDependencySubscriber, execute it 445 if s.baseDependencySubscriber != nil { 446 go s.baseDependencySubscriber.executeWithsAsync(ctx) 447 } 448 449 // if this leaf run has children (including with runs) execute them asynchronously 450 451 // set RuntimeDependenciesOnly if needed 452 s.DashboardParentImpl.executeChildrenAsync(ctx) 453 } 454 455 // called when the args are resolved - if anyone is subscribing to the args value, publish 456 func (s *RuntimeDependencySubscriberImpl) argsResolved(args []any) { 457 if s.baseDependencySubscriber != nil { 458 s.baseDependencySubscriber.argsResolved(args) 459 } 460 s.runtimeDependencyPublisherImpl.argsResolved(args) 461 }