-
Notifications
You must be signed in to change notification settings - Fork 10
/
fixup.go
200 lines (178 loc) · 6.9 KB
/
fixup.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
package blocktoattr
import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/snyk/policy-engine/pkg/internal/terraform/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.
//
// 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{}
}
return &fixupBody{
original: body,
schema: schema,
names: ambiguousNames(schema),
}
}
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
}