Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions tfexec/internal/e2etest/state_replace_provider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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",
"inputs": nil,
"outputs": nil,
"triggers": nil,
"values": 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)
}
})
}
87 changes: 87 additions & 0 deletions tfexec/state_replace_provider.go
Original file line number Diff line number Diff line change
@@ -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
}

// 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 {
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
}
59 changes: 59 additions & 0 deletions tfexec/state_replace_provider_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}