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. diff --git a/tfexec/apply.go b/tfexec/apply.go index 40d9e69b..6dfdb976 100644 --- a/tfexec/apply.go +++ b/tfexec/apply.go @@ -3,6 +3,7 @@ package tfexec import ( "context" "fmt" + "io" "os/exec" "strconv" ) @@ -99,6 +100,27 @@ 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 @@ -106,6 +128,32 @@ func (tf *Terraform) applyCmd(ctx context.Context, opts ...ApplyOption) (*exec.C 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 @@ -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) diff --git a/tfexec/apply_test.go b/tfexec/apply_test.go index 1cf2f562..50f32036 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", + "-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) + }) +} diff --git a/tfexec/destroy.go b/tfexec/destroy.go index 8011c0ba..189db7e4 100644 --- a/tfexec/destroy.go +++ b/tfexec/destroy.go @@ -3,6 +3,7 @@ package tfexec import ( "context" "fmt" + "io" "os/exec" "strconv" ) @@ -95,6 +96,27 @@ 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 @@ -102,6 +124,25 @@ func (tf *Terraform) destroyCmd(ctx context.Context, opts ...DestroyOption) (*ex 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 @@ -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) diff --git a/tfexec/destroy_test.go b/tfexec/destroy_test.go index eb28f58a..aa93bcbe 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", + "-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) + }) +} 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..9d157fd9 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 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) + } + }) +} diff --git a/tfexec/internal/e2etest/plan_test.go b/tfexec/internal/e2etest/plan_test.go index a5282b7f..83364fd6 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 plan -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..1cea79e4 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 refresh -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..36959ef3 100644 --- a/tfexec/internal/e2etest/util_test.go +++ b/tfexec/internal/e2etest/util_test.go @@ -42,6 +42,12 @@ func runTest(t *testing.T, fixtureName string, cb func(t *testing.T, tfVersion * versions = strings.Split(override, ",") } + 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)) { + t.Helper() + // 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. diff --git a/tfexec/plan.go b/tfexec/plan.go index bf41094b..5ea31552 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. +// +// 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. +// +// 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 { + return false, fmt.Errorf("terraform plan -json was added in 0.15.3: %w", err) + } + + 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) planCmd(ctx context.Context, opts ...PlanOption) (*exec.Cmd, error) { c := defaultPlanOptions @@ -115,6 +152,32 @@ func (tf *Terraform) planCmd(ctx context.Context, opts ...PlanOption) (*exec.Cmd o.configurePlan(&c) } + args, err := tf.buildPlanArgs(ctx, c) + if err != nil { + return nil, err + } + + return tf.buildPlanCmd(ctx, c, args) +} + +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 @@ -162,6 +225,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 7a467ac3..f84190bc 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", + "-no-color", + "-input=false", + "-detailed-exitcode", + "-lock-timeout=0s", + "-lock=true", + "-parallelism=10", + "-refresh=true", + "-json", + }, 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", + "-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", + "-json", + "earth", + }, nil, planCmd) + }) +} diff --git a/tfexec/refresh.go b/tfexec/refresh.go index 78f6b4b5..4bdd8960 100644 --- a/tfexec/refresh.go +++ b/tfexec/refresh.go @@ -2,6 +2,8 @@ package tfexec import ( "context" + "fmt" + "io" "os/exec" "strconv" ) @@ -78,6 +80,27 @@ 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. +// 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`. 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 { + return fmt.Errorf("terraform refresh -json was added in 0.15.3: %w", err) + } + + tf.SetStdout(w) + + cmd, err := tf.refreshJSONCmd(ctx, opts...) + if err != nil { + return err + } + + return tf.runTerraformCmd(ctx, cmd) +} + func (tf *Terraform) refreshCmd(ctx context.Context, opts ...RefreshCmdOption) (*exec.Cmd, error) { c := defaultRefreshOptions @@ -85,6 +108,26 @@ func (tf *Terraform) refreshCmd(ctx context.Context, opts ...RefreshCmdOption) ( o.configureRefresh(&c) } + args := tf.buildRefreshArgs(c) + + return tf.buildRefreshCmd(ctx, c, args) + +} + +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 @@ -119,6 +162,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 bd4a94c4..0b94a1e1 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", + "-no-color", + "-input=false", + "-lock-timeout=0s", + "-lock=true", + "-json", + }, 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", + "-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", + "-json", + "refreshdir", + }, nil, refreshCmd) + }) +} 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")) )