From 35bf6ccb09f95272a9b22b4cd451a027aaabcdf5 Mon Sep 17 00:00:00 2001 From: Katy Moe Date: Tue, 24 Nov 2020 15:47:07 +0000 Subject: [PATCH] add state mv --- tfexec/internal/e2etest/show_test.go | 6 -- tfexec/internal/e2etest/state_mv_test.go | 64 ++++++++++++++ tfexec/internal/e2etest/util_test.go | 6 ++ tfexec/options.go | 18 ++++ tfexec/state_mv.go | 105 +++++++++++++++++++++++ tfexec/state_mv_test.go | 58 +++++++++++++ 6 files changed, 251 insertions(+), 6 deletions(-) create mode 100644 tfexec/internal/e2etest/state_mv_test.go create mode 100644 tfexec/state_mv.go create mode 100644 tfexec/state_mv_test.go diff --git a/tfexec/internal/e2etest/show_test.go b/tfexec/internal/e2etest/show_test.go index 31e422aa..49be3901 100644 --- a/tfexec/internal/e2etest/show_test.go +++ b/tfexec/internal/e2etest/show_test.go @@ -17,12 +17,6 @@ import ( "github.com/hashicorp/terraform-exec/tfexec/internal/testutil" ) -var ( - showMinVersion = version.Must(version.NewVersion("0.12.0")) - - providerAddressMinVersion = version.Must(version.NewVersion("0.13.0")) -) - func TestShow(t *testing.T) { runTest(t, "basic_with_state", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) { if tfv.LessThan(showMinVersion) { diff --git a/tfexec/internal/e2etest/state_mv_test.go b/tfexec/internal/e2etest/state_mv_test.go new file mode 100644 index 00000000..2cd5304a --- /dev/null +++ b/tfexec/internal/e2etest/state_mv_test.go @@ -0,0 +1,64 @@ +package e2etest + +import ( + "context" + "testing" + + "github.com/hashicorp/go-version" + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-exec/tfexec" +) + +func TestStateMv(t *testing.T) { + runTest(t, "basic_with_state", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) { + if tfv.LessThan(showMinVersion) { + t.Skip("terraform show was added in Terraform 0.12, so test is not valid") + } + + providerName := "registry.terraform.io/-/null" + if tfv.LessThan(providerAddressMinVersion) { + providerName = "null" + } + + err := tf.Init(context.Background()) + if err != nil { + t.Fatalf("error running Init in test directory: %s", err) + } + + err = tf.StateMv(context.Background(), "null_resource.foo", "null_resource.bar") + if err != nil { + t.Fatalf("error running StateMv: %s", err) + } + + // test that the new state is as expected + expected := &tfjson.State{ + FormatVersion: "0.1", + // TerraformVersion is ignored to facilitate latest version testing + Values: &tfjson.StateValues{ + RootModule: &tfjson.StateModule{ + Resources: []*tfjson.StateResource{{ + Address: "null_resource.bar", + AttributeValues: map[string]interface{}{ + "id": "5510719323588825107", + "triggers": nil, + }, + Mode: tfjson.ManagedResourceMode, + Type: "null_resource", + Name: "bar", + ProviderName: providerName, + }}, + }, + }, + } + + actual, err := tf.Show(context.Background()) + if err != nil { + t.Fatal(err) + } + + if diff := diffState(expected, actual); diff != "" { + t.Fatalf("mismatch (-want +got):\n%s", diff) + } + }) +} diff --git a/tfexec/internal/e2etest/util_test.go b/tfexec/internal/e2etest/util_test.go index d5232a92..54f6f39c 100644 --- a/tfexec/internal/e2etest/util_test.go +++ b/tfexec/internal/e2etest/util_test.go @@ -20,6 +20,12 @@ import ( const testFixtureDir = "testdata" const masterRef = "refs/heads/master" +var ( + showMinVersion = version.Must(version.NewVersion("0.12.0")) + + providerAddressMinVersion = version.Must(version.NewVersion("0.13.0")) +) + func runTest(t *testing.T, fixtureName string, cb func(t *testing.T, tfVersion *version.Version, tf *tfexec.Terraform)) { t.Helper() diff --git a/tfexec/options.go b/tfexec/options.go index f5baf5d4..29cd6058 100644 --- a/tfexec/options.go +++ b/tfexec/options.go @@ -34,6 +34,15 @@ func BackendConfig(backendConfig string) *BackendConfigOption { return &BackendConfigOption{backendConfig} } +type BackupOutOption struct { + path string +} + +// BackupOutOption represents the -backup-out flag. +func BackupOut(path string) *BackupOutOption { + return &BackupOutOption{path} +} + // BackupOption represents the -backup flag. type BackupOption struct { path string @@ -99,6 +108,15 @@ func Destroy(destroy bool) *DestroyFlagOption { return &DestroyFlagOption{destroy} } +type DryRunOption struct { + dryRun bool +} + +// DryRun represents the -dry-run flag. +func DryRun(dryRun bool) *DryRunOption { + return &DryRunOption{dryRun} +} + type ForceCopyOption struct { forceCopy bool } diff --git a/tfexec/state_mv.go b/tfexec/state_mv.go new file mode 100644 index 00000000..1646e52c --- /dev/null +++ b/tfexec/state_mv.go @@ -0,0 +1,105 @@ +package tfexec + +import ( + "context" + "os/exec" + "strconv" +) + +type stateMvConfig struct { + backup string + backupOut string + dryRun bool + lock bool + lockTimeout string + state string + stateOut string +} + +var defaultStateMvOptions = stateMvConfig{ + lock: true, + lockTimeout: "0s", +} + +// StateMvCmdOption represents options used in the Refresh method. +type StateMvCmdOption interface { + configureStateMv(*stateMvConfig) +} + +func (opt *BackupOption) configureStateMv(conf *stateMvConfig) { + conf.backup = opt.path +} + +func (opt *BackupOutOption) configureStateMv(conf *stateMvConfig) { + conf.backupOut = opt.path +} + +func (opt *DryRunOption) configureStateMv(conf *stateMvConfig) { + conf.dryRun = opt.dryRun +} + +func (opt *LockOption) configureStateMv(conf *stateMvConfig) { + conf.lock = opt.lock +} + +func (opt *LockTimeoutOption) configureStateMv(conf *stateMvConfig) { + conf.lockTimeout = opt.timeout +} + +func (opt *StateOption) configureStateMv(conf *stateMvConfig) { + conf.state = opt.path +} + +func (opt *StateOutOption) configureStateMv(conf *stateMvConfig) { + conf.stateOut = opt.path +} + +// StateMv represents the terraform state mv subcommand. +func (tf *Terraform) StateMv(ctx context.Context, source string, destination string, opts ...StateMvCmdOption) error { + cmd, err := tf.stateMvCmd(ctx, source, destination, opts...) + if err != nil { + return err + } + return tf.runTerraformCmd(cmd) +} + +func (tf *Terraform) stateMvCmd(ctx context.Context, source string, destination string, opts ...StateMvCmdOption) (*exec.Cmd, error) { + c := defaultStateMvOptions + + for _, o := range opts { + o.configureStateMv(&c) + } + + args := []string{"state", "mv", "-no-color"} + + // string opts: only pass if set + if c.backup != "" { + args = append(args, "-backup="+c.backup) + } + if c.backupOut != "" { + args = append(args, "-backup-out="+c.backupOut) + } + if c.lockTimeout != "" { + args = append(args, "-lock-timeout="+c.lockTimeout) + } + if c.state != "" { + args = append(args, "-state="+c.state) + } + if c.stateOut != "" { + args = append(args, "-state-out="+c.stateOut) + } + + // boolean and numerical opts: always pass + args = append(args, "-lock="+strconv.FormatBool(c.lock)) + + // unary flags: pass if true + if c.dryRun { + args = append(args, "-dry-run") + } + + // positional arguments + args = append(args, source) + args = append(args, destination) + + return tf.buildTerraformCmd(ctx, nil, args...), nil +} diff --git a/tfexec/state_mv_test.go b/tfexec/state_mv_test.go new file mode 100644 index 00000000..9164185c --- /dev/null +++ b/tfexec/state_mv_test.go @@ -0,0 +1,58 @@ +package tfexec + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-exec/tfexec/internal/testutil" +) + +func TestStateMvCmd(t *testing.T) { + td := testTempDir(t) + + tf, err := NewTerraform(td, tfVersion(t, testutil.Latest013)) + 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) { + stateMvCmd, err := tf.stateMvCmd(context.Background(), "testsource", "testdestination") + if err != nil { + t.Fatal(err) + } + + assertCmd(t, []string{ + "state", + "mv", + "-no-color", + "-lock-timeout=0s", + "-lock=true", + "testsource", + "testdestination", + }, nil, stateMvCmd) + }) + + t.Run("override all defaults", func(t *testing.T) { + stateMvCmd, err := tf.stateMvCmd(context.Background(), "testsrc", "testdest", Backup("testbackup"), BackupOut("testbackupout"), LockTimeout("200s"), State("teststate"), StateOut("teststateout"), Lock(false)) + if err != nil { + t.Fatal(err) + } + + assertCmd(t, []string{ + "state", + "mv", + "-no-color", + "-backup=testbackup", + "-backup-out=testbackupout", + "-lock-timeout=200s", + "-state=teststate", + "-state-out=teststateout", + "-lock=false", + "testsrc", + "testdest", + }, nil, stateMvCmd) + }) +}