Introduce StateMigrateJSON method/command
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9bb720f..f970942 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml
@@ -148,6 +148,7 @@ - "1.8.5" - "1.9.8" - "1.10.4" + - "1.16.2" - "refs/heads/main" steps: -
diff --git a/tfexec/internal/e2etest/state_migrate_test.go b/tfexec/internal/e2etest/state_migrate_test.go new file mode 100644 index 0000000..e32e1b3 --- /dev/null +++ b/tfexec/internal/e2etest/state_migrate_test.go
@@ -0,0 +1,30 @@ +// Copyright IBM Corp. 2020, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package e2etest + +import ( + "context" + "testing" + + "github.com/hashicorp/go-version" + + "github.com/hashicorp/terraform-exec/tfexec" +) + +func TestStateMigrateJSON(t *testing.T) { + runTest(t, "basic", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) { + if tfv.LessThan(stateMigrateMinVersion) { + t.Skip("state migrate command not available in this Terraform version") + } + + // TODO: backend local(path foo) -> backend local(path boo) + err := tf.StateMigrateJSON(context.Background()) + if err != nil { + t.Fatalf("error running StateMigrateJSON: %s", err) + } + }) + + // TODO: upgrade test? Is there a provider to test with? + // TODO: state store test? Is there a provider to test with? +}
diff --git a/tfexec/internal/e2etest/util_test.go b/tfexec/internal/e2etest/util_test.go index 7fd872c..0b82d47 100644 --- a/tfexec/internal/e2etest/util_test.go +++ b/tfexec/internal/e2etest/util_test.go
@@ -29,6 +29,10 @@ providerAddressMinVersion = version.Must(version.NewVersion("0.13.0")) metadataFunctionsMinVersion = version.Must(version.NewVersion("1.4.0")) + + // TODO: Confirm how complete the implementation of that command actually is in this version + // it might be enough for the purpose of the E2E test + stateMigrateMinVersion = version.Must(version.NewVersion("1.16.0")) ) func runTest(t *testing.T, fixtureName string, cb func(t *testing.T, tfVersion *version.Version, tf *tfexec.Terraform)) {
diff --git a/tfexec/options.go b/tfexec/options.go index 30d6b6a..b132f60 100644 --- a/tfexec/options.go +++ b/tfexec/options.go
@@ -476,3 +476,19 @@ func LockFile(useLockFile bool) *LockFileOption { return &LockFileOption{useLockFile: useLockFile} } + +type SourceProviderLockFileOption struct { + path string +} + +func SourceProviderLockFile(path string) *SourceProviderLockFileOption { + return &SourceProviderLockFileOption{path: path} +} + +type DestinationProviderLockFileOption struct { + path string +} + +func DestinationProviderLockFile(path string) *DestinationProviderLockFileOption { + return &DestinationProviderLockFileOption{path: path} +}
diff --git a/tfexec/state_migrate.go b/tfexec/state_migrate.go new file mode 100644 index 0000000..72d3e60 --- /dev/null +++ b/tfexec/state_migrate.go
@@ -0,0 +1,84 @@ +// Copyright IBM Corp. 2020, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package tfexec + +import ( + "context" + "os/exec" +) + +const defaultLockFilePath = ".terraform.lock.hcl" + +type stateMigrateConfig struct { + srcProviderLockFile string + dstProviderLockFile string + + upgrade bool +} + +var defaultStateMigrateOptions = stateMigrateConfig{ + srcProviderLockFile: defaultLockFilePath, + dstProviderLockFile: defaultLockFilePath, +} + +// StateMigrateOption represents options used in the Refresh method. +type StateMigrateOption interface { + configureStateMigrate(*stateMigrateConfig) +} + +func (opt *SourceProviderLockFileOption) configureStateMigrate(conf *stateMigrateConfig) { + conf.srcProviderLockFile = opt.path +} + +func (opt *DestinationProviderLockFileOption) configureStateMigrate(conf *stateMigrateConfig) { + conf.dstProviderLockFile = opt.path +} + +func (opt *UpgradeOption) configureStateMigrate(conf *stateMigrateConfig) { + conf.upgrade = opt.upgrade +} + +// StateMigrateJSON executes `terraform state migrate` with `-json` flag +// and any specified options and waits for it to complete. +// +// StateMigrateJSON is likely to be removed in a future major version in favour of +// StateMigrate returning JSON by default. +func (tf *Terraform) StateMigrateJSON(ctx context.Context, opts ...StateMigrateOption) error { + cmd, err := tf.stateMigrateJSONCmd(ctx, opts...) + if err != nil { + return err + } + + // TODO: JSON parsing + // TODO: error handling (stderr and parsing?) + + return tf.runTerraformCmd(ctx, cmd) +} + +func (tf *Terraform) stateMigrateJSONCmd(ctx context.Context, opts ...StateMigrateOption) (*exec.Cmd, error) { + c := defaultStateMigrateOptions + + for _, o := range opts { + o.configureStateMigrate(&c) + } + + args := tf.buildStateMigrateArgs(c) + + return tf.buildTerraformCmd(ctx, nil, args...), nil +} + +func (tf *Terraform) buildStateMigrateArgs(c stateMigrateConfig) []string { + args := []string{"state", "migrate", "-no-color", "-input=false", "-json"} + + if c.srcProviderLockFile != "" { + args = append(args, "-source-provider-lock-file="+c.srcProviderLockFile) + } + if c.dstProviderLockFile != "" { + args = append(args, "-destination-provider-lock-file="+c.dstProviderLockFile) + } + if c.upgrade { + args = append(args, "-upgrade") + } + return args +}
diff --git a/tfexec/state_migrate_test.go b/tfexec/state_migrate_test.go new file mode 100644 index 0000000..24b09da --- /dev/null +++ b/tfexec/state_migrate_test.go
@@ -0,0 +1,61 @@ +// Copyright IBM Corp. 2020, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package tfexec + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-exec/tfexec/internal/testutil" +) + +func TestStateMigrateJSONCmd(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) { + stateMigrateJSONCmd, err := tf.stateMigrateJSONCmd(context.Background()) + if err != nil { + t.Fatal(err) + } + + assertCmd(t, []string{ + "state", + "migrate", + "-no-color", + "-input=false", + "-json", + "-source-provider-lock-file=.terraform.lock.hcl", + "-destination-provider-lock-file=.terraform.lock.hcl", + }, nil, stateMigrateJSONCmd) + }) + + t.Run("override all defaults", func(t *testing.T) { + stateMigrateJSONCmd, err := tf.stateMigrateJSONCmd(context.Background(), + SourceProviderLockFile("src.lock.hcl"), + DestinationProviderLockFile("dst.lock.hcl"), + Upgrade(true)) + if err != nil { + t.Fatal(err) + } + + assertCmd(t, []string{ + "state", + "migrate", + "-no-color", + "-input=false", + "-json", + "-source-provider-lock-file=src.lock.hcl", + "-destination-provider-lock-file=dst.lock.hcl", + "-upgrade", + }, nil, stateMigrateJSONCmd) + }) +}