diff --git a/cmd/migrate_command.go b/cmd/migrate_command.go index 7621face..6469b2cd 100644 --- a/cmd/migrate_command.go +++ b/cmd/migrate_command.go @@ -106,6 +106,9 @@ func (c *MigrateCommand) MigrateResources(terraform *tf.Terraform, resources []t if err := os.MkdirAll(tempDir, 0750); err != nil { log.Fatalf("creating temp workspace %q: %+v", tempDir, err) } + if err := os.RemoveAll(path.Join(tempDir, "terraform.tfstate")); err != nil { + log.Printf("[WARN] removing temp workspace %q: %+v", tempDir, err) + } defer func() { err := os.RemoveAll(path.Join(tempDir, "terraform.tfstate")) if err != nil { diff --git a/go.mod b/go.mod index 3284f417..677d0ff7 100644 --- a/go.mod +++ b/go.mod @@ -85,7 +85,7 @@ require ( github.com/huandu/xstrings v1.5.0 // indirect github.com/magodo/armid v0.0.0-20230511151020-27880e5961c3 // indirect github.com/magodo/tfpluginschema v0.0.0-20240902090353-0525d7d8c1c2 // indirect - github.com/magodo/tfstate v0.0.0-20240829105815-03d52976fa13 // indirect + github.com/magodo/tfstate v0.0.0-20241016043929-2c95177bf0e6 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect diff --git a/go.sum b/go.sum index 8aa44444..dda9731b 100644 --- a/go.sum +++ b/go.sum @@ -235,8 +235,8 @@ github.com/magodo/tfadd v0.10.1-0.20240902124619-bd18a56f410d h1:dmS4jSfNIfshWkh github.com/magodo/tfadd v0.10.1-0.20240902124619-bd18a56f410d/go.mod h1:G2Hc13YaLGHa+CPEP/HZzj9sIGyKDM5ZXInzQ8Dp86s= github.com/magodo/tfpluginschema v0.0.0-20240902090353-0525d7d8c1c2 h1:Unxx8WLxzSxINnq7hItp4cXD7drihgfPltTd91efoBo= github.com/magodo/tfpluginschema v0.0.0-20240902090353-0525d7d8c1c2/go.mod h1:mh3baLIzKdhegfmLrAX+mpXQBvs4sqiDRTGx5Z5FGo0= -github.com/magodo/tfstate v0.0.0-20240829105815-03d52976fa13 h1:HhTCs5IKRuJxqx3NDI5gWfAD4WCNXiYGXM1dKyPp9rA= -github.com/magodo/tfstate v0.0.0-20240829105815-03d52976fa13/go.mod h1:cm1odSE6eUeMQRjYRARg1sWLP3HPsWjwvmk/+T4eQxs= +github.com/magodo/tfstate v0.0.0-20241016043929-2c95177bf0e6 h1:Uy+WlvEHfZEVTs1Xf5N+177FTdPHx+mWUvsXHR4tGM4= +github.com/magodo/tfstate v0.0.0-20241016043929-2c95177bf0e6/go.mod h1:cm1odSE6eUeMQRjYRARg1sWLP3HPsWjwvmk/+T4eQxs= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= diff --git a/helper/utils.go b/helper/utils.go index a382c880..a06f4b63 100644 --- a/helper/utils.go +++ b/helper/utils.go @@ -81,11 +81,18 @@ func ListHclFiles(workingDirectory string) []fs.DirEntry { // GetTokensForExpression convert a literal value to hclwrite.Tokens func GetTokensForExpression(expression string) hclwrite.Tokens { - f, dialog := hclwrite.ParseConfig([]byte(fmt.Sprintf("%s=%s", "temp", expression)), "", hcl.InitialPos) - if dialog == nil || !dialog.HasErrors() && f != nil { - return f.Body().GetAttribute("temp").Expr().BuildTokens(nil) + syntaxTokens, diags := hclsyntax.LexConfig([]byte(expression), "main.tf", hcl.InitialPos) + if diags.HasErrors() { + return nil } - return nil + res := make([]*hclwrite.Token, 0) + for _, token := range syntaxTokens { + res = append(res, &hclwrite.Token{ + Type: token.Type, + Bytes: token.Bytes, + }) + } + return res } // ParseHclArray parse `attrValue` to an array, example `attrValue` `["a", "b", 0]` will return ["\"a\"", "\"b\"", "0"] @@ -121,9 +128,13 @@ func ToHclSearchReplace(input interface{}, search []string, replacement []string } attrs := make([]string, 0) for k, v := range value { + if v == nil { + attrs = append(attrs, fmt.Sprintf("%s = null", quotedKey(k))) + continue + } config, ok := ToHclSearchReplace(v, search, replacement) found = found || ok - attrs = append(attrs, fmt.Sprintf("%s = %s", k, config)) + attrs = append(attrs, fmt.Sprintf("%s = %s", quotedKey(k), config)) } return fmt.Sprintf("{\n%s\n}", strings.Join(attrs, "\n")), found case string: @@ -132,7 +143,7 @@ func ToHclSearchReplace(input interface{}, search []string, replacement []string return replacement[i], true } } - return fmt.Sprintf(`"%s"`, value), false + return fmt.Sprintf(`"%s"`, strings.ReplaceAll(value, "\"", "\\\"")), false default: return fmt.Sprintf("%v", value), false } @@ -150,3 +161,13 @@ func GetValueFromExpression(tokens hclwrite.Tokens) interface{} { } return nil } + +func quotedKey(input string) string { + if len(input) == 0 { + return input + } + if strings.Contains(input, ".") || strings.Contains(input, "/") || input[0] == '$' || input[0] >= '0' && input[0] <= '9' { + return fmt.Sprintf("\"%s\"", input) + } + return input +} diff --git a/types/azapi_resource.go b/types/azapi_resource.go index 48d2ba2e..68acd1b6 100644 --- a/types/azapi_resource.go +++ b/types/azapi_resource.go @@ -107,7 +107,8 @@ func (r *AzapiResource) GenerateNewConfig(terraform *tf.Terraform) error { // import and build combined block blocks := make([]*hclwrite.Block, 0) for _, instance := range r.Instances { - if block, err := importAndGenerateConfig(terraform, fmt.Sprintf("%s.%s_%v", r.ResourceType, r.Label, instance.Index), instance.ResourceId, r.ResourceType, false); err == nil { + instanceAddress := fmt.Sprintf("%s.%s_%v", r.ResourceType, r.Label, strings.ReplaceAll(fmt.Sprintf("%v", instance.Index), "/", "_")) + if block, err := importAndGenerateConfig(terraform, instanceAddress, instance.ResourceId, r.ResourceType, false); err == nil { blocks = append(blocks, block) } } @@ -211,7 +212,7 @@ func (r *AzapiResource) EmptyImportConfig() string { if !r.IsMultipleResources() { config += fmt.Sprintf("resource \"%s\" \"%s\" {}\n", r.ResourceType, r.Label) } else { - config += fmt.Sprintf("resource \"%s\" \"%s_%v\" {}\n", r.ResourceType, r.Label, instance.Index) + config += fmt.Sprintf("resource \"%s\" \"%s_%s\" {}\n", r.ResourceType, r.Label, strings.ReplaceAll(fmt.Sprintf("%v", instance.Index), "/", "_")) } } return config diff --git a/types/azurerm_resource.go b/types/azurerm_resource.go index 90bd0914..35874b6d 100644 --- a/types/azurerm_resource.go +++ b/types/azurerm_resource.go @@ -71,7 +71,8 @@ func (r *AzurermResource) GenerateNewConfig(terraform *tf.Terraform) error { log.Printf("[INFO] generating config...") blocks := make([]*hclwrite.Block, 0) for _, instance := range r.Instances { - if block, err := importAndGenerateConfig(terraform, fmt.Sprintf("%s.%s_%v", r.NewResourceType, r.NewLabel, instance.Index), instance.ResourceId, "", true); err == nil { + instanceAddress := fmt.Sprintf("%s.%s_%v", r.NewResourceType, r.NewLabel, strings.ReplaceAll(fmt.Sprintf("%v", instance.Index), "/", "_")) + if block, err := importAndGenerateConfig(terraform, instanceAddress, instance.ResourceId, "", true); err == nil { blocks = append(blocks, block) } } @@ -187,7 +188,7 @@ func (r *AzurermResource) EmptyImportConfig() string { config := "" if r.IsMultipleResources() { for _, instance := range r.Instances { - config += fmt.Sprintf("resource \"azapi_resource\" \"%s_%v\" {}\n", r.NewLabel, instance.Index) + config += fmt.Sprintf("resource \"azapi_resource\" \"%s_%s\" {}\n", r.NewLabel, strings.ReplaceAll(fmt.Sprintf("%v", instance.Index), "/", "_")) } } else { config += fmt.Sprintf("resource \"azapi_resource\" \"%s\" {}\n", r.NewLabel) diff --git a/types/from_plan.go b/types/from_plan.go index 47184fc3..81d72dbf 100644 --- a/types/from_plan.go +++ b/types/from_plan.go @@ -29,6 +29,10 @@ func ListResourcesFromPlan(p *tfjson.Plan) []AzureResource { if resourceChange == nil || resourceChange.Change == nil { continue } + if len(resourceChange.Change.Actions) != 0 && (resourceChange.Change.Actions[0] == tfjson.ActionCreate || resourceChange.Change.Actions[0] == tfjson.ActionDelete) { + log.Printf("[WARN] resource %s.%s's planned action is %v, which is not supported. Please apply the changes before running the migration tool", resourceChange.Type, resourceChange.Name, resourceChange.Change.Actions) + continue + } switch resourceChange.Type { case "azapi_resource": diff --git a/types/hcl.go b/types/hcl.go index 87ce42fc..f92b104b 100644 --- a/types/hcl.go +++ b/types/hcl.go @@ -259,6 +259,9 @@ func recursiveUpdate(old *hclwrite.Block, new *hclwrite.Block, before interface{ // InjectReference replaces `block`'s literal value with reference provided by `refs` func InjectReference(block *hclwrite.Block, refs []Reference) *hclwrite.Block { + if block.Body() == nil { + return block + } search := make([]string, 0) replacement := make([]string, 0) for _, ref := range refs { @@ -370,20 +373,31 @@ func CombineBlock(blocks []*hclwrite.Block, output *hclwrite.Block, isForEach bo // GetForEachConstants converts a map of difference to hcl object func GetForEachConstants(instances []Instance, items map[string][]hclwrite.Tokens) string { + config := "" i := 0 for _, instance := range instances { item := "" for key := range items { - item += fmt.Sprintf("%s = %s", key, string(items[key][i].Bytes())) + item += fmt.Sprintf("%s = %s\n", quotedKey(key), string(items[key][i].Bytes())) } - config += fmt.Sprintf("%s = {\n%s\n}\n", instance.Index, item) + config += fmt.Sprintf("%s = {\n%s\n}\n", quotedKey(fmt.Sprintf("%v", instance.Index)), item) i++ } config = fmt.Sprintf("{\n%s}\n", config) return config } +func quotedKey(input string) string { + if len(input) == 0 { + return input + } + if strings.Contains(input, ".") || strings.Contains(input, "/") || input[0] == '$' || input[0] >= '0' && input[0] <= '9' { + return fmt.Sprintf("\"%s\"", input) + } + return input +} + func CommentOutBlock(block *hclwrite.Block) hclwrite.Tokens { file := hclwrite.NewEmptyFile() file.Body().AppendBlock(block) diff --git a/vendor/github.com/magodo/tfstate/cty.go b/vendor/github.com/magodo/tfstate/cty.go index 8d5f80c6..8faf1db9 100644 --- a/vendor/github.com/magodo/tfstate/cty.go +++ b/vendor/github.com/magodo/tfstate/cty.go @@ -286,6 +286,12 @@ func unmarshalDynamic(v interface{}, path cty.Path) (cty.Type, cty.Value, error) return cty.Number, cty.NumberFloatVal(v), nil case string: return cty.String, cty.StringVal(v), nil + case json.Number: + val, err := cty.ParseNumberVal(v.String()) + if err != nil { + return cty.NilType, cty.NilVal, path.NewError(err) + } + return cty.Number, val, nil case []interface{}: eTypes := []cty.Type{} eVals := []cty.Value{} diff --git a/vendor/modules.txt b/vendor/modules.txt index 7aa13b9f..a31cc8c2 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -305,7 +305,7 @@ github.com/magodo/tfadd/tfadd/internal # github.com/magodo/tfpluginschema v0.0.0-20240902090353-0525d7d8c1c2 ## explicit; go 1.21 github.com/magodo/tfpluginschema/schema -# github.com/magodo/tfstate v0.0.0-20240829105815-03d52976fa13 +# github.com/magodo/tfstate v0.0.0-20241016043929-2c95177bf0e6 ## explicit; go 1.18 github.com/magodo/tfstate github.com/magodo/tfstate/terraform/jsonschema