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