-
Notifications
You must be signed in to change notification settings - Fork 9.4k
/
sdklike.go
287 lines (263 loc) · 9.71 KB
/
sdklike.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
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package backendbase
import (
"fmt"
"os"
"strconv"
"strings"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
)
// SDKLikeData offers an approximation of the legack SDK "ResourceData" API
// as a stopgap measure to help migrate all of the remote state backend
// implementations away from the legacy SDK.
//
// It's designed to wrap an object returned by [Base.PrepareConfig] which
// should therefore already have a fixed, known data type. Therefore the
// methods assume that the caller already knows what type each attribute
// should have and will panic if a caller asks for an incompatible type.
type SDKLikeData struct {
v cty.Value
}
func NewSDKLikeData(v cty.Value) SDKLikeData {
return SDKLikeData{v}
}
// String extracts a string attribute from a configuration object
// in a similar way to how the legacy SDK would interpret an attribute
// of type schema.TypeString, or panics if the wrapped object isn't of a
// suitable type.
func (d SDKLikeData) String(attrPath string) string {
v := d.GetAttr(attrPath, cty.String)
if v.IsNull() {
return ""
}
return v.AsString()
}
// Int extracts a string attribute from a configuration object
// in a similar way to how the legacy SDK would interpret an attribute
// of type schema.TypeInt, or panics if the wrapped object isn't of a
// suitable type.
//
// Since the Terraform language does not have an integers-only type, this
// can fail dynamically (returning an error) if the given value has a
// fractional component.
func (d SDKLikeData) Int64(attrPath string) (int64, error) {
// Legacy SDK used strconv.ParseInt to interpret values, so we'll
// follow its lead here for maximal compatibility.
v := d.GetAttr(attrPath, cty.String)
if v.IsNull() {
return 0, nil
}
return strconv.ParseInt(v.AsString(), 0, 0)
}
// Bool extracts a string attribute from a configuration object
// in a similar way to how the legacy SDK would interpret an attribute
// of type schema.TypeBool, or panics if the wrapped object isn't of a
// suitable type.
func (d SDKLikeData) Bool(attrPath string) bool {
// Legacy SDK used strconv.ParseBool to interpret values, but it
// did so only after the configuration was interpreted by HCL and
// thus HCL's more constrained definition of bool still "won",
// and we follow that tradition here.
v := d.GetAttr(attrPath, cty.Bool)
if v.IsNull() {
return false
}
return v.True()
}
// GetAttr is just a thin wrapper around [cty.Path.Apply] that accepts
// a legacy-SDK-like dot-separated string as attribute path, instead of
// a [cty.Path] directly.
//
// It uses [SDKLikePath] to interpret the given path, and so the limitations
// of that function apply equally to this function.
//
// This function will panic if asked to extract a path that isn't compatible
// with the object type of the enclosed value.
func (d SDKLikeData) GetAttr(attrPath string, wantType cty.Type) cty.Value {
path := SDKLikePath(attrPath)
v, err := path.Apply(d.v)
if err != nil {
panic("invalid attribute path: " + err.Error())
}
v, err = convert.Convert(v, wantType)
if err != nil {
panic("incorrect attribute type: " + err.Error())
}
return v
}
// SDKLikePath interprets a subset of the legacy SDK attribute path syntax --
// identifiers separated by dots -- into a cty.Path.
//
// This is designed only for migrating historical remote system backends that
// were originally written using the SDK, and so it's limited only to the
// simple cases they use. It's not suitable for the more complex legacy SDK
// uses made by Terraform providers.
func SDKLikePath(rawPath string) cty.Path {
var ret cty.Path
remain := rawPath
for {
dot := strings.IndexByte(remain, '.')
last := false
if dot == -1 {
dot = len(remain)
last = true
}
attrName := remain[:dot]
ret = append(ret, cty.GetAttrStep{Name: attrName})
if last {
return ret
}
remain = remain[dot+1:]
}
}
// SDKLikeEnvDefault emulates an SDK-style "EnvDefaultFunc" by taking the
// result of [SDKLikeData.String] and a series of environment variable names.
//
// If the given string is already non-empty then it just returns it directly.
// Otherwise it returns the value of the first environment variable that has
// a non-empty value. If everything turns out empty, the result is an empty
// string.
func SDKLikeEnvDefault(v string, envNames ...string) string {
if v == "" {
for _, envName := range envNames {
v = os.Getenv(envName)
if v != "" {
return v
}
}
}
return v
}
// SDKLikeRequiredWithEnvDefault is a convenience wrapper around
// [SDKLikeEnvDefault] which returns an error if the result is still the
// empty string even after trying all of the fallback environment variables.
//
// This wrapper requires an additional argument specifying the attribute name
// just because that becomes part of the returned error message.
func SDKLikeRequiredWithEnvDefault(attrPath string, v string, envNames ...string) (string, error) {
ret := SDKLikeEnvDefault(v, envNames...)
if ret == "" {
return "", fmt.Errorf("attribute %q is required", attrPath)
}
return ret, nil
}
// SDKLikeDefaults captures legacy-SDK-like default values to help fill the
// gap in abstraction level between the legacy SDK and Terraform's own
// configuration schema model.
type SDKLikeDefaults map[string]SDKLikeDefault
type SDKLikeDefault struct {
EnvVars []string
Fallback string
// Required is for situations where an argument is optional to set
// in the configuration but _must_ eventually be set through the
// combination of the configuration and the environment variables
// in this object.
//
// It doesn't make sense to set Fallback non-empty when this flag is
// set, because an attribute with a non-empty fallback is always
// effectively present.
Required bool
}
// ApplyTo is a convenience helper that allows inserting default
// values from environment variables into many different string attributes of
// an object value all at once, approximating what the legacy SDK would've
// done when the schema included an "EnvDefaultFunc".
//
// Like all of the "SDK-like" helpers. this expects that the base object has
// already been coerced into the correct type for a backend's schema and
// so this will panic if any of the keys in envVars do not match existing
// attributes in base, and if the value in any of those attributes is not
// of a cty primitive type.
func (d SDKLikeDefaults) ApplyTo(base cty.Value) (cty.Value, error) {
attrTypes := base.Type().AttributeTypes()
retAttrs := make(map[string]cty.Value, len(attrTypes))
for attrName, ty := range attrTypes {
defs, hasDefs := d[attrName]
givenVal := base.GetAttr(attrName)
if !hasDefs {
// Just pass through verbatim any attributes that are not
// accounted for in our defaults.
retAttrs[attrName] = givenVal
continue
}
// The legacy SDK shims convert all values into strings (for flatmap)
// and then do their work in terms of that, so we'll follow suit here.
vStr, err := convert.Convert(givenVal, cty.String)
if err != nil {
panic("cannot apply environment variable defaults for " + ty.GoString())
}
rawStr := ""
if !vStr.IsNull() {
rawStr = vStr.AsString()
}
if rawStr == "" {
for _, envName := range defs.EnvVars {
rawStr = os.Getenv(envName)
if rawStr != "" {
break
}
}
}
if rawStr == "" {
rawStr = defs.Fallback
}
if defs.Required && rawStr == "" {
return cty.NilVal, fmt.Errorf("argument %q is required", attrName)
}
// As a special case, if we still have an empty string and the original
// value was null then we'll preserve the null. This is a compromise,
// assuming that SDKLikeData knows how to treat a null value as a
// zero value anyway and if we preserve the null then the recipient
// of this result can still use the cty.Value result directly to
// distinguish between the value being set explicitly to empty in
// the config vs. being entirely unset.
if rawStr == "" && givenVal.IsNull() {
retAttrs[attrName] = givenVal
continue
}
// By the time we get here, rawStr should be empty only if the original
// value was unset and all of the fallback environment variables were
// also unset. Otherwise, rawStr contains a string representation of
// a value that we now need to convert back to the type that was
// originally wanted.
switch ty {
case cty.String:
retAttrs[attrName] = cty.StringVal(rawStr)
case cty.Bool:
if rawStr == "" {
rawStr = "false"
}
// Legacy SDK uses strconv.ParseBool and therefore tolerates a
// variety of different string representations of true and false,
// so we'll do the same here. The config itself can't use those
// alternate forms because HCL's definition of bool prevails there,
// but the environment variables can use any of these forms.
bv, err := strconv.ParseBool(rawStr)
if err != nil {
return cty.NilVal, fmt.Errorf("invalid value for %q: %s", attrName, err)
}
retAttrs[attrName] = cty.BoolVal(bv)
case cty.Number:
if rawStr == "" {
rawStr = "0"
}
// This case is a little trickier because cty.Number could be
// representing either an integer or a float, which each have
// different interpretations in the legacy SDK. Therefore we'll
// try integer first and use its result if successful, but then
// try float as a fallback if not.
if iv, err := strconv.ParseInt(rawStr, 0, 0); err == nil {
retAttrs[attrName] = cty.NumberIntVal(iv)
} else if fv, err := strconv.ParseFloat(rawStr, 64); err == nil {
retAttrs[attrName] = cty.NumberFloatVal(fv)
} else {
return cty.NilVal, fmt.Errorf("invalid value for %q: must be a number", attrName)
}
default:
panic("cannot apply environment variable defaults for " + ty.GoString())
}
}
return cty.ObjectVal(retAttrs), nil
}