-
Notifications
You must be signed in to change notification settings - Fork 11
/
state_shim.go
344 lines (298 loc) · 8.56 KB
/
state_shim.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
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package resource
import (
"encoding/json"
"fmt"
"strconv"
tfjson "github.com/hashicorp/terraform-json"
"github.com/hashicorp/terraform-plugin-testing/terraform"
"github.com/hashicorp/terraform-plugin-testing/internal/addrs"
"github.com/hashicorp/terraform-plugin-testing/internal/tfdiags"
)
type shimmedState struct {
state *terraform.State
}
func shimStateFromJson(jsonState *tfjson.State) (*terraform.State, error) {
state := terraform.NewState() //nolint:staticcheck // legacy usage
state.TFVersion = jsonState.TerraformVersion
if jsonState.Values == nil {
// the state is empty
return state, nil
}
for key, output := range jsonState.Values.Outputs {
os, err := shimOutputState(output)
if err != nil {
return nil, err
}
state.RootModule().Outputs[key] = os
}
ss := &shimmedState{state}
err := ss.shimStateModule(jsonState.Values.RootModule)
if err != nil {
return nil, err
}
return state, nil
}
func shimOutputState(so *tfjson.StateOutput) (*terraform.OutputState, error) {
os := &terraform.OutputState{
Sensitive: so.Sensitive,
}
switch v := so.Value.(type) {
case string:
os.Type = "string"
os.Value = v
return os, nil
case []interface{}:
os.Type = "list"
if len(v) == 0 {
os.Value = v
return os, nil
}
switch firstElem := v[0].(type) {
case string:
elements := make([]interface{}, len(v))
for i, el := range v {
strElement, ok := el.(string)
// If the type of the element doesn't match the first elem, it's a tuple, return the original value
if !ok {
os.Value = v
return os, nil
}
elements[i] = strElement
}
os.Value = elements
case bool:
elements := make([]interface{}, len(v))
for i, el := range v {
boolElement, ok := el.(bool)
// If the type of the element doesn't match the first elem, it's a tuple, return the original value
if !ok {
os.Value = v
return os, nil
}
elements[i] = boolElement
}
os.Value = elements
// unmarshalled number from JSON will always be json.Number
case json.Number:
elements := make([]interface{}, len(v))
for i, el := range v {
numberElement, ok := el.(json.Number)
// If the type of the element doesn't match the first elem, it's a tuple, return the original value
if !ok {
os.Value = v
return os, nil
}
elements[i] = numberElement
}
os.Value = elements
case []interface{}:
os.Value = v
case map[string]interface{}:
os.Value = v
default:
return nil, fmt.Errorf("unexpected output list element type: %T", firstElem)
}
return os, nil
case map[string]interface{}:
os.Type = "map"
os.Value = v
return os, nil
case bool:
os.Type = "string"
os.Value = strconv.FormatBool(v)
return os, nil
// unmarshalled number from JSON will always be json.Number
case json.Number:
os.Type = "string"
os.Value = v.String()
return os, nil
}
return nil, fmt.Errorf("unexpected output type: %T", so.Value)
}
func (ss *shimmedState) shimStateModule(sm *tfjson.StateModule) error {
var path addrs.ModuleInstance
if sm.Address == "" {
path = addrs.RootModuleInstance
} else {
var diags tfdiags.Diagnostics
path, diags = addrs.ParseModuleInstanceStr(sm.Address)
if diags.HasErrors() {
return diags.Err()
}
}
mod := ss.state.AddModule(path) //nolint:staticcheck // legacy usage
for _, res := range sm.Resources {
resourceState, err := shimResourceState(res)
if err != nil {
return err
}
key, err := shimResourceStateKey(res)
if err != nil {
return err
}
mod.Resources[key] = resourceState
}
if len(sm.ChildModules) > 0 {
return fmt.Errorf("Modules are not supported. Found %d modules.",
len(sm.ChildModules))
}
return nil
}
func shimResourceStateKey(res *tfjson.StateResource) (string, error) {
if res.Index == nil {
return res.Address, nil
}
var mode terraform.ResourceMode
switch res.Mode {
case tfjson.DataResourceMode:
mode = terraform.DataResourceMode
case tfjson.ManagedResourceMode:
mode = terraform.ManagedResourceMode
default:
return "", fmt.Errorf("unexpected resource mode for %q", res.Address)
}
var index int
switch idx := res.Index.(type) {
case json.Number:
i, err := idx.Int64()
if err != nil {
return "", fmt.Errorf("unexpected index value (%q) for %q, ",
idx, res.Address)
}
index = int(i)
default:
return "", fmt.Errorf("unexpected index type (%T) for %q, "+
"for_each is not supported", res.Index, res.Address)
}
rsk := &terraform.ResourceStateKey{
Mode: mode,
Type: res.Type,
Name: res.Name,
Index: index,
}
return rsk.String(), nil
}
func shimResourceState(res *tfjson.StateResource) (*terraform.ResourceState, error) {
sf := &shimmedFlatmap{}
err := sf.FromMap(res.AttributeValues)
if err != nil {
return nil, err
}
attributes := sf.Flatmap()
// The instance state identifier was a Terraform versions 0.11 and earlier
// concept which helped core and the then SDK determine if the resource
// should be removed and as an identifier value in the human readable
// output. This concept unfortunately carried over to the testing logic when
// the testing logic was mostly changed to use the public, machine-readable
// JSON interface with Terraform, rather than reusing prior internal logic
// from Terraform. Using the "id" attribute value for this identifier was
// the default implementation and therefore those older versions of
// Terraform required the attribute. This is no longer necessary after
// Terraform versions 0.12 and later.
//
// If the "id" attribute is not found, set the instance state identifier to
// a synthetic value that can hopefully lead someone encountering the value
// to these comments. The prior logic used to raise an error if the
// attribute was not present, but this value should now only be present in
// legacy logic of this Go module, such as unintentionally exported logic in
// the terraform package, and not encountered during normal testing usage.
//
// Reference: https://github.com/hashicorp/terraform-plugin-testing/issues/84
instanceStateID, ok := attributes["id"]
if !ok {
instanceStateID = "id-attribute-not-set"
}
return &terraform.ResourceState{
Provider: res.ProviderName,
Type: res.Type,
Primary: &terraform.InstanceState{
ID: instanceStateID,
Attributes: attributes,
Meta: map[string]interface{}{
"schema_version": int(res.SchemaVersion),
},
Tainted: res.Tainted,
},
Dependencies: res.DependsOn,
}, nil
}
type shimmedFlatmap struct {
m map[string]string
}
func (sf *shimmedFlatmap) FromMap(attributes map[string]interface{}) error {
if sf.m == nil {
sf.m = make(map[string]string, len(attributes))
}
return sf.AddMap("", attributes)
}
func (sf *shimmedFlatmap) AddMap(prefix string, m map[string]interface{}) error {
for key, value := range m {
k := key
if prefix != "" {
k = fmt.Sprintf("%s.%s", prefix, key)
}
err := sf.AddEntry(k, value)
if err != nil {
return fmt.Errorf("unable to add map key %q entry: %w", k, err)
}
}
mapLength := "%"
if prefix != "" {
mapLength = fmt.Sprintf("%s.%s", prefix, "%")
}
if err := sf.AddEntry(mapLength, strconv.Itoa(len(m))); err != nil {
return fmt.Errorf("unable to add map length %q entry: %w", mapLength, err)
}
return nil
}
func (sf *shimmedFlatmap) AddSlice(name string, elements []interface{}) error {
for i, elem := range elements {
key := fmt.Sprintf("%s.%d", name, i)
err := sf.AddEntry(key, elem)
if err != nil {
return fmt.Errorf("unable to add slice key %q entry: %w", key, err)
}
}
sliceLength := fmt.Sprintf("%s.#", name)
if err := sf.AddEntry(sliceLength, strconv.Itoa(len(elements))); err != nil {
return fmt.Errorf("unable to add slice length %q entry: %w", sliceLength, err)
}
return nil
}
func (sf *shimmedFlatmap) AddEntry(key string, value interface{}) error {
switch el := value.(type) {
case nil:
// omit the entry
return nil
case bool:
sf.m[key] = strconv.FormatBool(el)
case json.Number:
sf.m[key] = el.String()
case string:
sf.m[key] = el
case map[string]interface{}:
err := sf.AddMap(key, el)
if err != nil {
return err
}
case []interface{}:
err := sf.AddSlice(key, el)
if err != nil {
return err
}
default:
// This should never happen unless terraform-json
// changes how attributes (types) are represented.
//
// We handle all types which the JSON unmarshaler
// can possibly produce
// https://golang.org/pkg/encoding/json/#Unmarshal
return fmt.Errorf("%q: unexpected type (%T)", key, el)
}
return nil
}
func (sf *shimmedFlatmap) Flatmap() map[string]string {
return sf.m
}