From b2aad3b236e3daaee42032ccb5a7f585d17d2775 Mon Sep 17 00:00:00 2001 From: kmoe <5575356+kmoe@users.noreply.github.com> Date: Thu, 20 Aug 2020 15:11:14 +0100 Subject: [PATCH] add ShowState, ShowPlan (#54) * add ShowStateFile, ShowPlanFile * e2etests for ShowStateFile, ShowPlanFile --- tfexec/internal/e2etest/show_test.go | 213 ++++++++++++++++++ .../testdata/non_default_planfile_012/main.tf | 3 + .../non_default_planfile_012/planfilefoo | Bin 0 -> 931 bytes .../testdata/non_default_planfile_013/main.tf | 3 + .../non_default_planfile_013/planfilefoo | Bin 0 -> 919 bytes .../non_default_statefile_012/main.tf | 3 + .../non_default_statefile_012/statefilefoo | 25 ++ .../non_default_statefile_013/main.tf | 3 + .../non_default_statefile_013/statefilefoo | 25 ++ tfexec/internal/e2etest/util_test.go | 2 - tfexec/show.go | 73 ++++++ tfexec/show_test.go | 44 ++++ 12 files changed, 392 insertions(+), 2 deletions(-) create mode 100644 tfexec/internal/e2etest/testdata/non_default_planfile_012/main.tf create mode 100644 tfexec/internal/e2etest/testdata/non_default_planfile_012/planfilefoo create mode 100644 tfexec/internal/e2etest/testdata/non_default_planfile_013/main.tf create mode 100644 tfexec/internal/e2etest/testdata/non_default_planfile_013/planfilefoo create mode 100644 tfexec/internal/e2etest/testdata/non_default_statefile_012/main.tf create mode 100644 tfexec/internal/e2etest/testdata/non_default_statefile_012/statefilefoo create mode 100644 tfexec/internal/e2etest/testdata/non_default_statefile_013/main.tf create mode 100644 tfexec/internal/e2etest/testdata/non_default_statefile_013/statefilefoo diff --git a/tfexec/internal/e2etest/show_test.go b/tfexec/internal/e2etest/show_test.go index 77b458ac..078b3ee4 100644 --- a/tfexec/internal/e2etest/show_test.go +++ b/tfexec/internal/e2etest/show_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "reflect" + "runtime" "testing" "github.com/davecgh/go-spew/spew" @@ -11,6 +12,7 @@ import ( tfjson "github.com/hashicorp/terraform-json" "github.com/hashicorp/terraform-exec/tfexec" + "github.com/hashicorp/terraform-exec/tfexec/internal/testutil" ) var ( @@ -104,3 +106,214 @@ func TestShow_versionMismatch(t *testing.T) { } }) } + +// Non-default state files cannot be migrated from 0.12 to 0.13, +// so we maintain one fixture per supported version. +// See github.com/hashicorp/terraform/25920 +func TestShowStateFile012(t *testing.T) { + runTestVersions(t, []string{testutil.Latest012}, "non_default_statefile_012", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) { + expected := tfjson.State{ + FormatVersion: "0.1", + TerraformVersion: "0.12.29", + Values: &tfjson.StateValues{ + RootModule: &tfjson.StateModule{ + Resources: []*tfjson.StateResource{{ + Address: "null_resource.foo", + AttributeValues: map[string]interface{}{ + "id": "3610244792381545397", + "triggers": nil, + }, + Mode: tfjson.ManagedResourceMode, + Type: "null_resource", + Name: "foo", + ProviderName: "null", + }}, + }, + }, + } + + err := tf.Init(context.Background()) + if err != nil { + t.Fatalf("error running Init in test directory: %s", err) + } + + actual, err := tf.ShowStateFile(context.Background(), "statefilefoo") + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(actual, &expected) { + t.Fatalf("actual: %s\nexpected: %s", spew.Sdump(actual), spew.Sdump(expected)) + } + }) +} + +func TestShowStateFile013(t *testing.T) { + runTestVersions(t, []string{testutil.Latest013}, "non_default_statefile_013", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) { + expected := tfjson.State{ + FormatVersion: "0.1", + TerraformVersion: "0.13.0", + Values: &tfjson.StateValues{ + RootModule: &tfjson.StateModule{ + Resources: []*tfjson.StateResource{{ + Address: "null_resource.foo", + AttributeValues: map[string]interface{}{ + "id": "3610244792381545397", + "triggers": nil, + }, + Mode: tfjson.ManagedResourceMode, + Type: "null_resource", + Name: "foo", + ProviderName: "registry.terraform.io/hashicorp/null", + }}, + }, + }, + } + + err := tf.Init(context.Background()) + if err != nil { + t.Fatalf("error running Init in test directory: %s", err) + } + + actual, err := tf.ShowStateFile(context.Background(), "statefilefoo") + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(actual, &expected) { + t.Fatalf("actual: %s\nexpected: %s", spew.Sdump(actual), spew.Sdump(expected)) + } + }) +} + +// Plan files cannot be transferred between different Terraform versions, +// so we maintain one fixture per supported version +func TestShowPlanFile012(t *testing.T) { + runTestVersions(t, []string{testutil.Latest012}, "non_default_planfile_012", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) { + // plan file fixture was created in Linux, and is + // not compatible with Windows + if runtime.GOOS == "windows" { + t.Skip("plan file created in 0.12 on Linux is not compatible with Windows") + } + + providerName := "null" + + expected := tfjson.Plan{ + FormatVersion: "0.1", + TerraformVersion: "0.12.29", + PlannedValues: &tfjson.StateValues{ + RootModule: &tfjson.StateModule{ + Resources: []*tfjson.StateResource{{ + Address: "null_resource.foo", + AttributeValues: map[string]interface{}{ + "triggers": nil, + }, + Mode: tfjson.ManagedResourceMode, + Type: "null_resource", + Name: "foo", + ProviderName: providerName, + }}, + }, + }, + ResourceChanges: []*tfjson.ResourceChange{{ + Address: "null_resource.foo", + Mode: tfjson.ManagedResourceMode, + Type: "null_resource", + Name: "foo", + ProviderName: providerName, + Change: &tfjson.Change{ + Actions: tfjson.Actions{tfjson.ActionCreate}, + After: map[string]interface{}{"triggers": nil}, + AfterUnknown: map[string]interface{}{"id": (true)}, + }, + }}, + Config: &tfjson.Config{ + RootModule: &tfjson.ConfigModule{ + Resources: []*tfjson.ConfigResource{{ + Address: "null_resource.foo", + Mode: tfjson.ManagedResourceMode, + Type: "null_resource", + Name: "foo", + ProviderConfigKey: "null", + }}, + }, + }, + } + + err := tf.Init(context.Background()) + if err != nil { + t.Fatalf("error running Init in test directory: %s", err) + } + + actual, err := tf.ShowPlanFile(context.Background(), "planfilefoo") + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(actual, &expected) { + t.Fatalf("actual: %s\nexpected: %s", spew.Sdump(actual), spew.Sdump(expected)) + } + }) +} + +func TestShowPlanFile013(t *testing.T) { + runTestVersions(t, []string{testutil.Latest013}, "non_default_planfile_013", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) { + providerName := "registry.terraform.io/hashicorp/null" + + expected := tfjson.Plan{ + FormatVersion: "0.1", + TerraformVersion: "0.13.0", + PlannedValues: &tfjson.StateValues{ + RootModule: &tfjson.StateModule{ + Resources: []*tfjson.StateResource{{ + Address: "null_resource.foo", + AttributeValues: map[string]interface{}{ + "triggers": nil, + }, + Mode: tfjson.ManagedResourceMode, + Type: "null_resource", + Name: "foo", + ProviderName: providerName, + }}, + }, + }, + ResourceChanges: []*tfjson.ResourceChange{{ + Address: "null_resource.foo", + Mode: tfjson.ManagedResourceMode, + Type: "null_resource", + Name: "foo", + ProviderName: providerName, + Change: &tfjson.Change{ + Actions: tfjson.Actions{tfjson.ActionCreate}, + After: map[string]interface{}{"triggers": nil}, + AfterUnknown: map[string]interface{}{"id": true}, + }, + }}, + Config: &tfjson.Config{ + RootModule: &tfjson.ConfigModule{ + Resources: []*tfjson.ConfigResource{{ + Address: "null_resource.foo", + Mode: tfjson.ManagedResourceMode, + Type: "null_resource", + Name: "foo", + ProviderConfigKey: "null", + }}, + }, + }, + } + + err := tf.Init(context.Background()) + if err != nil { + t.Fatalf("error running Init in test directory: %s", err) + } + + actual, err := tf.ShowPlanFile(context.Background(), "planfilefoo") + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(actual, &expected) { + t.Fatalf("actual: %s\nexpected: %s", spew.Sdump(actual), spew.Sdump(expected)) + } + }) +} diff --git a/tfexec/internal/e2etest/testdata/non_default_planfile_012/main.tf b/tfexec/internal/e2etest/testdata/non_default_planfile_012/main.tf new file mode 100644 index 00000000..fca4eb6d --- /dev/null +++ b/tfexec/internal/e2etest/testdata/non_default_planfile_012/main.tf @@ -0,0 +1,3 @@ +resource null_resource "foo" { +} + diff --git a/tfexec/internal/e2etest/testdata/non_default_planfile_012/planfilefoo b/tfexec/internal/e2etest/testdata/non_default_planfile_012/planfilefoo new file mode 100644 index 0000000000000000000000000000000000000000..9da65dcf4a55176f3b5aa3d467074e5e0baa5934 GIT binary patch literal 931 zcmWIWW@Zs#-~hr4%p!pdNPvxjlc6N7ASW>|G=!CbQF5DY{G)_vToWIi)joT&Q{PMX zoW8D?Uucrw>GS1}bTv-tpZ4+8y~ONU)md!7#aYa0rhCd~=b-}|7A?^*wzb{5;)lb8 zh^;|2aaol$S+1UeNi~ITY3nA1mgeS86BEz&t1C+Co4_(X;_{@XBa?cVvrm8S-qg)4 zG&%Tjuu(Uk$HwJLmZ?sAHbrEk^b1L;my(fTnS~o0a)g5(1zF6x@~dY3E3R#SbL-cY z+`3ygX}ZeOZyo<9-d8`DTfZ}V-#wXcceVNgunVv-7dxhy)MGyJO#Ub z)~N{D>+HWCdEw^j?f?2-2TT$%W-H(>))LuovVC#!>i=P)%wIOPOemUc?)$Z3R^m6H zb07|UxLop8GLTn^*MY)d2PWs|rDdk;=j!U`CT8a8m88KPs|Aal6QHyl_~f@XWK&Q; z(51$LSfg%-7+Y06*4C(X5e`5rApV=XG+R~^$OGX3Z$>5&21M#ZjvP?xLj`DQ5nVHK zz=7fq0e%CSC26R!iLM(tEI@&a0CRxhRg#7~sL@SA_8=(i5P+Qt*(7Ws8Q{&z22#%g Ngn~eOYk)c!7ywIA5ugA7 literal 0 HcmV?d00001 diff --git a/tfexec/internal/e2etest/testdata/non_default_planfile_013/main.tf b/tfexec/internal/e2etest/testdata/non_default_planfile_013/main.tf new file mode 100644 index 00000000..fca4eb6d --- /dev/null +++ b/tfexec/internal/e2etest/testdata/non_default_planfile_013/main.tf @@ -0,0 +1,3 @@ +resource null_resource "foo" { +} + diff --git a/tfexec/internal/e2etest/testdata/non_default_planfile_013/planfilefoo b/tfexec/internal/e2etest/testdata/non_default_planfile_013/planfilefoo new file mode 100644 index 0000000000000000000000000000000000000000..b8658a75ac8a7c379428a163b9dd30740dc5309f GIT binary patch literal 919 zcmWIWW@Zs#-~hsvTq1!CNPvxjlc6N7ASW>|G=!Cb@!vt)c#+eFeuoW2*d8q6TA^eZ zu%yYTDy5+LhJ>VKq~Io<->P*FzD4erWm>iAiAG0btMwBB8IHn+{H<%+f92dip5YK# zWf62b?8!dQ3FhBg)LCCyh}XPuYRF}^KD&w6jJ>RB%irs-em*QJ-Lh}Rx#j-f_IDpG zXDpXhJl{XN*{E8IXRmm~Wajr*?k38_uUq|C`A(T<%ROXt>14eFZWEzl$W&ZPUtJ4fS`KXtPh4>Y?dr+@iS$D^krBsT4Bxp>L9 zpPTreFHkX5`SL(**$mA!ChJxg>sIS_$wkd~^4NK5e%r4aU-NXJYas4Zn%;LT3COF! z>po$y`;znX(lXQab9MD|6EpMlO48tN)$-Ckr?2bf7kWbb?8(md&r>da`aD%+)y}M) zI~W*YR(;P8l2rs+qlDKgQ5384Q%ZAEi}kXK^ARbb>se613Lpl8Pkw7dHU$L)U1}_d zHR^VVu~pS$ZH-zN;Q+J(;=j2|vt>1bJP;1>W@Hj!K%_Y2xB;a&RDhQL&^03m94Pt_ z-~*6Zl7@^0u;CiFaa1|C26>W8r>vh4}!uD0e+*Hge@ckyjj^m>REtL5U9F< H3B&^cbUhSA literal 0 HcmV?d00001 diff --git a/tfexec/internal/e2etest/testdata/non_default_statefile_012/main.tf b/tfexec/internal/e2etest/testdata/non_default_statefile_012/main.tf new file mode 100644 index 00000000..fca4eb6d --- /dev/null +++ b/tfexec/internal/e2etest/testdata/non_default_statefile_012/main.tf @@ -0,0 +1,3 @@ +resource null_resource "foo" { +} + diff --git a/tfexec/internal/e2etest/testdata/non_default_statefile_012/statefilefoo b/tfexec/internal/e2etest/testdata/non_default_statefile_012/statefilefoo new file mode 100644 index 00000000..48785900 --- /dev/null +++ b/tfexec/internal/e2etest/testdata/non_default_statefile_012/statefilefoo @@ -0,0 +1,25 @@ +{ + "version": 4, + "terraform_version": "0.12.29", + "serial": 1, + "lineage": "b69e96b9-250f-2004-c603-1b11dc3459c1", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "null_resource", + "name": "foo", + "provider": "provider.null", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "3610244792381545397", + "triggers": null + }, + "private": "bnVsbA==" + } + ] + } + ] +} diff --git a/tfexec/internal/e2etest/testdata/non_default_statefile_013/main.tf b/tfexec/internal/e2etest/testdata/non_default_statefile_013/main.tf new file mode 100644 index 00000000..fca4eb6d --- /dev/null +++ b/tfexec/internal/e2etest/testdata/non_default_statefile_013/main.tf @@ -0,0 +1,3 @@ +resource null_resource "foo" { +} + diff --git a/tfexec/internal/e2etest/testdata/non_default_statefile_013/statefilefoo b/tfexec/internal/e2etest/testdata/non_default_statefile_013/statefilefoo new file mode 100644 index 00000000..2af868b8 --- /dev/null +++ b/tfexec/internal/e2etest/testdata/non_default_statefile_013/statefilefoo @@ -0,0 +1,25 @@ +{ + "version": 4, + "terraform_version": "0.13.0", + "serial": 1, + "lineage": "b69e96b9-250f-2004-c603-1b11dc3459c1", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "null_resource", + "name": "foo", + "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "3610244792381545397", + "triggers": null + }, + "private": "bnVsbA==" + } + ] + } + ] +} diff --git a/tfexec/internal/e2etest/util_test.go b/tfexec/internal/e2etest/util_test.go index 9201317e..1915e39c 100644 --- a/tfexec/internal/e2etest/util_test.go +++ b/tfexec/internal/e2etest/util_test.go @@ -28,8 +28,6 @@ func runTest(t *testing.T, fixtureName string, cb func(t *testing.T, tfVersion * }, fixtureName, cb) } -// runTestVersions should probably not be used directly, better to use -// t.Skip in your test with a comment as to why you shouldn't test on a version func runTestVersions(t *testing.T, versions []string, fixtureName string, cb func(t *testing.T, tfVersion *version.Version, tf *tfexec.Terraform)) { t.Helper() diff --git a/tfexec/show.go b/tfexec/show.go index 937e8ccb..8c74c4bc 100644 --- a/tfexec/show.go +++ b/tfexec/show.go @@ -10,6 +10,8 @@ import ( tfjson "github.com/hashicorp/terraform-json" ) +// 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) (*tfjson.State, error) { err := tf.compatible(ctx, tf0_12_0, nil) if err != nil { @@ -40,6 +42,77 @@ func (tf *Terraform) Show(ctx context.Context) (*tfjson.State, error) { return &ret, nil } +// ShowStateFile reads a given state file and outputs the state. +func (tf *Terraform) ShowStateFile(ctx context.Context, statePath string) (*tfjson.State, error) { + err := tf.compatible(ctx, tf0_12_0, nil) + if err != nil { + return nil, fmt.Errorf("terraform show -json was added in 0.12.0: %w", err) + } + + if statePath == "" { + return nil, fmt.Errorf("statePath cannot be blank: use Show() if not passing statePath") + } + + showCmd := tf.showCmd(ctx, statePath) + + var ret tfjson.State + var outBuf bytes.Buffer + showCmd.Stdout = &outBuf + + err = tf.runTerraformCmd(showCmd) + if err != nil { + return nil, err + } + + err = json.Unmarshal(outBuf.Bytes(), &ret) + if err != nil { + return nil, err + } + + err = ret.Validate() + if err != nil { + return nil, err + } + + return &ret, nil +} + +// ShowPlanFile reads a given plan file and outputs the plan. +func (tf *Terraform) ShowPlanFile(ctx context.Context, planPath string) (*tfjson.Plan, error) { + err := tf.compatible(ctx, tf0_12_0, nil) + if err != nil { + return nil, fmt.Errorf("terraform show -json was added in 0.12.0: %w", err) + } + + if planPath == "" { + return nil, fmt.Errorf("planPath cannot be blank: use Show() if not passing planPath") + } + + showCmd := tf.showCmd(ctx, planPath) + + var ret tfjson.Plan + var outBuf bytes.Buffer + showCmd.Stdout = &outBuf + + err = tf.runTerraformCmd(showCmd) + if err != nil { + return nil, err + } + + err = json.Unmarshal(outBuf.Bytes(), &ret) + if err != nil { + return nil, err + } + + err = ret.Validate() + if err != nil { + return nil, err + } + + return &ret, nil + +} + func (tf *Terraform) showCmd(ctx context.Context, args ...string) *exec.Cmd { allArgs := []string{"show", "-json", "-no-color"} allArgs = append(allArgs, args...) diff --git a/tfexec/show_test.go b/tfexec/show_test.go index 79fd4b5b..381f6300 100644 --- a/tfexec/show_test.go +++ b/tfexec/show_test.go @@ -29,3 +29,47 @@ func TestShowCmd(t *testing.T) { "-no-color", }, nil, showCmd) } + +func TestShowStateFileCmd(t *testing.T) { + td := testTempDir(t) + defer os.RemoveAll(td) + + tf, err := NewTerraform(td, tfVersion(t, testutil.Latest012)) + if err != nil { + t.Fatal(err) + } + + // empty env, to avoid environ mismatch in testing + tf.SetEnv(map[string]string{}) + + showCmd := tf.showCmd(context.Background(), "statefilepath") + + assertCmd(t, []string{ + "show", + "-json", + "-no-color", + "statefilepath", + }, nil, showCmd) +} + +func TestShowPlanFileCmd(t *testing.T) { + td := testTempDir(t) + defer os.RemoveAll(td) + + tf, err := NewTerraform(td, tfVersion(t, testutil.Latest012)) + if err != nil { + t.Fatal(err) + } + + // empty env, to avoid environ mismatch in testing + tf.SetEnv(map[string]string{}) + + showCmd := tf.showCmd(context.Background(), "planfilepath") + + assertCmd(t, []string{ + "show", + "-json", + "-no-color", + "planfilepath", + }, nil, showCmd) +}