github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/internal/lang/blocktoattr/fixup.go (about)

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