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