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 }