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 }