From c387e4f7bf9806ce89f0cb8757ec5894e7063092 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Tue, 10 Jan 2023 16:34:00 +0000 Subject: [PATCH 1/8] Adding ApplyJSON(), DestroyJSON(), PlanJSON() and RefreshJSON() functions (#353) --- tfexec/apply.go | 27 ++++++++++++++- tfexec/apply_test.go | 60 +++++++++++++++++++++++++++++++++ tfexec/destroy.go | 27 ++++++++++++++- tfexec/destroy_test.go | 59 ++++++++++++++++++++++++++++++++ tfexec/plan.go | 37 ++++++++++++++++++++ tfexec/plan_test.go | 76 ++++++++++++++++++++++++++++++++++++++++++ tfexec/refresh.go | 27 ++++++++++++++- tfexec/refresh_test.go | 53 +++++++++++++++++++++++++++++ 8 files changed, 363 insertions(+), 3 deletions(-) diff --git a/tfexec/apply.go b/tfexec/apply.go index 40d9e69b..95c345e8 100644 --- a/tfexec/apply.go +++ b/tfexec/apply.go @@ -3,6 +3,7 @@ package tfexec import ( "context" "fmt" + "io" "os/exec" "strconv" ) @@ -90,7 +91,7 @@ func (opt *ReattachOption) configureApply(conf *applyConfig) { conf.reattachInfo = opt.info } -// Apply represents the terraform apply subcommand. +// Apply represents the Terraform apply subcommand. func (tf *Terraform) Apply(ctx context.Context, opts ...ApplyOption) error { cmd, err := tf.applyCmd(ctx, opts...) if err != nil { @@ -99,6 +100,30 @@ 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. +func (tf *Terraform) ApplyJSON(ctx context.Context, w io.Writer, opts ...ApplyOption) error { + tf.SetStdout(w) + + cmd, err := tf.applyJSONCmd(ctx, opts...) + if err != nil { + return err + } + + return tf.runTerraformCmd(ctx, cmd) +} + +func (tf *Terraform) applyJSONCmd(ctx context.Context, opts ...ApplyOption) (*exec.Cmd, error) { + cmd, err := tf.applyCmd(ctx, opts...) + if err != nil { + return nil, err + } + + cmd.Args = append(cmd.Args[:3], cmd.Args[2:]...) + cmd.Args[2] = "-json" + + return cmd, nil +} + func (tf *Terraform) applyCmd(ctx context.Context, opts ...ApplyOption) (*exec.Cmd, error) { c := defaultApplyOptions diff --git a/tfexec/apply_test.go b/tfexec/apply_test.go index 1cf2f562..8915ae90 100644 --- a/tfexec/apply_test.go +++ b/tfexec/apply_test.go @@ -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", + "-json", + "-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", + "testfile", + }, nil, applyCmd) + }) +} diff --git a/tfexec/destroy.go b/tfexec/destroy.go index 8011c0ba..eaf2e0f4 100644 --- a/tfexec/destroy.go +++ b/tfexec/destroy.go @@ -3,6 +3,7 @@ package tfexec import ( "context" "fmt" + "io" "os/exec" "strconv" ) @@ -86,7 +87,7 @@ func (opt *ReattachOption) configureDestroy(conf *destroyConfig) { conf.reattachInfo = opt.info } -// Destroy represents the terraform destroy subcommand. +// Destroy represents the Terraform destroy subcommand. func (tf *Terraform) Destroy(ctx context.Context, opts ...DestroyOption) error { cmd, err := tf.destroyCmd(ctx, opts...) if err != nil { @@ -95,6 +96,30 @@ 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. +func (tf *Terraform) DestroyJSON(ctx context.Context, w io.Writer, opts ...DestroyOption) error { + tf.SetStdout(w) + + cmd, err := tf.destroyJSONCmd(ctx, opts...) + if err != nil { + return err + } + + return tf.runTerraformCmd(ctx, cmd) +} + +func (tf *Terraform) destroyJSONCmd(ctx context.Context, opts ...DestroyOption) (*exec.Cmd, error) { + cmd, err := tf.destroyCmd(ctx, opts...) + if err != nil { + return nil, err + } + + cmd.Args = append(cmd.Args[:3], cmd.Args[2:]...) + cmd.Args[2] = "-json" + + return cmd, nil +} + func (tf *Terraform) destroyCmd(ctx context.Context, opts ...DestroyOption) (*exec.Cmd, error) { c := defaultDestroyOptions diff --git a/tfexec/destroy_test.go b/tfexec/destroy_test.go index eb28f58a..1447498c 100644 --- a/tfexec/destroy_test.go +++ b/tfexec/destroy_test.go @@ -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", + "-json", + "-no-color", + "-auto-approve", + "-input=false", + "-lock-timeout=0s", + "-lock=true", + "-parallelism=10", + "-refresh=true", + }, 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", + "-json", + "-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", + "destroydir", + }, nil, destroyCmd) + }) +} diff --git a/tfexec/plan.go b/tfexec/plan.go index bf41094b..23df8c8d 100644 --- a/tfexec/plan.go +++ b/tfexec/plan.go @@ -3,6 +3,7 @@ package tfexec import ( "context" "fmt" + "io" "os/exec" "strconv" ) @@ -108,6 +109,42 @@ func (tf *Terraform) Plan(ctx context.Context, opts ...PlanOption) (bool, error) return false, err } +// PlanJSON executes `terraform plan` with the specified options as well as the +// `-json` flag and waits for it to complete. +// +// The returned boolean is false when the plan diff is empty (no changes) and +// true when the plan diff is non-empty (changes present). +// +// The returned error is nil if `terraform plan` has been executed and exits +// with either 0 or 2. +func (tf *Terraform) PlanJSON(ctx context.Context, w io.Writer, opts ...PlanOption) (bool, error) { + tf.SetStdout(w) + + cmd, err := tf.planJSONCmd(ctx, opts...) + if err != nil { + return false, err + } + + err = tf.runTerraformCmd(ctx, cmd) + if err != nil && cmd.ProcessState.ExitCode() == 2 { + return true, nil + } + + return false, err +} + +func (tf *Terraform) planJSONCmd(ctx context.Context, opts ...PlanOption) (*exec.Cmd, error) { + cmd, err := tf.planCmd(ctx, opts...) + if err != nil { + return nil, err + } + + cmd.Args = append(cmd.Args[:3], cmd.Args[2:]...) + cmd.Args[2] = "-json" + + return cmd, nil +} + func (tf *Terraform) planCmd(ctx context.Context, opts ...PlanOption) (*exec.Cmd, error) { c := defaultPlanOptions diff --git a/tfexec/plan_test.go b/tfexec/plan_test.go index 7a467ac3..c0d03c28 100644 --- a/tfexec/plan_test.go +++ b/tfexec/plan_test.go @@ -80,3 +80,79 @@ func TestPlanCmd(t *testing.T) { }, nil, planCmd) }) } + +func TestPlanJSONCmd(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) { + planCmd, err := tf.planJSONCmd(context.Background()) + if err != nil { + t.Fatal(err) + } + + assertCmd(t, []string{ + "plan", + "-json", + "-no-color", + "-input=false", + "-detailed-exitcode", + "-lock-timeout=0s", + "-lock=true", + "-parallelism=10", + "-refresh=true", + }, nil, planCmd) + }) + + t.Run("override all defaults", func(t *testing.T) { + planCmd, err := tf.planJSONCmd(context.Background(), + Destroy(true), + Lock(false), + LockTimeout("22s"), + Out("whale"), + Parallelism(42), + Refresh(false), + Replace("ford.prefect"), + Replace("arthur.dent"), + State("marvin"), + Target("zaphod"), + Target("beeblebrox"), + Var("android=paranoid"), + Var("brain_size=planet"), + VarFile("trillian"), + Dir("earth")) + if err != nil { + t.Fatal(err) + } + + assertCmd(t, []string{ + "plan", + "-json", + "-no-color", + "-input=false", + "-detailed-exitcode", + "-lock-timeout=22s", + "-out=whale", + "-state=marvin", + "-var-file=trillian", + "-lock=false", + "-parallelism=42", + "-refresh=false", + "-replace=ford.prefect", + "-replace=arthur.dent", + "-destroy", + "-target=zaphod", + "-target=beeblebrox", + "-var", "android=paranoid", + "-var", "brain_size=planet", + "earth", + }, nil, planCmd) + }) +} diff --git a/tfexec/refresh.go b/tfexec/refresh.go index 78f6b4b5..3af74cd6 100644 --- a/tfexec/refresh.go +++ b/tfexec/refresh.go @@ -2,6 +2,7 @@ package tfexec import ( "context" + "io" "os/exec" "strconv" ) @@ -69,7 +70,7 @@ func (opt *VarFileOption) configureRefresh(conf *refreshConfig) { conf.varFiles = append(conf.varFiles, opt.path) } -// Refresh represents the terraform refresh subcommand. +// Refresh represents the Terraform refresh subcommand. func (tf *Terraform) Refresh(ctx context.Context, opts ...RefreshCmdOption) error { cmd, err := tf.refreshCmd(ctx, opts...) if err != nil { @@ -78,6 +79,30 @@ func (tf *Terraform) Refresh(ctx context.Context, opts ...RefreshCmdOption) erro return tf.runTerraformCmd(ctx, cmd) } +// RefreshJSON represents the Terraform refresh subcommand with the `-json` flag. +func (tf *Terraform) RefreshJSON(ctx context.Context, w io.Writer, opts ...RefreshCmdOption) error { + tf.SetStdout(w) + + cmd, err := tf.refreshJSONCmd(ctx, opts...) + if err != nil { + return err + } + + return tf.runTerraformCmd(ctx, cmd) +} + +func (tf *Terraform) refreshJSONCmd(ctx context.Context, opts ...RefreshCmdOption) (*exec.Cmd, error) { + cmd, err := tf.refreshCmd(ctx, opts...) + if err != nil { + return nil, err + } + + cmd.Args = append(cmd.Args[:3], cmd.Args[2:]...) + cmd.Args[2] = "-json" + + return cmd, nil +} + func (tf *Terraform) refreshCmd(ctx context.Context, opts ...RefreshCmdOption) (*exec.Cmd, error) { c := defaultRefreshOptions diff --git a/tfexec/refresh_test.go b/tfexec/refresh_test.go index bd4a94c4..d03824e6 100644 --- a/tfexec/refresh_test.go +++ b/tfexec/refresh_test.go @@ -57,3 +57,56 @@ func TestRefreshCmd(t *testing.T) { }, nil, refreshCmd) }) } + +func TestRefreshJSONCmd(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) { + refreshCmd, err := tf.refreshJSONCmd(context.Background()) + if err != nil { + t.Fatal(err) + } + + assertCmd(t, []string{ + "refresh", + "-json", + "-no-color", + "-input=false", + "-lock-timeout=0s", + "-lock=true", + }, nil, refreshCmd) + }) + + t.Run("override all defaults", func(t *testing.T) { + refreshCmd, err := tf.refreshJSONCmd(context.Background(), Backup("testbackup"), LockTimeout("200s"), State("teststate"), StateOut("teststateout"), VarFile("testvarfile"), Lock(false), Target("target1"), Target("target2"), Var("var1=foo"), Var("var2=bar"), Dir("refreshdir")) + if err != nil { + t.Fatal(err) + } + + assertCmd(t, []string{ + "refresh", + "-json", + "-no-color", + "-input=false", + "-backup=testbackup", + "-lock-timeout=200s", + "-state=teststate", + "-state-out=teststateout", + "-var-file=testvarfile", + "-lock=false", + "-target=target1", + "-target=target2", + "-var", "var1=foo", + "-var", "var2=bar", + "refreshdir", + }, nil, refreshCmd) + }) +} From e2c259b86abaa14717dbf5e58f3fc23ca1ca03c1 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Mon, 16 Jan 2023 08:48:32 +0000 Subject: [PATCH 2/8] Adding version compatibility check and e2e tests (#353) --- tfexec/apply.go | 8 +++++ tfexec/destroy.go | 8 +++++ tfexec/internal/e2etest/apply_test.go | 37 ++++++++++++++++++++++ tfexec/internal/e2etest/destroy_test.go | 37 ++++++++++++++++++++++ tfexec/internal/e2etest/plan_test.go | 42 +++++++++++++++++++++++++ tfexec/internal/e2etest/refresh_test.go | 37 ++++++++++++++++++++++ tfexec/internal/e2etest/util_test.go | 41 ++++++++++++++++++++++++ tfexec/plan.go | 9 ++++++ tfexec/refresh.go | 9 ++++++ tfexec/version.go | 1 + 10 files changed, 229 insertions(+) diff --git a/tfexec/apply.go b/tfexec/apply.go index 95c345e8..1fc0b9db 100644 --- a/tfexec/apply.go +++ b/tfexec/apply.go @@ -101,7 +101,15 @@ func (tf *Terraform) Apply(ctx context.Context, opts ...ApplyOption) error { } // 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`. 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...) diff --git a/tfexec/destroy.go b/tfexec/destroy.go index eaf2e0f4..f015b1c2 100644 --- a/tfexec/destroy.go +++ b/tfexec/destroy.go @@ -97,7 +97,15 @@ func (tf *Terraform) Destroy(ctx context.Context, opts ...DestroyOption) error { } // 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`. 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 apply -json was added in 0.15.3: %w", err) + } + tf.SetStdout(w) cmd, err := tf.destroyJSONCmd(ctx, opts...) diff --git a/tfexec/internal/e2etest/apply_test.go b/tfexec/internal/e2etest/apply_test.go index 8c3154a4..5211a3db 100644 --- a/tfexec/internal/e2etest/apply_test.go +++ b/tfexec/internal/e2etest/apply_test.go @@ -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) { @@ -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) + } + }) +} diff --git a/tfexec/internal/e2etest/destroy_test.go b/tfexec/internal/e2etest/destroy_test.go index da0c856d..88022e96 100644 --- a/tfexec/internal/e2etest/destroy_test.go +++ b/tfexec/internal/e2etest/destroy_test.go @@ -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) { @@ -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 apply -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) + } + }) +} diff --git a/tfexec/internal/e2etest/plan_test.go b/tfexec/internal/e2etest/plan_test.go index a5282b7f..58b7f65a 100644 --- a/tfexec/internal/e2etest/plan_test.go +++ b/tfexec/internal/e2etest/plan_test.go @@ -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 TestPlan(t *testing.T) { @@ -45,5 +48,44 @@ func TestPlanWithState(t *testing.T) { t.Fatalf("expected: false, got: %t", hasChanges) } }) +} + +func TestPlanJSON_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") + + hasChanges, err := tf.PlanJSON(context.Background(), io.Discard) + if err != nil && !re.MatchString(err.Error()) { + t.Fatalf("error running Apply: %s", err) + } + if hasChanges { + t.Fatalf("expected: false, got: %t", hasChanges) + } + }) +} + +func TestPlanJSON_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) + } + + hasChanges, err := tf.PlanJSON(context.Background(), io.Discard) + if err != nil { + t.Fatalf("error running Apply: %s", err) + } + if !hasChanges { + t.Fatalf("expected: true, got: %t", hasChanges) + } + }) } diff --git a/tfexec/internal/e2etest/refresh_test.go b/tfexec/internal/e2etest/refresh_test.go index 40dbb9d7..d359a7aa 100644 --- a/tfexec/internal/e2etest/refresh_test.go +++ b/tfexec/internal/e2etest/refresh_test.go @@ -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 TestRefresh(t *testing.T) { @@ -27,3 +30,37 @@ func TestRefresh(t *testing.T) { } }) } + +func TestRefreshJSON_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.RefreshJSON(context.Background(), io.Discard) + if err != nil && !re.MatchString(err.Error()) { + t.Fatalf("error running Apply: %s", err) + } + }) +} + +func TestRefreshJSON_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.RefreshJSON(context.Background(), io.Discard) + if err != nil { + t.Fatalf("error running Apply: %s", err) + } + }) +} diff --git a/tfexec/internal/e2etest/util_test.go b/tfexec/internal/e2etest/util_test.go index 54d4f335..284c5c60 100644 --- a/tfexec/internal/e2etest/util_test.go +++ b/tfexec/internal/e2etest/util_test.go @@ -76,6 +76,47 @@ func runTest(t *testing.T, fixtureName string, cb func(t *testing.T, tfVersion * runTestVersions(t, versions, fixtureName, cb) } +func runTestWithVersions(t *testing.T, fixtureName string, versions []string, cb func(t *testing.T, tfVersion *version.Version, tf *tfexec.Terraform)) { + t.Helper() + + if override := os.Getenv("TFEXEC_E2ETEST_VERSIONS"); override != "" { + versions = strings.Split(override, ",") + } + + // If the env var TFEXEC_E2ETEST_TERRAFORM_PATH is set to the path of a + // valid Terraform executable, only tests appropriate to that + // executable's version will be run. + if localBinPath := os.Getenv("TFEXEC_E2ETEST_TERRAFORM_PATH"); localBinPath != "" { + // By convention, every new Terraform struct is given a clean + // temp dir, even if we are only invoking tf.Version(). This + // prevents any possible confusion that could result from + // reusing an os.TempDir() (for example) that already contained + // Terraform files. + td, err := ioutil.TempDir("", "tf") + if err != nil { + t.Fatalf("error creating temporary test directory: %s", err) + } + t.Cleanup(func() { + os.RemoveAll(td) + }) + ltf, err := tfexec.NewTerraform(td, localBinPath) + if err != nil { + t.Fatal(err) + } + + ltf.SetAppendUserAgent("tfexec-e2etest") + + lVersion, _, err := ltf.Version(context.Background(), false) + if err != nil { + t.Fatalf("unable to determine version of Terraform binary at %s: %s", localBinPath, err) + } + + versions = []string{lVersion.String()} + } + + runTestVersions(t, versions, fixtureName, cb) +} + 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/plan.go b/tfexec/plan.go index 23df8c8d..76794300 100644 --- a/tfexec/plan.go +++ b/tfexec/plan.go @@ -112,12 +112,21 @@ func (tf *Terraform) Plan(ctx context.Context, opts ...PlanOption) (bool, error) // PlanJSON executes `terraform plan` with the specified options as well as the // `-json` flag and waits for it to complete. // +// 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`. +// // The returned boolean is false when the plan diff is empty (no changes) and // true when the plan diff is non-empty (changes present). // // The returned error is nil if `terraform plan` has been executed and exits // with either 0 or 2. func (tf *Terraform) PlanJSON(ctx context.Context, w io.Writer, opts ...PlanOption) (bool, error) { + err := tf.compatible(ctx, tf0_15_3, nil) + if err != nil { + return false, fmt.Errorf("terraform apply -json was added in 0.15.3: %w", err) + } + tf.SetStdout(w) cmd, err := tf.planJSONCmd(ctx, opts...) diff --git a/tfexec/refresh.go b/tfexec/refresh.go index 3af74cd6..a4623bba 100644 --- a/tfexec/refresh.go +++ b/tfexec/refresh.go @@ -2,6 +2,7 @@ package tfexec import ( "context" + "fmt" "io" "os/exec" "strconv" @@ -80,7 +81,15 @@ func (tf *Terraform) Refresh(ctx context.Context, opts ...RefreshCmdOption) erro } // RefreshJSON represents the Terraform refresh 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`. func (tf *Terraform) RefreshJSON(ctx context.Context, w io.Writer, opts ...RefreshCmdOption) 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.refreshJSONCmd(ctx, opts...) diff --git a/tfexec/version.go b/tfexec/version.go index 9978ae28..37825b53 100644 --- a/tfexec/version.go +++ b/tfexec/version.go @@ -25,6 +25,7 @@ var ( tf0_14_0 = version.Must(version.NewVersion("0.14.0")) tf0_15_0 = version.Must(version.NewVersion("0.15.0")) tf0_15_2 = version.Must(version.NewVersion("0.15.2")) + tf0_15_3 = version.Must(version.NewVersion("0.15.3")) tf1_1_0 = version.Must(version.NewVersion("1.1.0")) ) From b3f7e658afadaba6119b401fd8291b7dbf3f335e Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Mon, 16 Jan 2023 08:52:54 +0000 Subject: [PATCH 3/8] Add CHANGELOG entry (#353) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 586a4707..a2afc25d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. From bc76870e2b9b66210e7dab70474351b0faae0674 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Mon, 16 Jan 2023 09:59:35 +0000 Subject: [PATCH 4/8] Do not override TF version with version from matrix (#353) --- tfexec/internal/e2etest/util_test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tfexec/internal/e2etest/util_test.go b/tfexec/internal/e2etest/util_test.go index 284c5c60..ddaefe9a 100644 --- a/tfexec/internal/e2etest/util_test.go +++ b/tfexec/internal/e2etest/util_test.go @@ -79,10 +79,6 @@ func runTest(t *testing.T, fixtureName string, cb func(t *testing.T, tfVersion * func runTestWithVersions(t *testing.T, fixtureName string, versions []string, cb func(t *testing.T, tfVersion *version.Version, tf *tfexec.Terraform)) { t.Helper() - if override := os.Getenv("TFEXEC_E2ETEST_VERSIONS"); override != "" { - versions = strings.Split(override, ",") - } - // If the env var TFEXEC_E2ETEST_TERRAFORM_PATH is set to the path of a // valid Terraform executable, only tests appropriate to that // executable's version will be run. From 6481c6584617ddd499ff49588a1b1131492cfd2a Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Fri, 20 Jan 2023 06:58:48 +0000 Subject: [PATCH 5/8] Consolidating runTest() (#353) --- tfexec/internal/e2etest/util_test.go | 33 +--------------------------- 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/tfexec/internal/e2etest/util_test.go b/tfexec/internal/e2etest/util_test.go index ddaefe9a..36959ef3 100644 --- a/tfexec/internal/e2etest/util_test.go +++ b/tfexec/internal/e2etest/util_test.go @@ -42,38 +42,7 @@ func runTest(t *testing.T, fixtureName string, cb func(t *testing.T, tfVersion * versions = strings.Split(override, ",") } - // If the env var TFEXEC_E2ETEST_TERRAFORM_PATH is set to the path of a - // valid Terraform executable, only tests appropriate to that - // executable's version will be run. - if localBinPath := os.Getenv("TFEXEC_E2ETEST_TERRAFORM_PATH"); localBinPath != "" { - // By convention, every new Terraform struct is given a clean - // temp dir, even if we are only invoking tf.Version(). This - // prevents any possible confusion that could result from - // reusing an os.TempDir() (for example) that already contained - // Terraform files. - td, err := ioutil.TempDir("", "tf") - if err != nil { - t.Fatalf("error creating temporary test directory: %s", err) - } - t.Cleanup(func() { - os.RemoveAll(td) - }) - ltf, err := tfexec.NewTerraform(td, localBinPath) - if err != nil { - t.Fatal(err) - } - - ltf.SetAppendUserAgent("tfexec-e2etest") - - lVersion, _, err := ltf.Version(context.Background(), false) - if err != nil { - t.Fatalf("unable to determine version of Terraform binary at %s: %s", localBinPath, err) - } - - versions = []string{lVersion.String()} - } - - runTestVersions(t, versions, fixtureName, cb) + runTestWithVersions(t, fixtureName, versions, cb) } func runTestWithVersions(t *testing.T, fixtureName string, versions []string, cb func(t *testing.T, tfVersion *version.Version, tf *tfexec.Terraform)) { From e18d99c8e4c041a9660f6e15b5d23e117b5408f1 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Fri, 20 Jan 2023 12:43:11 +0000 Subject: [PATCH 6/8] Apply suggestions from code review Co-authored-by: Radek Simko --- tfexec/apply.go | 4 ++-- tfexec/destroy.go | 6 +++--- tfexec/internal/e2etest/destroy_test.go | 2 +- tfexec/internal/e2etest/plan_test.go | 2 +- tfexec/internal/e2etest/refresh_test.go | 2 +- tfexec/plan.go | 2 +- tfexec/refresh.go | 6 +++--- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tfexec/apply.go b/tfexec/apply.go index 1fc0b9db..53461692 100644 --- a/tfexec/apply.go +++ b/tfexec/apply.go @@ -91,7 +91,7 @@ func (opt *ReattachOption) configureApply(conf *applyConfig) { conf.reattachInfo = opt.info } -// Apply represents the Terraform apply subcommand. +// Apply represents the terraform apply subcommand. func (tf *Terraform) Apply(ctx context.Context, opts ...ApplyOption) error { cmd, err := tf.applyCmd(ctx, opts...) if err != nil { @@ -100,7 +100,7 @@ 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. +// 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`. diff --git a/tfexec/destroy.go b/tfexec/destroy.go index f015b1c2..4667105e 100644 --- a/tfexec/destroy.go +++ b/tfexec/destroy.go @@ -87,7 +87,7 @@ func (opt *ReattachOption) configureDestroy(conf *destroyConfig) { conf.reattachInfo = opt.info } -// Destroy represents the Terraform destroy subcommand. +// Destroy represents the terraform destroy subcommand. func (tf *Terraform) Destroy(ctx context.Context, opts ...DestroyOption) error { cmd, err := tf.destroyCmd(ctx, opts...) if err != nil { @@ -96,14 +96,14 @@ 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. +// 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`. 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 apply -json was added in 0.15.3: %w", err) + return fmt.Errorf("terraform destroy -json was added in 0.15.3: %w", err) } tf.SetStdout(w) diff --git a/tfexec/internal/e2etest/destroy_test.go b/tfexec/internal/e2etest/destroy_test.go index 88022e96..9d157fd9 100644 --- a/tfexec/internal/e2etest/destroy_test.go +++ b/tfexec/internal/e2etest/destroy_test.go @@ -40,7 +40,7 @@ func TestDestroyJSON_TF014AndEarlier(t *testing.T) { t.Fatalf("error running Init in test directory: %s", err) } - re := regexp.MustCompile("terraform apply -json was added in 0.15.3") + 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()) { diff --git a/tfexec/internal/e2etest/plan_test.go b/tfexec/internal/e2etest/plan_test.go index 58b7f65a..83364fd6 100644 --- a/tfexec/internal/e2etest/plan_test.go +++ b/tfexec/internal/e2etest/plan_test.go @@ -59,7 +59,7 @@ func TestPlanJSON_TF014AndEarlier(t *testing.T) { t.Fatalf("error running Init in test directory: %s", err) } - re := regexp.MustCompile("terraform apply -json was added in 0.15.3") + re := regexp.MustCompile("terraform plan -json was added in 0.15.3") hasChanges, err := tf.PlanJSON(context.Background(), io.Discard) if err != nil && !re.MatchString(err.Error()) { diff --git a/tfexec/internal/e2etest/refresh_test.go b/tfexec/internal/e2etest/refresh_test.go index d359a7aa..1cea79e4 100644 --- a/tfexec/internal/e2etest/refresh_test.go +++ b/tfexec/internal/e2etest/refresh_test.go @@ -40,7 +40,7 @@ func TestRefreshJSON_TF014AndEarlier(t *testing.T) { t.Fatalf("error running Init in test directory: %s", err) } - re := regexp.MustCompile("terraform apply -json was added in 0.15.3") + re := regexp.MustCompile("terraform refresh -json was added in 0.15.3") err = tf.RefreshJSON(context.Background(), io.Discard) if err != nil && !re.MatchString(err.Error()) { diff --git a/tfexec/plan.go b/tfexec/plan.go index 76794300..7826532d 100644 --- a/tfexec/plan.go +++ b/tfexec/plan.go @@ -124,7 +124,7 @@ func (tf *Terraform) Plan(ctx context.Context, opts ...PlanOption) (bool, error) func (tf *Terraform) PlanJSON(ctx context.Context, w io.Writer, opts ...PlanOption) (bool, error) { err := tf.compatible(ctx, tf0_15_3, nil) if err != nil { - return false, fmt.Errorf("terraform apply -json was added in 0.15.3: %w", err) + return false, fmt.Errorf("terraform plan -json was added in 0.15.3: %w", err) } tf.SetStdout(w) diff --git a/tfexec/refresh.go b/tfexec/refresh.go index a4623bba..9f227800 100644 --- a/tfexec/refresh.go +++ b/tfexec/refresh.go @@ -71,7 +71,7 @@ func (opt *VarFileOption) configureRefresh(conf *refreshConfig) { conf.varFiles = append(conf.varFiles, opt.path) } -// Refresh represents the Terraform refresh subcommand. +// Refresh represents the terraform refresh subcommand. func (tf *Terraform) Refresh(ctx context.Context, opts ...RefreshCmdOption) error { cmd, err := tf.refreshCmd(ctx, opts...) if err != nil { @@ -80,14 +80,14 @@ func (tf *Terraform) Refresh(ctx context.Context, opts ...RefreshCmdOption) erro return tf.runTerraformCmd(ctx, cmd) } -// RefreshJSON represents the Terraform refresh subcommand with the `-json` flag. +// RefreshJSON represents the terraform refresh 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`. func (tf *Terraform) RefreshJSON(ctx context.Context, w io.Writer, opts ...RefreshCmdOption) 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) + return fmt.Errorf("terraform refresh -json was added in 0.15.3: %w", err) } tf.SetStdout(w) From 9acd44a7e5da4de326a1a8affb5c5f09dcd28f08 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Fri, 20 Jan 2023 17:45:04 +0000 Subject: [PATCH 7/8] Refactoring ...JSONCmd() functions to append -json flag as final flag in terraform command (#353) --- tfexec/apply.go | 32 +++++++++++++++++++++++++------- tfexec/apply_test.go | 2 +- tfexec/destroy.go | 27 +++++++++++++++++++-------- tfexec/destroy_test.go | 4 ++-- tfexec/plan.go | 32 +++++++++++++++++++++++++------- tfexec/plan_test.go | 4 ++-- tfexec/refresh.go | 28 ++++++++++++++++++++-------- tfexec/refresh_test.go | 4 ++-- 8 files changed, 96 insertions(+), 37 deletions(-) diff --git a/tfexec/apply.go b/tfexec/apply.go index 53461692..b1003c1d 100644 --- a/tfexec/apply.go +++ b/tfexec/apply.go @@ -120,25 +120,39 @@ func (tf *Terraform) ApplyJSON(ctx context.Context, w io.Writer, opts ...ApplyOp return tf.runTerraformCmd(ctx, cmd) } -func (tf *Terraform) applyJSONCmd(ctx context.Context, opts ...ApplyOption) (*exec.Cmd, error) { - cmd, err := tf.applyCmd(ctx, opts...) +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 } - cmd.Args = append(cmd.Args[:3], cmd.Args[2:]...) - cmd.Args[2] = "-json" - - return cmd, nil + return tf.buildApplyCmd(ctx, c, args) } -func (tf *Terraform) applyCmd(ctx context.Context, opts ...ApplyOption) (*exec.Cmd, error) { +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 @@ -184,6 +198,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) diff --git a/tfexec/apply_test.go b/tfexec/apply_test.go index 8915ae90..50f32036 100644 --- a/tfexec/apply_test.go +++ b/tfexec/apply_test.go @@ -102,7 +102,6 @@ func TestApplyJSONCmd(t *testing.T) { assertCmd(t, []string{ "apply", - "-json", "-no-color", "-auto-approve", "-input=false", @@ -121,6 +120,7 @@ func TestApplyJSONCmd(t *testing.T) { "-target=target2", "-var", "var1=foo", "-var", "var2=bar", + "-json", "testfile", }, nil, applyCmd) }) diff --git a/tfexec/destroy.go b/tfexec/destroy.go index 4667105e..54be13ab 100644 --- a/tfexec/destroy.go +++ b/tfexec/destroy.go @@ -116,25 +116,32 @@ func (tf *Terraform) DestroyJSON(ctx context.Context, w io.Writer, opts ...Destr return tf.runTerraformCmd(ctx, cmd) } -func (tf *Terraform) destroyJSONCmd(ctx context.Context, opts ...DestroyOption) (*exec.Cmd, error) { - cmd, err := tf.destroyCmd(ctx, opts...) - if err != nil { - return nil, err +func (tf *Terraform) destroyCmd(ctx context.Context, opts ...DestroyOption) (*exec.Cmd, error) { + c := defaultDestroyOptions + + for _, o := range opts { + o.configureDestroy(&c) } - cmd.Args = append(cmd.Args[:3], cmd.Args[2:]...) - cmd.Args[2] = "-json" + args := tf.buildDestroyArgs(c) - return cmd, nil + return tf.buildDestroyCmd(ctx, c, args) } -func (tf *Terraform) destroyCmd(ctx context.Context, opts ...DestroyOption) (*exec.Cmd, error) { +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 @@ -171,6 +178,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) diff --git a/tfexec/destroy_test.go b/tfexec/destroy_test.go index 1447498c..aa93bcbe 100644 --- a/tfexec/destroy_test.go +++ b/tfexec/destroy_test.go @@ -83,7 +83,6 @@ func TestDestroyJSONCmd(t *testing.T) { assertCmd(t, []string{ "destroy", - "-json", "-no-color", "-auto-approve", "-input=false", @@ -91,6 +90,7 @@ func TestDestroyJSONCmd(t *testing.T) { "-lock=true", "-parallelism=10", "-refresh=true", + "-json", }, nil, destroyCmd) }) @@ -102,7 +102,6 @@ func TestDestroyJSONCmd(t *testing.T) { assertCmd(t, []string{ "destroy", - "-json", "-no-color", "-auto-approve", "-input=false", @@ -118,6 +117,7 @@ func TestDestroyJSONCmd(t *testing.T) { "-target=target2", "-var", "var1=foo", "-var", "var2=bar", + "-json", "destroydir", }, nil, destroyCmd) }) diff --git a/tfexec/plan.go b/tfexec/plan.go index 7826532d..4fad73c9 100644 --- a/tfexec/plan.go +++ b/tfexec/plan.go @@ -142,25 +142,39 @@ func (tf *Terraform) PlanJSON(ctx context.Context, w io.Writer, opts ...PlanOpti return false, err } -func (tf *Terraform) planJSONCmd(ctx context.Context, opts ...PlanOption) (*exec.Cmd, error) { - cmd, err := tf.planCmd(ctx, opts...) +func (tf *Terraform) planCmd(ctx context.Context, opts ...PlanOption) (*exec.Cmd, error) { + c := defaultPlanOptions + + for _, o := range opts { + o.configurePlan(&c) + } + + args, err := tf.buildPlanArgs(ctx, c) if err != nil { return nil, err } - cmd.Args = append(cmd.Args[:3], cmd.Args[2:]...) - cmd.Args[2] = "-json" - - return cmd, nil + return tf.buildPlanCmd(ctx, c, args) } -func (tf *Terraform) planCmd(ctx context.Context, opts ...PlanOption) (*exec.Cmd, error) { +func (tf *Terraform) planJSONCmd(ctx context.Context, opts ...PlanOption) (*exec.Cmd, error) { c := defaultPlanOptions for _, o := range opts { o.configurePlan(&c) } + args, err := tf.buildPlanArgs(ctx, c) + if err != nil { + return nil, err + } + + args = append(args, "-json") + + return tf.buildPlanCmd(ctx, c, args) +} + +func (tf *Terraform) buildPlanArgs(ctx context.Context, c planConfig) ([]string, error) { args := []string{"plan", "-no-color", "-input=false", "-detailed-exitcode"} // string opts: only pass if set @@ -208,6 +222,10 @@ func (tf *Terraform) planCmd(ctx context.Context, opts ...PlanOption) (*exec.Cmd } } + return args, nil +} + +func (tf *Terraform) buildPlanCmd(ctx context.Context, c planConfig, args []string) (*exec.Cmd, error) { // optional positional argument if c.dir != "" { args = append(args, c.dir) diff --git a/tfexec/plan_test.go b/tfexec/plan_test.go index c0d03c28..f84190bc 100644 --- a/tfexec/plan_test.go +++ b/tfexec/plan_test.go @@ -100,7 +100,6 @@ func TestPlanJSONCmd(t *testing.T) { assertCmd(t, []string{ "plan", - "-json", "-no-color", "-input=false", "-detailed-exitcode", @@ -108,6 +107,7 @@ func TestPlanJSONCmd(t *testing.T) { "-lock=true", "-parallelism=10", "-refresh=true", + "-json", }, nil, planCmd) }) @@ -134,7 +134,6 @@ func TestPlanJSONCmd(t *testing.T) { assertCmd(t, []string{ "plan", - "-json", "-no-color", "-input=false", "-detailed-exitcode", @@ -152,6 +151,7 @@ func TestPlanJSONCmd(t *testing.T) { "-target=beeblebrox", "-var", "android=paranoid", "-var", "brain_size=planet", + "-json", "earth", }, nil, planCmd) }) diff --git a/tfexec/refresh.go b/tfexec/refresh.go index 9f227800..9f7e6306 100644 --- a/tfexec/refresh.go +++ b/tfexec/refresh.go @@ -100,25 +100,33 @@ func (tf *Terraform) RefreshJSON(ctx context.Context, w io.Writer, opts ...Refre return tf.runTerraformCmd(ctx, cmd) } -func (tf *Terraform) refreshJSONCmd(ctx context.Context, opts ...RefreshCmdOption) (*exec.Cmd, error) { - cmd, err := tf.refreshCmd(ctx, opts...) - if err != nil { - return nil, err +func (tf *Terraform) refreshCmd(ctx context.Context, opts ...RefreshCmdOption) (*exec.Cmd, error) { + c := defaultRefreshOptions + + for _, o := range opts { + o.configureRefresh(&c) } - cmd.Args = append(cmd.Args[:3], cmd.Args[2:]...) - cmd.Args[2] = "-json" + args := tf.buildRefreshArgs(c) + + return tf.buildRefreshCmd(ctx, c, args) - return cmd, nil } -func (tf *Terraform) refreshCmd(ctx context.Context, opts ...RefreshCmdOption) (*exec.Cmd, error) { +func (tf *Terraform) refreshJSONCmd(ctx context.Context, opts ...RefreshCmdOption) (*exec.Cmd, error) { c := defaultRefreshOptions for _, o := range opts { o.configureRefresh(&c) } + args := tf.buildRefreshArgs(c) + args = append(args, "-json") + + return tf.buildRefreshCmd(ctx, c, args) +} + +func (tf *Terraform) buildRefreshArgs(c refreshConfig) []string { args := []string{"refresh", "-no-color", "-input=false"} // string opts: only pass if set @@ -153,6 +161,10 @@ func (tf *Terraform) refreshCmd(ctx context.Context, opts ...RefreshCmdOption) ( } } + return args +} + +func (tf *Terraform) buildRefreshCmd(ctx context.Context, c refreshConfig, args []string) (*exec.Cmd, error) { // optional positional argument if c.dir != "" { args = append(args, c.dir) diff --git a/tfexec/refresh_test.go b/tfexec/refresh_test.go index d03824e6..0b94a1e1 100644 --- a/tfexec/refresh_test.go +++ b/tfexec/refresh_test.go @@ -77,11 +77,11 @@ func TestRefreshJSONCmd(t *testing.T) { assertCmd(t, []string{ "refresh", - "-json", "-no-color", "-input=false", "-lock-timeout=0s", "-lock=true", + "-json", }, nil, refreshCmd) }) @@ -93,7 +93,6 @@ func TestRefreshJSONCmd(t *testing.T) { assertCmd(t, []string{ "refresh", - "-json", "-no-color", "-input=false", "-backup=testbackup", @@ -106,6 +105,7 @@ func TestRefreshJSONCmd(t *testing.T) { "-target=target2", "-var", "var1=foo", "-var", "var2=bar", + "-json", "refreshdir", }, nil, refreshCmd) }) From 3f97a191c6bad54c4be8df2f6a993e812a476c31 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Mon, 23 Jan 2023 08:29:32 +0000 Subject: [PATCH 8/8] Adding comments to indicate that ...JSON() functions are likely to be removed in the future (#353) --- tfexec/apply.go | 3 ++- tfexec/destroy.go | 3 ++- tfexec/plan.go | 3 +++ tfexec/refresh.go | 3 ++- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/tfexec/apply.go b/tfexec/apply.go index b1003c1d..6dfdb976 100644 --- a/tfexec/apply.go +++ b/tfexec/apply.go @@ -103,7 +103,8 @@ func (tf *Terraform) Apply(ctx context.Context, opts ...ApplyOption) error { // 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`. +// 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 { diff --git a/tfexec/destroy.go b/tfexec/destroy.go index 54be13ab..189db7e4 100644 --- a/tfexec/destroy.go +++ b/tfexec/destroy.go @@ -99,7 +99,8 @@ func (tf *Terraform) Destroy(ctx context.Context, opts ...DestroyOption) error { // 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`. +// 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 { diff --git a/tfexec/plan.go b/tfexec/plan.go index 4fad73c9..5ea31552 100644 --- a/tfexec/plan.go +++ b/tfexec/plan.go @@ -121,6 +121,9 @@ func (tf *Terraform) Plan(ctx context.Context, opts ...PlanOption) (bool, error) // // The returned error is nil if `terraform plan` has been executed and exits // with either 0 or 2. +// +// PlanJSON is likely to be removed in a future major version in favour of +// Plan returning JSON by default. func (tf *Terraform) PlanJSON(ctx context.Context, w io.Writer, opts ...PlanOption) (bool, error) { err := tf.compatible(ctx, tf0_15_3, nil) if err != nil { diff --git a/tfexec/refresh.go b/tfexec/refresh.go index 9f7e6306..4bdd8960 100644 --- a/tfexec/refresh.go +++ b/tfexec/refresh.go @@ -83,7 +83,8 @@ func (tf *Terraform) Refresh(ctx context.Context, opts ...RefreshCmdOption) erro // RefreshJSON represents the terraform refresh 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`. +// JSON being written to the supplied `io.Writer`. RefreshJSON is likely to be +// removed in a future major version in favour of Refresh returning JSON by default. func (tf *Terraform) RefreshJSON(ctx context.Context, w io.Writer, opts ...RefreshCmdOption) error { err := tf.compatible(ctx, tf0_15_3, nil) if err != nil {