in internal/langserver/handlers/command/aztfmigrate_command.go [40:348]
func (c AztfMigrateCommand) Handle(ctx context.Context, arguments []json.RawMessage) (interface{}, error) {
var params lsp.CodeActionParams
if len(arguments) != 0 {
err := json.Unmarshal(arguments[0], ¶ms)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal arguments: %w", err)
}
}
telemetrySender, err := context2.Telemetry(ctx)
if err != nil {
return nil, err
}
clientCaller, err := context2.ClientCaller(ctx)
if err != nil {
return nil, err
}
clientNotifier, err := context2.ClientNotifier(ctx)
if err != nil {
return nil, err
}
telemetrySender.SendEvent(ctx, "aztfmigrate", map[string]interface{}{
"status": "started",
})
reportProgress(ctx, "Parsing Terraform configurations...", 0)
defer reportProgress(ctx, "Migration completed.", 100)
fs, err := lsctx.DocumentStorage(ctx)
if err != nil {
return nil, err
}
doc, err := fs.GetDocument(ilsp.FileHandlerFromDocumentURI(params.TextDocument.URI))
if err != nil {
return nil, err
}
// creating temp workspace
workingDirectory := getWorkingDirectory(string(params.TextDocument.URI), runtime.GOOS)
tempDir := filepath.Join(workingDirectory, tempFolderName)
if err := os.MkdirAll(tempDir, 0750); err != nil {
return nil, fmt.Errorf("failed to create temp workspace %q, please check the permission: %w", tempDir, err)
}
defer func() {
err := os.RemoveAll(path.Join(tempDir, "terraform.tfstate"))
if err != nil {
log.Printf("[ERROR] removing temp workspace %q: %+v", tempDir, err)
}
}()
startDocPos := lsp.TextDocumentPositionParams{
TextDocument: params.TextDocument,
Position: params.Range.Start,
}
startPos, err := ilsp.FilePositionFromDocumentPosition(startDocPos, doc)
if err != nil {
return nil, err
}
endDocPos := lsp.TextDocumentPositionParams{
TextDocument: params.TextDocument,
Position: params.Range.End,
}
endPos, err := ilsp.FilePositionFromDocumentPosition(endDocPos, doc)
if err != nil {
return nil, err
}
data, err := doc.Text()
if err != nil {
return nil, err
}
// parsing the document
syntaxDoc, diags := hclsyntax.ParseConfig(data, "", hcl.InitialPos)
if diags.HasErrors() {
return nil, fmt.Errorf("parsing the HCL file: %s", diags.Error())
}
writeDoc, diags := hclwrite.ParseConfig(data, "", hcl.InitialPos)
if diags.HasErrors() {
return nil, fmt.Errorf("parsing the HCL file: %s", diags.Error())
}
syntaxBlockMap := map[string]*hclsyntax.Block{}
writeBlockMap := map[string]*hclwrite.Block{}
body, ok := syntaxDoc.Body.(*hclsyntax.Body)
if !ok {
return nil, fmt.Errorf("failed to parse HCL syntax")
}
addresses := make([]string, 0)
for _, block := range body.Blocks {
if startPos.Position().Byte <= block.Range().Start.Byte && block.Range().End.Byte <= endPos.Position().Byte {
if block.Type != "resource" {
continue
}
address := strings.Join(block.Labels, ".")
addresses = append(addresses, address)
syntaxBlockMap[address] = block
}
}
for _, block := range writeDoc.Body().Blocks() {
address := strings.Join(block.Labels(), ".")
if _, ok := syntaxBlockMap[address]; ok {
writeBlockMap[address] = block
}
}
reportProgress(ctx, "Running Terraform plan...", 10)
// running terraform plan
terraform, err := tf.NewTerraform(workingDirectory, false)
if err != nil {
return nil, err
}
// AzureRM provider will honor env.var "AZURE_HTTP_USER_AGENT" when constructing for HTTP "User-Agent" header.
// #nosec G104
_ = os.Setenv("AZURE_HTTP_USER_AGENT", "aztfmigrate-vscode")
// The following env.vars are used to disable enhanced validation and skip provider registration, to speed up the process.
// #nosec G104
_ = os.Setenv("ARM_PROVIDER_ENHANCED_VALIDATION", "false")
// #nosec G104
_ = os.Setenv("ARM_SKIP_PROVIDER_REGISTRATION", "true")
options := make([]tfexec.PlanOption, 0)
for address := range syntaxBlockMap {
options = append(options, tfexec.Target(address))
}
planfile := path.Join(tempDir, planFileName)
options = append(options, tfexec.Out(planfile))
_, err = terraform.GetExec().Plan(ctx, options...)
if err != nil {
_ = clientNotifier.Notify(ctx, "window/showMessage", lsp.ShowMessageParams{
Type: lsp.Error,
Message: fmt.Sprintf("Failed to run Terraform plan: %v", err),
})
return nil, nil
}
plan, err := terraform.GetExec().ShowPlanFile(ctx, planfile)
if err != nil {
return nil, err
}
allResources := types.ListResourcesFromPlan(plan)
resources := make([]types.AzureResource, 0)
srcAzapiTypes := make(map[string]bool)
srcAzurermTypes := make(map[string]bool)
for _, r := range allResources {
if syntaxBlockMap[r.OldAddress(nil)] == nil {
continue
}
switch resource := r.(type) {
case *types.AzapiResource:
if len(resource.Instances) == 0 {
continue
}
resourceId := resource.Instances[0].ResourceId
resourceTypes, exact, err := azurerm.GetAzureRMResourceType(resourceId)
if err != nil {
log.Printf("failed to get resource type for %s: %v", resourceId, err)
continue
}
if len(resourceTypes) == 0 {
log.Printf("failed to get resource type for %s", resourceId)
continue
}
if !exact {
log.Printf("multiple resource types found for %s: %v", resourceId, resourceTypes)
}
resource.ResourceType = resourceTypes[0]
resources = append(resources, resource)
azureResourceType := utils.GetResourceType(resourceId)
srcAzapiTypes[azureResourceType] = true
case *types.AzurermResource:
if len(resource.Instances) == 0 {
continue
}
resources = append(resources, resource)
srcAzurermTypes[resource.OldResourceType] = true
}
}
if len(resources) == 0 {
_ = clientNotifier.Notify(ctx, "window/showMessage", lsp.ShowMessageParams{
Type: lsp.Error,
Message: "No resources found in the selected range. Please check whether the target resource is deployed.",
})
return nil, nil
}
tempTerraform, err := tf.NewTerraform(tempDir, false)
if err != nil {
return nil, err
}
if err = os.WriteFile(filepath.Join(tempDir, importFileName), []byte(cmd.ImportConfig(resources, helper.FindHclBlock(workingDirectory, "terraform", nil))), 0600); err != nil {
return nil, err
}
for index, r := range resources {
// #nosec G115
reportProgress(ctx, fmt.Sprintf("Migrating resource %d/%d...", index+1, len(resources)), 40+uint32(50.0*index/len(resources)))
if err := r.GenerateNewConfig(tempTerraform); err != nil {
log.Printf("[ERROR] %+v", err)
_ = clientNotifier.Notify(ctx, "window/showMessage", lsp.ShowMessageParams{
Type: lsp.Error,
Message: fmt.Sprintf("Failed to generate new config for %s: %v", r.OldAddress(nil), err),
})
}
}
reportProgress(ctx, "Updating Terraform configurations...", 90)
// migrate depends_on, lifecycle, provisioner
for _, r := range resources {
existingBlock := writeBlockMap[r.OldAddress(nil)]
if existingBlock == nil {
continue
}
migratedBlock := r.MigratedBlock()
if attr := existingBlock.Body().GetAttribute("depends_on"); attr != nil {
migratedBlock.Body().SetAttributeRaw("depends_on", attr.Expr().BuildTokens(nil))
}
for _, block := range existingBlock.Body().Blocks() {
if block.Type() == "lifecycle" || block.Type() == "provisioner" {
migratedBlock.Body().AppendBlock(block)
}
}
}
// update config
emptyFile := hclwrite.NewEmptyFile()
outputs := make([]types.Output, 0)
resourcesMap := make(map[string]types.AzureResource)
for _, r := range resources {
if r.IsMigrated() {
outputs = append(outputs, r.Outputs()...)
}
resourcesMap[r.OldAddress(nil)] = r
}
for _, addr := range addresses {
r := resourcesMap[addr]
if r == nil {
emptyFile.Body().AppendBlock(writeBlockMap[addr])
emptyFile.Body().AppendNewline()
continue
}
if writeBlockMap[r.OldAddress(nil)] == nil {
continue
}
if !r.IsMigrated() {
emptyFile.Body().AppendBlock(writeBlockMap[r.OldAddress(nil)])
emptyFile.Body().AppendNewline()
continue
}
emptyFile.Body().AppendUnstructuredTokens(types.CommentOutBlock(writeBlockMap[r.OldAddress(nil)]))
emptyFile.Body().AppendNewline()
for _, blockToAdd := range r.StateUpdateBlocks() {
if blockToAdd == nil {
continue
}
emptyFile.Body().AppendBlock(blockToAdd)
emptyFile.Body().AppendNewline()
}
if migratedBlock := r.MigratedBlock(); migratedBlock != nil {
types.ReplaceOutputs(migratedBlock, outputs)
emptyFile.Body().AppendBlock(migratedBlock)
emptyFile.Body().AppendNewline()
}
}
_, _ = clientCaller.Callback(ctx, "workspace/applyEdit", lsp.ApplyWorkspaceEditParams{
Label: "Update config",
Edit: lsp.WorkspaceEdit{
Changes: map[string][]lsp.TextEdit{
string(params.TextDocument.URI): {
{
Range: params.Range,
NewText: string(hclwrite.Format(emptyFile.Bytes())),
},
},
},
},
})
azapiTypes := make([]string, 0)
for t := range srcAzapiTypes {
azapiTypes = append(azapiTypes, t)
}
azurermTypes := make([]string, 0)
for t := range srcAzurermTypes {
azurermTypes = append(azurermTypes, t)
}
telemetrySender.SendEvent(ctx, "aztfmigrate", map[string]interface{}{
"status": "completed",
"count": fmt.Sprintf("%d", len(resources)),
"azapi": strings.Join(azapiTypes, ","),
"azurerm": strings.Join(azurermTypes, ","),
})
return nil, nil
}