remove extra attrs found in state json

While removal of attributes can be handled by providers through the
UpgradeResourceState call, data sources may need to be evaluated before
reading, and they have no upgrade path in the provider protocol.

Strip out extra attributes during state decoding when they are no longer
present in the schema, and there is no schema upgrade pending.
This commit is contained in:
James Bardin 2020-08-06 21:45:57 -04:00
parent 47dc40d229
commit 998ba6e6e1
2 changed files with 244 additions and 0 deletions

View File

@ -1,6 +1,7 @@
package terraform
import (
"encoding/json"
"fmt"
"log"
@ -9,6 +10,7 @@ import (
"github.com/hashicorp/terraform/providers"
"github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/tfdiags"
"github.com/zclconf/go-cty/cty"
)
// UpgradeResourceState will, if necessary, run the provider-defined upgrade
@ -19,6 +21,17 @@ import (
// If any errors occur during upgrade, error diagnostics are returned. In that
// case it is not safe to proceed with using the original state object.
func UpgradeResourceState(addr addrs.AbsResourceInstance, provider providers.Interface, src *states.ResourceInstanceObjectSrc, currentSchema *configschema.Block, currentVersion uint64) (*states.ResourceInstanceObjectSrc, tfdiags.Diagnostics) {
// Remove any attributes from state that are not present in the schema.
// This was previously taken care of by the provider, but data sources do
// not go through the UpgradeResourceState process.
//
// Legacy flatmap state is already taken care of during conversion.
// If the schema version is be changed, then allow the provider to handle
// removed attributes.
if len(src.AttrsJSON) > 0 && src.SchemaVersion == currentVersion {
src.AttrsJSON = stripRemovedStateAttributes(src.AttrsJSON, currentSchema.ImpliedType())
}
if addr.Resource.Resource.Mode != addrs.ManagedResourceMode {
// We only do state upgrading for managed resources.
return src, nil
@ -105,3 +118,86 @@ func UpgradeResourceState(addr addrs.AbsResourceInstance, provider providers.Int
}
return new, diags
}
// stripRemovedStateAttributes deletes any attributes no longer present in the
// schema, so that the json can be correctly decoded.
func stripRemovedStateAttributes(state []byte, ty cty.Type) []byte {
jsonMap := map[string]interface{}{}
err := json.Unmarshal(state, &jsonMap)
if err != nil {
// we just log any errors here, and let the normal decode process catch
// invalid JSON.
log.Printf("[ERROR] UpgradeResourceState: %s", err)
return state
}
// if no changes were made, we return the original state to ensure nothing
// was altered in the marshaling process.
if !removeRemovedAttrs(jsonMap, ty) {
return state
}
js, err := json.Marshal(jsonMap)
if err != nil {
// if the json map was somehow mangled enough to not marhsal, something
// went horribly wrong
panic(err)
}
return js
}
// strip out the actual missing attributes, and return a bool indicating if any
// changes were made.
func removeRemovedAttrs(v interface{}, ty cty.Type) bool {
modified := false
// we're only concerned with finding maps that correspond to object
// attributes
switch v := v.(type) {
case []interface{}:
switch {
// If these aren't blocks the next call will be a noop
case ty.IsListType() || ty.IsSetType():
eTy := ty.ElementType()
for _, eV := range v {
modified = removeRemovedAttrs(eV, eTy) || modified
}
}
return modified
case map[string]interface{}:
switch {
case ty.IsMapType():
// map blocks aren't yet supported, but handle this just in case
eTy := ty.ElementType()
for _, eV := range v {
modified = removeRemovedAttrs(eV, eTy) || modified
}
return modified
case ty == cty.DynamicPseudoType:
log.Printf("[DEBUG] UpgradeResourceState: ignoring dynamic block: %#v\n", v)
return false
case ty.IsObjectType():
attrTypes := ty.AttributeTypes()
for attr, attrV := range v {
attrTy, ok := attrTypes[attr]
if !ok {
log.Printf("[DEBUG] UpgradeResourceState: attribute %q no longer present in schema", attr)
delete(v, attr)
modified = true
continue
}
modified = removeRemovedAttrs(attrV, attrTy) || modified
}
return modified
default:
// This shouldn't happen, and will fail to decode further on, so
// there's no need to handle it here.
log.Printf("[WARN] UpgradeResourceState: unexpected type %#v for map in json state", ty)
return false
}
}
return modified
}

View File

@ -0,0 +1,148 @@
package terraform
import (
"reflect"
"testing"
"github.com/zclconf/go-cty/cty"
)
func TestStripRemovedStateAttributes(t *testing.T) {
cases := []struct {
name string
state map[string]interface{}
expect map[string]interface{}
ty cty.Type
modified bool
}{
{
"removed string",
map[string]interface{}{
"a": "ok",
"b": "gone",
},
map[string]interface{}{
"a": "ok",
},
cty.Object(map[string]cty.Type{
"a": cty.String,
}),
true,
},
{
"removed null",
map[string]interface{}{
"a": "ok",
"b": nil,
},
map[string]interface{}{
"a": "ok",
},
cty.Object(map[string]cty.Type{
"a": cty.String,
}),
true,
},
{
"removed nested string",
map[string]interface{}{
"a": "ok",
"b": map[string]interface{}{
"a": "ok",
"b": "removed",
},
},
map[string]interface{}{
"a": "ok",
"b": map[string]interface{}{
"a": "ok",
},
},
cty.Object(map[string]cty.Type{
"a": cty.String,
"b": cty.Object(map[string]cty.Type{
"a": cty.String,
}),
}),
true,
},
{
"removed nested list",
map[string]interface{}{
"a": "ok",
"b": map[string]interface{}{
"a": "ok",
"b": []interface{}{"removed"},
},
},
map[string]interface{}{
"a": "ok",
"b": map[string]interface{}{
"a": "ok",
},
},
cty.Object(map[string]cty.Type{
"a": cty.String,
"b": cty.Object(map[string]cty.Type{
"a": cty.String,
}),
}),
true,
},
{
"removed keys in set of objs",
map[string]interface{}{
"a": "ok",
"b": map[string]interface{}{
"a": "ok",
"set": []interface{}{
map[string]interface{}{
"x": "ok",
"y": "removed",
},
map[string]interface{}{
"x": "ok",
"y": "removed",
},
},
},
},
map[string]interface{}{
"a": "ok",
"b": map[string]interface{}{
"a": "ok",
"set": []interface{}{
map[string]interface{}{
"x": "ok",
},
map[string]interface{}{
"x": "ok",
},
},
},
},
cty.Object(map[string]cty.Type{
"a": cty.String,
"b": cty.Object(map[string]cty.Type{
"a": cty.String,
"set": cty.Set(cty.Object(map[string]cty.Type{
"x": cty.String,
})),
}),
}),
true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
modified := removeRemovedAttrs(tc.state, tc.ty)
if !reflect.DeepEqual(tc.state, tc.expect) {
t.Fatalf("expected: %#v\n got: %#v\n", tc.expect, tc.state)
}
if modified != tc.modified {
t.Fatal("incorrect return value")
}
})
}
}