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

Adding ApplyJSON(), DestroyJSON(), PlanJSON() and RefreshJSON() functions #354

Merged
merged 8 commits into from
Jan 24, 2023
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)
radeksimko marked this conversation as resolved.
Show resolved Hide resolved

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) {
radeksimko marked this conversation as resolved.
Show resolved Hide resolved
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)
radeksimko marked this conversation as resolved.
Show resolved Hide resolved

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