github.com/muratcelep/terraform@v1.1.0-beta2-not-internal-4/not-internal/terraform/upgrade_resource_state.go (about)

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