mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-19 21:22:57 -06:00
e2b74247f2
Track individual instance drift rather than whole resources which contributed to the plan. This will allow the output to be more precise, and we can still use NoKey instances as a proxy for containing resources when needed.
607 lines
21 KiB
Go
607 lines
21 KiB
Go
package globalref
|
|
|
|
import (
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/terraform/internal/addrs"
|
|
"github.com/hashicorp/terraform/internal/configs/configschema"
|
|
"github.com/hashicorp/terraform/internal/lang"
|
|
"github.com/zclconf/go-cty/cty"
|
|
"github.com/zclconf/go-cty/cty/convert"
|
|
"github.com/zclconf/go-cty/cty/gocty"
|
|
)
|
|
|
|
// MetaReferences inspects the configuration to find the references contained
|
|
// within the most specific object that the given address refers to.
|
|
//
|
|
// This finds only the direct references in that object, not any indirect
|
|
// references from those. This is a building block for some other Analyzer
|
|
// functions that can walk through multiple levels of reference.
|
|
//
|
|
// If the given reference refers to something that doesn't exist in the
|
|
// configuration we're analyzing then MetaReferences will return no
|
|
// meta-references at all, which is indistinguishable from an existing
|
|
// object that doesn't refer to anything.
|
|
func (a *Analyzer) MetaReferences(ref Reference) []Reference {
|
|
// This function is aiming to encapsulate the fact that a reference
|
|
// is actually quite a complex notion which includes both a specific
|
|
// object the reference is to, where each distinct object type has
|
|
// a very different representation in the configuration, and then
|
|
// also potentially an attribute or block within the definition of that
|
|
// object. Our goal is to make all of these different situations appear
|
|
// mostly the same to the caller, in that all of them can be reduced to
|
|
// a set of references regardless of which expression or expressions we
|
|
// derive those from.
|
|
|
|
moduleAddr := ref.ModuleAddr()
|
|
remaining := ref.LocalRef.Remaining
|
|
|
|
// Our first task then is to select an appropriate implementation based
|
|
// on which address type the reference refers to.
|
|
switch targetAddr := ref.LocalRef.Subject.(type) {
|
|
case addrs.InputVariable:
|
|
return a.metaReferencesInputVariable(moduleAddr, targetAddr, remaining)
|
|
case addrs.LocalValue:
|
|
return a.metaReferencesLocalValue(moduleAddr, targetAddr, remaining)
|
|
case addrs.ModuleCallInstanceOutput:
|
|
return a.metaReferencesOutputValue(moduleAddr, targetAddr, remaining)
|
|
case addrs.ModuleCallInstance:
|
|
return a.metaReferencesModuleCall(moduleAddr, targetAddr, remaining)
|
|
case addrs.ModuleCall:
|
|
// TODO: It isn't really correct to say that a reference to a module
|
|
// call is a reference to its no-key instance. Really what we want to
|
|
// say here is that it's a reference to _all_ instances, or to an
|
|
// instance with an unknown key, but we don't have any representation
|
|
// of that. For the moment it's pretty immaterial since most of our
|
|
// other analysis ignores instance keys anyway, but maybe we'll revisit
|
|
// this latter to distingish these two cases better.
|
|
return a.metaReferencesModuleCall(moduleAddr, targetAddr.Instance(addrs.NoKey), remaining)
|
|
case addrs.CountAttr, addrs.ForEachAttr:
|
|
if resourceAddr, ok := ref.ResourceInstance(); ok {
|
|
return a.metaReferencesCountOrEach(resourceAddr.ContainingResource())
|
|
}
|
|
return nil
|
|
case addrs.ResourceInstance:
|
|
return a.metaReferencesResourceInstance(moduleAddr, targetAddr, remaining)
|
|
case addrs.Resource:
|
|
// TODO: It isn't really correct to say that a reference to a resource
|
|
// is a reference to its no-key instance. Really what we want to say
|
|
// here is that it's a reference to _all_ instances, or to an instance
|
|
// with an unknown key, but we don't have any representation of that.
|
|
// For the moment it's pretty immaterial since most of our other
|
|
// analysis ignores instance keys anyway, but maybe we'll revisit this
|
|
// latter to distingish these two cases better.
|
|
return a.metaReferencesResourceInstance(moduleAddr, targetAddr.Instance(addrs.NoKey), remaining)
|
|
default:
|
|
// For anything we don't explicitly support we'll just return no
|
|
// references. This includes the reference types that don't really
|
|
// refer to configuration objects at all, like "path.module",
|
|
// and so which cannot possibly generate any references.
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (a *Analyzer) metaReferencesInputVariable(calleeAddr addrs.ModuleInstance, addr addrs.InputVariable, remain hcl.Traversal) []Reference {
|
|
if calleeAddr.IsRoot() {
|
|
// A root module variable definition can never refer to anything,
|
|
// because it conceptually exists outside of any module.
|
|
return nil
|
|
}
|
|
|
|
callerAddr, callAddr := calleeAddr.Call()
|
|
|
|
// We need to find the module call inside the caller module.
|
|
callerCfg := a.ModuleConfig(callerAddr)
|
|
if callerCfg == nil {
|
|
return nil
|
|
}
|
|
call := callerCfg.ModuleCalls[callAddr.Name]
|
|
if call == nil {
|
|
return nil
|
|
}
|
|
|
|
// Now we need to look for an attribute matching the variable name inside
|
|
// the module block body.
|
|
body := call.Config
|
|
schema := &hcl.BodySchema{
|
|
Attributes: []hcl.AttributeSchema{
|
|
{Name: addr.Name},
|
|
},
|
|
}
|
|
// We don't check for errors here because we'll make a best effort to
|
|
// analyze whatever partial result HCL is able to extract.
|
|
content, _, _ := body.PartialContent(schema)
|
|
attr := content.Attributes[addr.Name]
|
|
if attr == nil {
|
|
return nil
|
|
}
|
|
refs, _ := lang.ReferencesInExpr(attr.Expr)
|
|
return absoluteRefs(callerAddr, refs)
|
|
}
|
|
|
|
func (a *Analyzer) metaReferencesOutputValue(callerAddr addrs.ModuleInstance, addr addrs.ModuleCallInstanceOutput, remain hcl.Traversal) []Reference {
|
|
calleeAddr := callerAddr.Child(addr.Call.Call.Name, addr.Call.Key)
|
|
|
|
// We need to find the output value declaration inside the callee module.
|
|
calleeCfg := a.ModuleConfig(calleeAddr)
|
|
if calleeCfg == nil {
|
|
return nil
|
|
}
|
|
|
|
oc := calleeCfg.Outputs[addr.Name]
|
|
if oc == nil {
|
|
return nil
|
|
}
|
|
|
|
// We don't check for errors here because we'll make a best effort to
|
|
// analyze whatever partial result HCL is able to extract.
|
|
refs, _ := lang.ReferencesInExpr(oc.Expr)
|
|
return absoluteRefs(calleeAddr, refs)
|
|
}
|
|
|
|
func (a *Analyzer) metaReferencesLocalValue(moduleAddr addrs.ModuleInstance, addr addrs.LocalValue, remain hcl.Traversal) []Reference {
|
|
modCfg := a.ModuleConfig(moduleAddr)
|
|
if modCfg == nil {
|
|
return nil
|
|
}
|
|
|
|
local := modCfg.Locals[addr.Name]
|
|
if local == nil {
|
|
return nil
|
|
}
|
|
|
|
// We don't check for errors here because we'll make a best effort to
|
|
// analyze whatever partial result HCL is able to extract.
|
|
refs, _ := lang.ReferencesInExpr(local.Expr)
|
|
return absoluteRefs(moduleAddr, refs)
|
|
}
|
|
|
|
func (a *Analyzer) metaReferencesModuleCall(callerAddr addrs.ModuleInstance, addr addrs.ModuleCallInstance, remain hcl.Traversal) []Reference {
|
|
calleeAddr := callerAddr.Child(addr.Call.Name, addr.Key)
|
|
|
|
// What we're really doing here is just rolling up all of the references
|
|
// from all of this module's output values.
|
|
calleeCfg := a.ModuleConfig(calleeAddr)
|
|
if calleeCfg == nil {
|
|
return nil
|
|
}
|
|
|
|
var ret []Reference
|
|
for name := range calleeCfg.Outputs {
|
|
outputAddr := addrs.ModuleCallInstanceOutput{
|
|
Call: addr,
|
|
Name: name,
|
|
}
|
|
moreRefs := a.metaReferencesOutputValue(callerAddr, outputAddr, nil)
|
|
ret = append(ret, moreRefs...)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func (a *Analyzer) metaReferencesCountOrEach(resourceAddr addrs.AbsResource) []Reference {
|
|
return a.ReferencesFromResourceRepetition(resourceAddr)
|
|
}
|
|
|
|
func (a *Analyzer) metaReferencesResourceInstance(moduleAddr addrs.ModuleInstance, addr addrs.ResourceInstance, remain hcl.Traversal) []Reference {
|
|
modCfg := a.ModuleConfig(moduleAddr)
|
|
if modCfg == nil {
|
|
return nil
|
|
}
|
|
|
|
rc := modCfg.ResourceByAddr(addr.Resource)
|
|
if rc == nil {
|
|
return nil
|
|
}
|
|
|
|
// In valid cases we should have the schema for this resource type
|
|
// available. In invalid cases we might be dealing with partial information,
|
|
// and so the schema might be nil so we won't be able to return reference
|
|
// information for this particular situation.
|
|
providerSchema := a.providerSchemas[rc.Provider]
|
|
if providerSchema == nil {
|
|
return nil
|
|
}
|
|
|
|
resourceTypeSchema, _ := providerSchema.SchemaForResourceAddr(addr.Resource)
|
|
if resourceTypeSchema == nil {
|
|
return nil
|
|
}
|
|
|
|
// When analyzing the resource configuration to look for references, we'll
|
|
// make a best effort to narrow down to only a particular sub-portion of
|
|
// the configuration by following the remaining traversal steps. In the
|
|
// ideal case this will lead us to a specific expression, but as a
|
|
// compromise it might lead us to some nested blocks where at least we
|
|
// can limit our searching only to those.
|
|
bodies := []hcl.Body{rc.Config}
|
|
var exprs []hcl.Expression
|
|
schema := resourceTypeSchema
|
|
var steppingThrough *configschema.NestedBlock
|
|
var steppingThroughType string
|
|
nextStep := func(newBodies []hcl.Body, newExprs []hcl.Expression) {
|
|
// We append exprs but replace bodies because exprs represent extra
|
|
// expressions we collected on the path, such as dynamic block for_each,
|
|
// which can potentially contribute to the final evalcontext, but
|
|
// bodies never contribute any values themselves, and instead just
|
|
// narrow down where we're searching.
|
|
bodies = newBodies
|
|
exprs = append(exprs, newExprs...)
|
|
steppingThrough = nil
|
|
steppingThroughType = ""
|
|
// Caller must also update "schema" if necessary.
|
|
}
|
|
traverseInBlock := func(name string) ([]hcl.Body, []hcl.Expression) {
|
|
if attr := schema.Attributes[name]; attr != nil {
|
|
// When we reach a specific attribute we can't traverse any deeper, because attributes are the leaves of the schema.
|
|
schema = nil
|
|
return traverseAttr(bodies, name)
|
|
} else if blockType := schema.BlockTypes[name]; blockType != nil {
|
|
// We need to take a different action here depending on
|
|
// the nesting mode of the block type. Some require us
|
|
// to traverse in two steps in order to select a specific
|
|
// child block, while others we can just step through
|
|
// directly.
|
|
switch blockType.Nesting {
|
|
case configschema.NestingSingle, configschema.NestingGroup:
|
|
// There should be only zero or one blocks of this
|
|
// type, so we can traverse in only one step.
|
|
schema = &blockType.Block
|
|
return traverseNestedBlockSingle(bodies, name)
|
|
case configschema.NestingMap, configschema.NestingList, configschema.NestingSet:
|
|
steppingThrough = blockType
|
|
return bodies, exprs // Preserve current selections for the second step
|
|
default:
|
|
// The above should be exhaustive, but just in case
|
|
// we add something new in future we'll bail out
|
|
// here and conservatively return everything under
|
|
// the current traversal point.
|
|
schema = nil
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
// We'll get here if the given name isn't in the schema at all. If so,
|
|
// there's nothing else to be done here.
|
|
schema = nil
|
|
return nil, nil
|
|
}
|
|
Steps:
|
|
for _, step := range remain {
|
|
// If we filter out all of our bodies before we finish traversing then
|
|
// we know we won't find anything else, because all of our subsequent
|
|
// traversal steps won't have any bodies to search.
|
|
if len(bodies) == 0 {
|
|
return nil
|
|
}
|
|
// If we no longer have a schema then that suggests we've
|
|
// traversed as deep as what the schema covers (e.g. we reached
|
|
// a specific attribute) and so we'll stop early, assuming that
|
|
// any remaining steps are traversals into an attribute expression
|
|
// result.
|
|
if schema == nil {
|
|
break
|
|
}
|
|
|
|
switch step := step.(type) {
|
|
|
|
case hcl.TraverseAttr:
|
|
switch {
|
|
case steppingThrough != nil:
|
|
// If we're stepping through a NestingMap block then
|
|
// it's valid to use attribute syntax to select one of
|
|
// the blocks by its label. Other nesting types require
|
|
// TraverseIndex, so can never be valid.
|
|
if steppingThrough.Nesting != configschema.NestingMap {
|
|
nextStep(nil, nil) // bail out
|
|
continue
|
|
}
|
|
nextStep(traverseNestedBlockMap(bodies, steppingThroughType, step.Name))
|
|
schema = &steppingThrough.Block
|
|
default:
|
|
nextStep(traverseInBlock(step.Name))
|
|
if schema == nil {
|
|
// traverseInBlock determined that we've traversed as
|
|
// deep as we can with reference to schema, so we'll
|
|
// stop here and just process whatever's selected.
|
|
break Steps
|
|
}
|
|
}
|
|
case hcl.TraverseIndex:
|
|
switch {
|
|
case steppingThrough != nil:
|
|
switch steppingThrough.Nesting {
|
|
case configschema.NestingMap:
|
|
keyVal, err := convert.Convert(step.Key, cty.String)
|
|
if err != nil { // Invalid traversal, so can't have any refs
|
|
nextStep(nil, nil) // bail out
|
|
continue
|
|
}
|
|
nextStep(traverseNestedBlockMap(bodies, steppingThroughType, keyVal.AsString()))
|
|
schema = &steppingThrough.Block
|
|
case configschema.NestingList:
|
|
idxVal, err := convert.Convert(step.Key, cty.Number)
|
|
if err != nil { // Invalid traversal, so can't have any refs
|
|
nextStep(nil, nil) // bail out
|
|
continue
|
|
}
|
|
var idx int
|
|
err = gocty.FromCtyValue(idxVal, &idx)
|
|
if err != nil { // Invalid traversal, so can't have any refs
|
|
nextStep(nil, nil) // bail out
|
|
continue
|
|
}
|
|
nextStep(traverseNestedBlockList(bodies, steppingThroughType, idx))
|
|
schema = &steppingThrough.Block
|
|
default:
|
|
// Note that NestingSet ends up in here because we don't
|
|
// actually allow traversing into set-backed block types,
|
|
// and so such a reference would be invalid.
|
|
nextStep(nil, nil) // bail out
|
|
continue
|
|
}
|
|
default:
|
|
// When indexing the contents of a block directly we always
|
|
// interpret the key as a string representing an attribute
|
|
// name.
|
|
nameVal, err := convert.Convert(step.Key, cty.String)
|
|
if err != nil { // Invalid traversal, so can't have any refs
|
|
nextStep(nil, nil) // bail out
|
|
continue
|
|
}
|
|
nextStep(traverseInBlock(nameVal.AsString()))
|
|
if schema == nil {
|
|
// traverseInBlock determined that we've traversed as
|
|
// deep as we can with reference to schema, so we'll
|
|
// stop here and just process whatever's selected.
|
|
break Steps
|
|
}
|
|
}
|
|
default:
|
|
// We shouldn't get here, because the above cases are exhaustive
|
|
// for all of the relative traversal types, but we'll be robust in
|
|
// case HCL adds more in future and just pretend the traversal
|
|
// ended a bit early if so.
|
|
break Steps
|
|
}
|
|
}
|
|
|
|
if steppingThrough != nil {
|
|
// If we ended in the middle of "stepping through" then we'll conservatively
|
|
// use the bodies of _all_ nested blocks of the type we were stepping
|
|
// through, because the recipient of this value could refer to any
|
|
// of them dynamically.
|
|
var labelNames []string
|
|
if steppingThrough.Nesting == configschema.NestingMap {
|
|
labelNames = []string{"key"}
|
|
}
|
|
blocks := findBlocksInBodies(bodies, steppingThroughType, labelNames)
|
|
for _, block := range blocks {
|
|
bodies, exprs = blockParts(block)
|
|
}
|
|
}
|
|
|
|
if len(bodies) == 0 && len(exprs) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var refs []*addrs.Reference
|
|
for _, expr := range exprs {
|
|
moreRefs, _ := lang.ReferencesInExpr(expr)
|
|
refs = append(refs, moreRefs...)
|
|
}
|
|
if schema != nil {
|
|
for _, body := range bodies {
|
|
moreRefs, _ := lang.ReferencesInBlock(body, schema)
|
|
refs = append(refs, moreRefs...)
|
|
}
|
|
}
|
|
return absoluteRefs(addr.Absolute(moduleAddr), refs)
|
|
}
|
|
|
|
func traverseAttr(bodies []hcl.Body, name string) ([]hcl.Body, []hcl.Expression) {
|
|
if len(bodies) == 0 {
|
|
return nil, nil
|
|
}
|
|
schema := &hcl.BodySchema{
|
|
Attributes: []hcl.AttributeSchema{
|
|
{Name: name},
|
|
},
|
|
}
|
|
// We can find at most one expression per body, because attribute names
|
|
// are always unique within a body.
|
|
retExprs := make([]hcl.Expression, 0, len(bodies))
|
|
for _, body := range bodies {
|
|
content, _, _ := body.PartialContent(schema)
|
|
if attr := content.Attributes[name]; attr != nil && attr.Expr != nil {
|
|
retExprs = append(retExprs, attr.Expr)
|
|
}
|
|
}
|
|
return nil, retExprs
|
|
}
|
|
|
|
func traverseNestedBlockSingle(bodies []hcl.Body, typeName string) ([]hcl.Body, []hcl.Expression) {
|
|
if len(bodies) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
blocks := findBlocksInBodies(bodies, typeName, nil)
|
|
var retBodies []hcl.Body
|
|
var retExprs []hcl.Expression
|
|
for _, block := range blocks {
|
|
moreBodies, moreExprs := blockParts(block)
|
|
retBodies = append(retBodies, moreBodies...)
|
|
retExprs = append(retExprs, moreExprs...)
|
|
}
|
|
return retBodies, retExprs
|
|
}
|
|
|
|
func traverseNestedBlockMap(bodies []hcl.Body, typeName string, key string) ([]hcl.Body, []hcl.Expression) {
|
|
if len(bodies) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
blocks := findBlocksInBodies(bodies, typeName, []string{"key"})
|
|
var retBodies []hcl.Body
|
|
var retExprs []hcl.Expression
|
|
for _, block := range blocks {
|
|
switch block.Type {
|
|
case "dynamic":
|
|
// For dynamic blocks we allow the key to be chosen dynamically
|
|
// and so we'll just conservatively include all dynamic block
|
|
// bodies. However, we need to also look for references in some
|
|
// arguments of the dynamic block itself.
|
|
argExprs, contentBody := dynamicBlockParts(block.Body)
|
|
retExprs = append(retExprs, argExprs...)
|
|
if contentBody != nil {
|
|
retBodies = append(retBodies, contentBody)
|
|
}
|
|
case typeName:
|
|
if len(block.Labels) == 1 && block.Labels[0] == key && block.Body != nil {
|
|
retBodies = append(retBodies, block.Body)
|
|
}
|
|
}
|
|
}
|
|
return retBodies, retExprs
|
|
}
|
|
|
|
func traverseNestedBlockList(bodies []hcl.Body, typeName string, idx int) ([]hcl.Body, []hcl.Expression) {
|
|
if len(bodies) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
schema := &hcl.BodySchema{
|
|
Blocks: []hcl.BlockHeaderSchema{
|
|
{Type: typeName, LabelNames: nil},
|
|
{Type: "dynamic", LabelNames: []string{"type"}},
|
|
},
|
|
}
|
|
var retBodies []hcl.Body
|
|
var retExprs []hcl.Expression
|
|
for _, body := range bodies {
|
|
content, _, _ := body.PartialContent(schema)
|
|
blocks := content.Blocks
|
|
|
|
// A tricky aspect of this scenario is that if there are any "dynamic"
|
|
// blocks then we can't statically predict how many concrete blocks they
|
|
// will generate, and so consequently we can't predict the indices of
|
|
// any statically-defined blocks that might appear after them.
|
|
firstDynamic := -1 // -1 means "no dynamic blocks"
|
|
for i, block := range blocks {
|
|
if block.Type == "dynamic" {
|
|
firstDynamic = i
|
|
break
|
|
}
|
|
}
|
|
|
|
switch {
|
|
case firstDynamic >= 0 && idx >= firstDynamic:
|
|
// This is the unfortunate case where the selection could be
|
|
// any of the blocks from firstDynamic onwards, and so we
|
|
// need to conservatively include all of them in our result.
|
|
for _, block := range blocks[firstDynamic:] {
|
|
moreBodies, moreExprs := blockParts(block)
|
|
retBodies = append(retBodies, moreBodies...)
|
|
retExprs = append(retExprs, moreExprs...)
|
|
}
|
|
default:
|
|
// This is the happier case where we can select just a single
|
|
// static block based on idx. Note that this one is guaranteed
|
|
// to never be dynamic but we're using blockParts here just
|
|
// for consistency.
|
|
moreBodies, moreExprs := blockParts(blocks[idx])
|
|
retBodies = append(retBodies, moreBodies...)
|
|
retExprs = append(retExprs, moreExprs...)
|
|
}
|
|
}
|
|
|
|
return retBodies, retExprs
|
|
}
|
|
|
|
func findBlocksInBodies(bodies []hcl.Body, typeName string, labelNames []string) []*hcl.Block {
|
|
// We need to look for both static blocks of the given type, and any
|
|
// dynamic blocks whose label gives the expected type name.
|
|
schema := &hcl.BodySchema{
|
|
Blocks: []hcl.BlockHeaderSchema{
|
|
{Type: typeName, LabelNames: labelNames},
|
|
{Type: "dynamic", LabelNames: []string{"type"}},
|
|
},
|
|
}
|
|
var blocks []*hcl.Block
|
|
for _, body := range bodies {
|
|
// We ignore errors here because we'll just make a best effort to analyze
|
|
// whatever partial result HCL returns in that case.
|
|
content, _, _ := body.PartialContent(schema)
|
|
|
|
for _, block := range content.Blocks {
|
|
switch block.Type {
|
|
case "dynamic":
|
|
if len(block.Labels) != 1 { // Invalid
|
|
continue
|
|
}
|
|
if block.Labels[0] == typeName {
|
|
blocks = append(blocks, block)
|
|
}
|
|
case typeName:
|
|
blocks = append(blocks, block)
|
|
}
|
|
}
|
|
}
|
|
|
|
// NOTE: The caller still needs to check for dynamic vs. static in order
|
|
// to do further processing. The callers above all aim to encapsulate
|
|
// that.
|
|
return blocks
|
|
}
|
|
|
|
func blockParts(block *hcl.Block) ([]hcl.Body, []hcl.Expression) {
|
|
switch block.Type {
|
|
case "dynamic":
|
|
exprs, contentBody := dynamicBlockParts(block.Body)
|
|
var bodies []hcl.Body
|
|
if contentBody != nil {
|
|
bodies = []hcl.Body{contentBody}
|
|
}
|
|
return bodies, exprs
|
|
default:
|
|
if block.Body == nil {
|
|
return nil, nil
|
|
}
|
|
return []hcl.Body{block.Body}, nil
|
|
}
|
|
}
|
|
|
|
func dynamicBlockParts(body hcl.Body) ([]hcl.Expression, hcl.Body) {
|
|
if body == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
// This is a subset of the "dynamic" block schema defined by the HCL
|
|
// dynblock extension, covering only the two arguments that are allowed
|
|
// to be arbitrary expressions possibly referring elsewhere.
|
|
schema := &hcl.BodySchema{
|
|
Attributes: []hcl.AttributeSchema{
|
|
{Name: "for_each"},
|
|
{Name: "labels"},
|
|
},
|
|
Blocks: []hcl.BlockHeaderSchema{
|
|
{Type: "content"},
|
|
},
|
|
}
|
|
content, _, _ := body.PartialContent(schema)
|
|
var exprs []hcl.Expression
|
|
if len(content.Attributes) != 0 {
|
|
exprs = make([]hcl.Expression, 0, len(content.Attributes))
|
|
}
|
|
for _, attr := range content.Attributes {
|
|
if attr.Expr != nil {
|
|
exprs = append(exprs, attr.Expr)
|
|
}
|
|
}
|
|
var contentBody hcl.Body
|
|
for _, block := range content.Blocks {
|
|
if block != nil && block.Type == "content" && block.Body != nil {
|
|
contentBody = block.Body
|
|
}
|
|
}
|
|
return exprs, contentBody
|
|
}
|