github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/lang/blocktoattr/fixup.go (about)

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