feat: add `-generate-config-out` option to `plan` (#563)
Co-authored-by: Hiromasa Kakehashi <hrmsk66@gmail.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ac93c8d..b035441 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
ENHANCEMENTS:
- tfexec: Added provider reattach support to all `terraform workspace` subcommands ([#556](https://github.com/hashicorp/terraform-exec/pull/556))
+- tfexec: Add `-generate-config-out` to the `(Terraform).Plan()` method ([#563](https://github.com/hashicorp/terraform-exec/pull/563))
# 0.24.0 (September 17, 2025)
diff --git a/tfexec/internal/e2etest/plan_test.go b/tfexec/internal/e2etest/plan_test.go
index 16b7a43..0b19dc7 100644
--- a/tfexec/internal/e2etest/plan_test.go
+++ b/tfexec/internal/e2etest/plan_test.go
@@ -6,6 +6,8 @@
import (
"context"
"io"
+ "os"
+ "path/filepath"
"regexp"
"testing"
@@ -15,6 +17,10 @@
"github.com/hashicorp/terraform-exec/tfexec/internal/testutil"
)
+var (
+ generateConfigOutMinVersion = version.Must(version.NewVersion("1.5.0"))
+)
+
func TestPlan(t *testing.T) {
runTest(t, "basic", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
err := tf.Init(context.Background())
@@ -92,3 +98,25 @@
}
})
}
+
+func TestPlanGenerateConfigOut(t *testing.T) {
+ runTest(t, "generate_config_out", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
+ if tfv.LessThan(generateConfigOutMinVersion) {
+ t.Skip("terraform plan -generate-config-out was added in Terraform 1.5.0, so test is not valid")
+ }
+
+ err := tf.Init(context.Background())
+ if err != nil {
+ t.Fatalf("error running Init in test directory: %s", err)
+ }
+
+ _, err = tf.Plan(context.Background(), tfexec.GenerateConfigOut("generated.tf"))
+ if err != nil {
+ t.Fatalf("error running Plan: %s", err)
+ }
+
+ if _, err = os.Stat(filepath.Join(tf.WorkingDir(), "generated.tf")); os.IsNotExist(err) {
+ t.Fatalf("expected generated.tf to be created, but it does not exist")
+ }
+ })
+}
diff --git a/tfexec/internal/e2etest/testdata/generate_config_out/main.tf b/tfexec/internal/e2etest/testdata/generate_config_out/main.tf
new file mode 100644
index 0000000..8257ac5
--- /dev/null
+++ b/tfexec/internal/e2etest/testdata/generate_config_out/main.tf
@@ -0,0 +1,4 @@
+import {
+ id = "bar"
+ to = terraform_data.foo
+}
diff --git a/tfexec/options.go b/tfexec/options.go
index 0129bd5..85d2acd 100644
--- a/tfexec/options.go
+++ b/tfexec/options.go
@@ -188,6 +188,7 @@
path string
}
+// GenerateConfigOut represents the -generate-config-out flag.
func GenerateConfigOut(path string) *GenerateConfigOutOption {
return &GenerateConfigOutOption{path}
}
diff --git a/tfexec/plan.go b/tfexec/plan.go
index c2ec1f9..48f486d 100644
--- a/tfexec/plan.go
+++ b/tfexec/plan.go
@@ -12,21 +12,22 @@
)
type planConfig struct {
- allowDeferral bool
- destroy bool
- dir string
- lock bool
- lockTimeout string
- out string
- parallelism int
- reattachInfo ReattachInfo
- refresh bool
- refreshOnly bool
- replaceAddrs []string
- state string
- targets []string
- vars []string
- varFiles []string
+ allowDeferral bool
+ destroy bool
+ dir string
+ generateConfigOut string
+ lock bool
+ lockTimeout string
+ out string
+ parallelism int
+ reattachInfo ReattachInfo
+ refresh bool
+ refreshOnly bool
+ replaceAddrs []string
+ state string
+ targets []string
+ vars []string
+ varFiles []string
}
var defaultPlanOptions = planConfig{
@@ -102,6 +103,10 @@
conf.allowDeferral = opt.allowDeferral
}
+func (opt *GenerateConfigOutOption) configurePlan(conf *planConfig) {
+ conf.generateConfigOut = opt.path
+}
+
// Plan executes `terraform plan` with the specified options and waits for it
// to complete.
//
@@ -194,6 +199,13 @@
args := []string{"plan", "-no-color", "-input=false", "-detailed-exitcode"}
// string opts: only pass if set
+ if c.generateConfigOut != "" {
+ err := tf.compatible(ctx, tf1_5_0, nil)
+ if err != nil {
+ return nil, fmt.Errorf("generate-config-out option was introduced in Terraform 1.5.0: %w", err)
+ }
+ args = append(args, "-generate-config-out="+c.generateConfigOut)
+ }
if c.lockTimeout != "" {
args = append(args, "-lock-timeout="+c.lockTimeout)
}
diff --git a/tfexec/plan_test.go b/tfexec/plan_test.go
index 91c13f5..ef45b12 100644
--- a/tfexec/plan_test.go
+++ b/tfexec/plan_test.go
@@ -13,7 +13,7 @@
func TestPlanCmd(t *testing.T) {
td := t.TempDir()
- tf, err := NewTerraform(td, tfVersion(t, testutil.Latest_v1))
+ tf, err := NewTerraform(td, tfVersion(t, testutil.Latest_v1_5))
if err != nil {
t.Fatal(err)
}
@@ -101,12 +101,31 @@
"-refresh-only",
}, nil, planCmd)
})
+
+ t.Run("run a generate-config-out plan", func(t *testing.T) {
+ planCmd, err := tf.planCmd(context.Background(), GenerateConfigOut("generated.tf"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ assertCmd(t, []string{
+ "plan",
+ "-no-color",
+ "-input=false",
+ "-detailed-exitcode",
+ "-generate-config-out=generated.tf",
+ "-lock-timeout=0s",
+ "-lock=true",
+ "-parallelism=10",
+ "-refresh=true",
+ }, nil, planCmd)
+ })
}
func TestPlanJSONCmd(t *testing.T) {
td := t.TempDir()
- tf, err := NewTerraform(td, tfVersion(t, testutil.Latest_v1))
+ tf, err := NewTerraform(td, tfVersion(t, testutil.Latest_v1_5))
if err != nil {
t.Fatal(err)
}
@@ -177,6 +196,26 @@
"earth",
}, nil, planCmd)
})
+
+ t.Run("generate-config-out", func(t *testing.T) {
+ planCmd, err := tf.planJSONCmd(context.Background(), GenerateConfigOut("generated.tf"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ assertCmd(t, []string{
+ "plan",
+ "-no-color",
+ "-input=false",
+ "-detailed-exitcode",
+ "-generate-config-out=generated.tf",
+ "-lock-timeout=0s",
+ "-lock=true",
+ "-parallelism=10",
+ "-refresh=true",
+ "-json",
+ }, nil, planCmd)
+ })
}
func TestPlanCmd_AllowDeferral(t *testing.T) {
diff --git a/tfexec/version.go b/tfexec/version.go
index 880c7b3..cbf28fb 100644
--- a/tfexec/version.go
+++ b/tfexec/version.go
@@ -32,6 +32,7 @@
tf0_15_4 = version.Must(version.NewVersion("0.15.4"))
tf1_1_0 = version.Must(version.NewVersion("1.1.0"))
tf1_4_0 = version.Must(version.NewVersion("1.4.0"))
+ tf1_5_0 = version.Must(version.NewVersion("1.5.0"))
tf1_6_0 = version.Must(version.NewVersion("1.6.0"))
tf1_9_0 = version.Must(version.NewVersion("1.9.0"))
tf1_13_0 = version.Must(version.NewVersion("1.13.0"))