Skip to content

Commit 272df8b

Browse files
paddycarverPaddy
authored andcommitted
Allow opting into using json.Number in state.
Go's JSON decoder, by default, uses a lossy conversion of JSON integers to float64s. For sufficiently large integers, this yields a loss of precision, and that causes problems with diffs and plans not matching. See hashicorp/terraform-plugin-sdk#655 for more details. This PR proposes a solution: keeping the lossless json.Number representation of integers in state files. This will ensure that no precision is lost, but it comes at a cost. json.Number is a string type, not a float64 type. That means that existing state upgraders that (correctly) cast a value to float64 will break, as the value will now be surfaced to them as a json.Number, which cannot be cast to a float64. To handle this, the schema.Resource type gains a `UseJSONNumber` property. When set to new, users are opted into the new json.Number values. When left false, the default, users get the existing float64 behavior. If a resource with state upgraders wants to use the new json.Number behavior, it must update all the state upgraders for that resource to use json.Number instead of float64 or users with old state files will panic during upgrades. Note: the backwards compatibility properties of this commit are unknown at this time, and there may be a way for us to avoid using schema.Resource.UseJSONNumber, instead preserving the choice until we get to the point where TypeInt casts the number to an int. Further investigation needed.
1 parent d56de1c commit 272df8b

File tree

14 files changed

+302
-35
lines changed

14 files changed

+302
-35
lines changed

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ require (
2525
github.com/hashicorp/go-version v1.2.1
2626
github.com/hashicorp/hcl/v2 v2.3.0
2727
github.com/hashicorp/logutils v1.0.0
28-
github.com/hashicorp/terraform-exec v0.10.0
29-
github.com/hashicorp/terraform-json v0.5.0
28+
github.com/hashicorp/terraform-exec v0.11.1-0.20201216194308-834cb4cce1d3
29+
github.com/hashicorp/terraform-json v0.7.1-0.20201216193920-93fe74c2941e
3030
github.com/hashicorp/terraform-plugin-go v0.1.0
3131
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect
3232
github.com/keybase/go-crypto v0.0.0-20161004153544-93f5b35093ba

go.sum

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7I
4242
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0=
4343
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
4444
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
45+
github.com/andybalholm/crlf v0.0.0-20171020200849-670099aa064f/go.mod h1:k8feO4+kXDxro6ErPXBRTJ/ro2mf0SsFG8s7doP9kJE=
4546
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
4647
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
4748
github.com/apparentlymart/go-cidr v1.0.1 h1:NmIwLZ/KdsjIUlhf+/Np40atNXm/+lZ5txfTJ/SpF+U=
@@ -199,10 +200,10 @@ github.com/hashicorp/hcl/v2 v2.3.0 h1:iRly8YaMwTBAKhn1Ybk7VSdzbnopghktCD031P8ggU
199200
github.com/hashicorp/hcl/v2 v2.3.0/go.mod h1:d+FwDBbOLvpAM3Z6J7gPj/VoAGkNe/gm352ZhjJ/Zv8=
200201
github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
201202
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
202-
github.com/hashicorp/terraform-exec v0.10.0 h1:3nh/1e3u9gYRUQGOKWp/8wPR7ABlL2F14sZMZBrp+dM=
203-
github.com/hashicorp/terraform-exec v0.10.0/go.mod h1:tOT8j1J8rP05bZBGWXfMyU3HkLi1LWyqL3Bzsc3CJjo=
204-
github.com/hashicorp/terraform-json v0.5.0 h1:7TV3/F3y7QVSuN4r9BEXqnWqrAyeOtON8f0wvREtyzs=
205-
github.com/hashicorp/terraform-json v0.5.0/go.mod h1:eAbqb4w0pSlRmdvl8fOyHAi/+8jnkVYN28gJkSJrLhU=
203+
github.com/hashicorp/terraform-exec v0.11.1-0.20201216194308-834cb4cce1d3 h1:WJyc/gjJl+313zgUw2XIKUg0Rpn7MuZEyuHHTHoLp20=
204+
github.com/hashicorp/terraform-exec v0.11.1-0.20201216194308-834cb4cce1d3/go.mod h1:HXWkZcOyyEcYWZzed3S5ZvBVO2AqamHV+31KMV1fjog=
205+
github.com/hashicorp/terraform-json v0.7.1-0.20201216193920-93fe74c2941e h1:eRFeH76HcGTdZ0+Y/tG4BoVUKEg52rXPf9aPDd1DGHU=
206+
github.com/hashicorp/terraform-json v0.7.1-0.20201216193920-93fe74c2941e/go.mod h1:3defM4kkMfttwiE7VakJDwCd4R+umhSQnvJwORXbprE=
206207
github.com/hashicorp/terraform-plugin-go v0.1.0 h1:kyXZ0nkHxiRev/q18N40IbRRk4AV0zE/MDJkDM3u8dY=
207208
github.com/hashicorp/terraform-plugin-go v0.1.0/go.mod h1:10V6F3taeDWVAoLlkmArKttR3IULlRWFAGtQIQTIDr4=
208209
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
@@ -291,6 +292,8 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0
291292
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
292293
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
293294
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
295+
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
296+
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
294297
github.com/ulikunitz/xz v0.5.5 h1:pFrO0lVpTBXLpYw+pnLj6TbvHuyjXMfjGeCwSqCVwok=
295298
github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
296299
github.com/ulikunitz/xz v0.5.8 h1:ERv8V6GKqVi23rgu5cj9pVfVzJbOqAY2Ntl88O6c2nQ=
@@ -306,6 +309,7 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
306309
github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
307310
github.com/zclconf/go-cty v1.2.1 h1:vGMsygfmeCl4Xb6OA5U5XVAaQZ69FvoG7X2jUtQujb8=
308311
github.com/zclconf/go-cty v1.2.1/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
312+
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=
309313
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
310314
go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4=
311315
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
@@ -578,6 +582,8 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
578582
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
579583
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
580584
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
585+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
586+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
581587
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
582588
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
583589
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

helper/resource/json.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package resource
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
)
7+
8+
func unmarshalJSON(data []byte, v interface{}) error {
9+
dec := json.NewDecoder(bytes.NewReader(data))
10+
dec.UseNumber()
11+
return dec.Decode(v)
12+
}

