github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/lang/blocktoattr/fixup.go (about)

     1  package blocktoattr
     2  
     3  import (
     4  	"github.com/hashicorp/hcl/v2"
     5  	"github.com/hashicorp/hcl/v2/hcldec"
     6  	"github.com/hashicorp/terraform-plugin-sdk/internal/configs/configschema"
     7  	"github.com/zclconf/go-cty/cty"
     8  )
     9  
    10  // FixUpBlockAttrs takes a raw HCL body and adds some additional normalization
    11  // functionality to allow attributes that are specified as having list or set
    12  // type in the schema to be written with HCL block syntax as multiple nested
    13  // blocks with the attribute name as the block type.
    14  //
    15  // This partially restores some of the block/attribute confusion from HCL 1
    16  // so that existing patterns that depended on that confusion can continue to
    17  // be used in the short term while we settle on a longer-term strategy.
    18  //
    19  // Most of the fixup work is actually done when the returned body is
    20  // subsequently decoded, so while FixUpBlockAttrs always succeeds, the eventual
    21  // decode of the body might not, if the content of the body is so ambiguous
    22  // that there's no safe way to map it to the schema.
    23  func FixUpBlockAttrs(body hcl.Body, schema *configschema.Block) hcl.Body {
    24  	// The schema should never be nil, but in practice it seems to be sometimes
    25  	// in the presence of poorly-configured test mocks, so we'll be robust
    26  	// by synthesizing an empty one.
    27  	if schema == nil {
    28  		schema = &configschema.Block{}
    29  	}
    30  
    31  	return &fixupBody{
    32  		original: body,
    33  		schema:   schema,
    34  		names:    ambiguousNames(schema),
    35  	}
    36  }
    37  
    38  type fixupBody struct {
    39  	original hcl.Body
    40  	schema   *configschema.Block
    41  	names    map[string]struct{}
    42  }
    43  
    44  // Content decodes content from the body. The given schema must be the lower-level
    45  // representation of the same schema that was previously passed to FixUpBlockAttrs,
    46  // or else the result is undefined.
    47  func (b *fixupBody) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) {
    48  	schema = b.effectiveSchema(schema)
    49  	content, diags := b.original.Content(schema)
    50  	return b.fixupContent(content), diags
    51  }
    52  
    53  func (b *fixupBody) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) {
    54  	schema = b.effectiveSchema(schema)
    55  	content, remain, diags := b.original.PartialContent(schema)
    56  	remain = &fixupBody{
    57  		original: remain,
    58  		schema:   b.schema,
    59  		names:    b.names,
    60  	}
    61  	return b.fixupContent(content), remain, diags
    62  }
    63  
    64  func (b *fixupBody) JustAttributes() (hcl.Attributes, hcl.Diagnostics) {
    65  	// FixUpBlockAttrs is not intended to be used in situations where we'd use
    66  	// JustAttributes, so we just pass this through verbatim to complete our
    67  	// implementation of hcl.Body.
    68  	return b.original.JustAttributes()
    69  }
    70  
    71  func (b *fixupBody) MissingItemRange() hcl.Range {
    72  	return b.original.MissingItemRange()
    73  }
    74  
    75  // effectiveSchema produces a derived *hcl.BodySchema by sniffing the body's
    76  // content to determine whether the author has used attribute or block syntax
    77  // for each of the ambigious attributes where both are permitted.
    78  //
    79  // The resulting schema will always contain all of the same names that are
    80  // in the given schema, but some attribute schemas may instead be replaced by
    81  // block header schemas.
    82  func (b *fixupBody) effectiveSchema(given *hcl.BodySchema) *hcl.BodySchema {
    83  	return effectiveSchema(given, b.original, b.names, true)
    84  }
    85  
    86  func (b *fixupBody) fixupContent(content *hcl.BodyContent) *hcl.BodyContent {
    87  	var ret hcl.BodyContent
    88  	ret.Attributes = make(hcl.Attributes)
    89  	for name, attr := range content.Attributes {
    90  		ret.Attributes[name] = attr
    91  	}
    92  	blockAttrVals := make(map[string][]*hcl.Block)
    93  	for _, block := range content.Blocks {
    94  		if _, exists := b.names[block.Type]; exists {
    95  			// If we get here then we've found a block type whose instances need
    96  			// to be re-interpreted as a list-of-objects attribute. We'll gather
    97  			// those up and fix them up below.
    98  			blockAttrVals[block.Type] = append(blockAttrVals[block.Type], block)
    99  			continue
   100  		}
   101  
   102  		// We need to now re-wrap our inner body so it will be subject to the
   103  		// same attribute-as-block fixup when recursively decoded.
   104  		retBlock := *block // shallow copy
   105  		if blockS, ok := b.schema.BlockTypes[block.Type]; ok {
   106  			// Would be weird if not ok, but we'll allow it for robustness; body just won't be fixed up, then
   107  			retBlock.Body = FixUpBlockAttrs(retBlock.Body, &blockS.Block)
   108  		}
   109  
   110  		ret.Blocks = append(ret.Blocks, &retBlock)
   111  	}
   112  	// No we'll install synthetic attributes for each of our fixups. We can't
   113  	// do this exactly because HCL's information model expects an attribute
   114  	// to be a single decl but we have multiple separate blocks. We'll
   115  	// approximate things, then, by using only our first block for the source
   116  	// location information. (We are guaranteed at least one by the above logic.)
   117  	for name, blocks := range blockAttrVals {
   118  		ret.Attributes[name] = &hcl.Attribute{
   119  			Name: name,
   120  			Expr: &fixupBlocksExpr{
   121  				blocks: blocks,
   122  				ety:    b.schema.Attributes[name].Type.ElementType(),
   123  			},
   124  
   125  			Range:     blocks[0].DefRange,
   126  			NameRange: blocks[0].TypeRange,
   127  		}
   128  	}
   129  	return &ret
   130  }
   131  
   132  type fixupBlocksExpr struct {
   133  	blocks hcl.Blocks
   134  	ety    cty.Type
   135  }
   136  
   137  func (e *fixupBlocksExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
   138  	// In order to produce a suitable value for our expression we need to
   139  	// now decode the whole descendent block structure under each of our block
   140  	// bodies.
   141  	//
   142  	// That requires us to do something rather strange: we must construct a
   143  	// synthetic block type schema derived from the element type of the
   144  	// attribute, thus inverting our usual direction of lowering a schema
   145  	// into an implied type. Because a type is less detailed than a schema,
   146  	// the result is imprecise and in particular will just consider all
   147  	// the attributes to be optional and let the provider eventually decide
   148  	// whether to return errors if they turn out to be null when required.
   149  	schema := SchemaForCtyElementType(e.ety) // this schema's ImpliedType will match e.ety
   150  	spec := schema.DecoderSpec()
   151  
   152  	vals := make([]cty.Value, len(e.blocks))
   153  	var diags hcl.Diagnostics
   154  	for i, block := range e.blocks {
   155  		body := FixUpBlockAttrs(block.Body, schema)
   156  		val, blockDiags := hcldec.Decode(body, spec, ctx)
   157  		diags = append(diags, blockDiags...)
   158  		if val == cty.NilVal {
   159  			val = cty.UnknownVal(e.ety)
   160  		}
   161  		vals[i] = val
   162  	}
   163  	if len(vals) == 0 {
   164  		return cty.ListValEmpty(e.ety), diags
   165  	}
   166  	return cty.ListVal(vals), diags
   167  }
   168  
   169  func (e *fixupBlocksExpr) Variables() []hcl.Traversal {
   170  	var ret []hcl.Traversal
   171  	schema := SchemaForCtyElementType(e.ety)
   172  	spec := schema.DecoderSpec()
   173  	for _, block := range e.blocks {
   174  		ret = append(ret, hcldec.Variables(block.Body, spec)...)
   175  	}
   176  	return ret
   177  }
   178  
   179  func (e *fixupBlocksExpr) Range() hcl.Range {
   180  	// This is not really an appropriate range for the expression but it's
   181  	// the best we can do from here.
   182  	return e.blocks[0].DefRange
   183  }
   184  
   185  func (e *fixupBlocksExpr) StartRange() hcl.Range {
   186  	return e.blocks[0].DefRange
   187  }