Skip to content
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# 0.18.0 (unreleased)

ENHANCEMENTS:

- tfexec: Add `(Terraform).ApplyJSON()`, `(Terraform).DestroyJSON()`, `(Terraform).PlanJSON()` and `(Terraform).RefreshJSON()` methods ([#354](https://github.com/hashicorp/terraform-exec/pull/354))

# 0.17.3 (August 31, 2022)

Please note that terraform-exec now requires Go 1.18.
Expand Down
52 changes: 52 additions & 0 deletions tfexec/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package tfexec
import (
"context"
"fmt"
"io"
"os/exec"
"strconv"
)
Expand Down Expand Up @@ -99,13 +100,60 @@ func (tf *Terraform) Apply(ctx context.Context, opts ...ApplyOption) error {
return tf.runTerraformCmd(ctx, cmd)
}

// ApplyJSON represents the terraform apply subcommand with the `-json` flag.
// Using the `-json` flag will result in
// [machine-readable](https://developer.hashicorp.com/terraform/internals/machine-readable-ui)
// JSON being written to the supplied `io.Writer`. ApplyJSON is likely to be
// removed in a future major version in favour of Apply returning JSON by default.
func (tf *Terraform) ApplyJSON(ctx context.Context, w io.Writer, opts ...ApplyOption) error {
err := tf.compatible(ctx, tf0_15_3, nil)
if err != nil {
return fmt.Errorf("terraform apply -json was added in 0.15.3: %w", err)
}

tf.SetStdout(w)

cmd, err := tf.applyJSONCmd(ctx, opts...)
if err != nil {
return err
}

return tf.runTerraformCmd(ctx, cmd)
}

func (tf *Terraform) applyCmd(ctx context.Context, opts ...ApplyOption) (*exec.Cmd, error) {
c := defaultApplyOptions

for _, o := range opts {
o.configureApply(&c)
}

args, err := tf.buildApplyArgs(ctx, c)
if err != nil {
return nil, err
}

return tf.buildApplyCmd(ctx, c, args)
}

func (tf *Terraform) applyJSONCmd(ctx context.Context, opts ...ApplyOption) (*exec.Cmd, error) {
c := defaultApplyOptions

for _, o := range opts {
o.configureApply(&c)
}

args, err := tf.buildApplyArgs(ctx, c)
if err != nil {
return nil, err
}

args = append(args, "-json")

return tf.buildApplyCmd(ctx, c, args)
}

func (tf *Terraform) buildApplyArgs(ctx context.Context, c applyConfig) ([]string, error) {
args := []string{"apply", "-no-color", "-auto-approve", "-input=false"}

// string opts: only pass if set
Expand Down Expand Up @@ -151,6 +199,10 @@ func (tf *Terraform) applyCmd(ctx context.Context, opts ...ApplyOption) (*exec.C
}
}

return args, nil
}

func (tf *Terraform) buildApplyCmd(ctx context.Context, c applyConfig, args []string) (*exec.Cmd, error) {
// string argument: pass if set
if c.dirOrPlan != "" {
args = append(args, c.dirOrPlan)
Expand Down
60 changes: 60 additions & 0 deletions tfexec/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,63 @@ func TestApplyCmd(t *testing.T) {
}, nil, applyCmd)
})
}

