forked from hashicorp/terraform
/
defaults.go
288 lines (265 loc) · 10.2 KB
/
defaults.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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
package funcs
import (
"fmt"
"github.com/hugorut/terraform/src/tfdiags"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"github.com/zclconf/go-cty/cty/function"
)
// DefaultsFunc is a helper function for substituting default values in
// place of null values in a given data structure.
//
// See the documentation for function Defaults for more information.
var DefaultsFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "input",
Type: cty.DynamicPseudoType,
AllowNull: true,
AllowMarked: true,
},
{
Name: "defaults",
Type: cty.DynamicPseudoType,
AllowMarked: true,
},
},
Type: func(args []cty.Value) (cty.Type, error) {
// The result type is guaranteed to be the same as the input type,
// since all we're doing is replacing null values with non-null
// values of the same type.
retType := args[0].Type()
defaultsType := args[1].Type()
// This function is aimed at filling in object types or collections
// of object types where some of the attributes might be null, so
// it doesn't make sense to use a primitive type directly with it.
// (The "coalesce" function may be appropriate for such cases.)
if retType.IsPrimitiveType() {
// This error message is a bit of a fib because we can actually
// apply defaults to tuples too, but we expect that to be so
// unusual as to not be worth mentioning here, because mentioning
// it would require using some less-well-known Terraform language
// terminology in the message (tuple types, structural types).
return cty.DynamicPseudoType, function.NewArgErrorf(1, "only object types and collections of object types can have defaults applied")
}
defaultsPath := make(cty.Path, 0, 4) // some capacity so that most structures won't reallocate
if err := defaultsAssertSuitableFallback(retType, defaultsType, defaultsPath); err != nil {
errMsg := tfdiags.FormatError(err) // add attribute path prefix
return cty.DynamicPseudoType, function.NewArgErrorf(1, "%s", errMsg)
}
return retType, nil
},
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
if args[0].Type().HasDynamicTypes() {
// If the types our input object aren't known yet for some reason
// then we'll defer all of our work here, because our
// interpretation of the defaults depends on the types in
// the input.
return cty.UnknownVal(retType), nil
}
v := defaultsApply(args[0], args[1])
return v, nil
},
})
func defaultsApply(input, fallback cty.Value) cty.Value {
wantTy := input.Type()
umInput, inputMarks := input.Unmark()
umFb, fallbackMarks := fallback.Unmark()
// If neither are known, we very conservatively return an unknown value
// with the union of marks on both input and default.
if !(umInput.IsKnown() && umFb.IsKnown()) {
return cty.UnknownVal(wantTy).WithMarks(inputMarks).WithMarks(fallbackMarks)
}
// For the rest of this function we're assuming that the given defaults
// will always be valid, because we expect to have caught any problems
// during the type checking phase. Any inconsistencies that reach here are
// therefore considered to be implementation bugs, and so will panic.
// Our strategy depends on the kind of type we're working with.
switch {
case wantTy.IsPrimitiveType():
// For leaf primitive values the rule is relatively simple: use the
// input if it's non-null, or fallback if input is null.
if !umInput.IsNull() {
return input
}
v, err := convert.Convert(umFb, wantTy)
if err != nil {
// Should not happen because we checked in defaultsAssertSuitableFallback
panic(err.Error())
}
return v.WithMarks(fallbackMarks)
case wantTy.IsObjectType():
// For structural types, a null input value must be passed through. We
// do not apply default values for missing optional structural values,
// only their contents.
//
// We also pass through the input if the fallback value is null. This
// can happen if the given defaults do not include a value for this
// attribute.
if umInput.IsNull() || umFb.IsNull() {
return input
}
atys := wantTy.AttributeTypes()
ret := map[string]cty.Value{}
for attr, aty := range atys {
inputSub := umInput.GetAttr(attr)
fallbackSub := cty.NullVal(aty)
if umFb.Type().HasAttribute(attr) {
fallbackSub = umFb.GetAttr(attr)
}
ret[attr] = defaultsApply(inputSub.WithMarks(inputMarks), fallbackSub.WithMarks(fallbackMarks))
}
return cty.ObjectVal(ret)
case wantTy.IsTupleType():
// For structural types, a null input value must be passed through. We
// do not apply default values for missing optional structural values,
// only their contents.
//
// We also pass through the input if the fallback value is null. This
// can happen if the given defaults do not include a value for this
// attribute.
if umInput.IsNull() || umFb.IsNull() {
return input
}
l := wantTy.Length()
ret := make([]cty.Value, l)
for i := 0; i < l; i++ {
inputSub := umInput.Index(cty.NumberIntVal(int64(i)))
fallbackSub := umFb.Index(cty.NumberIntVal(int64(i)))
ret[i] = defaultsApply(inputSub.WithMarks(inputMarks), fallbackSub.WithMarks(fallbackMarks))
}
return cty.TupleVal(ret)
case wantTy.IsCollectionType():
// For collection types we apply a single fallback value to each
// element of the input collection, because in the situations this
// function is intended for we assume that the number of elements
// is the caller's decision, and so we'll just apply the same defaults
// to all of the elements.
ety := wantTy.ElementType()
switch {
case wantTy.IsMapType():
newVals := map[string]cty.Value{}
if !umInput.IsNull() {
for it := umInput.ElementIterator(); it.Next(); {
k, v := it.Element()
newVals[k.AsString()] = defaultsApply(v.WithMarks(inputMarks), fallback.WithMarks(fallbackMarks))
}
}
if len(newVals) == 0 {
return cty.MapValEmpty(ety)
}
return cty.MapVal(newVals)
case wantTy.IsListType(), wantTy.IsSetType():
var newVals []cty.Value
if !umInput.IsNull() {
for it := umInput.ElementIterator(); it.Next(); {
_, v := it.Element()
newV := defaultsApply(v.WithMarks(inputMarks), fallback.WithMarks(fallbackMarks))
newVals = append(newVals, newV)
}
}
if len(newVals) == 0 {
if wantTy.IsSetType() {
return cty.SetValEmpty(ety)
}
return cty.ListValEmpty(ety)
}
if wantTy.IsSetType() {
return cty.SetVal(newVals)
}
return cty.ListVal(newVals)
default:
// There are no other collection types, so this should not happen
panic(fmt.Sprintf("invalid collection type %#v", wantTy))
}
default:
// We should've caught anything else in defaultsAssertSuitableFallback,
// so this should not happen.
panic(fmt.Sprintf("invalid target type %#v", wantTy))
}
}
func defaultsAssertSuitableFallback(wantTy, fallbackTy cty.Type, fallbackPath cty.Path) error {
// If the type we want is a collection type then we need to keep peeling
// away collection type wrappers until we find the non-collection-type
// that's underneath, which is what the fallback will actually be applied
// to.
inCollection := false
for wantTy.IsCollectionType() {
wantTy = wantTy.ElementType()
inCollection = true
}
switch {
case wantTy.IsPrimitiveType():
// The fallback is valid if it's equal to or convertible to what we want.
if fallbackTy.Equals(wantTy) {
return nil
}
conversion := convert.GetConversion(fallbackTy, wantTy)
if conversion == nil {
msg := convert.MismatchMessage(fallbackTy, wantTy)
return fallbackPath.NewErrorf("invalid default value for %s: %s", wantTy.FriendlyName(), msg)
}
return nil
case wantTy.IsObjectType():
if !fallbackTy.IsObjectType() {
if inCollection {
return fallbackPath.NewErrorf("the default value for a collection of an object type must itself be an object type, not %s", fallbackTy.FriendlyName())
}
return fallbackPath.NewErrorf("the default value for an object type must itself be an object type, not %s", fallbackTy.FriendlyName())
}
for attr, wantAty := range wantTy.AttributeTypes() {
if !fallbackTy.HasAttribute(attr) {
continue // it's always okay to not have a default value
}
fallbackSubpath := fallbackPath.GetAttr(attr)
fallbackSubTy := fallbackTy.AttributeType(attr)
err := defaultsAssertSuitableFallback(wantAty, fallbackSubTy, fallbackSubpath)
if err != nil {
return err
}
}
for attr := range fallbackTy.AttributeTypes() {
if !wantTy.HasAttribute(attr) {
fallbackSubpath := fallbackPath.GetAttr(attr)
return fallbackSubpath.NewErrorf("target type does not expect an attribute named %q", attr)
}
}
return nil
case wantTy.IsTupleType():
if !fallbackTy.IsTupleType() {
if inCollection {
return fallbackPath.NewErrorf("the default value for a collection of a tuple type must itself be a tuple type, not %s", fallbackTy.FriendlyName())
}
return fallbackPath.NewErrorf("the default value for a tuple type must itself be a tuple type, not %s", fallbackTy.FriendlyName())
}
wantEtys := wantTy.TupleElementTypes()
fallbackEtys := fallbackTy.TupleElementTypes()
if got, want := len(wantEtys), len(fallbackEtys); got != want {
return fallbackPath.NewErrorf("the default value for a tuple type of length %d must also have length %d, not %d", want, want, got)
}
for i := 0; i < len(wantEtys); i++ {
fallbackSubpath := fallbackPath.IndexInt(i)
wantSubTy := wantEtys[i]
fallbackSubTy := fallbackEtys[i]
err := defaultsAssertSuitableFallback(wantSubTy, fallbackSubTy, fallbackSubpath)
if err != nil {
return err
}
}
return nil
default:
// No other types are supported right now.
return fallbackPath.NewErrorf("cannot apply defaults to %s", wantTy.FriendlyName())
}
}
// Defaults is a helper function for substituting default values in
// place of null values in a given data structure.
//
// This is primarily intended for use with a module input variable that
// has an object type constraint (or a collection thereof) that has optional
// attributes, so that the receiver of a value that omits those attributes
// can insert non-null default values in place of the null values caused by
// omitting the attributes.
func Defaults(input, defaults cty.Value) (cty.Value, error) {
return DefaultsFunc.Call([]cty.Value{input, defaults})
}