diff --git a/go.mod b/go.mod index c3887693..750d3264 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( 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 3a91018c..aa37568d 100644 --- a/go.sum +++ b/go.sum @@ -41,8 +41,8 @@ github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mO 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 15022e5a..d1d84e6a 100644 --- a/tfexec/internal/e2etest/cmp.go +++ b/tfexec/internal/e2etest/cmp.go @@ -20,8 +20,10 @@ func diffState(expected *tfjson.State, actual *tfjson.State) string { // 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 98bbd8ac..c81d9793 100644 --- a/tfexec/internal/e2etest/show_test.go +++ b/tfexec/internal/e2etest/show_test.go @@ -9,11 +9,13 @@ import ( "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 TestShowBigInt(t *testing.T) { }) } +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 5f04680b..d783027a 100644 --- a/tfexec/options.go +++ b/tfexec/options.go @@ -243,6 +243,15 @@ func GraphPlan(file string) *GraphPlanOption { 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 8bf0779f..5854af1d 100644 --- a/tfexec/show.go +++ b/tfexec/show.go @@ -14,6 +14,7 @@ import ( type showConfig struct { reattachInfo ReattachInfo + jsonNumber *UseJSONNumberOption } var defaultShowOptions = showConfig{} @@ -26,6 +27,10 @@ func (opt *ReattachOption) configureShow(conf *showConfig) { 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 @@ func (tf *Terraform) Show(ctx context.Context, opts ...ShowOption) (*tfjson.Stat 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 @@ func (tf *Terraform) ShowStateFile(ctx context.Context, statePath string, opts . 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 @@ func (tf *Terraform) ShowPlanFile(ctx context.Context, planPath string, opts ... 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