Skip to content

Commit

Permalink
add ShowState, ShowPlan (#54)
Browse files Browse the repository at this point in the history
* add ShowStateFile, ShowPlanFile

* e2etests for ShowStateFile, ShowPlanFile
  • Loading branch information
kmoe authored Aug 20, 2020
1 parent e50c937 commit b2aad3b
Show file tree
Hide file tree
Showing 12 changed files with 392 additions and 2 deletions.
213 changes: 213 additions & 0 deletions tfexec/internal/e2etest/show_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import (
"context"
"errors"
"reflect"
"runtime"
"testing"

"github.com/davecgh/go-spew/spew"
"github.com/hashicorp/go-version"
tfjson "github.com/hashicorp/terraform-json"

"github.com/hashicorp/terraform-exec/tfexec"
"github.com/hashicorp/terraform-exec/tfexec/internal/testutil"
)

var (
Expand Down Expand Up @@ -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))
}
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
resource null_resource "foo" {
}

Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
resource null_resource "foo" {
}

Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
resource null_resource "foo" {
}

Original file line number Diff line number Diff line change
@@ -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=="
}
]
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
resource null_resource "foo" {
}

Original file line number Diff line number Diff line change
@@ -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=="
}
]
}
]
}
2 changes: 0 additions & 2 deletions tfexec/internal/e2etest/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
73 changes: 73 additions & 0 deletions tfexec/show.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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...)
Expand Down
Loading

0 comments on commit b2aad3b

Please sign in to comment.