github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/states/statefile/version3_upgrade.go (about)

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