github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/steampipeconfig/parse/parse_context.go (about)

     1  package parse
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	"github.com/hashicorp/hcl/v2"
     8  	"github.com/stevenle/topsort"
     9  	"github.com/turbot/go-kit/helpers"
    10  	"github.com/turbot/pipe-fittings/hclhelpers"
    11  	"github.com/turbot/steampipe/pkg/steampipeconfig/modconfig"
    12  	"github.com/zclconf/go-cty/cty"
    13  )
    14  
    15  type ParseContext struct {
    16  	UnresolvedBlocks map[string]*unresolvedBlock
    17  	FileData         map[string][]byte
    18  	// the eval context used to decode references in HCL
    19  	EvalCtx *hcl.EvalContext
    20  
    21  	RootEvalPath string
    22  
    23  	// if set, only decode these blocks
    24  	BlockTypes []string
    25  	// if set, exclude these block types
    26  	BlockTypeExclusions []string
    27  
    28  	dependencyGraph *topsort.Graph
    29  	blocks          hcl.Blocks
    30  }
    31  
    32  func NewParseContext(rootEvalPath string) ParseContext {
    33  	c := ParseContext{
    34  		UnresolvedBlocks: make(map[string]*unresolvedBlock),
    35  		RootEvalPath:     rootEvalPath,
    36  	}
    37  	// add root node - this will depend on all other nodes
    38  	c.dependencyGraph = c.newDependencyGraph()
    39  
    40  	return c
    41  }
    42  
    43  func (r *ParseContext) SetDecodeContent(content *hcl.BodyContent, fileData map[string][]byte) {
    44  	r.blocks = content.Blocks
    45  	r.FileData = fileData
    46  }
    47  
    48  func (r *ParseContext) ClearDependencies() {
    49  	r.UnresolvedBlocks = make(map[string]*unresolvedBlock)
    50  	r.dependencyGraph = r.newDependencyGraph()
    51  }
    52  
    53  // AddDependencies is called when a block could not be resolved as it has dependencies
    54  // 1) store block as unresolved
    55  // 2) add dependencies to our tree of dependencies
    56  func (r *ParseContext) AddDependencies(block *hcl.Block, name string, dependencies map[string]*modconfig.ResourceDependency) hcl.Diagnostics {
    57  	var diags hcl.Diagnostics
    58  	// store unresolved block
    59  	r.UnresolvedBlocks[name] = newUnresolvedBlock(block, name, dependencies)
    60  
    61  	// store dependency in tree - d
    62  	if !r.dependencyGraph.ContainsNode(name) {
    63  		r.dependencyGraph.AddNode(name)
    64  	}
    65  	// add root dependency
    66  	if err := r.dependencyGraph.AddEdge(rootDependencyNode, name); err != nil {
    67  		diags = append(diags, &hcl.Diagnostic{
    68  			Severity: hcl.DiagError,
    69  			Summary:  "failed to add root dependency to graph",
    70  			Detail:   err.Error()})
    71  	}
    72  
    73  	for _, dep := range dependencies {
    74  		// each dependency object may have multiple traversals
    75  		for _, t := range dep.Traversals {
    76  			parsedPropertyPath, err := modconfig.ParseResourcePropertyPath(hclhelpers.TraversalAsString(t))
    77  
    78  			if err != nil {
    79  				diags = append(diags, &hcl.Diagnostic{
    80  					Severity: hcl.DiagError,
    81  					Summary:  "failed to parse dependency",
    82  					Detail:   err.Error()})
    83  				continue
    84  
    85  			}
    86  
    87  			// 'd' may be a property path - when storing dependencies we only care about the resource names
    88  			dependencyResourceName := parsedPropertyPath.ToResourceName()
    89  			if !r.dependencyGraph.ContainsNode(dependencyResourceName) {
    90  				r.dependencyGraph.AddNode(dependencyResourceName)
    91  			}
    92  			if err := r.dependencyGraph.AddEdge(name, dependencyResourceName); err != nil {
    93  				diags = append(diags, &hcl.Diagnostic{
    94  					Severity: hcl.DiagError,
    95  					Summary:  "failed to add dependency to graph",
    96  					Detail:   err.Error()})
    97  			}
    98  		}
    99  	}
   100  	return diags
   101  }
   102  
   103  // BlocksToDecode builds a list of blocks to decode, the order of which is determined by the dependency order
   104  func (r *ParseContext) BlocksToDecode() (hcl.Blocks, error) {
   105  	depOrder, err := r.getDependencyOrder()
   106  	if err != nil {
   107  		return nil, err
   108  	}
   109  	if len(depOrder) == 0 {
   110  		return r.blocks, nil
   111  	}
   112  
   113  	// NOTE: a block may appear more than once in unresolved blocks
   114  	// if it defines multiple unresolved resources, e.g a locals block
   115  
   116  	// make a map of blocks we have already included, keyed by the block def range
   117  	blocksMap := make(map[string]bool)
   118  	var blocksToDecode hcl.Blocks
   119  	for _, name := range depOrder {
   120  		// depOrder is all the blocks required to resolve dependencies.
   121  		// if this one is unparsed, added to list
   122  		block, ok := r.UnresolvedBlocks[name]
   123  		if ok && !blocksMap[block.DeclRange.String()] && ok {
   124  			blocksToDecode = append(blocksToDecode, block.Block)
   125  			// add to map
   126  			blocksMap[block.DeclRange.String()] = true
   127  		}
   128  	}
   129  	return blocksToDecode, nil
   130  }
   131  
   132  // EvalComplete returns whether all elements in the dependency tree fully evaluated
   133  func (r *ParseContext) EvalComplete() bool {
   134  	return len(r.UnresolvedBlocks) == 0
   135  }
   136  
   137  func (r *ParseContext) FormatDependencies() string {
   138  	// first get the dependency order
   139  	dependencyOrder, err := r.getDependencyOrder()
   140  	if err != nil {
   141  		return err.Error()
   142  	}
   143  	// build array of dependency strings - processes dependencies in reverse order for presentation reasons
   144  	numDeps := len(dependencyOrder)
   145  	depStrings := make([]string, numDeps)
   146  	for i := 0; i < len(dependencyOrder); i++ {
   147  		srcIdx := len(dependencyOrder) - i - 1
   148  		resourceName := dependencyOrder[srcIdx]
   149  		// find dependency
   150  		dep, ok := r.UnresolvedBlocks[resourceName]
   151  
   152  		if ok {
   153  			depStrings[i] = dep.String()
   154  		} else {
   155  			// this could happen if there is a dependency on a missing item
   156  			depStrings[i] = fmt.Sprintf("  MISSING: %s", resourceName)
   157  		}
   158  	}
   159  
   160  	return helpers.Tabify(strings.Join(depStrings, "\n"), "   ")
   161  }
   162  
   163  func (r *ParseContext) ShouldIncludeBlock(block *hcl.Block) bool {
   164  	if len(r.BlockTypes) > 0 && !helpers.StringSliceContains(r.BlockTypes, block.Type) {
   165  		return false
   166  	}
   167  	if len(r.BlockTypeExclusions) > 0 && helpers.StringSliceContains(r.BlockTypeExclusions, block.Type) {
   168  		return false
   169  	}
   170  	return true
   171  }
   172  
   173  func (r *ParseContext) newDependencyGraph() *topsort.Graph {
   174  	dependencyGraph := topsort.NewGraph()
   175  	// add root node - this will depend on all other nodes
   176  	dependencyGraph.AddNode(rootDependencyNode)
   177  	return dependencyGraph
   178  }
   179  
   180  // return the optimal run order required to resolve dependencies
   181  
   182  func (r *ParseContext) getDependencyOrder() ([]string, error) {
   183  	rawDeps, err := r.dependencyGraph.TopSort(rootDependencyNode)
   184  	if err != nil {
   185  		return nil, err
   186  	}
   187  
   188  	// now remove the variable names and dedupe
   189  	var deps []string
   190  	for _, d := range rawDeps {
   191  		if d == rootDependencyNode {
   192  			continue
   193  		}
   194  
   195  		propertyPath, err := modconfig.ParseResourcePropertyPath(d)
   196  		if err != nil {
   197  			return nil, err
   198  		}
   199  		dep := modconfig.BuildModResourceName(propertyPath.ItemType, propertyPath.Name)
   200  		if !helpers.StringSliceContains(deps, dep) {
   201  			deps = append(deps, dep)
   202  		}
   203  	}
   204  	return deps, nil
   205  }
   206  
   207  // eval functions
   208  func (r *ParseContext) buildEvalContext(variables map[string]cty.Value) {
   209  
   210  	// create evaluation context
   211  	r.EvalCtx = &hcl.EvalContext{
   212  		Variables: variables,
   213  		// use the mod path as the file root for functions
   214  		Functions: ContextFunctions(r.RootEvalPath),
   215  	}
   216  }