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  }