Adding ApplyJSON(), DestroyJSON(), PlanJSON() and RefreshJSON() functions (#354)
* Adding ApplyJSON(), DestroyJSON(), PlanJSON() and RefreshJSON() functions (#353)
* Adding version compatibility check and e2e tests (#353)
* Add CHANGELOG entry (#353)
* Do not override TF version with version from matrix (#353)
* Consolidating runTest() (#353)
* Apply suggestions from code review
Co-authored-by: Radek Simko <radek.simko@gmail.com>
* Refactoring ...JSONCmd() functions to append -json flag as final flag in terraform command (#353)
* Adding comments to indicate that ...JSON() functions are likely to be removed in the future (#353)
Co-authored-by: Radek Simko <radek.simko@gmail.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 586a470..a2afc25 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 40d9e69..6dfdb97 100644
--- a/tfexec/apply.go
+++ b/tfexec/apply.go
@@ -3,6 +3,7 @@
import (
"context"
"fmt"
+ "io"
"os/exec"
"strconv"
)
@@ -99,6 +100,27 @@
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 @@
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 @@
}
}
+ 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 1cf2f56..50f3203 100644
--- a/tfexec/apply_test.go
+++ b/tfexec/apply_test.go
@@ -65,3 +65,63 @@
}, 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 8011c0b..189db7e 100644
--- a/tfexec/destroy.go
+++ b/tfexec/destroy.go
@@ -3,6 +3,7 @@
import (
"context"
"fmt"
+ "io"
"os/exec"
"strconv"
)
@@ -95,6 +96,27 @@
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 @@
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 @@
}
}
+ 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 eb28f58..aa93bcb 100644
--- a/tfexec/destroy_test.go
+++ b/tfexec/destroy_test.go
@@ -63,3 +63,62 @@
}, 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 8c3154a..5211a3d 100644
--- a/tfexec/internal/e2etest/apply_test.go
+++ b/tfexec/internal/e2etest/apply_test.go
@@ -2,11 +2,14 @@
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 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 da0c856..9d157fd 100644
--- a/tfexec/internal/e2etest/destroy_test.go
+++ b/tfexec/internal/e2etest/destroy_test.go
@@ -2,11 +2,14 @@
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 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 a5282b7..83364fd 100644
--- a/tfexec/internal/e2etest/plan_test.go
+++ b/tfexec/internal/e2etest/plan_test.go
@@ -2,11 +2,14 @@
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 @@
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 40dbb9d..1cea79e 100644
--- a/tfexec/internal/e2etest/refresh_test.go
+++ b/tfexec/internal/e2etest/refresh_test.go
@@ -2,11 +2,14 @@
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 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 54d4f33..36959ef 100644
--- a/tfexec/internal/e2etest/util_test.go
+++ b/tfexec/internal/e2etest/util_test.go
@@ -42,6 +42,12 @@
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 bf41094..5ea3155 100644
--- a/tfexec/plan.go
+++ b/tfexec/plan.go
@@ -3,6 +3,7 @@
import (
"context"
"fmt"
+ "io"
"os/exec"
"strconv"
)
@@ -108,6 +109,42 @@
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 @@
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 @@
}
}
+ 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 7a467ac..f84190b 100644
--- a/tfexec/plan_test.go
+++ b/tfexec/plan_test.go
@@ -80,3 +80,79 @@
}, 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 78f6b4b..4bdd896 100644
--- a/tfexec/refresh.go
+++ b/tfexec/refresh.go
@@ -2,6 +2,8 @@
import (
"context"
+ "fmt"
+ "io"
"os/exec"
"strconv"
)
@@ -78,6 +80,27 @@
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 @@
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 @@
}
}
+ 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 bd4a94c..0b94a1e 100644
--- a/tfexec/refresh_test.go
+++ b/tfexec/refresh_test.go
@@ -57,3 +57,56 @@
}, 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 9978ae2..37825b5 100644
--- a/tfexec/version.go
+++ b/tfexec/version.go
@@ -25,6 +25,7 @@
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"))
)