Add ShowOption for configuring JSON decoding (#427)
* Adding ShowOption for JSON handling (#426)
* Temporarily pointing at terraform-json PR for dependent changes (#426)
* Ignoring fields that differ between TF versions (#426)
* Rename structs and func (#426)
* Removing redundant struct (#426)
* Update tfexec/options.go
Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com>
* Updating to use renamed struct (#426)
* Bumping terraform-json to v0.19.0 (#426)
---------
Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com>
diff --git a/go.mod b/go.mod
index c388769..750d326 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,7 @@
github.com/google/go-cmp v0.6.0
github.com/hashicorp/go-version v1.6.0
github.com/hashicorp/hc-install v0.6.2
- github.com/hashicorp/terraform-json v0.18.0
+ github.com/hashicorp/terraform-json v0.19.0
github.com/zclconf/go-cty v1.14.1
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b
)
diff --git a/go.sum b/go.sum
index 3a91018..aa37568 100644
--- a/go.sum
+++ b/go.sum
@@ -41,8 +41,8 @@
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/hc-install v0.6.2 h1:V1k+Vraqz4olgZ9UzKiAcbman9i9scg9GgSt/U3mw/M=
github.com/hashicorp/hc-install v0.6.2/go.mod h1:2JBpd+NCFKiHiu/yYCGaPyPHhZLxXTpz8oreHa/a3Ps=
-github.com/hashicorp/terraform-json v0.18.0 h1:pCjgJEqqDESv4y0Tzdqfxr/edOIGkjs8keY42xfNBwU=
-github.com/hashicorp/terraform-json v0.18.0/go.mod h1:qdeBs11ovMzo5puhrRibdD6d2Dq6TyE/28JiU4tIQxk=
+github.com/hashicorp/terraform-json v0.19.0 h1:e9DBKC5sxDfiJT7Zoi+yRIwqLVtFur/fwK/FuE6AWsA=
+github.com/hashicorp/terraform-json v0.19.0/go.mod h1:qdeBs11ovMzo5puhrRibdD6d2Dq6TyE/28JiU4tIQxk=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
diff --git a/tfexec/internal/e2etest/cmp.go b/tfexec/internal/e2etest/cmp.go
index 15022e5..d1d84e6 100644
--- a/tfexec/internal/e2etest/cmp.go
+++ b/tfexec/internal/e2etest/cmp.go
@@ -20,8 +20,10 @@
// diffPlan returns a human-readable report of the differences between two
// plan values. It returns an empty string if the two values are equal.
-func diffPlan(expected *tfjson.Plan, actual *tfjson.Plan) string {
- return cmp.Diff(expected, actual, cmpopts.IgnoreFields(tfjson.Plan{}, "TerraformVersion"))
+func diffPlan(expected *tfjson.Plan, actual *tfjson.Plan, opts ...cmp.Option) string {
+ opts = append(opts, cmpopts.IgnoreFields(tfjson.Plan{}, "TerraformVersion", "useJSONNumber"))
+
+ return cmp.Diff(expected, actual, opts...)
}
// diffSchema returns a human-readable report of the differences between two
diff --git a/tfexec/internal/e2etest/show_test.go b/tfexec/internal/e2etest/show_test.go
index 98bbd8a..c81d979 100644
--- a/tfexec/internal/e2etest/show_test.go
+++ b/tfexec/internal/e2etest/show_test.go
@@ -9,11 +9,13 @@
"errors"
"io/ioutil"
"os"
+ "path/filepath"
"runtime"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/go-version"
tfjson "github.com/hashicorp/terraform-json"
@@ -764,6 +766,452 @@
})
}
+func TestShowFloat64(t *testing.T) {
+ runTest(t, "bigint", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
+ if tfv.LessThan(showMinVersion) {
+ t.Skip("terraform show was added in Terraform 0.12, so test is not valid")
+ }
+
+ providerName := "registry.terraform.io/hashicorp/random"
+ if tfv.LessThan(providerAddressMinVersion) {
+ providerName = "random"
+ }
+
+ formatVersion := "0.1"
+ var sensitiveValues json.RawMessage
+
+ if tfv.Core().GreaterThanOrEqual(v1_0_1) {
+ formatVersion = "0.2"
+ sensitiveValues = json.RawMessage([]byte("{}"))
+ }
+ if tfv.Core().GreaterThanOrEqual(v1_1) {
+ formatVersion = "1.0"
+ }
+
+ expected := &tfjson.State{
+ FormatVersion: formatVersion,
+ // TerraformVersion is ignored to facilitate latest version testing
+ Values: &tfjson.StateValues{
+ RootModule: &tfjson.StateModule{
+ Resources: []*tfjson.StateResource{{
+ Address: "random_integer.bigint",
+ AttributeValues: map[string]interface{}{
+ "id": "7227701560655103598",
+ "max": float64(7227701560655103598),
+ "min": float64(7227701560655103597),
+ "result": float64(7227701560655103598),
+ "seed": "12345",
+ "keepers": nil,
+ },
+ SensitiveValues: sensitiveValues,
+ Mode: tfjson.ManagedResourceMode,
+ Type: "random_integer",
+ Name: "bigint",
+ ProviderName: providerName,
+ }},
+ },
+ },
+ }
+
+ err := tf.Init(context.Background())
+ if err != nil {
+ t.Fatalf("error running Init in test directory: %s", err)
+ }
+
+ err = tf.Apply(context.Background())
+ if err != nil {
+ t.Fatalf("error running Apply in test directory: %s", err)
+ }
+
+ actual, err := tf.Show(context.Background(), tfexec.JSONNumber(false))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if diff := diffState(expected, actual); diff != "" {
+ t.Fatalf("mismatch (-want +got):\n%s", diff)
+ }
+ })
+}
+
+func TestShowStateFileBigInt(t *testing.T) {
+ runTest(t, "bigint", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
+ if tfv.LessThan(showMinVersion) {
+ t.Skip("terraform show was added in Terraform 0.12, so test is not valid")
+ }
+
+ providerName := "registry.terraform.io/hashicorp/random"
+ if tfv.LessThan(providerAddressMinVersion) {
+ providerName = "random"
+ }
+
+ formatVersion := "0.1"
+ var sensitiveValues json.RawMessage
+
+ if tfv.Core().GreaterThanOrEqual(v1_0_1) {
+ formatVersion = "0.2"
+ sensitiveValues = json.RawMessage([]byte("{}"))
+ }
+ if tfv.Core().GreaterThanOrEqual(v1_1) {
+ formatVersion = "1.0"
+ }
+
+ expected := &tfjson.State{
+ FormatVersion: formatVersion,
+ // TerraformVersion is ignored to facilitate latest version testing
+ Values: &tfjson.StateValues{
+ RootModule: &tfjson.StateModule{
+ Resources: []*tfjson.StateResource{{
+ Address: "random_integer.bigint",
+ AttributeValues: map[string]interface{}{
+ "id": "7227701560655103598",
+ "max": json.Number("7227701560655103598"),
+ "min": json.Number("7227701560655103597"),
+ "result": json.Number("7227701560655103598"),
+ "seed": "12345",
+ "keepers": nil,
+ },
+ SensitiveValues: sensitiveValues,
+ Mode: tfjson.ManagedResourceMode,
+ Type: "random_integer",
+ Name: "bigint",
+ ProviderName: providerName,
+ }},
+ },
+ },
+ }
+
+ err := tf.Init(context.Background())
+ if err != nil {
+ t.Fatalf("error running Init in test directory: %s", err)
+ }
+
+ err = tf.Apply(context.Background())
+ if err != nil {
+ t.Fatalf("error running Apply in test directory: %s", err)
+ }
+
+ actual, err := tf.ShowStateFile(context.Background(), filepath.Join(tf.WorkingDir(), "terraform.tfstate"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if diff := diffState(expected, actual); diff != "" {
+ t.Fatalf("mismatch (-want +got):\n%s", diff)
+ }
+ })
+}
+
+func TestShowStateFileFloat64(t *testing.T) {
+ runTest(t, "bigint", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
+ if tfv.LessThan(showMinVersion) {
+ t.Skip("terraform show was added in Terraform 0.12, so test is not valid")
+ }
+
+ providerName := "registry.terraform.io/hashicorp/random"
+ if tfv.LessThan(providerAddressMinVersion) {
+ providerName = "random"
+ }
+
+ formatVersion := "0.1"
+ var sensitiveValues json.RawMessage
+
+ if tfv.Core().GreaterThanOrEqual(v1_0_1) {
+ formatVersion = "0.2"
+ sensitiveValues = json.RawMessage([]byte("{}"))
+ }
+ if tfv.Core().GreaterThanOrEqual(v1_1) {
+ formatVersion = "1.0"
+ }
+
+ expected := &tfjson.State{
+ FormatVersion: formatVersion,
+ // TerraformVersion is ignored to facilitate latest version testing
+ Values: &tfjson.StateValues{
+ RootModule: &tfjson.StateModule{
+ Resources: []*tfjson.StateResource{{
+ Address: "random_integer.bigint",
+ AttributeValues: map[string]interface{}{
+ "id": "7227701560655103598",
+ "max": float64(7227701560655103598),
+ "min": float64(7227701560655103598),
+ "result": float64(7227701560655103598),
+ "seed": "12345",
+ "keepers": nil,
+ },
+ SensitiveValues: sensitiveValues,
+ Mode: tfjson.ManagedResourceMode,
+ Type: "random_integer",
+ Name: "bigint",
+ ProviderName: providerName,
+ }},
+ },
+ },
+ }
+
+ err := tf.Init(context.Background())
+ if err != nil {
+ t.Fatalf("error running Init in test directory: %s", err)
+ }
+
+ err = tf.Apply(context.Background())
+ if err != nil {
+ t.Fatalf("error running Apply in test directory: %s", err)
+ }
+
+ actual, err := tf.ShowStateFile(context.Background(), filepath.Join(tf.WorkingDir(), "terraform.tfstate"), tfexec.JSONNumber(false))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if diff := diffState(expected, actual); diff != "" {
+ t.Fatalf("mismatch (-want +got):\n%s", diff)
+ }
+ })
+}
+
+func TestShowPlanFileBigInt(t *testing.T) {
+ runTest(t, "bigint", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
+ if tfv.LessThan(showMinVersion) {
+ t.Skip("terraform show was added in Terraform 0.12, so test is not valid")
+ }
+
+ providerName := "registry.terraform.io/hashicorp/random"
+ if tfv.LessThan(providerAddressMinVersion) {
+ providerName = "random"
+ }
+
+ var sensitiveValues json.RawMessage
+
+ if tfv.Core().GreaterThanOrEqual(v1_0_1) {
+ sensitiveValues = json.RawMessage([]byte("{}"))
+ }
+ if tfv.Core().GreaterThanOrEqual(v1_1) {
+ }
+
+ expected := &tfjson.Plan{
+ // TerraformVersion is ignored to facilitate latest version testing
+ PlannedValues: &tfjson.StateValues{
+ RootModule: &tfjson.StateModule{
+ Resources: []*tfjson.StateResource{{
+ Address: "random_integer.bigint",
+ AttributeValues: map[string]interface{}{
+ "keepers": nil,
+ "max": json.Number("7227701560655103598"),
+ "min": json.Number("7227701560655103597"),
+ "seed": "12345",
+ },
+ SensitiveValues: sensitiveValues,
+ Mode: tfjson.ManagedResourceMode,
+ Type: "random_integer",
+ Name: "bigint",
+ ProviderName: providerName,
+ }},
+ },
+ },
+ ResourceChanges: []*tfjson.ResourceChange{{
+ Address: "random_integer.bigint",
+ Mode: tfjson.ManagedResourceMode,
+ Type: "random_integer",
+ Name: "bigint",
+ ProviderName: providerName,
+ Change: &tfjson.Change{
+ Actions: tfjson.Actions{tfjson.ActionCreate},
+ After: map[string]interface{}{
+ "keepers": nil,
+ "max": json.Number("7227701560655103598"),
+ "min": json.Number("7227701560655103597"),
+ "seed": "12345",
+ },
+ AfterUnknown: map[string]interface{}{
+ "id": true,
+ "result": true,
+ },
+ },
+ }},
+ Config: &tfjson.Config{
+ RootModule: &tfjson.ConfigModule{
+ Resources: []*tfjson.ConfigResource{{
+ Address: "random_integer.bigint",
+ Mode: tfjson.ManagedResourceMode,
+ Type: "random_integer",
+ Name: "bigint",
+ ProviderConfigKey: "random",
+ Expressions: map[string]*tfjson.Expression{
+ "max": {
+ ExpressionData: &tfjson.ExpressionData{
+ ConstantValue: float64(7227701560655104000),
+ },
+ },
+ "min": {
+ ExpressionData: &tfjson.ExpressionData{
+ ConstantValue: float64(7227701560655104000),
+ },
+ },
+ "seed": {
+ ExpressionData: &tfjson.ExpressionData{
+ ConstantValue: float64(12345),
+ },
+ },
+ },
+ }},
+ },
+ },
+ }
+
+ err := tf.Init(context.Background())
+ if err != nil {
+ t.Fatalf("error running Init in test directory: %s", err)
+ }
+
+ _, err = tf.Plan(context.Background(), tfexec.Out(filepath.Join(tf.WorkingDir(), "tfplan")))
+ if err != nil {
+ t.Fatalf("error running Plan in test directory: %s", err)
+ }
+
+ actual, err := tf.ShowPlanFile(context.Background(), filepath.Join(tf.WorkingDir(), "tfplan"), tfexec.JSONNumber(true))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ opts := []cmp.Option{
+ cmpopts.IgnoreFields(tfjson.Change{}, "BeforeSensitive"),
+ cmpopts.IgnoreFields(tfjson.Change{}, "AfterSensitive"),
+ cmpopts.IgnoreFields(tfjson.Config{}, "ProviderConfigs"),
+ cmpopts.IgnoreFields(tfjson.Plan{}, "FormatVersion"),
+ cmpopts.IgnoreFields(tfjson.Plan{}, "Timestamp"),
+ }
+
+ if diff := diffPlan(expected, actual, opts...); diff != "" {
+ t.Fatalf("mismatch (-want +got):\n%s", diff)
+ }
+ })
+}
+
+func TestShowPlanFileFloat64(t *testing.T) {
+ runTest(t, "bigint", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
+ if tfv.LessThan(showMinVersion) {
+ t.Skip("terraform show was added in Terraform 0.12, so test is not valid")
+ }
+
+ providerName := "registry.terraform.io/hashicorp/random"
+ if tfv.LessThan(providerAddressMinVersion) {
+ providerName = "random"
+ }
+
+ var sensitiveValues json.RawMessage
+
+ if tfv.Core().GreaterThanOrEqual(v1_0_1) {
+ sensitiveValues = json.RawMessage([]byte("{}"))
+ }
+ if tfv.Core().GreaterThanOrEqual(v1_1) {
+ }
+
+ expected := &tfjson.Plan{
+ // TerraformVersion is ignored to facilitate latest version testing
+ PlannedValues: &tfjson.StateValues{
+ RootModule: &tfjson.StateModule{
+ Resources: []*tfjson.StateResource{{
+ Address: "random_integer.bigint",
+ AttributeValues: map[string]interface{}{
+ "keepers": nil,
+ "max": float64(7227701560655103598),
+ "min": float64(7227701560655103597),
+ "seed": "12345",
+ },
+ SensitiveValues: sensitiveValues,
+ Mode: tfjson.ManagedResourceMode,
+ Type: "random_integer",
+ Name: "bigint",
+ ProviderName: providerName,
+ }},
+ },
+ },
+ ResourceChanges: []*tfjson.ResourceChange{{
+ Address: "random_integer.bigint",
+ Mode: tfjson.ManagedResourceMode,
+ Type: "random_integer",
+ Name: "bigint",
+ ProviderName: providerName,
+ Change: &tfjson.Change{
+ Actions: tfjson.Actions{tfjson.ActionCreate},
+ After: map[string]interface{}{
+ "keepers": nil,
+ "max": float64(7227701560655103598),
+ "min": float64(7227701560655103597),
+ "seed": "12345",
+ },
+ AfterUnknown: map[string]interface{}{
+ "id": true,
+ "result": true,
+ },
+ },
+ }},
+ Config: &tfjson.Config{
+ ProviderConfigs: map[string]*tfjson.ProviderConfig{
+ "random": {
+ Name: "random",
+ VersionConstraint: "3.1.3",
+ },
+ },
+ RootModule: &tfjson.ConfigModule{
+ Resources: []*tfjson.ConfigResource{{
+ Address: "random_integer.bigint",
+ Mode: tfjson.ManagedResourceMode,
+ Type: "random_integer",
+ Name: "bigint",
+ ProviderConfigKey: "random",
+ Expressions: map[string]*tfjson.Expression{
+ "max": {
+ ExpressionData: &tfjson.ExpressionData{
+ ConstantValue: float64(7227701560655104000),
+ },
+ },
+ "min": {
+ ExpressionData: &tfjson.ExpressionData{
+ ConstantValue: float64(7227701560655104000),
+ },
+ },
+ "seed": {
+ ExpressionData: &tfjson.ExpressionData{
+ ConstantValue: float64(12345),
+ },
+ },
+ },
+ }},
+ },
+ },
+ }
+
+ err := tf.Init(context.Background())
+ if err != nil {
+ t.Fatalf("error running Init in test directory: %s", err)
+ }
+
+ _, err = tf.Plan(context.Background(), tfexec.Out(filepath.Join(tf.WorkingDir(), "tfplan")))
+ if err != nil {
+ t.Fatalf("error running Plan in test directory: %s", err)
+ }
+
+ actual, err := tf.ShowPlanFile(context.Background(), filepath.Join(tf.WorkingDir(), "tfplan"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ opts := []cmp.Option{
+ cmpopts.IgnoreFields(tfjson.Change{}, "BeforeSensitive"),
+ cmpopts.IgnoreFields(tfjson.Change{}, "AfterSensitive"),
+ cmpopts.IgnoreFields(tfjson.Config{}, "ProviderConfigs"),
+ cmpopts.IgnoreFields(tfjson.Plan{}, "FormatVersion"),
+ cmpopts.IgnoreFields(tfjson.Plan{}, "Timestamp"),
+ }
+
+ if diff := diffPlan(expected, actual, opts...); diff != "" {
+ t.Fatalf("mismatch (-want +got):\n%s", diff)
+ }
+ })
+}
+
// Since our plan strings are not large, prefer simple cross-platform
// normalization handling over pulling in a dependency.
func normalizePlanOutput(str string) string {
diff --git a/tfexec/options.go b/tfexec/options.go
index 5f04680..d783027 100644
--- a/tfexec/options.go
+++ b/tfexec/options.go
@@ -243,6 +243,15 @@
return &GraphPlanOption{file}
}
+type UseJSONNumberOption struct {
+ useJSONNumber bool
+}
+
+// JSONNumber determines how numerical values are handled during JSON decoding.
+func JSONNumber(useJSONNumber bool) *UseJSONNumberOption {
+ return &UseJSONNumberOption{useJSONNumber}
+}
+
type PlatformOption struct {
platform string
}
diff --git a/tfexec/show.go b/tfexec/show.go
index 8bf0779..5854af1 100644
--- a/tfexec/show.go
+++ b/tfexec/show.go
@@ -14,6 +14,7 @@
type showConfig struct {
reattachInfo ReattachInfo
+ jsonNumber *UseJSONNumberOption
}
var defaultShowOptions = showConfig{}
@@ -26,6 +27,10 @@
conf.reattachInfo = opt.info
}
+func (opt *UseJSONNumberOption) configureShow(conf *showConfig) {
+ conf.jsonNumber = opt
+}
+
// Show reads the default state path and outputs the state.
// To read a state or plan file, ShowState or ShowPlan must be used instead.
func (tf *Terraform) Show(ctx context.Context, opts ...ShowOption) (*tfjson.State, error) {
@@ -53,6 +58,11 @@
var ret tfjson.State
ret.UseJSONNumber(true)
+
+ if c.jsonNumber != nil {
+ ret.UseJSONNumber(c.jsonNumber.useJSONNumber)
+ }
+
err = tf.runTerraformCmdJSON(ctx, showCmd, &ret)
if err != nil {
return nil, err
@@ -96,6 +106,11 @@
var ret tfjson.State
ret.UseJSONNumber(true)
+
+ if c.jsonNumber != nil {
+ ret.UseJSONNumber(c.jsonNumber.useJSONNumber)
+ }
+
err = tf.runTerraformCmdJSON(ctx, showCmd, &ret)
if err != nil {
return nil, err
@@ -138,6 +153,11 @@
showCmd := tf.showCmd(ctx, true, mergeEnv, planPath)
var ret tfjson.Plan
+
+ if c.jsonNumber != nil {
+ ret.UseJSONNumber(c.jsonNumber.useJSONNumber)
+ }
+
err = tf.runTerraformCmdJSON(ctx, showCmd, &ret)
if err != nil {
return nil, err