From 0b8f2d7321111694a1d40379e42ff67ecec6e5ce Mon Sep 17 00:00:00 2001 From: James Bardin Date: Fri, 6 Sep 2024 10:18:42 -0400 Subject: [PATCH] use json.Number for decoding state During the state upgrade process we may have attributes which are no longer part of the schema type. Because cty requires the data to strictly match the schema we can't ignore these extra attributes and must actively filter them. In order to do that we use encoding/json to decode the state in a generic manner, but we need to account for large cty.Number values which may exceed float64 precision. --- internal/terraform/upgrade_resource_state.go | 7 ++- .../terraform/upgrade_resource_state_test.go | 59 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/internal/terraform/upgrade_resource_state.go b/internal/terraform/upgrade_resource_state.go index 07d5ea3bb8..c133f483b2 100644 --- a/internal/terraform/upgrade_resource_state.go +++ b/internal/terraform/upgrade_resource_state.go @@ -4,6 +4,7 @@ package terraform import ( + "bytes" "encoding/json" "fmt" "log" @@ -136,8 +137,12 @@ func upgradeResourceState(addr addrs.AbsResourceInstance, provider providers.Int // 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 { + // we must use json.Number to avoid changing the precision of cty.Number values + decoder := json.NewDecoder(bytes.NewReader(state)) + decoder.UseNumber() + jsonMap := map[string]interface{}{} - err := json.Unmarshal(state, &jsonMap) + err := decoder.Decode(&jsonMap) if err != nil { // we just log any errors here, and let the normal decode process catch // invalid JSON. diff --git a/internal/terraform/upgrade_resource_state_test.go b/internal/terraform/upgrade_resource_state_test.go index bde9cfe061..ce998cf65e 100644 --- a/internal/terraform/upgrade_resource_state_test.go +++ b/internal/terraform/upgrade_resource_state_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/zclconf/go-cty/cty" + ctyjson "github.com/zclconf/go-cty/cty/json" ) func TestStripRemovedStateAttributes(t *testing.T) { @@ -46,6 +47,20 @@ func TestStripRemovedStateAttributes(t *testing.T) { }), true, }, + { + "has large number", + 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{}{ @@ -149,3 +164,47 @@ func TestStripRemovedStateAttributes(t *testing.T) { }) } } + +func TestStripRemovedStateAttributesDecoder(t *testing.T) { + cases := []struct { + name string + state string + expect cty.Value + }{ + { + "removed string", + `{"a": "ok","b": "gone"}`, + cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal("ok"), + }), + }, + { + "removed null", + `{"a": "ok","b": "gone"}`, + cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal("ok"), + }), + }, + { + "with large number", + `{"a": 123456789123456789.123456789,"b": "gone"}`, + cty.ObjectVal(map[string]cty.Value{ + "a": cty.MustParseNumberVal("123456789123456789.123456789"), + }), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + upgraded := stripRemovedStateAttributes([]byte(tc.state), tc.expect.Type()) + got, err := ctyjson.Unmarshal(upgraded, tc.expect.Type()) + if err != nil { + t.Fatal(err) + } + + if !tc.expect.RawEquals(got) { + t.Fatalf("expected: %#v\n got: %#v\n", tc.expect, got) + } + }) + } +}