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 }