github.com/opentofu/opentofu@v1.7.1/internal/tofu/upgrade_resource_state.go (about)

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