github.com/opentofu/opentofu@v1.7.1/internal/lang/blocktoattr/fixup.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package blocktoattr
     7  
     8  import (
     9  	"log"
    10  
    11  	"github.com/hashicorp/hcl/v2"
    12  	"github.com/hashicorp/hcl/v2/hcldec"
    13  	"github.com/opentofu/opentofu/internal/configs/configschema"
    14  	"github.com/zclconf/go-cty/cty"
    15  )
    16  
    17  // FixUpBlockAttrs takes a raw HCL body and adds some additional normalization
    18  // functionality to allow attributes that are specified as having list or set
    19  // type in the schema to be written with HCL block syntax as multiple nested
    20  // blocks with the attribute name as the block type.
    21  //
    22  // The fixup is only applied in the absence of structural attribute types. The
    23  // presence of these types indicate the use of a provider which does not
    24  // support mapping blocks to attributes.
    25  //
    26  // This partially restores some of the block/attribute confusion from HCL 1
    27  // so that existing patterns that depended on that confusion can continue to
    28  // be used in the short term while we settle on a longer-term strategy.
    29  //
    30  // Most of the fixup work is actually done when the returned body is
    31  // subsequently decoded, so while FixUpBlockAttrs always succeeds, the eventual
    32  // decode of the body might not, if the content of the body is so ambiguous
    33  // that there's no safe way to map it to the schema.
    34  func FixUpBlockAttrs(body hcl.Body, schema *configschema.Block) hcl.Body {
    35  	// The schema should never be nil, but in practice it seems to be sometimes
    36  	// in the presence of poorly-configured test mocks, so we'll be robust
    37  	// by synthesizing an empty one.
    38  	if schema == nil {
    39  		schema = &configschema.Block{}
    40  	}
    41  
    42  	if skipFixup(schema) {
    43  		// we don't have any context for the resource name or type, but
    44  		// hopefully this could help locate the evaluation in the logs if there
    45  		// were a problem
    46  		log.Println("[DEBUG] skipping FixUpBlockAttrs")
    47  		return body
    48  	}
    49  
    50  	return &fixupBody{
    51  		original: body,
    52  		schema:   schema,
    53  		names:    ambiguousNames(schema),
    54  	}
    55  }
    56  
    57  // skipFixup detects any use of Attribute.NestedType, or Types which could not
    58  // be generate by the legacy SDK when taking SchemaConfigModeAttr into account.
    59  func skipFixup(schema *configschema.Block) bool {
    60  	for _, attr := range schema.Attributes {
    61  		if attr.NestedType != nil {
    62  			return true
    63  		}
    64  		ty := attr.Type
    65  
    66  		// Lists and sets of objects could be generated by
    67  		// SchemaConfigModeAttr, but some other combinations can be ruled out.
    68  
    69  		// Tuples and objects could not be generated at all.
    70  		if ty.IsTupleType() || ty.IsObjectType() {
    71  			return true
    72  		}
    73  
    74  		// A map of objects was not possible.
    75  		if ty.IsMapType() && ty.ElementType().IsObjectType() {
    76  			return true
    77  		}
    78  
    79  		// Nested collections were not really supported, but could be generated
    80  		// with string types (though we conservatively limit this to primitive types)
    81  		if ty.IsCollectionType() {
    82  			ety := ty.ElementType()
    83  			if ety.IsCollectionType() && !ety.ElementType().IsPrimitiveType() {
    84  				return true
    85  			}
    86  		}
    87  	}
    88  
    89  	for _, block := range schema.BlockTypes {
    90  		if skipFixup(&block.Block) {
    91  			return true
    92  		}
    93  	}
    94  
    95  	return false
    96  }
    97  
    98  type fixupBody struct {
    99  	original hcl.Body
   100  	schema   *configschema.Block
   101  	names    map[string]struct{}
   102  }
   103  
   104  type unknownBlock interface {
   105  	Unknown() bool
   106  }
   107  
   108  func (b *fixupBody) Unknown() bool {
   109  	if u, ok := b.original.(unknownBlock); ok {
   110  		return u.Unknown()
   111  	}
   112  	return false
   113  }
   114  
   115  // Content decodes content from the body. The given schema must be the lower-level
   116  // representation of the same schema that was previously passed to FixUpBlockAttrs,
   117  // or else the result is undefined.
   118  func (b *fixupBody) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) {
   119  	schema = b.effectiveSchema(schema)
   120  	content, diags := b.original.Content(schema)
   121  	return b.fixupContent(content), diags
   122  }
   123  
   124  func (b *fixupBody) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) {
   125  	schema = b.effectiveSchema(schema)
   126  	content, remain, diags := b.original.PartialContent(schema)
   127  	remain = &fixupBody{
   128  		original: remain,
   129  		schema:   b.schema,
   130  		names:    b.names,
   131  	}
   132  	return b.fixupContent(content), remain, diags
   133  }
   134  
   135  func (b *fixupBody) JustAttributes() (hcl.Attributes, hcl.Diagnostics) {
   136  	// FixUpBlockAttrs is not intended to be used in situations where we'd use
   137  	// JustAttributes, so we just pass this through verbatim to complete our
   138  	// implementation of hcl.Body.
   139  	return b.original.JustAttributes()
   140  }
   141  
   142  func (b *fixupBody) MissingItemRange() hcl.Range {
   143  	return b.original.MissingItemRange()
   144  }
   145  
   146  // effectiveSchema produces a derived *hcl.BodySchema by sniffing the body's
   147  // content to determine whether the author has used attribute or block syntax
   148  // for each of the ambigious attributes where both are permitted.
   149  //
   150  // The resulting schema will always contain all of the same names that are
   151  // in the given schema, but some attribute schemas may instead be replaced by
   152  // block header schemas.
   153  func (b *fixupBody) effectiveSchema(given *hcl.BodySchema) *hcl.BodySchema {
   154  	return effectiveSchema(given, b.original, b.names, true)
   155  }
   156  
   157  func (b *fixupBody) fixupContent(content *hcl.BodyContent) *hcl.BodyContent {
   158  	var ret hcl.BodyContent
   159  	ret.Attributes = make(hcl.Attributes)
   160  	for name, attr := range content.Attributes {
   161  		ret.Attributes[name] = attr
   162  	}
   163  	blockAttrVals := make(map[string][]*hcl.Block)
   164  	for _, block := range content.Blocks {
   165  		if _, exists := b.names[block.Type]; exists {
   166  			// If we get here then we've found a block type whose instances need
   167  			// to be re-interpreted as a list-of-objects attribute. We'll gather
   168  			// those up and fix them up below.
   169  			blockAttrVals[block.Type] = append(blockAttrVals[block.Type], block)
   170  			continue
   171  		}
   172  
   173  		// We need to now re-wrap our inner body so it will be subject to the
   174  		// same attribute-as-block fixup when recursively decoded.
   175  		retBlock := *block // shallow copy
   176  		if blockS, ok := b.schema.BlockTypes[block.Type]; ok {
   177  			// Would be weird if not ok, but we'll allow it for robustness; body just won't be fixed up, then
   178  			retBlock.Body = FixUpBlockAttrs(retBlock.Body, &blockS.Block)
   179  		}
   180  
   181  		ret.Blocks = append(ret.Blocks, &retBlock)
   182  	}
   183  	// No we'll install synthetic attributes for each of our fixups. We can't
   184  	// do this exactly because HCL's information model expects an attribute
   185  	// to be a single decl but we have multiple separate blocks. We'll
   186  	// approximate things, then, by using only our first block for the source
   187  	// location information. (We are guaranteed at least one by the above logic.)
   188  	for name, blocks := range blockAttrVals {
   189  		ret.Attributes[name] = &hcl.Attribute{
   190  			Name: name,
   191  			Expr: &fixupBlocksExpr{
   192  				blocks: blocks,
   193  				ety:    b.schema.Attributes[name].Type.ElementType(),
   194  			},
   195  
   196  			Range:     blocks[0].DefRange,
   197  			NameRange: blocks[0].TypeRange,
   198  		}
   199  	}
   200  
   201  	ret.MissingItemRange = b.MissingItemRange()
   202  	return &ret
   203  }
   204  
   205  type fixupBlocksExpr struct {
   206  	blocks hcl.Blocks
   207  	ety    cty.Type
   208  }
   209  
   210  func (e *fixupBlocksExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
   211  	// In order to produce a suitable value for our expression we need to
   212  	// now decode the whole descendent block structure under each of our block
   213  	// bodies.
   214  	//
   215  	// That requires us to do something rather strange: we must construct a
   216  	// synthetic block type schema derived from the element type of the
   217  	// attribute, thus inverting our usual direction of lowering a schema
   218  	// into an implied type. Because a type is less detailed than a schema,
   219  	// the result is imprecise and in particular will just consider all
   220  	// the attributes to be optional and let the provider eventually decide
   221  	// whether to return errors if they turn out to be null when required.
   222  	schema := SchemaForCtyElementType(e.ety) // this schema's ImpliedType will match e.ety
   223  	spec := schema.DecoderSpec()
   224  
   225  	vals := make([]cty.Value, len(e.blocks))
   226  	var diags hcl.Diagnostics
   227  	for i, block := range e.blocks {
   228  		body := FixUpBlockAttrs(block.Body, schema)
   229  		val, blockDiags := hcldec.Decode(body, spec, ctx)
   230  		diags = append(diags, blockDiags...)
   231  		if val == cty.NilVal {
   232  			val = cty.UnknownVal(e.ety)
   233  		}
   234  		vals[i] = val
   235  	}
   236  	if len(vals) == 0 {
   237  		return cty.ListValEmpty(e.ety), diags
   238  	}
   239  	return cty.ListVal(vals), diags
   240  }
   241  
   242  func (e *fixupBlocksExpr) Variables() []hcl.Traversal {
   243  	var ret []hcl.Traversal
   244  	schema := SchemaForCtyElementType(e.ety)
   245  	spec := schema.DecoderSpec()
   246  	for _, block := range e.blocks {
   247  		ret = append(ret, hcldec.Variables(block.Body, spec)...)
   248  	}
   249  	return ret
   250  }
   251  
   252  func (e *fixupBlocksExpr) Functions() []hcl.Traversal {
   253  	var ret []hcl.Traversal
   254  	schema := SchemaForCtyElementType(e.ety)
   255  	spec := schema.DecoderSpec()
   256  	for _, block := range e.blocks {
   257  		ret = append(ret, hcldec.Functions(block.Body, spec)...)
   258  	}
   259  	return ret
   260  }
   261  
   262  func (e *fixupBlocksExpr) Range() hcl.Range {
   263  	// This is not really an appropriate range for the expression but it's
   264  	// the best we can do from here.
   265  	return e.blocks[0].DefRange
   266  }
   267  
   268  func (e *fixupBlocksExpr) StartRange() hcl.Range {
   269  	return e.blocks[0].DefRange
   270  }