| package blocktoattr |
| |
| import ( |
| "log" |
| |
| "github.com/hashicorp/hcl/v2" |
| "github.com/hashicorp/hcl/v2/hcldec" |
| "github.com/hashicorp/terraform/internal/configs/configschema" |
| "github.com/zclconf/go-cty/cty" |
| ) |
| |
| // FixUpBlockAttrs takes a raw HCL body and adds some additional normalization |
| // functionality to allow attributes that are specified as having list or set |
| // type in the schema to be written with HCL block syntax as multiple nested |
| // blocks with the attribute name as the block type. |
| // |
| // The fixup is only applied in the absence of structural attribute types. The |
| // presence of these types indicate the use of a provider which does not |
| // support mapping blocks to attributes. |
| // |
| // This partially restores some of the block/attribute confusion from HCL 1 |
| // so that existing patterns that depended on that confusion can continue to |
| // be used in the short term while we settle on a longer-term strategy. |
| // |
| // Most of the fixup work is actually done when the returned body is |
| // subsequently decoded, so while FixUpBlockAttrs always succeeds, the eventual |
| // decode of the body might not, if the content of the body is so ambiguous |
| // that there's no safe way to map it to the schema. |
| func FixUpBlockAttrs(body hcl.Body, schema *configschema.Block) hcl.Body { |
| // The schema should never be nil, but in practice it seems to be sometimes |
| // in the presence of poorly-configured test mocks, so we'll be robust |
| // by synthesizing an empty one. |
| if schema == nil { |
| schema = &configschema.Block{} |
| } |
| |
| if skipFixup(schema) { |
| // we don't have any context for the resource name or type, but |
| // hopefully this could help locate the evaluation in the logs if there |
| // were a problem |
| log.Println("[DEBUG] skipping FixUpBlockAttrs") |
| return body |
| } |
| |
| return &fixupBody{ |
| original: body, |
| schema: schema, |
| names: ambiguousNames(schema), |
| } |
| } |
| |
| // skipFixup detects any use of Attribute.NestedType, or Types which could not |
| // be generate by the legacy SDK when taking SchemaConfigModeAttr into account. |
| func skipFixup(schema *configschema.Block) bool { |
| for _, attr := range schema.Attributes { |
| if attr.NestedType != nil { |
| return true |
| } |
| ty := attr.Type |
| |
| // Lists and sets of objects could be generated by |
| // SchemaConfigModeAttr, but some other combinations can be ruled out. |
| |
| // Tuples and objects could not be generated at all. |
| if ty.IsTupleType() || ty.IsObjectType() { |
| return true |
| } |
| |
| // A map of objects was not possible. |
| if ty.IsMapType() && ty.ElementType().IsObjectType() { |
| return true |
| } |
| |
| // Nested collections were not really supported, but could be generated |
| // with string types (though we conservatively limit this to primitive types) |
| if ty.IsCollectionType() { |
| ety := ty.ElementType() |
| if ety.IsCollectionType() && !ety.ElementType().IsPrimitiveType() { |
| return true |
| } |
| } |
| } |
| |
| for _, block := range schema.BlockTypes { |
| if skipFixup(&block.Block) { |
| return true |
| } |
| } |
| |
| return false |
| } |
| |
| type fixupBody struct { |
| original hcl.Body |
| schema *configschema.Block |
| names map[string]struct{} |
| } |
| |
| type unknownBlock interface { |
| Unknown() bool |
| } |
| |
| func (b *fixupBody) Unknown() bool { |
| if u, ok := b.original.(unknownBlock); ok { |
| return u.Unknown() |
| } |
| return false |
| } |
| |
| // Content decodes content from the body. The given schema must be the lower-level |
| // representation of the same schema that was previously passed to FixUpBlockAttrs, |
| // or else the result is undefined. |
| func (b *fixupBody) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) { |
| schema = b.effectiveSchema(schema) |
| content, diags := b.original.Content(schema) |
| return b.fixupContent(content), diags |
| } |
| |
| func (b *fixupBody) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) { |
| schema = b.effectiveSchema(schema) |
| content, remain, diags := b.original.PartialContent(schema) |
| remain = &fixupBody{ |
| original: remain, |
| schema: b.schema, |
| names: b.names, |
| } |
| return b.fixupContent(content), remain, diags |
| } |
| |
| func (b *fixupBody) JustAttributes() (hcl.Attributes, hcl.Diagnostics) { |
| // FixUpBlockAttrs is not intended to be used in situations where we'd use |
| // JustAttributes, so we just pass this through verbatim to complete our |
| // implementation of hcl.Body. |
| return b.original.JustAttributes() |
| } |
| |
| func (b *fixupBody) MissingItemRange() hcl.Range { |
| return b.original.MissingItemRange() |
| } |
| |
| // effectiveSchema produces a derived *hcl.BodySchema by sniffing the body's |
| // content to determine whether the author has used attribute or block syntax |
| // for each of the ambigious attributes where both are permitted. |
| // |
| // The resulting schema will always contain all of the same names that are |
| // in the given schema, but some attribute schemas may instead be replaced by |
| // block header schemas. |
| func (b *fixupBody) effectiveSchema(given *hcl.BodySchema) *hcl.BodySchema { |
| return effectiveSchema(given, b.original, b.names, true) |
| } |
| |
| func (b *fixupBody) fixupContent(content *hcl.BodyContent) *hcl.BodyContent { |
| var ret hcl.BodyContent |
| ret.Attributes = make(hcl.Attributes) |
| for name, attr := range content.Attributes { |
| ret.Attributes[name] = attr |
| } |
| blockAttrVals := make(map[string][]*hcl.Block) |
| for _, block := range content.Blocks { |
| if _, exists := b.names[block.Type]; exists { |
| // If we get here then we've found a block type whose instances need |
| // to be re-interpreted as a list-of-objects attribute. We'll gather |
| // those up and fix them up below. |
| blockAttrVals[block.Type] = append(blockAttrVals[block.Type], block) |
| continue |
| } |
| |
| // We need to now re-wrap our inner body so it will be subject to the |
| // same attribute-as-block fixup when recursively decoded. |
| retBlock := *block // shallow copy |
| if blockS, ok := b.schema.BlockTypes[block.Type]; ok { |
| // Would be weird if not ok, but we'll allow it for robustness; body just won't be fixed up, then |
| retBlock.Body = FixUpBlockAttrs(retBlock.Body, &blockS.Block) |
| } |
| |
| ret.Blocks = append(ret.Blocks, &retBlock) |
| } |
| // No we'll install synthetic attributes for each of our fixups. We can't |
| // do this exactly because HCL's information model expects an attribute |
| // to be a single decl but we have multiple separate blocks. We'll |
| // approximate things, then, by using only our first block for the source |
| // location information. (We are guaranteed at least one by the above logic.) |
| for name, blocks := range blockAttrVals { |
| ret.Attributes[name] = &hcl.Attribute{ |
| Name: name, |
| Expr: &fixupBlocksExpr{ |
| blocks: blocks, |
| ety: b.schema.Attributes[name].Type.ElementType(), |
| }, |
| |
| Range: blocks[0].DefRange, |
| NameRange: blocks[0].TypeRange, |
| } |
| } |
| |
| ret.MissingItemRange = b.MissingItemRange() |
| return &ret |
| } |
| |
| type fixupBlocksExpr struct { |
| blocks hcl.Blocks |
| ety cty.Type |
| } |
| |
| func (e *fixupBlocksExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { |
| // In order to produce a suitable value for our expression we need to |
| // now decode the whole descendent block structure under each of our block |
| // bodies. |
| // |
| // That requires us to do something rather strange: we must construct a |
| // synthetic block type schema derived from the element type of the |
| // attribute, thus inverting our usual direction of lowering a schema |
| // into an implied type. Because a type is less detailed than a schema, |
| // the result is imprecise and in particular will just consider all |
| // the attributes to be optional and let the provider eventually decide |
| // whether to return errors if they turn out to be null when required. |
| schema := SchemaForCtyElementType(e.ety) // this schema's ImpliedType will match e.ety |
| spec := schema.DecoderSpec() |
| |
| vals := make([]cty.Value, len(e.blocks)) |
| var diags hcl.Diagnostics |
| for i, block := range e.blocks { |
| body := FixUpBlockAttrs(block.Body, schema) |
| val, blockDiags := hcldec.Decode(body, spec, ctx) |
| diags = append(diags, blockDiags...) |
| if val == cty.NilVal { |
| val = cty.UnknownVal(e.ety) |
| } |
| vals[i] = val |
| } |
| if len(vals) == 0 { |
| return cty.ListValEmpty(e.ety), diags |
| } |
| return cty.ListVal(vals), diags |
| } |
| |
| func (e *fixupBlocksExpr) Variables() []hcl.Traversal { |
| var ret []hcl.Traversal |
| schema := SchemaForCtyElementType(e.ety) |
| spec := schema.DecoderSpec() |
| for _, block := range e.blocks { |
| ret = append(ret, hcldec.Variables(block.Body, spec)...) |
| } |
| return ret |
| } |
| |
| func (e *fixupBlocksExpr) Range() hcl.Range { |
| // This is not really an appropriate range for the expression but it's |
| // the best we can do from here. |
| return e.blocks[0].DefRange |
| } |
| |
| func (e *fixupBlocksExpr) StartRange() hcl.Range { |
| return e.blocks[0].DefRange |
| } |