github.com/muratcelep/terraform@v1.1.0-beta2-not-internal-4/not-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/muratcelep/terraform/not-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 }