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  }