github.com/terraform-linters/tflint@v0.51.2-0.20240520175844-3750771571b6/terraform/tfhcl/expand_body.go (about)

     1  package tfhcl
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/hashicorp/hcl/v2"
     7  	"github.com/terraform-linters/tflint-plugin-sdk/hclext"
     8  	"github.com/zclconf/go-cty/cty"
     9  )
    10  
    11  // expandBody wraps another hcl.Body and expands any "dynamic" blocks, count/for-each
    12  // resources found inside whenever Content or PartialContent is called.
    13  type expandBody struct {
    14  	original         hcl.Body
    15  	ctx              *hcl.EvalContext
    16  	dynamicIteration *dynamicIteration // non-nil if we're nested inside a "dynamic" block
    17  	metaArgIteration *metaArgIteration // non-nil if we're nested inside a block with meta-arguments
    18  
    19  	// These are used with PartialContent to produce a "remaining items"
    20  	// body to return. They are nil on all bodies fresh out of the transformer.
    21  	//
    22  	// Note that this is re-implemented here rather than delegating to the
    23  	// existing support required by the underlying body because we need to
    24  	// retain access to the entire original body on subsequent decode operations
    25  	// so we can retain any "dynamic" blocks for types we didn't take consume
    26  	// on the first pass.
    27  	hiddenAttrs  map[string]struct{}
    28  	hiddenBlocks map[string]hcl.BlockHeaderSchema
    29  }
    30  
    31  func (b *expandBody) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) {
    32  	extSchema := b.extendSchema(schema)
    33  	rawContent, diags := b.original.Content(extSchema)
    34  
    35  	blocks, blockDiags := b.expandBlocks(schema, rawContent.Blocks, false)
    36  	diags = append(diags, blockDiags...)
    37  	attrs, attrDiags := b.prepareAttributes(rawContent.Attributes)
    38  	diags = append(diags, attrDiags...)
    39  
    40  	content := &hcl.BodyContent{
    41  		Attributes:       attrs,
    42  		Blocks:           blocks,
    43  		MissingItemRange: b.original.MissingItemRange(),
    44  	}
    45  
    46  	return content, diags
    47  }
    48  
    49  func (b *expandBody) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) {
    50  	extSchema := b.extendSchema(schema)
    51  	rawContent, _, diags := b.original.PartialContent(extSchema)
    52  	// We discard the "remain" argument above because we're going to construct
    53  	// our own remain that also takes into account remaining "dynamic" blocks.
    54  
    55  	blocks, blockDiags := b.expandBlocks(schema, rawContent.Blocks, true)
    56  	diags = append(diags, blockDiags...)
    57  	attrs, attrDiags := b.prepareAttributes(rawContent.Attributes)
    58  	diags = append(diags, attrDiags...)
    59  
    60  	content := &hcl.BodyContent{
    61  		Attributes:       attrs,
    62  		Blocks:           blocks,
    63  		MissingItemRange: b.original.MissingItemRange(),
    64  	}
    65  
    66  	remain := &expandBody{
    67  		original:         b.original,
    68  		ctx:              b.ctx,
    69  		dynamicIteration: b.dynamicIteration,
    70  		metaArgIteration: b.metaArgIteration,
    71  		hiddenAttrs:      make(map[string]struct{}),
    72  		hiddenBlocks:     make(map[string]hcl.BlockHeaderSchema),
    73  	}
    74  	for name := range b.hiddenAttrs {
    75  		remain.hiddenAttrs[name] = struct{}{}
    76  	}
    77  	for typeName, blockS := range b.hiddenBlocks {
    78  		remain.hiddenBlocks[typeName] = blockS
    79  	}
    80  	for _, attrS := range schema.Attributes {
    81  		remain.hiddenAttrs[attrS.Name] = struct{}{}
    82  	}
    83  	for _, blockS := range schema.Blocks {
    84  		remain.hiddenBlocks[blockS.Type] = blockS
    85  	}
    86  
    87  	return content, remain, diags
    88  }
    89  
    90  func (b *expandBody) extendSchema(schema *hcl.BodySchema) *hcl.BodySchema {
    91  	// We augment the requested schema to also include our special "dynamic"
    92  	// block type, since then we'll get instances of it interleaved with
    93  	// all of the literal child blocks we must also include.
    94  	extSchema := &hcl.BodySchema{
    95  		Attributes: schema.Attributes,
    96  		Blocks:     make([]hcl.BlockHeaderSchema, len(schema.Blocks), len(schema.Blocks)+len(b.hiddenBlocks)+1),
    97  	}
    98  	copy(extSchema.Blocks, schema.Blocks)
    99  	extSchema.Blocks = append(extSchema.Blocks, dynamicBlockHeaderSchema)
   100  
   101  	// If we have any hiddenBlocks then we also need to register those here
   102  	// so that a call to "Content" on the underlying body won't fail.
   103  	// (We'll filter these out again once we process the result of either
   104  	// Content or PartialContent.)
   105  	for _, blockS := range b.hiddenBlocks {
   106  		extSchema.Blocks = append(extSchema.Blocks, blockS)
   107  	}
   108  
   109  	// If we have any hiddenAttrs then we also need to register these, for
   110  	// the same reason as we deal with hiddenBlocks above.
   111  	if len(b.hiddenAttrs) != 0 {
   112  		newAttrs := make([]hcl.AttributeSchema, len(schema.Attributes), len(schema.Attributes)+len(b.hiddenAttrs))
   113  		copy(newAttrs, extSchema.Attributes)
   114  		for name := range b.hiddenAttrs {
   115  			newAttrs = append(newAttrs, hcl.AttributeSchema{
   116  				Name:     name,
   117  				Required: false,
   118  			})
   119  		}
   120  		extSchema.Attributes = newAttrs
   121  	}
   122  
   123  	return extSchema
   124  }
   125  
   126  func (b *expandBody) prepareAttributes(rawAttrs hcl.Attributes) (hcl.Attributes, hcl.Diagnostics) {
   127  	var diags hcl.Diagnostics
   128  
   129  	if len(b.hiddenAttrs) == 0 && b.dynamicIteration == nil && b.metaArgIteration == nil {
   130  		// Easy path: just pass through the attrs from the original body verbatim
   131  		return rawAttrs, diags
   132  	}
   133  
   134  	// Otherwise we have some work to do: we must filter out any attributes
   135  	// that are hidden (since a previous PartialContent call already saw these)
   136  	// and wrap the expressions of the inner attributes so that they will
   137  	// have access to our iteration variables.
   138  	attrs := make(hcl.Attributes, len(rawAttrs))
   139  	for name, rawAttr := range rawAttrs {
   140  		if _, hidden := b.hiddenAttrs[name]; hidden {
   141  			continue
   142  		}
   143  		if b.dynamicIteration != nil || b.metaArgIteration != nil {
   144  			attr := *rawAttr // shallow copy so we can mutate it
   145  			expr := exprWrap{
   146  				Expression: attr.Expr,
   147  				di:         b.dynamicIteration,
   148  				mi:         b.metaArgIteration,
   149  			}
   150  			// Unlike hcl/ext/dynblock, wrapped expressions are evaluated immediately.
   151  			// The result is bound to the expression and can be accessed without
   152  			// the iterator context.
   153  			val, evalDiags := expr.Value(b.ctx)
   154  			if evalDiags.HasErrors() {
   155  				diags = append(diags, evalDiags...)
   156  				continue
   157  			}
   158  			// Marked values (e.g. sensitive values) are unbound for serialization.
   159  			if !val.ContainsMarked() {
   160  				attr.Expr = hclext.BindValue(val, expr)
   161  			}
   162  			attrs[name] = &attr
   163  		} else {
   164  			// If we have no active iteration then no wrapping is required.
   165  			attrs[name] = rawAttr
   166  		}
   167  	}
   168  	return attrs, diags
   169  }
   170  
   171  func (b *expandBody) expandBlocks(schema *hcl.BodySchema, rawBlocks hcl.Blocks, partial bool) (hcl.Blocks, hcl.Diagnostics) {
   172  	var blocks hcl.Blocks
   173  	var diags hcl.Diagnostics
   174  
   175  	for _, rawBlock := range rawBlocks {
   176  		switch rawBlock.Type {
   177  		case "dynamic":
   178  			expandedBlocks, expandDiags := b.expandDynamicBlock(schema, rawBlock, partial)
   179  			blocks = append(blocks, expandedBlocks...)
   180  			diags = append(diags, expandDiags...)
   181  
   182  		case "resource", "module":
   183  			expandedBlocks, expandDiags := b.expandMetaArgBlock(schema, rawBlock)
   184  			blocks = append(blocks, expandedBlocks...)
   185  			diags = append(diags, expandDiags...)
   186  
   187  		default:
   188  			if _, hidden := b.hiddenBlocks[rawBlock.Type]; !hidden {
   189  				blocks = append(blocks, b.expandStaticBlock(rawBlock))
   190  			}
   191  		}
   192  	}
   193  
   194  	return blocks, diags
   195  }
   196  
   197  func (b *expandBody) expandDynamicBlock(schema *hcl.BodySchema, rawBlock *hcl.Block, partial bool) (hcl.Blocks, hcl.Diagnostics) {
   198  	var diags hcl.Diagnostics
   199  
   200  	realBlockType := rawBlock.Labels[0]
   201  	if _, hidden := b.hiddenBlocks[realBlockType]; hidden {
   202  		return hcl.Blocks{}, diags
   203  	}
   204  
   205  	var blockS *hcl.BlockHeaderSchema
   206  	for _, candidate := range schema.Blocks {
   207  		if candidate.Type == realBlockType {
   208  			blockS = &candidate
   209  			break
   210  		}
   211  	}
   212  	if blockS == nil {
   213  		// Not a block type that the caller requested.
   214  		if !partial {
   215  			diags = append(diags, &hcl.Diagnostic{
   216  				Severity: hcl.DiagError,
   217  				Summary:  "Unsupported block type",
   218  				Detail:   fmt.Sprintf("Blocks of type %q are not expected here.", realBlockType),
   219  				Subject:  &rawBlock.LabelRanges[0],
   220  			})
   221  		}
   222  		return hcl.Blocks{}, diags
   223  	}
   224  
   225  	spec, specDiags := b.decodeDynamicSpec(blockS, rawBlock)
   226  	diags = append(diags, specDiags...)
   227  	if specDiags.HasErrors() {
   228  		return hcl.Blocks{}, diags
   229  	}
   230  
   231  	if !spec.forEachVal.IsKnown() {
   232  		// If for_each is unknown, no blocks are returned
   233  		return hcl.Blocks{}, diags
   234  	}
   235  
   236  	var blocks hcl.Blocks
   237  
   238  	for it := spec.forEachVal.ElementIterator(); it.Next(); {
   239  		key, value := it.Element()
   240  		i := b.dynamicIteration.MakeChild(spec.iteratorName, key, value)
   241  
   242  		block, blockDiags := spec.newBlock(i, b.ctx)
   243  		diags = append(diags, blockDiags...)
   244  		if block != nil {
   245  			// Attach our new iteration context so that attributes
   246  			// and other nested blocks can refer to our iterator.
   247  			block.Body = b.expandChild(block.Body, i, b.metaArgIteration)
   248  			blocks = append(blocks, block)
   249  		}
   250  	}
   251  	return blocks, diags
   252  }
   253  
   254  func (b *expandBody) expandMetaArgBlock(schema *hcl.BodySchema, rawBlock *hcl.Block) (hcl.Blocks, hcl.Diagnostics) {
   255  	var diags hcl.Diagnostics
   256  
   257  	if _, hidden := b.hiddenBlocks[rawBlock.Type]; hidden {
   258  		return hcl.Blocks{}, diags
   259  	}
   260  
   261  	spec, specDiags := b.decodeMetaArgSpec(rawBlock)
   262  	diags = append(diags, specDiags...)
   263  	if specDiags.HasErrors() {
   264  		return hcl.Blocks{}, diags
   265  	}
   266  
   267  	//// count attribute
   268  
   269  	if spec.countSet {
   270  		if !spec.countVal.IsKnown() {
   271  			// If count is unknown, no blocks are returned
   272  			return hcl.Blocks{}, diags
   273  		}
   274  
   275  		var blocks hcl.Blocks
   276  
   277  		for idx := 0; idx < spec.countNum; idx++ {
   278  			i := MakeCountIteration(cty.NumberIntVal(int64(idx)))
   279  
   280  			expandedBlock := *rawBlock // shallow copy
   281  			expandedBlock.Body = b.expandChild(rawBlock.Body, b.dynamicIteration, i)
   282  			blocks = append(blocks, &expandedBlock)
   283  		}
   284  
   285  		return blocks, diags
   286  	}
   287  
   288  	//// for_each attribute
   289  
   290  	if spec.forEachSet {
   291  		if !spec.forEachVal.IsKnown() {
   292  			// If for_each is unknown, no blocks are returned
   293  			return hcl.Blocks{}, diags
   294  		}
   295  
   296  		var blocks hcl.Blocks
   297  
   298  		for it := spec.forEachVal.ElementIterator(); it.Next(); {
   299  			i := MakeForEachIteration(it.Element())
   300  
   301  			expandedBlock := *rawBlock // shallow copy
   302  			expandedBlock.Body = b.expandChild(rawBlock.Body, b.dynamicIteration, i)
   303  			blocks = append(blocks, &expandedBlock)
   304  		}
   305  
   306  		return blocks, diags
   307  	}
   308  
   309  	//// Neither count/for_each
   310  
   311  	return hcl.Blocks{b.expandStaticBlock(rawBlock)}, diags
   312  }
   313  
   314  func (b *expandBody) expandStaticBlock(rawBlock *hcl.Block) *hcl.Block {
   315  	// A static block doesn't create a new iteration context, but
   316  	// it does need to inherit _our own_ iteration context in
   317  	// case it contains expressions that refer to our inherited
   318  	// iterators, or nested "dynamic" blocks.
   319  	expandedBlock := *rawBlock
   320  	expandedBlock.Body = b.expandChild(rawBlock.Body, b.dynamicIteration, b.metaArgIteration)
   321  	return &expandedBlock
   322  }
   323  
   324  func (b *expandBody) expandChild(child hcl.Body, i *dynamicIteration, mi *metaArgIteration) hcl.Body {
   325  	chiCtx := i.EvalContext(mi.EvalContext(b.ctx))
   326  	ret := Expand(child, chiCtx)
   327  	ret.(*expandBody).dynamicIteration = i
   328  	ret.(*expandBody).metaArgIteration = mi
   329  	return ret
   330  }
   331  
   332  func (b *expandBody) JustAttributes() (hcl.Attributes, hcl.Diagnostics) {
   333  	// blocks aren't allowed in JustAttributes mode and this body can
   334  	// only produce blocks, so we'll just pass straight through to our
   335  	// underlying body here.
   336  	return b.original.JustAttributes()
   337  }
   338  
   339  func (b *expandBody) MissingItemRange() hcl.Range {
   340  	return b.original.MissingItemRange()
   341  }