mirror of
https://github.com/turbot/steampipe.git
synced 2026-02-17 10:00:17 -05:00
217 lines
6.3 KiB
Go
217 lines
6.3 KiB
Go
package parse
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/stevenle/topsort"
|
|
"github.com/turbot/go-kit/helpers"
|
|
"github.com/turbot/pipe-fittings/hclhelpers"
|
|
"github.com/turbot/steampipe/pkg/steampipeconfig/modconfig"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
type ParseContext struct {
|
|
UnresolvedBlocks map[string]*unresolvedBlock
|
|
FileData map[string][]byte
|
|
// the eval context used to decode references in HCL
|
|
EvalCtx *hcl.EvalContext
|
|
|
|
RootEvalPath string
|
|
|
|
// if set, only decode these blocks
|
|
BlockTypes []string
|
|
// if set, exclude these block types
|
|
BlockTypeExclusions []string
|
|
|
|
dependencyGraph *topsort.Graph
|
|
blocks hcl.Blocks
|
|
}
|
|
|
|
func NewParseContext(rootEvalPath string) ParseContext {
|
|
c := ParseContext{
|
|
UnresolvedBlocks: make(map[string]*unresolvedBlock),
|
|
RootEvalPath: rootEvalPath,
|
|
}
|
|
// add root node - this will depend on all other nodes
|
|
c.dependencyGraph = c.newDependencyGraph()
|
|
|
|
return c
|
|
}
|
|
|
|
func (r *ParseContext) SetDecodeContent(content *hcl.BodyContent, fileData map[string][]byte) {
|
|
r.blocks = content.Blocks
|
|
r.FileData = fileData
|
|
}
|
|
|
|
func (r *ParseContext) ClearDependencies() {
|
|
r.UnresolvedBlocks = make(map[string]*unresolvedBlock)
|
|
r.dependencyGraph = r.newDependencyGraph()
|
|
}
|
|
|
|
// AddDependencies is called when a block could not be resolved as it has dependencies
|
|
// 1) store block as unresolved
|
|
// 2) add dependencies to our tree of dependencies
|
|
func (r *ParseContext) AddDependencies(block *hcl.Block, name string, dependencies map[string]*modconfig.ResourceDependency) hcl.Diagnostics {
|
|
var diags hcl.Diagnostics
|
|
// store unresolved block
|
|
r.UnresolvedBlocks[name] = newUnresolvedBlock(block, name, dependencies)
|
|
|
|
// store dependency in tree - d
|
|
if !r.dependencyGraph.ContainsNode(name) {
|
|
r.dependencyGraph.AddNode(name)
|
|
}
|
|
// add root dependency
|
|
if err := r.dependencyGraph.AddEdge(rootDependencyNode, name); err != nil {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "failed to add root dependency to graph",
|
|
Detail: err.Error()})
|
|
}
|
|
|
|
for _, dep := range dependencies {
|
|
// each dependency object may have multiple traversals
|
|
for _, t := range dep.Traversals {
|
|
parsedPropertyPath, err := modconfig.ParseResourcePropertyPath(hclhelpers.TraversalAsString(t))
|
|
|
|
if err != nil {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "failed to parse dependency",
|
|
Detail: err.Error()})
|
|
continue
|
|
|
|
}
|
|
|
|
// 'd' may be a property path - when storing dependencies we only care about the resource names
|
|
dependencyResourceName := parsedPropertyPath.ToResourceName()
|
|
if !r.dependencyGraph.ContainsNode(dependencyResourceName) {
|
|
r.dependencyGraph.AddNode(dependencyResourceName)
|
|
}
|
|
if err := r.dependencyGraph.AddEdge(name, dependencyResourceName); err != nil {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "failed to add dependency to graph",
|
|
Detail: err.Error()})
|
|
}
|
|
}
|
|
}
|
|
return diags
|
|
}
|
|
|
|
// BlocksToDecode builds a list of blocks to decode, the order of which is determined by the dependency order
|
|
func (r *ParseContext) BlocksToDecode() (hcl.Blocks, error) {
|
|
depOrder, err := r.getDependencyOrder()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(depOrder) == 0 {
|
|
return r.blocks, nil
|
|
}
|
|
|
|
// NOTE: a block may appear more than once in unresolved blocks
|
|
// if it defines multiple unresolved resources, e.g a locals block
|
|
|
|
// make a map of blocks we have already included, keyed by the block def range
|
|
blocksMap := make(map[string]bool)
|
|
var blocksToDecode hcl.Blocks
|
|
for _, name := range depOrder {
|
|
// depOrder is all the blocks required to resolve dependencies.
|
|
// if this one is unparsed, added to list
|
|
block, ok := r.UnresolvedBlocks[name]
|
|
if ok && !blocksMap[block.DeclRange.String()] && ok {
|
|
blocksToDecode = append(blocksToDecode, block.Block)
|
|
// add to map
|
|
blocksMap[block.DeclRange.String()] = true
|
|
}
|
|
}
|
|
return blocksToDecode, nil
|
|
}
|
|
|
|
// EvalComplete returns whether all elements in the dependency tree fully evaluated
|
|
func (r *ParseContext) EvalComplete() bool {
|
|
return len(r.UnresolvedBlocks) == 0
|
|
}
|
|
|
|
func (r *ParseContext) FormatDependencies() string {
|
|
// first get the dependency order
|
|
dependencyOrder, err := r.getDependencyOrder()
|
|
if err != nil {
|
|
return err.Error()
|
|
}
|
|
// build array of dependency strings - processes dependencies in reverse order for presentation reasons
|
|
numDeps := len(dependencyOrder)
|
|
depStrings := make([]string, numDeps)
|
|
for i := 0; i < len(dependencyOrder); i++ {
|
|
srcIdx := len(dependencyOrder) - i - 1
|
|
resourceName := dependencyOrder[srcIdx]
|
|
// find dependency
|
|
dep, ok := r.UnresolvedBlocks[resourceName]
|
|
|
|
if ok {
|
|
depStrings[i] = dep.String()
|
|
} else {
|
|
// this could happen if there is a dependency on a missing item
|
|
depStrings[i] = fmt.Sprintf(" MISSING: %s", resourceName)
|
|
}
|
|
}
|
|
|
|
return helpers.Tabify(strings.Join(depStrings, "\n"), " ")
|
|
}
|
|
|
|
func (r *ParseContext) ShouldIncludeBlock(block *hcl.Block) bool {
|
|
if len(r.BlockTypes) > 0 && !helpers.StringSliceContains(r.BlockTypes, block.Type) {
|
|
return false
|
|
}
|
|
if len(r.BlockTypeExclusions) > 0 && helpers.StringSliceContains(r.BlockTypeExclusions, block.Type) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (r *ParseContext) newDependencyGraph() *topsort.Graph {
|
|
dependencyGraph := topsort.NewGraph()
|
|
// add root node - this will depend on all other nodes
|
|
dependencyGraph.AddNode(rootDependencyNode)
|
|
return dependencyGraph
|
|
}
|
|
|
|
// return the optimal run order required to resolve dependencies
|
|
|
|
func (r *ParseContext) getDependencyOrder() ([]string, error) {
|
|
rawDeps, err := r.dependencyGraph.TopSort(rootDependencyNode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// now remove the variable names and dedupe
|
|
var deps []string
|
|
for _, d := range rawDeps {
|
|
if d == rootDependencyNode {
|
|
continue
|
|
}
|
|
|
|
propertyPath, err := modconfig.ParseResourcePropertyPath(d)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dep := modconfig.BuildModResourceName(propertyPath.ItemType, propertyPath.Name)
|
|
if !helpers.StringSliceContains(deps, dep) {
|
|
deps = append(deps, dep)
|
|
}
|
|
}
|
|
return deps, nil
|
|
}
|
|
|
|
// eval functions
|
|
func (r *ParseContext) buildEvalContext(variables map[string]cty.Value) {
|
|
|
|
// create evaluation context
|
|
r.EvalCtx = &hcl.EvalContext{
|
|
Variables: variables,
|
|
// use the mod path as the file root for functions
|
|
Functions: ContextFunctions(r.RootEvalPath),
|
|
}
|
|
}
|