Add support for state push/pull
diff --git a/tfexec/state_pull.go b/tfexec/state_pull.go
new file mode 100644
index 0000000..48a97c1
--- /dev/null
+++ b/tfexec/state_pull.go
@@ -0,0 +1,49 @@
+package tfexec
+
+import (
+ "context"
+ "os/exec"
+
+ tfjson "github.com/hashicorp/terraform-json"
+)
+
+type statePullConfig struct {
+ reattachInfo ReattachInfo
+}
+
+var defaultStatePullConfig = statePullConfig{}
+
+func (tf *Terraform) StatePull(ctx context.Context) (*tfjson.State, error) {
+ c := defaultStatePullConfig
+
+ mergeEnv := map[string]string{}
+ if c.reattachInfo != nil {
+ reattachStr, err := c.reattachInfo.marshalString()
+ if err != nil {
+ return nil, err
+ }
+ mergeEnv[reattachEnvVar] = reattachStr
+ }
+
+ cmd := tf.statePullCmd(ctx)
+
+ var ret tfjson.State
+ ret.UseJSONNumber(true)
+ err := tf.runTerraformCmdJSON(ctx, cmd, &ret)
+ if err != nil {
+ return nil, err
+ }
+
+ err = ret.Validate()
+ if err != nil {
+ return nil, err
+ }
+
+ return &ret, nil
+}
+
+func (tf *Terraform) statePullCmd(ctx context.Context) *exec.Cmd {
+ args := []string{"state", "pull"}
+
+ return tf.buildTerraformCmd(ctx, nil, args...)
+}
diff --git a/tfexec/state_pull_test.go b/tfexec/state_pull_test.go
new file mode 100644
index 0000000..48c347a
--- /dev/null
+++ b/tfexec/state_pull_test.go
@@ -0,0 +1,28 @@
+package tfexec
+
+import (
+ "context"
+ "testing"
+
+ "github.com/hashicorp/terraform-exec/tfexec/internal/testutil"
+)
+
+func TestStatePull(t *testing.T) {
+ td := testTempDir(t)
+
+ tf, err := NewTerraform(td, tfVersion(t, testutil.Latest_v1))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ tf.SetEnv(map[string]string{})
+
+ t.Run("tfstate", func(t *testing.T) {
+ statePullCmd := tf.statePullCmd(context.Background())
+
+ assertCmd(t, []string{
+ "state",
+ "pull",
+ }, nil, statePullCmd)
+ })
+}
diff --git a/tfexec/state_push.go b/tfexec/state_push.go
new file mode 100644
index 0000000..98f50f1
--- /dev/null
+++ b/tfexec/state_push.go
@@ -0,0 +1,65 @@
+package tfexec
+
+import (
+ "context"
+ "os/exec"
+ "strconv"
+)
+
+type statePushConfig struct {
+ force bool
+ lock bool
+ lockTimeout string
+}
+
+var defaultStatePushOptions = statePushConfig{
+ lock: false,
+ lockTimeout: "0s",
+}
+
+// StatePushCmdOption represents options used in the Refresh method.
+type StatePushCmdOption interface {
+ configureStatePush(*statePushConfig)
+}
+
+func (opt *ForceOption) configureStatePush(conf *statePushConfig) {
+ conf.force = opt.force
+}
+
+func (opt *LockOption) configureStatePush(conf *statePushConfig) {
+ conf.lock = opt.lock
+}
+
+func (opt *LockTimeoutOption) configureStatePush(conf *statePushConfig) {
+ conf.lockTimeout = opt.timeout
+}
+
+func (tf *Terraform) StatePush(ctx context.Context, path string, opts ...StatePushCmdOption) error {
+ cmd, err := tf.statePushCmd(ctx, path, opts...)
+ if err != nil {
+ return err
+ }
+ return tf.runTerraformCmd(ctx, cmd)
+}
+
+func (tf *Terraform) statePushCmd(ctx context.Context, path string, opts ...StatePushCmdOption) (*exec.Cmd, error) {
+ c := defaultStatePushOptions
+
+ for _, o := range opts {
+ o.configureStatePush(&c)
+ }
+
+ args := []string{"state", "push"}
+
+ if c.force {
+ args = append(args, "-force")
+ }
+
+ args = append(args, "-lock="+strconv.FormatBool(c.lock))
+
+ if c.lockTimeout != "" {
+ args = append(args, "-lock-timeout="+c.lockTimeout)
+ }
+
+ return tf.buildTerraformCmd(ctx, nil, args...), nil
+}
diff --git a/tfexec/state_push_test.go b/tfexec/state_push_test.go
new file mode 100644
index 0000000..7967718
--- /dev/null
+++ b/tfexec/state_push_test.go
@@ -0,0 +1,48 @@
+package tfexec
+
+import (
+ "context"
+ "testing"
+
+ "github.com/hashicorp/terraform-exec/tfexec/internal/testutil"
+)
+
+func TestStatePushCmd(t *testing.T) {
+ td := testTempDir(t)
+
+ tf, err := NewTerraform(td, tfVersion(t, testutil.Latest_v1))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ tf.SetEnv(map[string]string{})
+
+ t.Run("defaults", func(t *testing.T) {
+ statePushCmd, err := tf.statePushCmd(context.Background(), "testpath")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ assertCmd(t, []string{
+ "state",
+ "push",
+ "-lock=false",
+ "-lock-timeout=0s",
+ }, nil, statePushCmd)
+ })
+
+ t.Run("override all defaults", func(t *testing.T) {
+ statePushCmd, err := tf.statePushCmd(context.Background(), "testpath", Force(true), Lock(true), LockTimeout("10s"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ assertCmd(t, []string{
+ "state",
+ "push",
+ "-force",
+ "-lock=true",
+ "-lock-timeout=10s",
+ }, nil, statePushCmd)
+ })
+}