Skip to content

Commit

Permalink
Split a wildcard implementation to xmvExpander
Browse files Browse the repository at this point in the history
  • Loading branch information
minamijoyo committed Dec 26, 2022
1 parent eac5268 commit 39c4b1e
Show file tree
Hide file tree
Showing 5 changed files with 261 additions and 232 deletions.
2 changes: 1 addition & 1 deletion tfmigrate/multi_state_mv_action.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
85 changes: 4 additions & 81 deletions tfmigrate/state_xmv_action.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ package tfmigrate

import (
"context"
"fmt"
"regexp"
"strings"

"github.com/minamijoyo/tfmigrate/tfexec"
)
Expand All @@ -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 {
Expand All @@ -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)
}
151 changes: 1 addition & 150 deletions tfmigrate/state_xmv_action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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)
}

})
}
}
98 changes: 98 additions & 0 deletions tfmigrate/xmv_expander.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 39c4b1e

Please sign in to comment.