From 7d97ddc063539fad602ab0a469c7ccd20df0813d Mon Sep 17 00:00:00 2001 From: Rambabu Duddukuri Date: Mon, 3 Jan 2022 14:00:26 -0800 Subject: [PATCH 1/3] Support terraform state replace-provider --- .../e2etest/state_replace_provider_test.go | 78 +++++++++++++++++ tfexec/state_replace_provider.go | 87 +++++++++++++++++++ tfexec/state_replace_provider_test.go | 59 +++++++++++++ 3 files changed, 224 insertions(+) create mode 100644 tfexec/internal/e2etest/state_replace_provider_test.go create mode 100644 tfexec/state_replace_provider.go create mode 100644 tfexec/state_replace_provider_test.go diff --git a/tfexec/internal/e2etest/state_replace_provider_test.go b/tfexec/internal/e2etest/state_replace_provider_test.go new file mode 100644 index 00000000..6457859f --- /dev/null +++ b/tfexec/internal/e2etest/state_replace_provider_test.go @@ -0,0 +1,78 @@ +package e2etest + +import ( + "context" + "encoding/json" + "testing" + + "github.com/hashicorp/go-version" + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-exec/tfexec" +) + +func TestStateReplaceProvider(t *testing.T) { + runTest(t, "basic_with_state", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) { + if tfv.LessThan(providerAddressMinVersion) { + t.Skip("state file provider FQNs not compatible with this Terraform version") + } + + providerName := "registry.terraform.io/mildred/null" + + err := tf.Init(context.Background()) + if err != nil { + t.Fatalf("error running Init in test directory: %s", err) + } + + err = tf.StateReplaceProvider(context.Background(), "hashicorp/null", "mildred/null") + if err != nil { + t.Fatalf("error running StateReplaceProvider: %s", err) + } + + err = tf.Init(context.Background()) + if err != nil { + t.Fatalf("error running Init in test directory: %s", err) + } + + formatVersion := "0.1" + var sensitiveValues json.RawMessage + if tfv.Core().GreaterThanOrEqual(v1_0_1) { + formatVersion = "0.2" + sensitiveValues = json.RawMessage([]byte("{}")) + } + if tfv.Core().GreaterThanOrEqual(v1_1) { + formatVersion = "1.0" + } + + // test that the new state is as expected + expected := &tfjson.State{ + FormatVersion: formatVersion, + // TerraformVersion is ignored to facilitate latest version testing + Values: &tfjson.StateValues{ + RootModule: &tfjson.StateModule{ + Resources: []*tfjson.StateResource{{ + Address: "null_resource.foo", + AttributeValues: map[string]interface{}{ + "id": "5510719323588825107", + "triggers": nil, + }, + SensitiveValues: sensitiveValues, + Mode: tfjson.ManagedResourceMode, + Type: "null_resource", + Name: "foo", + 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/state_replace_provider.go b/tfexec/state_replace_provider.go new file mode 100644 index 00000000..c6edfdaf --- /dev/null +++ b/tfexec/state_replace_provider.go @@ -0,0 +1,87 @@ +package tfexec + +import ( + "context" + "os/exec" + "strconv" +) + +type stateReplaceProviderConfig struct { + backup string + lock bool + lockTimeout string + state string + stateOut string +} + +var defaultStateReplaceProviderOptions = stateReplaceProviderConfig{ + lock: true, + lockTimeout: "0s", +} + +// StateReplaceProviderCmdOption represents options used in the Refresh method. +type StateReplaceProviderCmdOption interface { + configureStateReplaceProvider(*stateReplaceProviderConfig) +} + +func (opt *BackupOption) configureStateReplaceProvider(conf *stateReplaceProviderConfig) { + conf.backup = opt.path +} + +func (opt *LockOption) configureStateReplaceProvider(conf *stateReplaceProviderConfig) { + conf.lock = opt.lock +} + +func (opt *LockTimeoutOption) configureStateReplaceProvider(conf *stateReplaceProviderConfig) { + conf.lockTimeout = opt.timeout +} + +func (opt *StateOption) configureStateReplaceProvider(conf *stateReplaceProviderConfig) { + conf.state = opt.path +} + +func (opt *StateOutOption) configureStateReplaceProvider(conf *stateReplaceProviderConfig) { + conf.stateOut = opt.path +} + +// StateMv represents the terraform state mv subcommand. +func (tf *Terraform) StateReplaceProvider(ctx context.Context, fromProviderFqn string, toProviderFqn string, opts ...StateReplaceProviderCmdOption) error { + cmd, err := tf.stateReplaceProviderCmd(ctx, fromProviderFqn, toProviderFqn, opts...) + if err != nil { + return err + } + return tf.runTerraformCmd(ctx, cmd) +} + +func (tf *Terraform) stateReplaceProviderCmd(ctx context.Context, fromProviderFqn string, toProviderFqn string, opts ...StateReplaceProviderCmdOption) (*exec.Cmd, error) { + c := defaultStateReplaceProviderOptions + + for _, o := range opts { + o.configureStateReplaceProvider(&c) + } + + args := []string{"state", "replace-provider", "-no-color", "-auto-approve"} + + // string opts: only pass if set + if c.backup != "" { + args = append(args, "-backup="+c.backup) + } + 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)) + + // positional arguments + args = append(args, fromProviderFqn) + args = append(args, toProviderFqn) + + return tf.buildTerraformCmd(ctx, nil, args...), nil +} diff --git a/tfexec/state_replace_provider_test.go b/tfexec/state_replace_provider_test.go new file mode 100644 index 00000000..c91d8a62 --- /dev/null +++ b/tfexec/state_replace_provider_test.go @@ -0,0 +1,59 @@ +package tfexec + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-exec/tfexec/internal/testutil" +) + +func TestStateReplaceProviderCmd(t *testing.T) { + td := t.TempDir() + + 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) { + stateReplaceProviderCmd, err := tf.stateReplaceProviderCmd(context.Background(), "testfromprovider", "testtoprovider") + if err != nil { + t.Fatal(err) + } + + assertCmd(t, []string{ + "state", + "replace-provider", + "-no-color", + "-auto-approve", + "-lock-timeout=0s", + "-lock=true", + "testfromprovider", + "testtoprovider", + }, nil, stateReplaceProviderCmd) + }) + + t.Run("override all defaults", func(t *testing.T) { + stateReplaceProviderCmd, err := tf.stateReplaceProviderCmd(context.Background(), "testfromprovider", "testtoprovider", Backup("testbackup"), LockTimeout("200s"), State("teststate"), StateOut("teststateout"), Lock(false)) + if err != nil { + t.Fatal(err) + } + + assertCmd(t, []string{ + "state", + "replace-provider", + "-no-color", + "-auto-approve", + "-backup=testbackup", + "-lock-timeout=200s", + "-state=teststate", + "-state-out=teststateout", + "-lock=false", + "testfromprovider", + "testtoprovider", + }, nil, stateReplaceProviderCmd) + }) +} From b2d169b89f526bad8e5a1a304b1488c88c8d0245 Mon Sep 17 00:00:00 2001 From: Rambabu Duddukuri Date: Mon, 3 Jan 2022 14:26:18 -0800 Subject: [PATCH 2/3] Updating comment --- tfexec/state_replace_provider.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tfexec/state_replace_provider.go b/tfexec/state_replace_provider.go index c6edfdaf..a1a44ffb 100644 --- a/tfexec/state_replace_provider.go +++ b/tfexec/state_replace_provider.go @@ -44,7 +44,7 @@ func (opt *StateOutOption) configureStateReplaceProvider(conf *stateReplaceProvi conf.stateOut = opt.path } -// StateMv represents the terraform state mv subcommand. +// StateReplaceProvider represents the terraform state replace-provider subcommand. func (tf *Terraform) StateReplaceProvider(ctx context.Context, fromProviderFqn string, toProviderFqn string, opts ...StateReplaceProviderCmdOption) error { cmd, err := tf.stateReplaceProviderCmd(ctx, fromProviderFqn, toProviderFqn, opts...) if err != nil { From c055a2115ef69ae961a78c46dbb794e6bd9ff2af Mon Sep 17 00:00:00 2001 From: Rambabu Duddukuri Date: Mon, 3 Jan 2022 14:58:23 -0800 Subject: [PATCH 3/3] Fixing e2e expected output --- tfexec/internal/e2etest/state_replace_provider_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tfexec/internal/e2etest/state_replace_provider_test.go b/tfexec/internal/e2etest/state_replace_provider_test.go index 6457859f..3a154ec6 100644 --- a/tfexec/internal/e2etest/state_replace_provider_test.go +++ b/tfexec/internal/e2etest/state_replace_provider_test.go @@ -54,7 +54,10 @@ func TestStateReplaceProvider(t *testing.T) { Address: "null_resource.foo", AttributeValues: map[string]interface{}{ "id": "5510719323588825107", + "inputs": nil, + "outputs": nil, "triggers": nil, + "values": nil, }, SensitiveValues: sensitiveValues, Mode: tfjson.ManagedResourceMode,