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