| package dynblock |
| |
| import ( |
| "github.com/hashicorp/hcl/v2" |
| "github.com/zclconf/go-cty/cty" |
| ) |
| |
| // WalkVariables begins the recursive process of walking all expressions and |
| // nested blocks in the given body and its child bodies while taking into |
| // account any "dynamic" blocks. |
| // |
| // This function requires that the caller walk through the nested block |
| // structure in the given body level-by-level so that an appropriate schema |
| // can be provided at each level to inform further processing. This workflow |
| // is thus easiest to use for calling applications that have some higher-level |
| // schema representation available with which to drive this multi-step |
| // process. If your application uses the hcldec package, you may be able to |
| // use VariablesHCLDec instead for a more automatic approach. |
| func WalkVariables(body hcl.Body) WalkVariablesNode { |
| return WalkVariablesNode{ |
| body: body, |
| includeContent: true, |
| } |
| } |
| |
| // WalkExpandVariables is like Variables but it includes only the variables |
| // required for successful block expansion, ignoring any variables referenced |
| // inside block contents. The result is the minimal set of all variables |
| // required for a call to Expand, excluding variables that would only be |
| // needed to subsequently call Content or PartialContent on the expanded |
| // body. |
| func WalkExpandVariables(body hcl.Body) WalkVariablesNode { |
| return WalkVariablesNode{ |
| body: body, |
| } |
| } |
| |
| type WalkVariablesNode struct { |
| body hcl.Body |
| it *iteration |
| |
| includeContent bool |
| } |
| |
| type WalkVariablesChild struct { |
| BlockTypeName string |
| Node WalkVariablesNode |
| } |
| |
| // Body returns the HCL Body associated with the child node, in case the caller |
| // wants to do some sort of inspection of it in order to decide what schema |
| // to pass to Visit. |
| // |
| // Most implementations should just fetch a fixed schema based on the |
| // BlockTypeName field and not access this. Deciding on a schema dynamically |
| // based on the body is a strange thing to do and generally necessary only if |
| // your caller is already doing other bizarre things with HCL bodies. |
| func (c WalkVariablesChild) Body() hcl.Body { |
| return c.Node.body |
| } |
| |
| // Visit returns the variable traversals required for any "dynamic" blocks |
| // directly in the body associated with this node, and also returns any child |
| // nodes that must be visited in order to continue the walk. |
| // |
| // Each child node has its associated block type name given in its BlockTypeName |
| // field, which the calling application should use to determine the appropriate |
| // schema for the content of each child node and pass it to the child node's |
| // own Visit method to continue the walk recursively. |
| func (n WalkVariablesNode) Visit(schema *hcl.BodySchema) (vars []hcl.Traversal, children []WalkVariablesChild) { |
| extSchema := n.extendSchema(schema) |
| container, _, _ := n.body.PartialContent(extSchema) |
| if container == nil { |
| return vars, children |
| } |
| |
| children = make([]WalkVariablesChild, 0, len(container.Blocks)) |
| |
| if n.includeContent { |
| for _, attr := range container.Attributes { |
| for _, traversal := range attr.Expr.Variables() { |
| var ours, inherited bool |
| if n.it != nil { |
| ours = traversal.RootName() == n.it.IteratorName |
| _, inherited = n.it.Inherited[traversal.RootName()] |
| } |
| |
| if !(ours || inherited) { |
| vars = append(vars, traversal) |
| } |
| } |
| } |
| } |
| |
| for _, block := range container.Blocks { |
| switch block.Type { |
| |
| case "dynamic": |
| blockTypeName := block.Labels[0] |
| inner, _, _ := block.Body.PartialContent(variableDetectionInnerSchema) |
| if inner == nil { |
| continue |
| } |
| |
| iteratorName := blockTypeName |
| if attr, exists := inner.Attributes["iterator"]; exists { |
| iterTraversal, _ := hcl.AbsTraversalForExpr(attr.Expr) |
| if len(iterTraversal) == 0 { |
| // Ignore this invalid dynamic block, since it'll produce |
| // an error if someone tries to extract content from it |
| // later anyway. |
| continue |
| } |
| iteratorName = iterTraversal.RootName() |
| } |
| blockIt := n.it.MakeChild(iteratorName, cty.DynamicVal, cty.DynamicVal) |
| |
| if attr, exists := inner.Attributes["for_each"]; exists { |
| // Filter out iterator names inherited from parent blocks |
| for _, traversal := range attr.Expr.Variables() { |
| if _, inherited := blockIt.Inherited[traversal.RootName()]; !inherited { |
| vars = append(vars, traversal) |
| } |
| } |
| } |
| if attr, exists := inner.Attributes["labels"]; exists { |
| // Filter out both our own iterator name _and_ those inherited |
| // from parent blocks, since we provide _both_ of these to the |
| // label expressions. |
| for _, traversal := range attr.Expr.Variables() { |
| ours := traversal.RootName() == iteratorName |
| _, inherited := blockIt.Inherited[traversal.RootName()] |
| |
| if !(ours || inherited) { |
| vars = append(vars, traversal) |
| } |
| } |
| } |
| |
| for _, contentBlock := range inner.Blocks { |
| // We only request "content" blocks in our schema, so we know |
| // any blocks we find here will be content blocks. We require |
| // exactly one content block for actual expansion, but we'll |
| // be more liberal here so that callers can still collect |
| // variables from erroneous "dynamic" blocks. |
| children = append(children, WalkVariablesChild{ |
| BlockTypeName: blockTypeName, |
| Node: WalkVariablesNode{ |
| body: contentBlock.Body, |
| it: blockIt, |
| includeContent: n.includeContent, |
| }, |
| }) |
| } |
| |
| default: |
| children = append(children, WalkVariablesChild{ |
| BlockTypeName: block.Type, |
| Node: WalkVariablesNode{ |
| body: block.Body, |
| it: n.it, |
| includeContent: n.includeContent, |
| }, |
| }) |
| |
| } |
| } |
| |
| return vars, children |
| } |
| |
| func (n WalkVariablesNode) extendSchema(schema *hcl.BodySchema) *hcl.BodySchema { |
| // We augment the requested schema to also include our special "dynamic" |
| // block type, since then we'll get instances of it interleaved with |
| // all of the literal child blocks we must also include. |
| extSchema := &hcl.BodySchema{ |
| Attributes: schema.Attributes, |
| Blocks: make([]hcl.BlockHeaderSchema, len(schema.Blocks), len(schema.Blocks)+1), |
| } |
| copy(extSchema.Blocks, schema.Blocks) |
| extSchema.Blocks = append(extSchema.Blocks, dynamicBlockHeaderSchema) |
| |
| return extSchema |
| } |
| |
| // This is a more relaxed schema than what's in schema.go, since we |
| // want to maximize the amount of variables we can find even if there |
| // are erroneous blocks. |
| var variableDetectionInnerSchema = &hcl.BodySchema{ |
| Attributes: []hcl.AttributeSchema{ |
| { |
| Name: "for_each", |
| Required: false, |
| }, |
| { |
| Name: "labels", |
| Required: false, |
| }, |
| { |
| Name: "iterator", |
| Required: false, |
| }, |
| }, |
| Blocks: []hcl.BlockHeaderSchema{ |
| { |
| Type: "content", |
| }, |
| }, |
| } |