github.com/kanishk98/terraform@v1.3.0-dev.0.20220917174235-661ca8088a6a/internal/terraform/upgrade_resource_state.go (about)

     1  package terraform
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"log"
     7  
     8  	"github.com/hashicorp/terraform/internal/addrs"
     9  	"github.com/hashicorp/terraform/internal/configs/configschema"
    10  	"github.com/hashicorp/terraform/internal/providers"
    11  	"github.com/hashicorp/terraform/internal/states"
    12  	"github.com/hashicorp/terraform/internal/tfdiags"
    13  	"github.com/zclconf/go-cty/cty"
    14  )
    15  
    16  // upgradeResourceState will, if necessary, run the provider-defined upgrade
    17  // logic against the given state object to make it compliant with the
    18  // current schema version. This is a no-op if the given state object is
    19  // already at the latest version.
    20  //
    21  // If any errors occur during upgrade, error diagnostics are returned. In that
    22  // case it is not safe to proceed with using the original state object.
    23  func upgradeResourceState(addr addrs.AbsResourceInstance, provider providers.Interface, src *states.ResourceInstanceObjectSrc, currentSchema *configschema.Block, currentVersion uint64) (*states.ResourceInstanceObjectSrc, tfdiags.Diagnostics) {
    24  	if addr.Resource.Resource.Mode != addrs.ManagedResourceMode {
    25  		// We only do state upgrading for managed resources.
    26  		// This was a part of the normal workflow in older versions and
    27  		// returned early, so we are only going to log the error for now.
    28  		log.Printf("[ERROR] data resource %s should not require state upgrade", addr)
    29  		return src, nil
    30  	}
    31  
    32  	// Remove any attributes from state that are not present in the schema.
    33  	// This was previously taken care of by the provider, but data sources do
    34  	// not go through the UpgradeResourceState process.
    35  	//
    36  	// Legacy flatmap state is already taken care of during conversion.
    37  	// If the schema version is be changed, then allow the provider to handle
    38  	// removed attributes.
    39  	if len(src.AttrsJSON) > 0 && src.SchemaVersion == currentVersion {
    40  		src.AttrsJSON = stripRemovedStateAttributes(src.AttrsJSON, currentSchema.ImpliedType())
    41  	}
    42  
    43  	stateIsFlatmap := len(src.AttrsJSON) == 0
    44  
    45  	// TODO: This should eventually use a proper FQN.
    46  	providerType := addr.Resource.Resource.ImpliedProvider()
    47  	if src.SchemaVersion > currentVersion {
    48  		log.Printf("[TRACE] upgradeResourceState: can't downgrade state for %s from version %d to %d", addr, src.SchemaVersion, currentVersion)
    49  		var diags tfdiags.Diagnostics
    50  		diags = diags.Append(tfdiags.Sourceless(
    51  			tfdiags.Error,
    52  			"Resource instance managed by newer provider version",
    53  			// This is not a very good error message, but we don't retain enough
    54  			// information in state to give good feedback on what provider
    55  			// version might be required here. :(
    56  			fmt.Sprintf("The current state of %s was created by a newer provider version than is currently selected. Upgrade the %s provider to work with this state.", addr, providerType),
    57  		))
    58  		return nil, diags
    59  	}
    60  
    61  	// If we get down here then we need to upgrade the state, with the
    62  	// provider's help.
    63  	// If this state was originally created by a version of Terraform prior to
    64  	// v0.12, this also includes translating from legacy flatmap to new-style
    65  	// representation, since only the provider has enough information to
    66  	// understand a flatmap built against an older schema.
    67  	if src.SchemaVersion != currentVersion {
    68  		log.Printf("[TRACE] upgradeResourceState: upgrading state for %s from version %d to %d using provider %q", addr, src.SchemaVersion, currentVersion, providerType)
    69  	} else {
    70  		log.Printf("[TRACE] upgradeResourceState: schema version of %s is still %d; calling provider %q for any other minor fixups", addr, currentVersion, providerType)
    71  	}
    72  
    73  	req := providers.UpgradeResourceStateRequest{
    74  		TypeName: addr.Resource.Resource.Type,
    75  
    76  		// TODO: The internal schema version representations are all using
    77  		// uint64 instead of int64, but unsigned integers aren't friendly
    78  		// to all protobuf target languages so in practice we use int64
    79  		// on the wire. In future we will change all of our internal
    80  		// representations to int64 too.
    81  		Version: int64(src.SchemaVersion),
    82  	}
    83  
    84  	if stateIsFlatmap {
    85  		req.RawStateFlatmap = src.AttrsFlat
    86  	} else {
    87  		req.RawStateJSON = src.AttrsJSON
    88  	}
    89  
    90  	resp := provider.UpgradeResourceState(req)
    91  	diags := resp.Diagnostics
    92  	if diags.HasErrors() {
    93  		return nil, diags
    94  	}
    95  
    96  	// After upgrading, the new value must conform to the current schema. When
    97  	// going over RPC this is actually already ensured by the
    98  	// marshaling/unmarshaling of the new value, but we'll check it here
    99  	// anyway for robustness, e.g. for in-process providers.
   100  	newValue := resp.UpgradedState
   101  	if errs := newValue.Type().TestConformance(currentSchema.ImpliedType()); len(errs) > 0 {
   102  		for _, err := range errs {
   103  			diags = diags.Append(tfdiags.Sourceless(
   104  				tfdiags.Error,
   105  				"Invalid resource state upgrade",
   106  				fmt.Sprintf("The %s provider upgraded the state for %s from a previous version, but produced an invalid result: %s.", providerType, addr, tfdiags.FormatError(err)),
   107  			))
   108  		}
   109  		return nil, diags
   110  	}
   111  
   112  	new, err := src.CompleteUpgrade(newValue, currentSchema.ImpliedType(), uint64(currentVersion))
   113  	if err != nil {
   114  		// We already checked for type conformance above, so getting into this
   115  		// codepath should be rare and is probably a bug somewhere under CompleteUpgrade.
   116  		diags = diags.Append(tfdiags.Sourceless(
   117  			tfdiags.Error,
   118  			"Failed to encode result of resource state upgrade",
   119  			fmt.Sprintf("Failed to encode state for %s after resource schema upgrade: %s.", addr, tfdiags.FormatError(err)),
   120  		))
   121  	}
   122  	return new, diags
   123  }
   124  
   125  // stripRemovedStateAttributes deletes any attributes no longer present in the
   126  // schema, so that the json can be correctly decoded.
   127  func stripRemovedStateAttributes(state []byte, ty cty.Type) []byte {
   128  	jsonMap := map[string]interface{}{}
   129  	err := json.Unmarshal(state, &jsonMap)
   130  	if err != nil {
   131  		// we just log any errors here, and let the normal decode process catch
   132  		// invalid JSON.
   133  		log.Printf("[ERROR] UpgradeResourceState: stripRemovedStateAttributes: %s", err)
   134  		return state
   135  	}
   136  
   137  	// if no changes were made, we return the original state to ensure nothing
   138  	// was altered in the marshaling process.
   139  	if !removeRemovedAttrs(jsonMap, ty) {
   140  		return state
   141  	}
   142  
   143  	js, err := json.Marshal(jsonMap)
   144  	if err != nil {
   145  		// if the json map was somehow mangled enough to not marhsal, something
   146  		// went horribly wrong
   147  		panic(err)
   148  	}
   149  
   150  	return js
   151  }
   152  
   153  // strip out the actual missing attributes, and return a bool indicating if any
   154  // changes were made.
   155  func removeRemovedAttrs(v interface{}, ty cty.Type) bool {
   156  	modified := false
   157  	// we're only concerned with finding maps that correspond to object
   158  	// attributes
   159  	switch v := v.(type) {
   160  	case []interface{}:
   161  		switch {
   162  		// If these aren't blocks the next call will be a noop
   163  		case ty.IsListType() || ty.IsSetType():
   164  			eTy := ty.ElementType()
   165  			for _, eV := range v {
   166  				modified = removeRemovedAttrs(eV, eTy) || modified
   167  			}
   168  		}
   169  		return modified
   170  	case map[string]interface{}:
   171  		switch {
   172  		case ty.IsMapType():
   173  			// map blocks aren't yet supported, but handle this just in case
   174  			eTy := ty.ElementType()
   175  			for _, eV := range v {
   176  				modified = removeRemovedAttrs(eV, eTy) || modified
   177  			}
   178  			return modified
   179  
   180  		case ty == cty.DynamicPseudoType:
   181  			log.Printf("[DEBUG] UpgradeResourceState: ignoring dynamic block: %#v\n", v)
   182  			return false
   183  
   184  		case ty.IsObjectType():
   185  			attrTypes := ty.AttributeTypes()
   186  			for attr, attrV := range v {
   187  				attrTy, ok := attrTypes[attr]
   188  				if !ok {
   189  					log.Printf("[DEBUG] UpgradeResourceState: attribute %q no longer present in schema", attr)
   190  					delete(v, attr)
   191  					modified = true
   192  					continue
   193  				}
   194  
   195  				modified = removeRemovedAttrs(attrV, attrTy) || modified
   196  			}
   197  			return modified
   198  		default:
   199  			// This shouldn't happen, and will fail to decode further on, so
   200  			// there's no need to handle it here.
   201  			log.Printf("[WARN] UpgradeResourceState: unexpected type %#v for map in json state", ty)
   202  			return false
   203  		}
   204  	}
   205  	return modified
   206  }