From 39c4b1e63a8695adf92fe631287bd83d70df19dd Mon Sep 17 00:00:00 2001 From: Masayuki Morita Date: Mon, 26 Dec 2022 10:51:05 +0900 Subject: [PATCH] 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) + } + + }) + } +}