github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/states/statefile/version3_upgrade.go (about)

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