func TestApplyJSONCmd(t *testing.T) {
td := t.TempDir()

tf, err := NewTerraform(td, tfVersion(t, testutil.Latest_v1))
if err != nil {
t.Fatal(err)
}

// empty env, to avoid environ mismatch in testing
tf.SetEnv(map[string]string{})

t.Run("basic", func(t *testing.T) {
applyCmd, err := tf.applyJSONCmd(context.Background(),
Backup("testbackup"),
LockTimeout("200s"),
State("teststate"),
StateOut("teststateout"),
VarFile("foo.tfvars"),
VarFile("bar.tfvars"),
Lock(false),
Parallelism(99),
Refresh(false),
Replace("aws_instance.test"),
Replace("google_pubsub_topic.test"),
Target("target1"),
Target("target2"),
Var("var1=foo"),
Var("var2=bar"),
DirOrPlan("testfile"),
)
if err != nil {
t.Fatal(err)
}

assertCmd(t, []string{
"apply",
"-no-color",
"-auto-approve",
"-input=false",
"-backup=testbackup",
"-lock-timeout=200s",
"-state=teststate",
"-state-out=teststateout",
"-var-file=foo.tfvars",
"-var-file=bar.tfvars",
"-lock=false",
"-parallelism=99",
"-refresh=false",
"-replace=aws_instance.test",
"-replace=google_pubsub_topic.test",
"-target=target1",
"-target=target2",
"-var", "var1=foo",
"-var", "var2=bar",
"-json",
"testfile",
}, nil, applyCmd)
})
}
45 changes: 45 additions & 0 deletions tfexec/destroy.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package tfexec
import (
"context"
"fmt"
"io"
"os/exec"
"strconv"
)
Expand Down Expand Up @@ -95,13 +96,53 @@ func (tf *Terraform) Destroy(ctx context.Context, opts ...DestroyOption) error {
return tf.runTerraformCmd(ctx, cmd)
}

// DestroyJSON represents the terraform destroy subcommand with the `-json` flag.
// Using the `-json` flag will result in
// [machine-readable](https://developer.hashicorp.com/terraform/internals/machine-readable-ui)
// JSON being written to the supplied `io.Writer`. DestroyJSON is likely to be
// removed in a future major version in favour of Destroy returning JSON by default.
func (tf *Terraform) DestroyJSON(ctx context.Context, w io.Writer, opts ...DestroyOption) error {
err := tf.compatible(ctx, tf0_15_3, nil)
if err != nil {
return fmt.Errorf("terraform destroy -json was added in 0.15.3: %w", err)
}

tf.SetStdout(w)

cmd, err := tf.destroyJSONCmd(ctx, opts...)
if err != nil {
return err
}

return tf.runTerraformCmd(ctx, cmd)
}

func (tf *Terraform) destroyCmd(ctx context.Context, opts ...DestroyOption) (*exec.Cmd, error) {
c := defaultDestroyOptions

for _, o := range opts {
o.configureDestroy(&c)
}

args := tf.buildDestroyArgs(c)

return tf.buildDestroyCmd(ctx, c, args)
}

func (tf *Terraform) destroyJSONCmd(ctx context.Context, opts ...DestroyOption) (*exec.Cmd, error) {
c := defaultDestroyOptions

for _, o := range opts {
o.configureDestroy(&c)
}

args := tf.buildDestroyArgs(c)
args = append(args, "-json")

return tf.buildDestroyCmd(ctx, c, args)
}

func (tf *Terraform) buildDestroyArgs(c destroyConfig) []string {
args := []string{"destroy", "-no-color", "-auto-approve", "-input=false"}

// string opts: only pass if set
Expand Down Expand Up @@ -138,6 +179,10 @@ func (tf *Terraform) destroyCmd(ctx context.Context, opts ...DestroyOption) (*ex
}
}

return args
}

func (tf *Terraform) buildDestroyCmd(ctx context.Context, c destroyConfig, args []string) (*exec.Cmd, error) {
// optional positional argument
if c.dir != "" {
args = append(args, c.dir)
Expand Down
59 changes: 59 additions & 0 deletions tfexec/destroy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,62 @@ func TestDestroyCmd(t *testing.T) {
}, nil, destroyCmd)
})
}

