Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support terraform state replace-provider #265

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
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)
})
}