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 }