github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/states/statefile/version3_upgrade.go (about)

     1  package statefile
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"strconv"
     7  	"strings"
     8  
     9  	"github.com/zclconf/go-cty/cty"
    10  	ctyjson "github.com/zclconf/go-cty/cty/json"
    11  
    12  	"github.com/hashicorp/terraform-plugin-sdk/internal/addrs"
    13  	"github.com/hashicorp/terraform-plugin-sdk/internal/states"
    14  	"github.com/hashicorp/terraform-plugin-sdk/internal/tfdiags"
    15  )
    16  
    17  func upgradeStateV3ToV4(old *stateV3) (*stateV4, error) {
    18  
    19  	if old.Serial < 0 {
    20  		// The new format is using uint64 here, which should be fine for any
    21  		// real state (we only used positive integers in practice) but we'll
    22  		// catch this explicitly here to avoid weird behavior if a state file
    23  		// has been tampered with in some way.
    24  		return nil, fmt.Errorf("state has serial less than zero, which is invalid")
    25  	}
    26  
    27  	new := &stateV4{
    28  		TerraformVersion: old.TFVersion,
    29  		Serial:           uint64(old.Serial),
    30  		Lineage:          old.Lineage,
    31  		RootOutputs:      map[string]outputStateV4{},
    32  		Resources:        []resourceStateV4{},
    33  	}
    34  
    35  	if new.TerraformVersion == "" {
    36  		// Older formats considered this to be optional, but now it's required
    37  		// and so we'll stub it out with something that's definitely older
    38  		// than the version that really created this state.
    39  		new.TerraformVersion = "0.0.0"
    40  	}
    41  
    42  	for _, msOld := range old.Modules {
    43  		if len(msOld.Path) < 1 || msOld.Path[0] != "root" {
    44  			return nil, fmt.Errorf("state contains invalid module path %#v", msOld.Path)
    45  		}
    46  
    47  		// Convert legacy-style module address into our newer address type.
    48  		// Since these old formats are only generated by versions of Terraform
    49  		// that don't support count and for_each on modules, we can just assume
    50  		// all of the modules are unkeyed.
    51  		moduleAddr := make(addrs.ModuleInstance, len(msOld.Path)-1)
    52  		for i, name := range msOld.Path[1:] {
    53  			moduleAddr[i] = addrs.ModuleInstanceStep{
    54  				Name:        name,
    55  				InstanceKey: addrs.NoKey,
    56  			}
    57  		}
    58  
    59  		// In a v3 state file, a "resource state" is actually an instance
    60  		// state, so we need to fill in a missing level of heirarchy here
    61  		// by lazily creating resource states as we encounter them.
    62  		// We'll track them in here, keyed on the string representation of
    63  		// the resource address.
    64  		resourceStates := map[string]*resourceStateV4{}
    65  
    66  		for legacyAddr, rsOld := range msOld.Resources {
    67  			instAddr, err := parseLegacyResourceAddress(legacyAddr)
    68  			if err != nil {
    69  				return nil, err
    70  			}
    71  
    72  			resAddr := instAddr.Resource
    73  			rs, exists := resourceStates[resAddr.String()]
    74  			if !exists {
    75  				var modeStr string
    76  				switch resAddr.Mode {
    77  				case addrs.ManagedResourceMode:
    78  					modeStr = "managed"
    79  				case addrs.DataResourceMode:
    80  					modeStr = "data"
    81  				default:
    82  					return nil, fmt.Errorf("state contains resource %s with an unsupported resource mode %#v", resAddr, resAddr.Mode)
    83  				}
    84  
    85  				// In state versions prior to 4 we allowed each instance of a
    86  				// resource to have its own provider configuration address,
    87  				// which makes no real sense in practice because providers
    88  				// are associated with resources in the configuration. We
    89  				// elevate that to the resource level during this upgrade,
    90  				// implicitly taking the provider address of the first instance
    91  				// we encounter for each resource. While this is lossy in
    92  				// theory, in practice there is no reason for these values to
    93  				// differ between instances.
    94  				var providerAddr addrs.AbsProviderConfig
    95  				oldProviderAddr := rsOld.Provider
    96  				if strings.Contains(oldProviderAddr, "provider.") {
    97  					// Smells like a new-style provider address, but we'll test it.
    98  					var diags tfdiags.Diagnostics
    99  					providerAddr, diags = addrs.ParseAbsProviderConfigStr(oldProviderAddr)
   100  					if diags.HasErrors() {
   101  						return nil, fmt.Errorf("invalid provider config reference %q for %s: %s", oldProviderAddr, instAddr, diags.Err())
   102  					}
   103  				} else {
   104  					// Smells like an old-style module-local provider address,
   105  					// which we'll need to migrate. We'll assume it's referring
   106  					// to the same module the resource is in, which might be
   107  					// incorrect but it'll get fixed up next time any updates
   108  					// are made to an instance.
   109  					if oldProviderAddr != "" {
   110  						localAddr, diags := addrs.ParseProviderConfigCompactStr(oldProviderAddr)
   111  						if diags.HasErrors() {
   112  							return nil, fmt.Errorf("invalid legacy provider config reference %q for %s: %s", oldProviderAddr, instAddr, diags.Err())
   113  						}
   114  						providerAddr = localAddr.Absolute(moduleAddr)
   115  					} else {
   116  						providerAddr = resAddr.DefaultProviderConfig().Absolute(moduleAddr)
   117  					}
   118  				}
   119  
   120  				rs = &resourceStateV4{
   121  					Module:         moduleAddr.String(),
   122  					Mode:           modeStr,
   123  					Type:           resAddr.Type,
   124  					Name:           resAddr.Name,
   125  					Instances:      []instanceObjectStateV4{},
   126  					ProviderConfig: providerAddr.String(),
   127  				}
   128  				resourceStates[resAddr.String()] = rs
   129  			}
   130  
   131  			// Now we'll deal with the instance itself, which may either be
   132  			// the first instance in a resource we just created or an additional
   133  			// instance for a resource added on a prior loop.
   134  			instKey := instAddr.Key
   135  			if isOld := rsOld.Primary; isOld != nil {
   136  				isNew, err := upgradeInstanceObjectV3ToV4(rsOld, isOld, instKey, states.NotDeposed)
   137  				if err != nil {
   138  					return nil, fmt.Errorf("failed to migrate primary generation of %s: %s", instAddr, err)
   139  				}
   140  				rs.Instances = append(rs.Instances, *isNew)
   141  			}
   142  			for i, isOld := range rsOld.Deposed {
   143  				// When we migrate old instances we'll use sequential deposed
   144  				// keys just so that the upgrade result is deterministic. New
   145  				// deposed keys allocated moving forward will be pseudorandomly
   146  				// selected, but we check for collisions and so these
   147  				// non-random ones won't hurt.
   148  				deposedKey := states.DeposedKey(fmt.Sprintf("%08x", i+1))
   149  				isNew, err := upgradeInstanceObjectV3ToV4(rsOld, isOld, instKey, deposedKey)
   150  				if err != nil {
   151  					return nil, fmt.Errorf("failed to migrate deposed generation index %d of %s: %s", i, instAddr, err)
   152  				}
   153  				rs.Instances = append(rs.Instances, *isNew)
   154  			}
   155  
   156  			if instKey != addrs.NoKey && rs.EachMode == "" {
   157  				rs.EachMode = "list"
   158  			}
   159  		}
   160  
   161  		for _, rs := range resourceStates {
   162  			new.Resources = append(new.Resources, *rs)
   163  		}
   164  
   165  		if len(msOld.Path) == 1 && msOld.Path[0] == "root" {
   166  			// We'll migrate the outputs for this module too, then.
   167  			for name, oldOS := range msOld.Outputs {
   168  				newOS := outputStateV4{
   169  					Sensitive: oldOS.Sensitive,
   170  				}
   171  
   172  				valRaw := oldOS.Value
   173  				valSrc, err := json.Marshal(valRaw)
   174  				if err != nil {
   175  					// Should never happen, because this value came from JSON
   176  					// in the first place and so we're just round-tripping here.
   177  					return nil, fmt.Errorf("failed to serialize output %q value as JSON: %s", name, err)
   178  				}
   179  
   180  				// The "type" field in state V2 wasn't really that useful
   181  				// since it was only able to capture string vs. list vs. map.
   182  				// For this reason, during upgrade we'll just discard it
   183  				// altogether and use cty's idea of the implied type of
   184  				// turning our old value into JSON.
   185  				ty, err := ctyjson.ImpliedType(valSrc)
   186  				if err != nil {
   187  					// REALLY should never happen, because we literally just
   188  					// encoded this as JSON above!
   189  					return nil, fmt.Errorf("failed to parse output %q value from JSON: %s", name, err)
   190  				}
   191  
   192  				// ImpliedType tends to produce structural types, but since older
   193  				// version of Terraform didn't support those a collection type
   194  				// is probably what was intended, so we'll see if we can
   195  				// interpret our value as one.
   196  				ty = simplifyImpliedValueType(ty)
   197  
   198  				tySrc, err := ctyjson.MarshalType(ty)
   199  				if err != nil {
   200  					return nil, fmt.Errorf("failed to serialize output %q type as JSON: %s", name, err)
   201  				}
   202  
   203  				newOS.ValueRaw = json.RawMessage(valSrc)
   204  				newOS.ValueTypeRaw = json.RawMessage(tySrc)
   205  
   206  				new.RootOutputs[name] = newOS
   207  			}
   208  		}
   209  	}
   210  
   211  	new.normalize()
   212  
   213  	return new, nil
   214  }
   215  
   216  func upgradeInstanceObjectV3ToV4(rsOld *resourceStateV2, isOld *instanceStateV2, instKey addrs.InstanceKey, deposedKey states.DeposedKey) (*instanceObjectStateV4, error) {
   217  
   218  	// Schema versions were, in prior formats, a private concern of the provider
   219  	// SDK, and not a first-class concept in the state format. Here we're
   220  	// sniffing for the pre-0.12 SDK's way of representing schema versions
   221  	// and promoting it to our first-class field if we find it. We'll ignore
   222  	// it if it doesn't look like what the SDK would've written. If this
   223  	// sniffing fails then we'll assume schema version 0.
   224  	var schemaVersion uint64
   225  	migratedSchemaVersion := false
   226  	if raw, exists := isOld.Meta["schema_version"]; exists {
   227  		switch tv := raw.(type) {
   228  		case string:
   229  			v, err := strconv.ParseUint(tv, 10, 64)
   230  			if err == nil {
   231  				schemaVersion = v
   232  				migratedSchemaVersion = true
   233  			}
   234  		case int:
   235  			schemaVersion = uint64(tv)
   236  			migratedSchemaVersion = true
   237  		case float64:
   238  			schemaVersion = uint64(tv)
   239  			migratedSchemaVersion = true
   240  		}
   241  	}
   242  
   243  	private := map[string]interface{}{}
   244  	for k, v := range isOld.Meta {
   245  		if k == "schema_version" && migratedSchemaVersion {
   246  			// We're gonna promote this into our first-class schema version field
   247  			continue
   248  		}
   249  		private[k] = v
   250  	}
   251  	var privateJSON []byte
   252  	if len(private) != 0 {
   253  		var err error
   254  		privateJSON, err = json.Marshal(private)
   255  		if err != nil {
   256  			// This shouldn't happen, because the Meta values all came from JSON
   257  			// originally anyway.
   258  			return nil, fmt.Errorf("cannot serialize private instance object data: %s", err)
   259  		}
   260  	}
   261  
   262  	var status string
   263  	if isOld.Tainted {
   264  		status = "tainted"
   265  	}
   266  
   267  	var instKeyRaw interface{}
   268  	switch tk := instKey.(type) {
   269  	case addrs.IntKey:
   270  		instKeyRaw = int(tk)
   271  	case addrs.StringKey:
   272  		instKeyRaw = string(tk)
   273  	default:
   274  		if instKeyRaw != nil {
   275  			return nil, fmt.Errorf("unsupported instance key: %#v", instKey)
   276  		}
   277  	}
   278  
   279  	var attributes map[string]string
   280  	if isOld.Attributes != nil {
   281  		attributes = make(map[string]string, len(isOld.Attributes))
   282  		for k, v := range isOld.Attributes {
   283  			attributes[k] = v
   284  		}
   285  	}
   286  	if isOld.ID != "" {
   287  		// As a special case, if we don't already have an "id" attribute and
   288  		// yet there's a non-empty first-class ID on the old object then we'll
   289  		// create a synthetic id attribute to avoid losing that first-class id.
   290  		// In practice this generally arises only in tests where state literals
   291  		// are hand-written in a non-standard way; real code prior to 0.12
   292  		// would always force the first-class ID to be copied into the
   293  		// id attribute before storing.
   294  		if attributes == nil {
   295  			attributes = make(map[string]string, len(isOld.Attributes))
   296  		}
   297  		if idVal := attributes["id"]; idVal == "" {
   298  			attributes["id"] = isOld.ID
   299  		}
   300  	}
   301  
   302  	dependencies := make([]string, len(rsOld.Dependencies))
   303  	for i, v := range rsOld.Dependencies {
   304  		depStr, err := parseLegacyDependency(v)
   305  		if err != nil {
   306  			return nil, fmt.Errorf("invalid dependency reference %q: %s", v, err)
   307  		}
   308  		dependencies[i] = depStr
   309  	}
   310  
   311  	return &instanceObjectStateV4{
   312  		IndexKey:       instKeyRaw,
   313  		Status:         status,
   314  		Deposed:        string(deposedKey),
   315  		AttributesFlat: attributes,
   316  		Dependencies:   dependencies,
   317  		SchemaVersion:  schemaVersion,
   318  		PrivateRaw:     privateJSON,
   319  	}, nil
   320  }
   321  
   322  // parseLegacyResourceAddress parses the different identifier format used
   323  // state formats before version 4, like "instance.name.0".
   324  func parseLegacyResourceAddress(s string) (addrs.ResourceInstance, error) {
   325  	var ret addrs.ResourceInstance
   326  
   327  	// Split based on ".". Every resource address should have at least two
   328  	// elements (type and name).
   329  	parts := strings.Split(s, ".")
   330  	if len(parts) < 2 || len(parts) > 4 {
   331  		return ret, fmt.Errorf("invalid internal resource address format: %s", s)
   332  	}
   333  
   334  	// Data resource if we have at least 3 parts and the first one is data
   335  	ret.Resource.Mode = addrs.ManagedResourceMode
   336  	if len(parts) > 2 && parts[0] == "data" {
   337  		ret.Resource.Mode = addrs.DataResourceMode
   338  		parts = parts[1:]
   339  	}
   340  
   341  	// If we're not a data resource and we have more than 3, then it is an error
   342  	if len(parts) > 3 && ret.Resource.Mode != addrs.DataResourceMode {
   343  		return ret, fmt.Errorf("invalid internal resource address format: %s", s)
   344  	}
   345  
   346  	// Build the parts of the resource address that are guaranteed to exist
   347  	ret.Resource.Type = parts[0]
   348  	ret.Resource.Name = parts[1]
   349  	ret.Key = addrs.NoKey
   350  
   351  	// If we have more parts, then we have an index. Parse that.
   352  	if len(parts) > 2 {
   353  		idx, err := strconv.ParseInt(parts[2], 0, 0)
   354  		if err != nil {
   355  			return ret, fmt.Errorf("error parsing resource address %q: %s", s, err)
   356  		}
   357  
   358  		ret.Key = addrs.IntKey(idx)
   359  	}
   360  
   361  	return ret, nil
   362  }
   363  
   364  // simplifyImpliedValueType attempts to heuristically simplify a value type
   365  // derived from a legacy stored output value into something simpler that
   366  // is closer to what would've fitted into the pre-v0.12 value type system.
   367  func simplifyImpliedValueType(ty cty.Type) cty.Type {
   368  	switch {
   369  	case ty.IsTupleType():
   370  		// If all of the element types are the same then we'll make this
   371  		// a list instead. This is very likely to be true, since prior versions
   372  		// of Terraform did not officially support mixed-type collections.
   373  
   374  		if ty.Equals(cty.EmptyTuple) {
   375  			// Don't know what the element type would be, then.
   376  			return ty
   377  		}
   378  
   379  		etys := ty.TupleElementTypes()
   380  		ety := etys[0]
   381  		for _, other := range etys[1:] {
   382  			if !other.Equals(ety) {
   383  				// inconsistent types
   384  				return ty
   385  			}
   386  		}
   387  		ety = simplifyImpliedValueType(ety)
   388  		return cty.List(ety)
   389  
   390  	case ty.IsObjectType():
   391  		// If all of the attribute types are the same then we'll make this
   392  		// a map instead. This is very likely to be true, since prior versions
   393  		// of Terraform did not officially support mixed-type collections.
   394  
   395  		if ty.Equals(cty.EmptyObject) {
   396  			// Don't know what the element type would be, then.
   397  			return ty
   398  		}
   399  
   400  		atys := ty.AttributeTypes()
   401  		var ety cty.Type
   402  		for _, other := range atys {
   403  			if ety == cty.NilType {
   404  				ety = other
   405  				continue
   406  			}
   407  			if !other.Equals(ety) {
   408  				// inconsistent types
   409  				return ty
   410  			}
   411  		}
   412  		ety = simplifyImpliedValueType(ety)
   413  		return cty.Map(ety)
   414  
   415  	default:
   416  		// No other normalizations are possible
   417  		return ty
   418  	}
   419  }
   420  
   421  func parseLegacyDependency(s string) (string, error) {
   422  	parts := strings.Split(s, ".")
   423  	ret := parts[0]
   424  	for _, part := range parts[1:] {
   425  		if part == "*" {
   426  			break
   427  		}
   428  		if i, err := strconv.Atoi(part); err == nil {
   429  			ret = ret + fmt.Sprintf("[%d]", i)
   430  			break
   431  		}
   432  		ret = ret + "." + part
   433  	}
   434  
   435  	// The result must parse as a reference, or else we'll create an invalid
   436  	// state file.
   437  	var diags tfdiags.Diagnostics
   438  	_, diags = addrs.ParseRefStr(ret)
   439  	if diags.HasErrors() {
   440  		return "", diags.Err()
   441  	}
   442  
   443  	return ret, nil
   444  }