github.com/hashicorp/terraform-plugin-sdk@v1.17.2/helper/resource/state_shim.go (about) 1 package resource 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "strconv" 7 8 tfjson "github.com/hashicorp/terraform-json" 9 "github.com/hashicorp/terraform-plugin-sdk/helper/schema" 10 "github.com/hashicorp/terraform-plugin-sdk/internal/addrs" 11 "github.com/hashicorp/terraform-plugin-sdk/internal/configs/hcl2shim" 12 "github.com/hashicorp/terraform-plugin-sdk/internal/states" 13 "github.com/hashicorp/terraform-plugin-sdk/internal/tfdiags" 14 "github.com/hashicorp/terraform-plugin-sdk/terraform" 15 "github.com/zclconf/go-cty/cty" 16 ) 17 18 // shimState takes a new *states.State and reverts it to a legacy state for the provider ACC tests 19 func shimNewState(newState *states.State, providers map[string]terraform.ResourceProvider) (*terraform.State, error) { 20 state := terraform.NewState() 21 22 // in the odd case of a nil state, let the helper packages handle it 23 if newState == nil { 24 return nil, nil 25 } 26 27 for _, newMod := range newState.Modules { 28 mod := state.AddModule(newMod.Addr) 29 30 for name, out := range newMod.OutputValues { 31 outputType := "" 32 val := hcl2shim.ConfigValueFromHCL2(out.Value) 33 ty := out.Value.Type() 34 switch { 35 case ty == cty.String: 36 outputType = "string" 37 case ty.IsTupleType() || ty.IsListType(): 38 outputType = "list" 39 case ty.IsMapType(): 40 outputType = "map" 41 } 42 43 mod.Outputs[name] = &terraform.OutputState{ 44 Type: outputType, 45 Value: val, 46 Sensitive: out.Sensitive, 47 } 48 } 49 50 for _, res := range newMod.Resources { 51 resType := res.Addr.Type 52 providerType := res.ProviderConfig.ProviderConfig.Type 53 54 resource := getResource(providers, providerType, res.Addr) 55 56 for key, i := range res.Instances { 57 resState := &terraform.ResourceState{ 58 Type: resType, 59 Provider: res.ProviderConfig.String(), 60 } 61 62 // We should always have a Current instance here, but be safe about checking. 63 if i.Current != nil { 64 flatmap, err := shimmedAttributes(i.Current, resource) 65 if err != nil { 66 return nil, fmt.Errorf("error decoding state for %q: %s", resType, err) 67 } 68 69 var meta map[string]interface{} 70 if i.Current.Private != nil { 71 err := json.Unmarshal(i.Current.Private, &meta) 72 if err != nil { 73 return nil, err 74 } 75 } 76 77 resState.Primary = &terraform.InstanceState{ 78 ID: flatmap["id"], 79 Attributes: flatmap, 80 Tainted: i.Current.Status == states.ObjectTainted, 81 Meta: meta, 82 } 83 84 if i.Current.SchemaVersion != 0 { 85 if resState.Primary.Meta == nil { 86 resState.Primary.Meta = map[string]interface{}{} 87 } 88 resState.Primary.Meta["schema_version"] = i.Current.SchemaVersion 89 } 90 91 for _, dep := range i.Current.Dependencies { 92 resState.Dependencies = append(resState.Dependencies, dep.String()) 93 } 94 95 // convert the indexes to the old style flapmap indexes 96 idx := "" 97 switch key.(type) { 98 case addrs.IntKey: 99 // don't add numeric index values to resources with a count of 0 100 if len(res.Instances) > 1 { 101 idx = fmt.Sprintf(".%d", key) 102 } 103 case addrs.StringKey: 104 idx = "." + key.String() 105 } 106 107 mod.Resources[res.Addr.String()+idx] = resState 108 } 109 110 // add any deposed instances 111 for _, dep := range i.Deposed { 112 flatmap, err := shimmedAttributes(dep, resource) 113 if err != nil { 114 return nil, fmt.Errorf("error decoding deposed state for %q: %s", resType, err) 115 } 116 117 var meta map[string]interface{} 118 if dep.Private != nil { 119 err := json.Unmarshal(dep.Private, &meta) 120 if err != nil { 121 return nil, err 122 } 123 } 124 125 deposed := &terraform.InstanceState{ 126 ID: flatmap["id"], 127 Attributes: flatmap, 128 Tainted: dep.Status == states.ObjectTainted, 129 Meta: meta, 130 } 131 if dep.SchemaVersion != 0 { 132 deposed.Meta = map[string]interface{}{ 133 "schema_version": dep.SchemaVersion, 134 } 135 } 136 137 resState.Deposed = append(resState.Deposed, deposed) 138 } 139 } 140 } 141 } 142 143 return state, nil 144 } 145 146 func getResource(providers map[string]terraform.ResourceProvider, providerName string, addr addrs.Resource) *schema.Resource { 147 p := providers[providerName] 148 if p == nil { 149 panic(fmt.Sprintf("provider %q not found in test step", providerName)) 150 } 151 152 // this is only for tests, so should only see schema.Providers 153 provider := p.(*schema.Provider) 154 155 switch addr.Mode { 156 case addrs.ManagedResourceMode: 157 resource := provider.ResourcesMap[addr.Type] 158 if resource != nil { 159 return resource 160 } 161 case addrs.DataResourceMode: 162 resource := provider.DataSourcesMap[addr.Type] 163 if resource != nil { 164 return resource 165 } 166 } 167 168 panic(fmt.Sprintf("resource %s not found in test step", addr.Type)) 169 } 170 171 func shimmedAttributes(instance *states.ResourceInstanceObjectSrc, res *schema.Resource) (map[string]string, error) { 172 flatmap := instance.AttrsFlat 173 if flatmap != nil { 174 return flatmap, nil 175 } 176 177 // if we have json attrs, they need to be decoded 178 rio, err := instance.Decode(res.CoreConfigSchema().ImpliedType()) 179 if err != nil { 180 return nil, err 181 } 182 183 instanceState, err := res.ShimInstanceStateFromValue(rio.Value) 184 if err != nil { 185 return nil, err 186 } 187 188 return instanceState.Attributes, nil 189 } 190 191 type shimmedState struct { 192 state *terraform.State 193 } 194 195 func shimStateFromJson(jsonState *tfjson.State) (*terraform.State, error) { 196 state := terraform.NewState() 197 state.TFVersion = jsonState.TerraformVersion 198 199 if jsonState.Values == nil { 200 // the state is empty 201 return state, nil 202 } 203 204 for key, output := range jsonState.Values.Outputs { 205 os, err := shimOutputState(output) 206 if err != nil { 207 return nil, err 208 } 209 state.RootModule().Outputs[key] = os 210 } 211 212 ss := &shimmedState{state} 213 err := ss.shimStateModule(jsonState.Values.RootModule) 214 if err != nil { 215 return nil, err 216 } 217 218 return state, nil 219 } 220 221 func shimOutputState(so *tfjson.StateOutput) (*terraform.OutputState, error) { 222 os := &terraform.OutputState{ 223 Sensitive: so.Sensitive, 224 } 225 226 switch v := so.Value.(type) { 227 case string: 228 os.Type = "string" 229 os.Value = v 230 return os, nil 231 case []interface{}: 232 os.Type = "list" 233 if len(v) == 0 { 234 os.Value = v 235 return os, nil 236 } 237 switch firstElem := v[0].(type) { 238 case string: 239 elements := make([]interface{}, len(v)) 240 for i, el := range v { 241 elements[i] = el.(string) 242 } 243 os.Value = elements 244 case bool: 245 elements := make([]interface{}, len(v)) 246 for i, el := range v { 247 elements[i] = el.(bool) 248 } 249 os.Value = elements 250 // unmarshalled number from JSON will always be json.Number 251 case json.Number: 252 elements := make([]interface{}, len(v)) 253 for i, el := range v { 254 elements[i] = el.(json.Number) 255 } 256 os.Value = elements 257 case []interface{}: 258 os.Value = v 259 case map[string]interface{}: 260 os.Value = v 261 default: 262 return nil, fmt.Errorf("unexpected output list element type: %T", firstElem) 263 } 264 return os, nil 265 case map[string]interface{}: 266 os.Type = "map" 267 os.Value = v 268 return os, nil 269 case bool: 270 os.Type = "string" 271 os.Value = strconv.FormatBool(v) 272 return os, nil 273 // unmarshalled number from JSON will always be json.Number 274 case json.Number: 275 os.Type = "string" 276 os.Value = v.String() 277 return os, nil 278 } 279 280 return nil, fmt.Errorf("unexpected output type: %T", so.Value) 281 } 282 283 func (ss *shimmedState) shimStateModule(sm *tfjson.StateModule) error { 284 var path addrs.ModuleInstance 285 286 if sm.Address == "" { 287 path = addrs.RootModuleInstance 288 } else { 289 var diags tfdiags.Diagnostics 290 path, diags = addrs.ParseModuleInstanceStr(sm.Address) 291 if diags.HasErrors() { 292 return diags.Err() 293 } 294 } 295 296 mod := ss.state.AddModule(path) 297 for _, res := range sm.Resources { 298 resourceState, err := shimResourceState(res) 299 if err != nil { 300 return err 301 } 302 303 key, err := shimResourceStateKey(res) 304 if err != nil { 305 return err 306 } 307 308 mod.Resources[key] = resourceState 309 } 310 311 if len(sm.ChildModules) > 0 { 312 return fmt.Errorf("Modules are not supported. Found %d modules.", 313 len(sm.ChildModules)) 314 } 315 return nil 316 } 317 318 func shimResourceStateKey(res *tfjson.StateResource) (string, error) { 319 if res.Index == nil { 320 return res.Address, nil 321 } 322 323 var mode terraform.ResourceMode 324 switch res.Mode { 325 case tfjson.DataResourceMode: 326 mode = terraform.DataResourceMode 327 case tfjson.ManagedResourceMode: 328 mode = terraform.ManagedResourceMode 329 default: 330 return "", fmt.Errorf("unexpected resource mode for %q", res.Address) 331 } 332 333 var index int 334 switch idx := res.Index.(type) { 335 case json.Number: 336 i, err := idx.Int64() 337 if err != nil { 338 return "", fmt.Errorf("unexpected index value (%q) for %q, ", 339 idx, res.Address) 340 } 341 index = int(i) 342 default: 343 return "", fmt.Errorf("unexpected index type (%T) for %q, "+ 344 "for_each is not supported", res.Index, res.Address) 345 } 346 347 rsk := &terraform.ResourceStateKey{ 348 Mode: mode, 349 Type: res.Type, 350 Name: res.Name, 351 Index: index, 352 } 353 354 return rsk.String(), nil 355 } 356 357 func shimResourceState(res *tfjson.StateResource) (*terraform.ResourceState, error) { 358 sf := &shimmedFlatmap{} 359 err := sf.FromMap(res.AttributeValues) 360 if err != nil { 361 return nil, err 362 } 363 attributes := sf.Flatmap() 364 365 if _, ok := attributes["id"]; !ok { 366 return nil, fmt.Errorf("no %q found in attributes", "id") 367 } 368 369 return &terraform.ResourceState{ 370 Provider: res.ProviderName, 371 Type: res.Type, 372 Primary: &terraform.InstanceState{ 373 ID: attributes["id"], 374 Attributes: attributes, 375 Meta: map[string]interface{}{ 376 "schema_version": int(res.SchemaVersion), 377 }, 378 Tainted: res.Tainted, 379 }, 380 Dependencies: res.DependsOn, 381 }, nil 382 } 383 384 type shimmedFlatmap struct { 385 m map[string]string 386 } 387 388 func (sf *shimmedFlatmap) FromMap(attributes map[string]interface{}) error { 389 if sf.m == nil { 390 sf.m = make(map[string]string, len(attributes)) 391 } 392 393 return sf.AddMap("", attributes) 394 } 395 396 func (sf *shimmedFlatmap) AddMap(prefix string, m map[string]interface{}) error { 397 for key, value := range m { 398 k := key 399 if prefix != "" { 400 k = fmt.Sprintf("%s.%s", prefix, key) 401 } 402 403 err := sf.AddEntry(k, value) 404 if err != nil { 405 return err 406 } 407 } 408 409 mapLength := "%" 410 if prefix != "" { 411 mapLength = fmt.Sprintf("%s.%s", prefix, "%") 412 } 413 414 sf.AddEntry(mapLength, strconv.Itoa(len(m))) 415 416 return nil 417 } 418 419 func (sf *shimmedFlatmap) AddSlice(name string, elements []interface{}) error { 420 for i, elem := range elements { 421 key := fmt.Sprintf("%s.%d", name, i) 422 err := sf.AddEntry(key, elem) 423 if err != nil { 424 return err 425 } 426 } 427 428 sliceLength := fmt.Sprintf("%s.#", name) 429 sf.AddEntry(sliceLength, strconv.Itoa(len(elements))) 430 431 return nil 432 } 433 434 func (sf *shimmedFlatmap) AddEntry(key string, value interface{}) error { 435 switch el := value.(type) { 436 case nil: 437 // omit the entry 438 return nil 439 case bool: 440 sf.m[key] = strconv.FormatBool(el) 441 case json.Number: 442 sf.m[key] = el.String() 443 case string: 444 sf.m[key] = el 445 case map[string]interface{}: 446 err := sf.AddMap(key, el) 447 if err != nil { 448 return err 449 } 450 case []interface{}: 451 err := sf.AddSlice(key, el) 452 if err != nil { 453 return err 454 } 455 default: 456 // This should never happen unless terraform-json 457 // changes how attributes (types) are represented. 458 // 459 // We handle all types which the JSON unmarshaler 460 // can possibly produce 461 // https://golang.org/pkg/encoding/json/#Unmarshal 462 463 return fmt.Errorf("%q: unexpected type (%T)", key, el) 464 } 465 return nil 466 } 467 468 func (sf *shimmedFlatmap) Flatmap() map[string]string { 469 return sf.m 470 }