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 }