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  }