helper/resource/state_shim.go

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package resource
22

33
import (
4+
"encoding/json"
45
"fmt"
56
"strconv"
67

@@ -70,11 +71,11 @@ func shimOutputState(so *tfjson.StateOutput) (*terraform.OutputState, error) {
7071
elements[i] = el.(bool)
7172
}
7273
os.Value = elements
73-
// unmarshalled number from JSON will always be float64
74-
case float64:
74+
// unmarshalled number from JSON will always be json.Number
75+
case json.Number:
7576
elements := make([]interface{}, len(v))
7677
for i, el := range v {
77-
elements[i] = el.(float64)
78+
elements[i] = el.(json.Number)
7879
}
7980
os.Value = elements
8081
case []interface{}:
@@ -93,10 +94,10 @@ func shimOutputState(so *tfjson.StateOutput) (*terraform.OutputState, error) {
9394
os.Type = "string"
9495
os.Value = strconv.FormatBool(v)
9596
return os, nil
96-
// unmarshalled number from JSON will always be float64
97-
case float64:
97+
// unmarshalled number from JSON will always be json.Number
98+
case json.Number:
9899
os.Type = "string"
99-
os.Value = strconv.FormatFloat(v, 'f', -1, 64)
100+
os.Value = v.String()
100101
return os, nil
101102
}
102103

@@ -155,8 +156,13 @@ func shimResourceStateKey(res *tfjson.StateResource) (string, error) {
155156

156157
var index int
157158
switch idx := res.Index.(type) {
158-
case float64:
159-
index = int(idx)
159+
case json.Number:
160+
i, err := idx.Int64()
161+
if err != nil {
162+
return "", fmt.Errorf("unexpected index value (%q) for %q, ",
163+
idx, res.Address)
164+
}
165+
index = int(i)
160166
default:
161167
return "", fmt.Errorf("unexpected index type (%T) for %q, "+
162168
"for_each is not supported", res.Index, res.Address)
@@ -256,8 +262,8 @@ func (sf *shimmedFlatmap) AddEntry(key string, value interface{}) error {
256262
return nil
257263
case bool:
258264
sf.m[key] = strconv.FormatBool(el)
259-
case float64:
260-
sf.m[key] = strconv.FormatFloat(el, 'f', -1, 64)
265+
case json.Number:
266+
sf.m[key] = el.String()
261267
case string:
262268
sf.m[key] = el
263269
case map[string]interface{}:

helper/resource/testing_new_test.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -171,18 +171,18 @@ func TestShimState(t *testing.T) {
171171
"list_of_int": {
172172
Type: "list",
173173
Value: []interface{}{
174-
// TODO: Not sure if this is expectable
175-
// but we have no way of distinguishing between int and float
176-
// due outputs being schema-less and numbers
177-
// always being unmarshaled into float64
178-
1.0, 4.0, 9.0,
174+
json.Number("1"),
175+
json.Number("4"),
176+
json.Number("9"),
179177
},
180178
Sensitive: false,
181179
},
182180
"list_of_float": {
183181
Type: "list",
184182
Value: []interface{}{
185-
1.2, 4.2, 9.8,
183+
json.Number("1.2"),
184+
json.Number("4.2"),
185+
json.Number("9.8"),
186186
},
187187
Sensitive: false,
188188
},
@@ -280,12 +280,12 @@ func TestShimState(t *testing.T) {
280280
Value: []interface{}{
281281
map[string]interface{}{
282282
"allow_bool": true,
283-
"port": float64(443),
283+
"port": json.Number("443"),
284284
"rule": "allow",
285285
},
286286
map[string]interface{}{
287287
"allow_bool": false,
288-
"port": float64(80),
288+
"port": json.Number("80"),
289289
"rule": "deny",
290290
},
291291
},
@@ -1087,7 +1087,7 @@ func TestShimState(t *testing.T) {
10871087
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
10881088
var rawState tfjson.State
10891089

1090-
err := json.Unmarshal([]byte(tc.RawState), &rawState)
1090+
err := unmarshalJSON([]byte(tc.RawState), &rawState)
10911091
if err != nil {
10921092
t.Fatal(err)
10931093
}

helper/schema/field_writer_map_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,15 @@ func TestMapFieldWriter(t *testing.T) {
8888
},
8989
},
9090

91+
"bigint": {
92+
[]string{"int"},
93+
7227701560655103598,
94+
false,
95+
map[string]string{
96+
"int": "7227701560655103598",
97+
},
98+
},
99+
91100
"string": {
92101
[]string{"string"},
93102
"42",

helper/schema/grpc_provider.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,11 @@ func (s *GRPCProviderServer) UpgradeResourceState(ctx context.Context, req *tfpr
282282
}
283283
// if there's a JSON state, we need to decode it.
284284
case len(req.RawState.JSON) > 0:
285-
err = json.Unmarshal(req.RawState.JSON, &jsonMap)
285+
if res.UseJSONNumber {
286+
err = unmarshalJSON(req.RawState.JSON, &jsonMap)
287+
} else {
288+
err = json.Unmarshal(req.RawState.JSON, &jsonMap)
289+
}
286290
if err != nil {
287291
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
288292
return resp, nil
@@ -405,7 +409,7 @@ func (s *GRPCProviderServer) upgradeFlatmapState(ctx context.Context, version in
405409
return nil, 0, err
406410
}
407411

408-
jsonMap, err := StateValueToJSONMap(newConfigVal, schemaType)
412+
jsonMap, err := stateValueToJSONMap(newConfigVal, schemaType, res.UseJSONNumber)
409413
return jsonMap, upgradedVersion, err
410414
}
411415

@@ -551,7 +555,7 @@ func (s *GRPCProviderServer) ReadResource(ctx context.Context, req *tfprotov5.Re
551555

552556
private := make(map[string]interface{})
553557
if len(req.Private) > 0 {
554-
if err := json.Unmarshal(req.Private, &private); err != nil {
558+
if err := unmarshalJSON(req.Private, &private); err != nil {
555559
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
556560
return resp, nil
557561
}
@@ -659,7 +663,7 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot
659663
}
660664
priorPrivate := make(map[string]interface{})
661665
if len(req.PriorPrivate) > 0 {
662-
if err := json.Unmarshal(req.PriorPrivate, &priorPrivate); err != nil {
666+
if err := unmarshalJSON(req.PriorPrivate, &priorPrivate); err != nil {
663667
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
664668
return resp, nil
665669
}
@@ -873,7 +877,7 @@ func (s *GRPCProviderServer) ApplyResourceChange(ctx context.Context, req *tfpro
873877

874878
private := make(map[string]interface{})
875879
if len(req.PlannedPrivate) > 0 {
876-
if err := json.Unmarshal(req.PlannedPrivate, &private); err != nil {
880+
if err := unmarshalJSON(req.PlannedPrivate, &private); err != nil {
877881
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
878882
return resp, nil
879883
}

0 commit comments

Comments
 (0)