From 39c4b1e63a8695adf92fe631287bd83d70df19dd Mon Sep 17 00:00:00 2001 From: Masayuki Morita Date: Mon, 26 Dec 2022 10:51:05 +0900 Subject: [PATCH 01/10] Split a wildcard implementation to xmvExpander --- tfmigrate/multi_state_mv_action.go | 2 +- tfmigrate/state_xmv_action.go | 85 +--------------- tfmigrate/state_xmv_action_test.go | 151 +-------------------------- tfmigrate/xmv_expander.go | 98 ++++++++++++++++++ tfmigrate/xmv_expander_test.go | 157 +++++++++++++++++++++++++++++ 5 files changed, 261 insertions(+), 232 deletions(-) create mode 100644 tfmigrate/xmv_expander.go create mode 100644 tfmigrate/xmv_expander_test.go diff --git a/tfmigrate/multi_state_mv_action.go b/tfmigrate/multi_state_mv_action.go index 669abb5..c51d5d2 100644 --- a/tfmigrate/multi_state_mv_action.go +++ b/tfmigrate/multi_state_mv_action.go @@ -12,7 +12,7 @@ import ( type MultiStateMvAction struct { // source is an address of resource or module to be moved. source string - // // destination is a new address of resource or module to move. + // destination is a new address of resource or module to move. destination string } diff --git a/tfmigrate/state_xmv_action.go b/tfmigrate/state_xmv_action.go index 1b411f6..2ab25c7 100644 --- a/tfmigrate/state_xmv_action.go +++ b/tfmigrate/state_xmv_action.go @@ -2,9 +2,6 @@ package tfmigrate import ( "context" - "fmt" - "regexp" - "strings" "github.com/minamijoyo/tfmigrate/tfexec" ) @@ -30,8 +27,8 @@ func NewStateXMvAction(source string, destination string) *StateXMvAction { } // StateUpdate updates a given state and returns a new state. -// Source resources have wildcards which should be matched against the tf state. Each occurrence will generate -// a move command. +// Source resources have wildcards which should be matched against the tf state. +// Each occurrence will generate a move command. func (a *StateXMvAction) StateUpdate(ctx context.Context, tf tfexec.TerraformCLI, state *tfexec.State) (*tfexec.State, error) { stateMvActions, err := a.generateMvActions(ctx, tf, state) if err != nil { @@ -53,81 +50,7 @@ func (a *StateXMvAction) generateMvActions(ctx context.Context, tf tfexec.Terraf if err != nil { return nil, err } - return a.getStateMvActionsForStateList(stateList) -} - -// A wildcardChar will greedy match with any character in the resource path. -const matchWildcardRegex = "(.*)" -const wildcardChar = "*" - -func (a *StateXMvAction) nrOfWildcards() int { - return strings.Count(a.source, wildcardChar) -} - -// Return regex pattern that matches the wildcard source and make sure characters are not treated as -// special meta characters. -func makeSourceMatchPattern(s string) string { - safeString := regexp.QuoteMeta(s) - quotedWildCardChar := regexp.QuoteMeta(wildcardChar) - return strings.ReplaceAll(safeString, quotedWildCardChar, matchWildcardRegex) -} - -// Get a regex that will do matching based on the wildcard source that was given. -func makeSrcRegex(source string) (*regexp.Regexp, error) { - regPattern := makeSourceMatchPattern(source) - regExpression, err := regexp.Compile(regPattern) - if err != nil { - return nil, fmt.Errorf("could not make pattern out of %s (%s) due to %s", source, regPattern, err) - } - return regExpression, nil -} - -// Look into the state and find sources that match pattern with wild cards. -func (a *StateXMvAction) getMatchingSourcesFromState(stateList []string) ([]string, error) { - r, err := makeSrcRegex(a.source) - if err != nil { - return nil, err - } - - var matchingStateSources []string - for _, s := range stateList { - match := r.FindString(s) - if match != "" { - matchingStateSources = append(matchingStateSources, match) - } - } - return matchingStateSources, err -} - -// When you have the stateXMvAction with wildcards get the destination for a source -func (a *StateXMvAction) getDestinationForStateSrc(stateSource string) (string, error) { - r, err := makeSrcRegex(a.source) - if err != nil { - return "", err - } - destination := r.ReplaceAllString(stateSource, a.destination) - return destination, err -} - -// Get actions matching wildcard move actions based on the list of resources. -func (a *StateXMvAction) getStateMvActionsForStateList(stateList []string) ([]*StateMvAction, error) { - if a.nrOfWildcards() == 0 { - staticActionAsList := make([]*StateMvAction, 1) - staticActionAsList[0] = NewStateMvAction(a.source, a.destination) - return staticActionAsList, nil - } - matchingSources, err := a.getMatchingSourcesFromState(stateList) - if err != nil { - return nil, err - } - matchingActions := make([]*StateMvAction, len(matchingSources)) - for i, matchingSource := range matchingSources { - destination, e2 := a.getDestinationForStateSrc(matchingSource) - if e2 != nil { - return nil, e2 - } - matchingActions[i] = NewStateMvAction(matchingSource, destination) - } - return matchingActions, nil + e := newXMvExpander(a) + return e.expand(stateList) } diff --git a/tfmigrate/state_xmv_action_test.go b/tfmigrate/state_xmv_action_test.go index 5e4d3c2..65a5fe7 100644 --- a/tfmigrate/state_xmv_action_test.go +++ b/tfmigrate/state_xmv_action_test.go @@ -4,12 +4,10 @@ import ( "context" "testing" - "github.com/davecgh/go-spew/spew" - "github.com/google/go-cmp/cmp" "github.com/minamijoyo/tfmigrate/tfexec" ) -func TestAccStateMvActionWildcardRenameSecurityGroupResourceNamesFromDocs(t *testing.T) { +func TestAccStateMvActionWildcardRename(t *testing.T) { tfexec.SkipUnlessAcceptanceTestEnabled(t) backend := tfexec.GetTestAccBackendS3Config(t.Name()) @@ -47,150 +45,3 @@ resource "null_resource" "bar2" {} t.Fatalf("failed to run migrator plan: %s", err) } } - -func TestGetNrOfWildcard(t *testing.T) { - cases := []struct { - desc string - resource *StateXMvAction - nrWildcards int - }{ - { - desc: "Simple resource no wildcardChar", - resource: NewStateXMvAction("null_resource.foo", "null_resource.foo2"), - nrWildcards: 0, - }, - { - desc: "Simple wildcardChar for a resource", - resource: NewStateXMvAction("null_resource.*", "null_resource.$1"), - nrWildcards: 1, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - got := tc.resource.nrOfWildcards() - if got != tc.nrWildcards { - t.Errorf("Number of wildcards for %d is not expected %s", got, tc.resource) - } - }) - } -} - -func TestGetStateMvActionsForStateList(t *testing.T) { - cases := []struct { - desc string - stateList []string - inputXMvAction *StateXMvAction - outputMvActions []*StateMvAction - }{ - { - desc: "Simple resource no wildcardChar", - stateList: nil, - inputXMvAction: &StateXMvAction{ - source: "null_resource.foo", - destination: "null_resource.foo2", - }, - outputMvActions: []*StateMvAction{ - { - source: "null_resource.foo", - destination: "null_resource.foo2", - }, - }, - }, - { - desc: "Simple resource with wildcardChar", - stateList: []string{"null_resource.foo"}, - inputXMvAction: &StateXMvAction{ - source: "null_resource.*", - destination: "module.example[\"$1\"].this", - }, - outputMvActions: []*StateMvAction{ - { - source: "null_resource.foo", - destination: "module.example[\"foo\"].this", - }, - }, - }, - { - desc: "Simple module name refactor with wildcardChar", - stateList: []string{"module.example1[\"foo\"].this"}, - inputXMvAction: &StateXMvAction{ - source: "module.example1[\"*\"].this", - destination: "module.example2[\"$1\"].this", - }, - outputMvActions: []*StateMvAction{ - { - source: "module.example1[\"foo\"].this", - destination: "module.example2[\"foo\"].this", - }, - }, - }, - { - desc: "No matching resources in state", - stateList: []string{"time_static.foo"}, - inputXMvAction: &StateXMvAction{ - source: "null_resource.*", - destination: "module.example[\"$1\"].this", - }, - outputMvActions: []*StateMvAction{}, - }, - { - desc: "Documented feature; positional matching for example to allow switching matches from place", - stateList: []string{"module[\"bar\"].null_resource.foo"}, - inputXMvAction: &StateXMvAction{ - source: "module[\"*\"].null_resource.*", - destination: "module[\"$2\"].null_resource.$1", - }, - outputMvActions: []*StateMvAction{ - { - source: "module[\"bar\"].null_resource.foo", - destination: "module[\"foo\"].null_resource.bar", - }, - }, - }, - { - desc: "Multiple resources refactored into a module", - stateList: []string{ - "null_resource.foo", - "null_resource.bar", - "null_resource.baz", - }, - inputXMvAction: &StateXMvAction{ - source: "null_resource.*", - destination: "module.example[\"$1\"].null_resource.this", - }, - outputMvActions: []*StateMvAction{ - { - source: "null_resource.foo", - destination: "module.example[\"foo\"].null_resource.this", - }, - { - source: "null_resource.bar", - destination: "module.example[\"bar\"].null_resource.this", - }, - { - source: "null_resource.baz", - destination: "module.example[\"baz\"].null_resource.this", - }, - }, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - got, err := tc.inputXMvAction.getStateMvActionsForStateList(tc.stateList) - // Errors are not expected. At this stage the only location from which errors are expected is if the regular - // expression that comes from the source cannot compile but since meta-characters are quoted and we only - // introduce matched braces and unmatched meta-characters there are no known cases where we would hit this. - // Still this case gets handled explicitly as it can be helpful info if the author missed a case. - if err != nil { - t.Fatalf("Encountered error %v", err) - } - - if diff := cmp.Diff(got, tc.outputMvActions, cmp.AllowUnexported(StateMvAction{})); diff != "" { - t.Errorf("got: %s, want = %s, diff = %s", spew.Sdump(got), spew.Sdump(tc.outputMvActions), diff) - } - - }) - } -} diff --git a/tfmigrate/xmv_expander.go b/tfmigrate/xmv_expander.go new file mode 100644 index 0000000..7691c1a --- /dev/null +++ b/tfmigrate/xmv_expander.go @@ -0,0 +1,98 @@ +package tfmigrate + +import ( + "fmt" + "regexp" + "strings" +) + +// xmvExpander is a helper method for implementing wildcard expansion for xmv actions. +type xmvExpander struct { + // xmv action to be expanded + action *StateXMvAction +} + +// newXMvExpander returns a new XMvExpander instance. +func newXMvExpander(action *StateXMvAction) *xmvExpander { + return &xmvExpander{ + action: action, + } +} + +// A wildcardChar will greedy match with any character in the resource path. +const matchWildcardRegex = "(.*)" +const wildcardChar = "*" + +// makeSourceMatchPattern returns regex pattern that matches the wildcard +// source and make sure characters are not treated as special meta characters. +func makeSourceMatchPattern(s string) string { + safeString := regexp.QuoteMeta(s) + quotedWildCardChar := regexp.QuoteMeta(wildcardChar) + return strings.ReplaceAll(safeString, quotedWildCardChar, matchWildcardRegex) +} + +// makeSrcRegex returns a regex that will do matching based on the wildcard +// source that was given. +func makeSrcRegex(source string) (*regexp.Regexp, error) { + regPattern := makeSourceMatchPattern(source) + regExpression, err := regexp.Compile(regPattern) + if err != nil { + return nil, fmt.Errorf("could not make pattern out of %s (%s) due to %s", source, regPattern, err) + } + return regExpression, nil +} + +// expand returns actions matching wildcard move actions based on the list of resources. +func (e *xmvExpander) expand(stateList []string) ([]*StateMvAction, error) { + if e.nrOfWildcards() == 0 { + staticActionAsList := make([]*StateMvAction, 1) + staticActionAsList[0] = NewStateMvAction(e.action.source, e.action.destination) + return staticActionAsList, nil + } + matchingSources, err := e.getMatchingSourcesFromState(stateList) + if err != nil { + return nil, err + } + matchingActions := make([]*StateMvAction, len(matchingSources)) + for i, matchingSource := range matchingSources { + destination, e2 := e.getDestinationForStateSrc(matchingSource) + if e2 != nil { + return nil, e2 + } + matchingActions[i] = NewStateMvAction(matchingSource, destination) + } + return matchingActions, nil +} + +func (e *xmvExpander) nrOfWildcards() int { + return strings.Count(e.action.source, wildcardChar) +} + +// getMatchingSourcesFromState looks into the state and find sources that match +// pattern with wild cards. +func (e *xmvExpander) getMatchingSourcesFromState(stateList []string) ([]string, error) { + re, err := makeSrcRegex(e.action.source) + if err != nil { + return nil, err + } + + var matchingStateSources []string + + for _, s := range stateList { + match := re.FindString(s) + if match != "" { + matchingStateSources = append(matchingStateSources, match) + } + } + return matchingStateSources, err +} + +// getDestinationForStateSrc returns the destination for a source. +func (e *xmvExpander) getDestinationForStateSrc(stateSource string) (string, error) { + re, err := makeSrcRegex(e.action.source) + if err != nil { + return "", err + } + destination := re.ReplaceAllString(stateSource, e.action.destination) + return destination, err +} diff --git a/tfmigrate/xmv_expander_test.go b/tfmigrate/xmv_expander_test.go new file mode 100644 index 0000000..8ccbca6 --- /dev/null +++ b/tfmigrate/xmv_expander_test.go @@ -0,0 +1,157 @@ +package tfmigrate + +import ( + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/google/go-cmp/cmp" +) + +func TestGetNrOfWildcard(t *testing.T) { + cases := []struct { + desc string + action *StateXMvAction + want int + }{ + { + desc: "Simple resource no wildcardChar", + action: NewStateXMvAction("null_resource.foo", "null_resource.foo2"), + want: 0, + }, + { + desc: "Simple wildcardChar for a resource", + action: NewStateXMvAction("null_resource.*", "null_resource.$1"), + want: 1, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + e := newXMvExpander(tc.action) + got := e.nrOfWildcards() + if got != tc.want { + t.Errorf("got: %d, but want: %d", got, tc.want) + } + }) + } +} + +func TestGetStateMvActionsForStateList(t *testing.T) { + cases := []struct { + desc string + stateList []string + inputXMvAction *StateXMvAction + outputMvActions []*StateMvAction + }{ + { + desc: "Simple resource no wildcardChar", + stateList: nil, + inputXMvAction: &StateXMvAction{ + source: "null_resource.foo", + destination: "null_resource.foo2", + }, + outputMvActions: []*StateMvAction{ + { + source: "null_resource.foo", + destination: "null_resource.foo2", + }, + }, + }, + { + desc: "Simple resource with wildcardChar", + stateList: []string{"null_resource.foo"}, + inputXMvAction: &StateXMvAction{ + source: "null_resource.*", + destination: "module.example[\"$1\"].this", + }, + outputMvActions: []*StateMvAction{ + { + source: "null_resource.foo", + destination: "module.example[\"foo\"].this", + }, + }, + }, + { + desc: "Simple module name refactor with wildcardChar", + stateList: []string{"module.example1[\"foo\"].this"}, + inputXMvAction: &StateXMvAction{ + source: "module.example1[\"*\"].this", + destination: "module.example2[\"$1\"].this", + }, + outputMvActions: []*StateMvAction{ + { + source: "module.example1[\"foo\"].this", + destination: "module.example2[\"foo\"].this", + }, + }, + }, + { + desc: "No matching resources in state", + stateList: []string{"time_static.foo"}, + inputXMvAction: &StateXMvAction{ + source: "null_resource.*", + destination: "module.example[\"$1\"].this", + }, + outputMvActions: []*StateMvAction{}, + }, + { + desc: "Documented feature; positional matching for example to allow switching matches from place", + stateList: []string{"module[\"bar\"].null_resource.foo"}, + inputXMvAction: &StateXMvAction{ + source: "module[\"*\"].null_resource.*", + destination: "module[\"$2\"].null_resource.$1", + }, + outputMvActions: []*StateMvAction{ + { + source: "module[\"bar\"].null_resource.foo", + destination: "module[\"foo\"].null_resource.bar", + }, + }, + }, + { + desc: "Multiple resources refactored into a module", + stateList: []string{ + "null_resource.foo", + "null_resource.bar", + "null_resource.baz", + }, + inputXMvAction: &StateXMvAction{ + source: "null_resource.*", + destination: "module.example[\"$1\"].null_resource.this", + }, + outputMvActions: []*StateMvAction{ + { + source: "null_resource.foo", + destination: "module.example[\"foo\"].null_resource.this", + }, + { + source: "null_resource.bar", + destination: "module.example[\"bar\"].null_resource.this", + }, + { + source: "null_resource.baz", + destination: "module.example[\"baz\"].null_resource.this", + }, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + e := newXMvExpander(tc.inputXMvAction) + got, err := e.expand(tc.stateList) + // Errors are not expected. At this stage the only location from which errors are expected is if the regular + // expression that comes from the source cannot compile but since meta-characters are quoted and we only + // introduce matched braces and unmatched meta-characters there are no known cases where we would hit this. + // Still this case gets handled explicitly as it can be helpful info if the author missed a case. + if err != nil { + t.Fatalf("Encountered error %v", err) + } + + if diff := cmp.Diff(got, tc.outputMvActions, cmp.AllowUnexported(StateMvAction{})); diff != "" { + t.Errorf("got: %s, want = %s, diff = %s", spew.Sdump(got), spew.Sdump(tc.outputMvActions), diff) + } + + }) + } +} From dc13324a182e57283532b078d50dbe4eeb676cc1 Mon Sep 17 00:00:00 2001 From: Masayuki Morita Date: Mon, 26 Dec 2022 10:57:03 +0900 Subject: [PATCH 02/10] Rename StateXMvAction to StateXmvAction for consistency --- tfmigrate/state_action.go | 2 +- tfmigrate/state_action_test.go | 2 +- tfmigrate/state_xmv_action.go | 18 +++++++++--------- tfmigrate/state_xmv_action_test.go | 2 +- tfmigrate/xmv_expander.go | 4 ++-- tfmigrate/xmv_expander_test.go | 20 ++++++++++---------- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/tfmigrate/state_action.go b/tfmigrate/state_action.go index 617ba1c..6dc0796 100644 --- a/tfmigrate/state_action.go +++ b/tfmigrate/state_action.go @@ -51,7 +51,7 @@ func NewStateActionFromString(cmdStr string) (StateAction, error) { } src := args[1] dst := args[2] - action = NewStateXMvAction(src, dst) + action = NewStateXmvAction(src, dst) case "rm": if len(args) < 2 { diff --git a/tfmigrate/state_action_test.go b/tfmigrate/state_action_test.go index a082883..8180aff 100644 --- a/tfmigrate/state_action_test.go +++ b/tfmigrate/state_action_test.go @@ -42,7 +42,7 @@ func TestNewStateActionFromString(t *testing.T) { { desc: "xmv action (valid)", cmdStr: "xmv null_resource.* null_resource.$1", - want: &StateXMvAction{ + want: &StateXmvAction{ source: "null_resource.*", destination: "null_resource.$1", }, diff --git a/tfmigrate/state_xmv_action.go b/tfmigrate/state_xmv_action.go index 2ab25c7..4e66106 100644 --- a/tfmigrate/state_xmv_action.go +++ b/tfmigrate/state_xmv_action.go @@ -6,21 +6,21 @@ import ( "github.com/minamijoyo/tfmigrate/tfexec" ) -// StateXMvAction implements the StateAction interface. -// StateXMvAction moves a resource from source address to destination address in +// StateXmvAction implements the StateAction interface. +// StateXmvAction moves a resource from source address to destination address in // the same tfstate file. -type StateXMvAction struct { +type StateXmvAction struct { // source is a address of resource or module to be moved which can contain wildcards. source string // destination is a new address of resource or module to move which can contain placeholders. destination string } -var _ StateAction = (*StateXMvAction)(nil) +var _ StateAction = (*StateXmvAction)(nil) -// NewStateXMvAction returns a new StateXMvAction instance. -func NewStateXMvAction(source string, destination string) *StateXMvAction { - return &StateXMvAction{ +// NewStateXmvAction returns a new StateXmvAction instance. +func NewStateXmvAction(source string, destination string) *StateXmvAction { + return &StateXmvAction{ source: source, destination: destination, } @@ -29,7 +29,7 @@ func NewStateXMvAction(source string, destination string) *StateXMvAction { // StateUpdate updates a given state and returns a new state. // Source resources have wildcards which should be matched against the tf state. // Each occurrence will generate a move command. -func (a *StateXMvAction) StateUpdate(ctx context.Context, tf tfexec.TerraformCLI, state *tfexec.State) (*tfexec.State, error) { +func (a *StateXmvAction) StateUpdate(ctx context.Context, tf tfexec.TerraformCLI, state *tfexec.State) (*tfexec.State, error) { stateMvActions, err := a.generateMvActions(ctx, tf, state) if err != nil { return nil, err @@ -45,7 +45,7 @@ func (a *StateXMvAction) StateUpdate(ctx context.Context, tf tfexec.TerraformCLI } // Use an xmv and use the state to determine the corresponding mv actions. -func (a *StateXMvAction) generateMvActions(ctx context.Context, tf tfexec.TerraformCLI, state *tfexec.State) ([]*StateMvAction, error) { +func (a *StateXmvAction) generateMvActions(ctx context.Context, tf tfexec.TerraformCLI, state *tfexec.State) ([]*StateMvAction, error) { stateList, err := tf.StateList(ctx, state, nil) if err != nil { return nil, err diff --git a/tfmigrate/state_xmv_action_test.go b/tfmigrate/state_xmv_action_test.go index 65a5fe7..ff14297 100644 --- a/tfmigrate/state_xmv_action_test.go +++ b/tfmigrate/state_xmv_action_test.go @@ -36,7 +36,7 @@ resource "null_resource" "bar2" {} } actions := []StateAction{ - NewStateXMvAction("null_resource.*", "null_resource.${1}2"), + NewStateXmvAction("null_resource.*", "null_resource.${1}2"), } m := NewStateMigrator(tf.Dir(), workspace, actions, &MigratorOption{}, false) diff --git a/tfmigrate/xmv_expander.go b/tfmigrate/xmv_expander.go index 7691c1a..0673b6c 100644 --- a/tfmigrate/xmv_expander.go +++ b/tfmigrate/xmv_expander.go @@ -9,11 +9,11 @@ import ( // xmvExpander is a helper method for implementing wildcard expansion for xmv actions. type xmvExpander struct { // xmv action to be expanded - action *StateXMvAction + action *StateXmvAction } // newXMvExpander returns a new XMvExpander instance. -func newXMvExpander(action *StateXMvAction) *xmvExpander { +func newXMvExpander(action *StateXmvAction) *xmvExpander { return &xmvExpander{ action: action, } diff --git a/tfmigrate/xmv_expander_test.go b/tfmigrate/xmv_expander_test.go index 8ccbca6..b7f8af0 100644 --- a/tfmigrate/xmv_expander_test.go +++ b/tfmigrate/xmv_expander_test.go @@ -10,17 +10,17 @@ import ( func TestGetNrOfWildcard(t *testing.T) { cases := []struct { desc string - action *StateXMvAction + action *StateXmvAction want int }{ { desc: "Simple resource no wildcardChar", - action: NewStateXMvAction("null_resource.foo", "null_resource.foo2"), + action: NewStateXmvAction("null_resource.foo", "null_resource.foo2"), want: 0, }, { desc: "Simple wildcardChar for a resource", - action: NewStateXMvAction("null_resource.*", "null_resource.$1"), + action: NewStateXmvAction("null_resource.*", "null_resource.$1"), want: 1, }, } @@ -40,13 +40,13 @@ func TestGetStateMvActionsForStateList(t *testing.T) { cases := []struct { desc string stateList []string - inputXMvAction *StateXMvAction + inputXMvAction *StateXmvAction outputMvActions []*StateMvAction }{ { desc: "Simple resource no wildcardChar", stateList: nil, - inputXMvAction: &StateXMvAction{ + inputXMvAction: &StateXmvAction{ source: "null_resource.foo", destination: "null_resource.foo2", }, @@ -60,7 +60,7 @@ func TestGetStateMvActionsForStateList(t *testing.T) { { desc: "Simple resource with wildcardChar", stateList: []string{"null_resource.foo"}, - inputXMvAction: &StateXMvAction{ + inputXMvAction: &StateXmvAction{ source: "null_resource.*", destination: "module.example[\"$1\"].this", }, @@ -74,7 +74,7 @@ func TestGetStateMvActionsForStateList(t *testing.T) { { desc: "Simple module name refactor with wildcardChar", stateList: []string{"module.example1[\"foo\"].this"}, - inputXMvAction: &StateXMvAction{ + inputXMvAction: &StateXmvAction{ source: "module.example1[\"*\"].this", destination: "module.example2[\"$1\"].this", }, @@ -88,7 +88,7 @@ func TestGetStateMvActionsForStateList(t *testing.T) { { desc: "No matching resources in state", stateList: []string{"time_static.foo"}, - inputXMvAction: &StateXMvAction{ + inputXMvAction: &StateXmvAction{ source: "null_resource.*", destination: "module.example[\"$1\"].this", }, @@ -97,7 +97,7 @@ func TestGetStateMvActionsForStateList(t *testing.T) { { desc: "Documented feature; positional matching for example to allow switching matches from place", stateList: []string{"module[\"bar\"].null_resource.foo"}, - inputXMvAction: &StateXMvAction{ + inputXMvAction: &StateXmvAction{ source: "module[\"*\"].null_resource.*", destination: "module[\"$2\"].null_resource.$1", }, @@ -115,7 +115,7 @@ func TestGetStateMvActionsForStateList(t *testing.T) { "null_resource.bar", "null_resource.baz", }, - inputXMvAction: &StateXMvAction{ + inputXMvAction: &StateXmvAction{ source: "null_resource.*", destination: "module.example[\"$1\"].null_resource.this", }, From 3ad405335a8221c685f34e2ff95f3472e2335632 Mon Sep 17 00:00:00 2001 From: Masayuki Morita Date: Mon, 26 Dec 2022 11:00:09 +0900 Subject: [PATCH 03/10] Rename TestAccStateMvActionWildcardRename to TestAccStateXmvAction --- tfmigrate/state_xmv_action_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tfmigrate/state_xmv_action_test.go b/tfmigrate/state_xmv_action_test.go index ff14297..2d45854 100644 --- a/tfmigrate/state_xmv_action_test.go +++ b/tfmigrate/state_xmv_action_test.go @@ -7,7 +7,7 @@ import ( "github.com/minamijoyo/tfmigrate/tfexec" ) -func TestAccStateMvActionWildcardRename(t *testing.T) { +func TestAccStateXmvAction(t *testing.T) { tfexec.SkipUnlessAcceptanceTestEnabled(t) backend := tfexec.GetTestAccBackendS3Config(t.Name()) From abc738d56ac9a1b063d593c150b625daefa4c023 Mon Sep 17 00:00:00 2001 From: Masayuki Morita Date: Mon, 26 Dec 2022 11:04:09 +0900 Subject: [PATCH 04/10] Add acceptance test for TestAccMultiStateMvAction --- tfmigrate/multi_state_mv_action_test.go | 72 +++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 tfmigrate/multi_state_mv_action_test.go diff --git a/tfmigrate/multi_state_mv_action_test.go b/tfmigrate/multi_state_mv_action_test.go new file mode 100644 index 0000000..90521cc --- /dev/null +++ b/tfmigrate/multi_state_mv_action_test.go @@ -0,0 +1,72 @@ +package tfmigrate + +import ( + "context" + "testing" + + "github.com/minamijoyo/tfmigrate/tfexec" +) + +func TestAccMultiStateMvAction(t *testing.T) { + tfexec.SkipUnlessAcceptanceTestEnabled(t) + ctx := context.Background() + + // setup the initial files and states + fromBackend := tfexec.GetTestAccBackendS3Config(t.Name() + "/fromDir") + fromSource := ` +resource "null_resource" "foo" {} +resource "null_resource" "bar" {} +resource "null_resource" "baz" {} +` + fromWorkspace := "default" + fromTf := tfexec.SetupTestAccWithApply(t, fromWorkspace, fromBackend+fromSource) + + toBackend := tfexec.GetTestAccBackendS3Config(t.Name() + "/toDir") + toSource := ` +resource "null_resource" "qux" {} +` + toWorkspace := "default" + toTf := tfexec.SetupTestAccWithApply(t, toWorkspace, toBackend+toSource) + + // update terraform resource files for migration + fromUpdatedSource := ` +resource "null_resource" "baz" {} +` + tfexec.UpdateTestAccSource(t, fromTf, fromBackend+fromUpdatedSource) + + toUpdatedSource := ` +resource "null_resource" "foo" {} +resource "null_resource" "bar2" {} +resource "null_resource" "qux" {} +` + tfexec.UpdateTestAccSource(t, toTf, toBackend+toUpdatedSource) + + fromChanged, err := fromTf.PlanHasChange(ctx, nil) + if err != nil { + t.Fatalf("failed to run PlanHasChange in fromDir: %s", err) + } + if !fromChanged { + t.Fatalf("expect to have changes in fromDir") + } + + toChanged, err := toTf.PlanHasChange(ctx, nil) + if err != nil { + t.Fatalf("failed to run PlanHasChange in toDir: %s", err) + } + if !toChanged { + t.Fatalf("expect to have changes in toDir") + } + + // perform state migration + actions := []MultiStateAction{ + NewMultiStateMvAction("null_resource.foo", "null_resource.foo"), + NewMultiStateMvAction("null_resource.bar", "null_resource.bar2"), + } + o := &MigratorOption{} + force := false + m := NewMultiStateMigrator(fromTf.Dir(), toTf.Dir(), fromWorkspace, toWorkspace, actions, o, force) + err = m.Plan(ctx) + if err != nil { + t.Fatalf("failed to run migrator plan: %s", err) + } +} From 1271fd44e9c6223c3ae1790e1e6388680408dd57 Mon Sep 17 00:00:00 2001 From: Masayuki Morita Date: Mon, 26 Dec 2022 12:11:16 +0900 Subject: [PATCH 05/10] Add support for multi_state xmv (wildcard expansion) We have added the wildcard support for state mv in #111, but this was limited within a single tfstate. Let's extend this feature also to multi_state. This makes it easier to merge two states. --- README.md | 35 +++++++++++- tfmigrate/multi_state_action.go | 9 +++ tfmigrate/multi_state_xmv_action.go | 72 ++++++++++++++++++++++++ tfmigrate/multi_state_xmv_action_test.go | 71 +++++++++++++++++++++++ tfmigrate/state_action.go | 2 +- tfmigrate/state_xmv_action.go | 6 +- tfmigrate/xmv_expander_test.go | 39 ++++++++++--- 7 files changed, 219 insertions(+), 15 deletions(-) create mode 100644 tfmigrate/multi_state_xmv_action.go create mode 100644 tfmigrate/multi_state_xmv_action_test.go diff --git a/README.md b/README.md index ac1bc36..6948c24 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ A Terraform state migration tool for GitOps. * [state import](#state-import) * [migration block (multi_state)](#migration-block-multi_state) * [multi_state mv](#multi_state-mv) + * [multi_state xmv](#multi_state-xmv) * [Integrations](#integrations) * [License](#license) @@ -566,6 +567,7 @@ The `state` migration updates the state in a single directory. It has the follow - `workspace` (optional): A terraform workspace. Defaults to "default". - `actions` (required): Actions is a list of state action. An action is a plain text for state operation. Valid formats are the following. - `"mv "` + - `"xmv "` - `"rm ...` - `"import
"` - `force` (optional): Apply migrations even if plan show changes @@ -591,10 +593,10 @@ migration "state" "test" { #### state xmv The `xmv` command works like the `mv` command but allows usage of -wildcards `*` in the source definition. The source expressions will be +wildcards `*` in the source definition. The source expressions will be matched against resources defined in the terraform state. The matched value can be used in the destination definition via a dollar sign and their ordinal number. Note that dollar signs need to be -escaped and therefore are placed twice: +escaped and therefore are placed twice: `$$1`, `$$2`, ... When there is ambiguity the ordinal number can be put in curly braces (e.g. `$${1}`). For example if `foo` and `bar` in the `mv` command example above are the only 2 security group resources @@ -641,6 +643,7 @@ The `multi_state` migration updates states in two different directories. It is i - `to_workspace` (optional): A terraform workspace in the TO directory. Defaults to "default". - `actions` (required): Actions is a list of multi state action. An action is a plain text for state operation. Valid formats are the following. - `"mv "` + - `"xmv "` - `force` (optional): Apply migrations even if plan show changes Note that `from_dir` and `to_dir` are relative path to the current working directory where `tfmigrate` command is invoked. @@ -660,6 +663,34 @@ migration "multi_state" "mv_dir1_dir2" { } ``` +#### multi_state xmv + +The `xmv` command works like the `mv` command but allows usage of +wildcards `*` in the source definition. +The wildcard expansion rules are the same as for the single state xmv. + +```hcl +migration "multi_state" "mv_dir1_dir2" { + from_dir = "dir1" + to_dir = "dir2" + actions = [ + "xmv aws_security_group.* aws_security_group.$${1}2", + ] +} +``` + +If you want to move all resources for merging two state files: + +```hcl +migration "multi_state" "mv_dir1_dir2" { + from_dir = "dir1" + to_dir = "dir2" + actions = [ + "xmv * $$1", + ] +} +``` + ## Integrations You can integrate tfmigrate with your favorite CI/CD services. Examples are as follows: diff --git a/tfmigrate/multi_state_action.go b/tfmigrate/multi_state_action.go index 9f7a749..8ae31fe 100644 --- a/tfmigrate/multi_state_action.go +++ b/tfmigrate/multi_state_action.go @@ -20,6 +20,7 @@ type MultiStateAction interface { // This method is useful to build an action from terraform state command. // Valid formats are the following. // "mv " +// "xmv " func NewMultiStateActionFromString(cmdStr string) (MultiStateAction, error) { args, err := splitStateAction(cmdStr) if err != nil { @@ -42,6 +43,14 @@ func NewMultiStateActionFromString(cmdStr string) (MultiStateAction, error) { dst := args[2] action = NewMultiStateMvAction(src, dst) + case "xmv": + if len(args) != 3 { + return nil, fmt.Errorf("multi state xmv action is invalid: %s", cmdStr) + } + src := args[1] + dst := args[2] + action = NewMultiStateXmvAction(src, dst) + default: return nil, fmt.Errorf("unknown multi state action type: %s", cmdStr) } diff --git a/tfmigrate/multi_state_xmv_action.go b/tfmigrate/multi_state_xmv_action.go new file mode 100644 index 0000000..72c3439 --- /dev/null +++ b/tfmigrate/multi_state_xmv_action.go @@ -0,0 +1,72 @@ +package tfmigrate + +import ( + "context" + + "github.com/minamijoyo/tfmigrate/tfexec" +) + +// MultiStateXmvAction implements the MultiStateAction interface. +// MultiStateXmvAction is an extended version of MultiStateMvAction. +// It allows you to move multiple resouces with a wildcard match. +type MultiStateXmvAction struct { + // source is a address of resource or module to be moved which can contain wildcards. + source string + // destination is a new address of resource or module to move which can contain placeholders. + destination string +} + +var _ MultiStateAction = (*MultiStateXmvAction)(nil) + +// NewMultiStateXmvAction returns a new MultiStateXmvAction instance. +func NewMultiStateXmvAction(source string, destination string) *MultiStateXmvAction { + return &MultiStateXmvAction{ + source: source, + destination: destination, + } +} + +// MultiStateUpdate updates given two states and returns new two states. +// It moves a resource from a dir to another. +// It also can rename an address of resource. +func (a *MultiStateXmvAction) MultiStateUpdate(ctx context.Context, fromTf tfexec.TerraformCLI, toTf tfexec.TerraformCLI, fromState *tfexec.State, toState *tfexec.State) (*tfexec.State, *tfexec.State, error) { + multiStateMvActions, err := a.generateMvActions(ctx, fromTf, fromState) + if err != nil { + return nil, nil, err + } + + for _, action := range multiStateMvActions { + fromState, toState, err = action.MultiStateUpdate(ctx, fromTf, toTf, fromState, toState) + if err != nil { + return nil, nil, err + } + } + return fromState, toState, nil +} + +// generateMvActions uses an xmv and use the state to determine the corresponding mv actions. +func (a *MultiStateXmvAction) generateMvActions(ctx context.Context, fromTf tfexec.TerraformCLI, fromState *tfexec.State) ([]*MultiStateMvAction, error) { + stateList, err := fromTf.StateList(ctx, fromState, nil) + if err != nil { + return nil, err + } + + // create a temporary single state mv actions. + // It may look a bit strange as a type. + // This is only because sharing the logic while maintaining consistency. + stateXmv := NewStateXmvAction(a.source, a.destination) + + e := newXMvExpander(stateXmv) + stateMvActions, err := e.expand(stateList) + if err != nil { + return nil, err + } + + // convert StateMvAction to MultiStateMvAction. + multiStateMvActions := []*MultiStateMvAction{} + for _, action := range stateMvActions { + multiStateMvActions = append(multiStateMvActions, NewMultiStateMvAction(action.source, action.destination)) + } + + return multiStateMvActions, nil +} diff --git a/tfmigrate/multi_state_xmv_action_test.go b/tfmigrate/multi_state_xmv_action_test.go new file mode 100644 index 0000000..dafe972 --- /dev/null +++ b/tfmigrate/multi_state_xmv_action_test.go @@ -0,0 +1,71 @@ +package tfmigrate + +import ( + "context" + "testing" + + "github.com/minamijoyo/tfmigrate/tfexec" +) + +func TestAccMultiStateXmvAction(t *testing.T) { + tfexec.SkipUnlessAcceptanceTestEnabled(t) + ctx := context.Background() + + // setup the initial files and states + fromBackend := tfexec.GetTestAccBackendS3Config(t.Name() + "/fromDir") + fromSource := ` +resource "null_resource" "foo" {} +resource "null_resource" "bar" {} +resource "time_static" "foo" {} +` + fromWorkspace := "default" + fromTf := tfexec.SetupTestAccWithApply(t, fromWorkspace, fromBackend+fromSource) + + toBackend := tfexec.GetTestAccBackendS3Config(t.Name() + "/toDir") + toSource := ` +resource "null_resource" "qux" {} +` + toWorkspace := "default" + toTf := tfexec.SetupTestAccWithApply(t, toWorkspace, toBackend+toSource) + + // update terraform resource files for migration + fromUpdatedSource := ` +resource "time_static" "foo" {} +` + tfexec.UpdateTestAccSource(t, fromTf, fromBackend+fromUpdatedSource) + + toUpdatedSource := ` +resource "null_resource" "foo2" {} +resource "null_resource" "bar2" {} +resource "null_resource" "qux" {} +` + tfexec.UpdateTestAccSource(t, toTf, toBackend+toUpdatedSource) + + fromChanged, err := fromTf.PlanHasChange(ctx, nil) + if err != nil { + t.Fatalf("failed to run PlanHasChange in fromDir: %s", err) + } + if !fromChanged { + t.Fatalf("expect to have changes in fromDir") + } + + toChanged, err := toTf.PlanHasChange(ctx, nil) + if err != nil { + t.Fatalf("failed to run PlanHasChange in toDir: %s", err) + } + if !toChanged { + t.Fatalf("expect to have changes in toDir") + } + + // perform state migration + actions := []MultiStateAction{ + NewMultiStateXmvAction("null_resource.*", "null_resource.${1}2"), + } + o := &MigratorOption{} + force := false + m := NewMultiStateMigrator(fromTf.Dir(), toTf.Dir(), fromWorkspace, toWorkspace, actions, o, force) + err = m.Plan(ctx) + if err != nil { + t.Fatalf("failed to run migrator plan: %s", err) + } +} diff --git a/tfmigrate/state_action.go b/tfmigrate/state_action.go index 6dc0796..2f7a6e5 100644 --- a/tfmigrate/state_action.go +++ b/tfmigrate/state_action.go @@ -22,7 +22,7 @@ type StateAction interface { // "mv " // "rm ... // "import
" -// "xmv " // To support moves with wildcards +// "xmv " func NewStateActionFromString(cmdStr string) (StateAction, error) { args, err := splitStateAction(cmdStr) if err != nil { diff --git a/tfmigrate/state_xmv_action.go b/tfmigrate/state_xmv_action.go index 4e66106..6476e28 100644 --- a/tfmigrate/state_xmv_action.go +++ b/tfmigrate/state_xmv_action.go @@ -7,8 +7,8 @@ import ( ) // StateXmvAction implements the StateAction interface. -// StateXmvAction moves a resource from source address to destination address in -// the same tfstate file. +// StateXmvAction is an extended version of StateMvAction. +// It allows you to move multiple resouces with a wildcard match. type StateXmvAction struct { // source is a address of resource or module to be moved which can contain wildcards. source string @@ -44,7 +44,7 @@ func (a *StateXmvAction) StateUpdate(ctx context.Context, tf tfexec.TerraformCLI return state, err } -// Use an xmv and use the state to determine the corresponding mv actions. +// generateMvActions uses an xmv and use the state to determine the corresponding mv actions. func (a *StateXmvAction) generateMvActions(ctx context.Context, tf tfexec.TerraformCLI, state *tfexec.State) ([]*StateMvAction, error) { stateList, err := tf.StateList(ctx, state, nil) if err != nil { diff --git a/tfmigrate/xmv_expander_test.go b/tfmigrate/xmv_expander_test.go index b7f8af0..be33ae1 100644 --- a/tfmigrate/xmv_expander_test.go +++ b/tfmigrate/xmv_expander_test.go @@ -14,12 +14,12 @@ func TestGetNrOfWildcard(t *testing.T) { want int }{ { - desc: "Simple resource no wildcardChar", + desc: "simple resource no wildcardChar", action: NewStateXmvAction("null_resource.foo", "null_resource.foo2"), want: 0, }, { - desc: "Simple wildcardChar for a resource", + desc: "simple wildcardChar for a resource", action: NewStateXmvAction("null_resource.*", "null_resource.$1"), want: 1, }, @@ -36,7 +36,7 @@ func TestGetNrOfWildcard(t *testing.T) { } } -func TestGetStateMvActionsForStateList(t *testing.T) { +func TestXmvExpanderExpand(t *testing.T) { cases := []struct { desc string stateList []string @@ -44,7 +44,7 @@ func TestGetStateMvActionsForStateList(t *testing.T) { outputMvActions []*StateMvAction }{ { - desc: "Simple resource no wildcardChar", + desc: "simple resource no wildcardChar", stateList: nil, inputXMvAction: &StateXmvAction{ source: "null_resource.foo", @@ -58,7 +58,7 @@ func TestGetStateMvActionsForStateList(t *testing.T) { }, }, { - desc: "Simple resource with wildcardChar", + desc: "simple resource with wildcardChar", stateList: []string{"null_resource.foo"}, inputXMvAction: &StateXmvAction{ source: "null_resource.*", @@ -72,7 +72,7 @@ func TestGetStateMvActionsForStateList(t *testing.T) { }, }, { - desc: "Simple module name refactor with wildcardChar", + desc: "simple module name refactor with wildcardChar", stateList: []string{"module.example1[\"foo\"].this"}, inputXMvAction: &StateXmvAction{ source: "module.example1[\"*\"].this", @@ -86,7 +86,7 @@ func TestGetStateMvActionsForStateList(t *testing.T) { }, }, { - desc: "No matching resources in state", + desc: "no matching resources in state", stateList: []string{"time_static.foo"}, inputXMvAction: &StateXmvAction{ source: "null_resource.*", @@ -95,7 +95,7 @@ func TestGetStateMvActionsForStateList(t *testing.T) { outputMvActions: []*StateMvAction{}, }, { - desc: "Documented feature; positional matching for example to allow switching matches from place", + desc: "documented feature; positional matching for example to allow switching matches from place", stateList: []string{"module[\"bar\"].null_resource.foo"}, inputXMvAction: &StateXmvAction{ source: "module[\"*\"].null_resource.*", @@ -109,7 +109,7 @@ func TestGetStateMvActionsForStateList(t *testing.T) { }, }, { - desc: "Multiple resources refactored into a module", + desc: "multiple resources refactored into a module", stateList: []string{ "null_resource.foo", "null_resource.bar", @@ -134,6 +134,27 @@ func TestGetStateMvActionsForStateList(t *testing.T) { }, }, }, + { + desc: "match all for merging multiple tfstates", + stateList: []string{ + "null_resource.foo", + "null_resource.bar", + }, + inputXMvAction: &StateXmvAction{ + source: "*", + destination: "$1", + }, + outputMvActions: []*StateMvAction{ + { + source: "null_resource.foo", + destination: "null_resource.foo", + }, + { + source: "null_resource.bar", + destination: "null_resource.bar", + }, + }, + }, } for _, tc := range cases { From 7c55a012326702c724a6aa08099f937d6ae28395 Mon Sep 17 00:00:00 2001 From: Masayuki Morita Date: Mon, 26 Dec 2022 14:51:41 +0900 Subject: [PATCH 06/10] Fixes for escaping the $ sign Strictly speaking, `${` is a template literal notation and must be escaped with `$${`, but the `$` symbol by itself does not need escaping. That is, `$1` does not need to be escaped, but `$${1}` does. --- README.md | 12 +++++------- config/migration_test.go | 42 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 6948c24..e8b9903 100644 --- a/README.md +++ b/README.md @@ -592,12 +592,10 @@ migration "state" "test" { #### state xmv -The `xmv` command works like the `mv` command but allows usage of -wildcards `*` in the source definition. The source expressions will be -matched against resources defined in the terraform state. The matched value -can be used in the destination definition via a dollar sign and their ordinal number. Note that dollar signs need to be -escaped and therefore are placed twice: -`$$1`, `$$2`, ... When there is ambiguity the ordinal number can be put in curly braces (e.g. `$${1}`). +The `xmv` command works like the `mv` command but allows usage of wildcards `*` in the source definition. +The source expressions will be matched against resources defined in the terraform state. +The matched value can be used in the destination definition via a dollar sign and their ordinal number (e.g. `$1`, `$2`, ...). +When there is ambiguity, you need to put the ordinal number in curly braces, in this case, the dollar sign need to be escaped and therefore are placed twice (e.g. `$${1}`). For example if `foo` and `bar` in the `mv` command example above are the only 2 security group resources defined at the top level then you can rename them using: @@ -686,7 +684,7 @@ migration "multi_state" "mv_dir1_dir2" { from_dir = "dir1" to_dir = "dir2" actions = [ - "xmv * $$1", + "xmv * $1", ] } ``` diff --git a/config/migration_test.go b/config/migration_test.go index 7e764b4..ab71aea 100644 --- a/config/migration_test.go +++ b/config/migration_test.go @@ -81,6 +81,48 @@ migration "state" "test" { }, ok: true, }, + { + desc: "state with a simple wildcard action", + source: ` +migration "state" "test" { + actions = [ + "xmv null_resource.* null_resource.new_$1", + ] +} +`, + want: &tfmigrate.MigrationConfig{ + Type: "state", + Name: "test", + Migrator: &tfmigrate.StateMigratorConfig{ + Dir: "", + Actions: []string{ + "xmv null_resource.* null_resource.new_$1", + }, + }, + }, + ok: true, + }, + { + desc: "state with a escaped wildcard action", + source: ` +migration "state" "test" { + actions = [ + "xmv null_resource.* null_resource.$${1}2", + ] +} +`, + want: &tfmigrate.MigrationConfig{ + Type: "state", + Name: "test", + Migrator: &tfmigrate.StateMigratorConfig{ + Dir: "", + Actions: []string{ + "xmv null_resource.* null_resource.${1}2", + }, + }, + }, + ok: true, + }, { desc: "state without actions", source: ` From b61b5dec3d245c26ee1739db5914c892271bf638 Mon Sep 17 00:00:00 2001 From: Masayuki Morita Date: Mon, 26 Dec 2022 15:01:12 +0900 Subject: [PATCH 07/10] Fix docs --- tfmigrate/multi_state_xmv_action.go | 2 +- tfmigrate/state_xmv_action.go | 2 +- tfmigrate/xmv_expander.go | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tfmigrate/multi_state_xmv_action.go b/tfmigrate/multi_state_xmv_action.go index 72c3439..7f3f6a5 100644 --- a/tfmigrate/multi_state_xmv_action.go +++ b/tfmigrate/multi_state_xmv_action.go @@ -8,7 +8,7 @@ import ( // MultiStateXmvAction implements the MultiStateAction interface. // MultiStateXmvAction is an extended version of MultiStateMvAction. -// It allows you to move multiple resouces with a wildcard match. +// It allows you to move multiple resouces with wildcard matching. type MultiStateXmvAction struct { // source is a address of resource or module to be moved which can contain wildcards. source string diff --git a/tfmigrate/state_xmv_action.go b/tfmigrate/state_xmv_action.go index 6476e28..1c54345 100644 --- a/tfmigrate/state_xmv_action.go +++ b/tfmigrate/state_xmv_action.go @@ -8,7 +8,7 @@ import ( // StateXmvAction implements the StateAction interface. // StateXmvAction is an extended version of StateMvAction. -// It allows you to move multiple resouces with a wildcard match. +// It allows you to move multiple resouces with wildcard matching. type StateXmvAction struct { // source is a address of resource or module to be moved which can contain wildcards. source string diff --git a/tfmigrate/xmv_expander.go b/tfmigrate/xmv_expander.go index 0673b6c..025e9ec 100644 --- a/tfmigrate/xmv_expander.go +++ b/tfmigrate/xmv_expander.go @@ -6,13 +6,13 @@ import ( "strings" ) -// xmvExpander is a helper method for implementing wildcard expansion for xmv actions. +// xmvExpander is a helper object for implementing wildcard expansion for xmv actions. type xmvExpander struct { // xmv action to be expanded action *StateXmvAction } -// newXMvExpander returns a new XMvExpander instance. +// newXMvExpander returns a new xmvExpander instance. func newXMvExpander(action *StateXmvAction) *xmvExpander { return &xmvExpander{ action: action, @@ -64,12 +64,13 @@ func (e *xmvExpander) expand(stateList []string) ([]*StateMvAction, error) { return matchingActions, nil } +// nrOfWildcards counts a number of wildcard characters. func (e *xmvExpander) nrOfWildcards() int { return strings.Count(e.action.source, wildcardChar) } // getMatchingSourcesFromState looks into the state and find sources that match -// pattern with wild cards. +// pattern with wildcards. func (e *xmvExpander) getMatchingSourcesFromState(stateList []string) ([]string, error) { re, err := makeSrcRegex(e.action.source) if err != nil { From 39af101d01c01d82912a38923f398e70b22cfe23 Mon Sep 17 00:00:00 2001 From: Masayuki Morita Date: Mon, 26 Dec 2022 15:04:24 +0900 Subject: [PATCH 08/10] Add tests for parsing multi_state xmv actions --- tfmigrate/multi_state_action_test.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tfmigrate/multi_state_action_test.go b/tfmigrate/multi_state_action_test.go index e9c741f..2b211ee 100644 --- a/tfmigrate/multi_state_action_test.go +++ b/tfmigrate/multi_state_action_test.go @@ -39,6 +39,33 @@ func TestNewMultiStateActionFromString(t *testing.T) { want: nil, ok: false, }, + { + desc: "xmv action (valid)", + cmdStr: "xmv null_resource.* null_resource.$1", + want: &MultiStateXmvAction{ + source: "null_resource.*", + destination: "null_resource.$1", + }, + ok: true, + }, + { + desc: "xmv action (no args)", + cmdStr: "xmv", + want: nil, + ok: false, + }, + { + desc: "xmv action (1 arg)", + cmdStr: "xmv null_resource.foo", + want: nil, + ok: false, + }, + { + desc: "xmv action (3 args)", + cmdStr: "xmv null_resource.foo null_resource.foo2 null_resource.foo3", + want: nil, + ok: false, + }, { desc: "duplicated white spaces", cmdStr: " mv null_resource.foo null_resource.foo2 ", From 944da2f759ef45a6520ab4360dc11582487f3a5d Mon Sep 17 00:00:00 2001 From: Masayuki Morita Date: Mon, 26 Dec 2022 15:08:46 +0900 Subject: [PATCH 09/10] Rename newXMvExpander to newXmvExpander for consistency --- tfmigrate/multi_state_xmv_action.go | 2 +- tfmigrate/state_xmv_action.go | 2 +- tfmigrate/xmv_expander.go | 4 ++-- tfmigrate/xmv_expander_test.go | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tfmigrate/multi_state_xmv_action.go b/tfmigrate/multi_state_xmv_action.go index 7f3f6a5..d401584 100644 --- a/tfmigrate/multi_state_xmv_action.go +++ b/tfmigrate/multi_state_xmv_action.go @@ -56,7 +56,7 @@ func (a *MultiStateXmvAction) generateMvActions(ctx context.Context, fromTf tfex // This is only because sharing the logic while maintaining consistency. stateXmv := NewStateXmvAction(a.source, a.destination) - e := newXMvExpander(stateXmv) + e := newXmvExpander(stateXmv) stateMvActions, err := e.expand(stateList) if err != nil { return nil, err diff --git a/tfmigrate/state_xmv_action.go b/tfmigrate/state_xmv_action.go index 1c54345..69e1cfc 100644 --- a/tfmigrate/state_xmv_action.go +++ b/tfmigrate/state_xmv_action.go @@ -51,6 +51,6 @@ func (a *StateXmvAction) generateMvActions(ctx context.Context, tf tfexec.Terraf return nil, err } - e := newXMvExpander(a) + e := newXmvExpander(a) return e.expand(stateList) } diff --git a/tfmigrate/xmv_expander.go b/tfmigrate/xmv_expander.go index 025e9ec..ab899a0 100644 --- a/tfmigrate/xmv_expander.go +++ b/tfmigrate/xmv_expander.go @@ -12,8 +12,8 @@ type xmvExpander struct { action *StateXmvAction } -// newXMvExpander returns a new xmvExpander instance. -func newXMvExpander(action *StateXmvAction) *xmvExpander { +// newXmvExpander returns a new xmvExpander instance. +func newXmvExpander(action *StateXmvAction) *xmvExpander { return &xmvExpander{ action: action, } diff --git a/tfmigrate/xmv_expander_test.go b/tfmigrate/xmv_expander_test.go index be33ae1..e3da0dd 100644 --- a/tfmigrate/xmv_expander_test.go +++ b/tfmigrate/xmv_expander_test.go @@ -27,7 +27,7 @@ func TestGetNrOfWildcard(t *testing.T) { for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { - e := newXMvExpander(tc.action) + e := newXmvExpander(tc.action) got := e.nrOfWildcards() if got != tc.want { t.Errorf("got: %d, but want: %d", got, tc.want) @@ -159,7 +159,7 @@ func TestXmvExpanderExpand(t *testing.T) { for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { - e := newXMvExpander(tc.inputXMvAction) + e := newXmvExpander(tc.inputXMvAction) got, err := e.expand(tc.stateList) // Errors are not expected. At this stage the only location from which errors are expected is if the regular // expression that comes from the source cannot compile but since meta-characters are quoted and we only From df749bdf5b07bb3d413c5c746260693db83c0cdb Mon Sep 17 00:00:00 2001 From: Masayuki Morita Date: Mon, 26 Dec 2022 15:11:36 +0900 Subject: [PATCH 10/10] Fix docs --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e8b9903..2075eb2 100644 --- a/README.md +++ b/README.md @@ -677,10 +677,10 @@ migration "multi_state" "mv_dir1_dir2" { } ``` -If you want to move all resources for merging two state files: +If you want to move all resources to another dir for merging two tfstates, you can write something like this: ```hcl -migration "multi_state" "mv_dir1_dir2" { +migration "multi_state" "merge_dir1_to_dir2" { from_dir = "dir1" to_dir = "dir2" actions = [