github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/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/iaas-resource-provision/iaas-rpc/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  type unknownBlock interface {
    45  	Unknown() bool
    46  }
    47  
    48  func (b *fixupBody) Unknown() bool {
    49  	if u, ok := b.original.(unknownBlock); ok {
    50  		return u.Unknown()
    51  	}
    52  	return false
    53  }
    54  
    55  // Content decodes content from the body. The given schema must be the lower-level
    56  // representation of the same schema that was previously passed to FixUpBlockAttrs,
    57  // or else the result is undefined.
    58  func (b *fixupBody) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) {
    59  	schema = b.effectiveSchema(schema)
    60  	content, diags := b.original.Content(schema)
    61  	return b.fixupContent(content), diags
    62  }
    63  
    64  func (b *fixupBody) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) {
    65  	schema = b.effectiveSchema(schema)
    66  	content, remain, diags := b.original.PartialContent(schema)
    67  	remain = &fixupBody{
    68  		original: remain,
    69  		schema:   b.schema,
    70  		names:    b.names,
    71  	}
    72  	return b.fixupContent(content), remain, diags
    73  }
    74  
    75  func (b *fixupBody) JustAttributes() (hcl.Attributes, hcl.Diagnostics) {
    76  	// FixUpBlockAttrs is not intended to be used in situations where we'd use
    77  	// JustAttributes, so we just pass this through verbatim to complete our
    78  	// implementation of hcl.Body.
    79  	return b.original.JustAttributes()
    80  }
    81  
    82  func (b *fixupBody) MissingItemRange() hcl.Range {
    83  	return b.original.MissingItemRange()
    84  }
    85  
    86  // effectiveSchema produces a derived *hcl.BodySchema by sniffing the body's
    87  // content to determine whether the author has used attribute or block syntax
    88  // for each of the ambigious attributes where both are permitted.
    89  //
    90  // The resulting schema will always contain all of the same names that are
    91  // in the given schema, but some attribute schemas may instead be replaced by
    92  // block header schemas.
    93  func (b *fixupBody) effectiveSchema(given *hcl.BodySchema) *hcl.BodySchema {
    94  	return effectiveSchema(given, b.original, b.names, true)
    95  }
    96  
    97  func (b *fixupBody) fixupContent(content *hcl.BodyContent) *hcl.BodyContent {
    98  	var ret hcl.BodyContent
    99  	ret.Attributes = make(hcl.Attributes)
   100  	for name, attr := range content.Attributes {
   101  		ret.Attributes[name] = attr
   102  	}
   103  	blockAttrVals := make(map[string][]*hcl.Block)
   104  	for _, block := range content.Blocks {
   105  		if _, exists := b.names[block.Type]; exists {
   106  			// If we get here then we've found a block type whose instances need
   107  			// to be re-interpreted as a list-of-objects attribute. We'll gather
   108  			// those up and fix them up below.
   109  			blockAttrVals[block.Type] = append(blockAttrVals[block.Type], block)
   110  			continue
   111  		}
   112  
   113  		// We need to now re-wrap our inner body so it will be subject to the
   114  		// same attribute-as-block fixup when recursively decoded.
   115  		retBlock := *block // shallow copy
   116  		if blockS, ok := b.schema.BlockTypes[block.Type]; ok {
   117  			// Would be weird if not ok, but we'll allow it for robustness; body just won't be fixed up, then
   118  			retBlock.Body = FixUpBlockAttrs(retBlock.Body, &blockS.Block)
   119  		}
   120  
   121  		ret.Blocks = append(ret.Blocks, &retBlock)
   122  	}
   123  	// No we'll install synthetic attributes for each of our fixups. We can't
   124  	// do this exactly because HCL's information model expects an attribute
   125  	// to be a single decl but we have multiple separate blocks. We'll
   126  	// approximate things, then, by using only our first block for the source
   127  	// location information. (We are guaranteed at least one by the above logic.)
   128  	for name, blocks := range blockAttrVals {
   129  		ret.Attributes[name] = &hcl.Attribute{
   130  			Name: name,
   131  			Expr: &fixupBlocksExpr{
   132  				blocks: blocks,
   133  				ety:    b.schema.Attributes[name].Type.ElementType(),
   134  			},
   135  
   136  			Range:     blocks[0].DefRange,
   137  			NameRange: blocks[0].TypeRange,
   138  		}
   139  	}
   140  
   141  	ret.MissingItemRange = b.MissingItemRange()
   142  	return &ret
   143  }
   144  
   145  type fixupBlocksExpr struct {
   146  	blocks hcl.Blocks
   147  	ety    cty.Type
   148  }
   149  
   150  func (e *fixupBlocksExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
   151  	// In order to produce a suitable value for our expression we need to
   152  	// now decode the whole descendent block structure under each of our block
   153  	// bodies.
   154  	//
   155  	// That requires us to do something rather strange: we must construct a
   156  	// synthetic block type schema derived from the element type of the
   157  	// attribute, thus inverting our usual direction of lowering a schema
   158  	// into an implied type. Because a type is less detailed than a schema,
   159  	// the result is imprecise and in particular will just consider all
   160  	// the attributes to be optional and let the provider eventually decide
   161  	// whether to return errors if they turn out to be null when required.
   162  	schema := SchemaForCtyElementType(e.ety) // this schema's ImpliedType will match e.ety
   163  	spec := schema.DecoderSpec()
   164  
   165  	vals := make([]cty.Value, len(e.blocks))
   166  	var diags hcl.Diagnostics
   167  	for i, block := range e.blocks {
   168  		body := FixUpBlockAttrs(block.Body, schema)
   169  		val, blockDiags := hcldec.Decode(body, spec, ctx)
   170  		diags = append(diags, blockDiags...)
   171  		if val == cty.NilVal {
   172  			val = cty.UnknownVal(e.ety)
   173  		}
   174  		vals[i] = val
   175  	}
   176  	if len(vals) == 0 {
   177  		return cty.ListValEmpty(e.ety), diags
   178  	}
   179  	return cty.ListVal(vals), diags
   180  }
   181  
   182  func (e *fixupBlocksExpr) Variables() []hcl.Traversal {
   183  	var ret []hcl.Traversal
   184  	schema := SchemaForCtyElementType(e.ety)
   185  	spec := schema.DecoderSpec()
   186  	for _, block := range e.blocks {
   187  		ret = append(ret, hcldec.Variables(block.Body, spec)...)
   188  	}
   189  	return ret
   190  }
   191  
   192  func (e *fixupBlocksExpr) Range() hcl.Range {
   193  	// This is not really an appropriate range for the expression but it's
   194  	// the best we can do from here.
   195  	return e.blocks[0].DefRange
   196  }
   197  
   198  func (e *fixupBlocksExpr) StartRange() hcl.Range {
   199  	return e.blocks[0].DefRange
   200  }