Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add ShowState, ShowPlan #54

Merged
merged 2 commits into from
Aug 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -27,8 +27,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) {
kmoe marked this conversation as resolved.
Show resolved Hide resolved
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