func TestDestroyJSONCmd(t *testing.T) {
td := t.TempDir()

tf, err := NewTerraform(td, tfVersion(t, testutil.Latest_v1))
if err != nil {
t.Fatal(err)
}

// empty env, to avoid environ mismatch in testing
tf.SetEnv(map[string]string{})

t.Run("defaults", func(t *testing.T) {
destroyCmd, err := tf.destroyJSONCmd(context.Background())
if err != nil {
t.Fatal(err)
}

assertCmd(t, []string{
"destroy",
"-no-color",
"-auto-approve",
"-input=false",
"-lock-timeout=0s",
"-lock=true",
"-parallelism=10",
"-refresh=true",
"-json",
}, nil, destroyCmd)
})

t.Run("override all defaults", func(t *testing.T) {
destroyCmd, err := tf.destroyJSONCmd(context.Background(), Backup("testbackup"), LockTimeout("200s"), State("teststate"), StateOut("teststateout"), VarFile("testvarfile"), Lock(false), Parallelism(99), Refresh(false), Target("target1"), Target("target2"), Var("var1=foo"), Var("var2=bar"), Dir("destroydir"))
if err != nil {
t.Fatal(err)
}

assertCmd(t, []string{
"destroy",
"-no-color",
"-auto-approve",
"-input=false",
"-backup=testbackup",
"-lock-timeout=200s",
"-state=teststate",
"-state-out=teststateout",
"-var-file=testvarfile",
"-lock=false",
"-parallelism=99",
"-refresh=false",
"-target=target1",
"-target=target2",
"-var", "var1=foo",
"-var", "var2=bar",
"-json",
"destroydir",
}, nil, destroyCmd)
})
}
37 changes: 37 additions & 0 deletions tfexec/internal/e2etest/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ package e2etest

import (
"context"
"io"
"regexp"
"testing"

"github.com/hashicorp/go-version"

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

func TestApply(t *testing.T) {
Expand All @@ -22,3 +25,37 @@ func TestApply(t *testing.T) {
}
})
}

func TestApplyJSON_TF014AndEarlier(t *testing.T) {
versions := []string{testutil.Latest011, testutil.Latest012, testutil.Latest013, testutil.Latest014}

runTestWithVersions(t, "basic", versions, func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
err := tf.Init(context.Background())
if err != nil {
t.Fatalf("error running Init in test directory: %s", err)
}

re := regexp.MustCompile("terraform apply -json was added in 0.15.3")

err = tf.ApplyJSON(context.Background(), io.Discard)
if err != nil && !re.MatchString(err.Error()) {
t.Fatalf("error running Apply: %s", err)
}
})
}

func TestApplyJSON_TF015AndLater(t *testing.T) {
versions := []string{testutil.Latest015, testutil.Latest_v1, testutil.Latest_v1_1}

runTestWithVersions(t, "basic", versions, func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
err := tf.Init(context.Background())
if err != nil {
t.Fatalf("error running Init in test directory: %s", err)
}

err = tf.ApplyJSON(context.Background(), io.Discard)
if err != nil {
t.Fatalf("error running Apply: %s", err)
}
})
}
37 changes: 37 additions & 0 deletions tfexec/internal/e2etest/destroy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ package e2etest

import (
"context"
"io"
"regexp"
"testing"

"github.com/hashicorp/go-version"

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

func TestDestroy(t *testing.T) {
Expand All @@ -27,3 +30,37 @@ func TestDestroy(t *testing.T) {
}
})
}

func TestDestroyJSON_TF014AndEarlier(t *testing.T) {
versions := []string{testutil.Latest011, testutil.Latest012, testutil.Latest013, testutil.Latest014}

runTestWithVersions(t, "basic", versions, func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
err := tf.Init(context.Background())
if err != nil {
t.Fatalf("error running Init in test directory: %s", err)
}

re := regexp.MustCompile("terraform destroy -json was added in 0.15.3")

err = tf.DestroyJSON(context.Background(), io.Discard)
if err != nil && !re.MatchString(err.Error()) {
t.Fatalf("error running Apply: %s", err)
}
})
}

func TestDestroyJSON_TF015AndLater(t *testing.T) {
versions := []string{testutil.Latest015, testutil.Latest_v1, testutil.Latest_v1_1}

runTestWithVersions(t, "basic", versions, func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
err := tf.Init(context.Background())
if err != nil {
t.Fatalf("error running Init in test directory: %s", err)
}

err = tf.DestroyJSON(context.Background(), io.Discard)
if err != nil {
t.Fatalf("error running Apply: %s", err)
}
})
}
Loading