cmd/migrate_command.go (213 lines of code) (raw):

package cmd import ( "flag" "fmt" "log" "os" "path" "path/filepath" "strings" "github.com/Azure/aztfmigrate/helper" "github.com/Azure/aztfmigrate/tf" "github.com/Azure/aztfmigrate/types" "github.com/hashicorp/hcl/v2/hclwrite" "github.com/mitchellh/cli" ) const filenameImport = "imports.tf" const tempFolderName = "aztfmigrate_temp" type MigrateCommand struct { Ui cli.Ui Verbose bool Strict bool workingDir string varFile string TargetProvider string } func (c *MigrateCommand) flags() *flag.FlagSet { fs := defaultFlagSet("plan") fs.BoolVar(&c.Verbose, "v", false, "whether show terraform logs") fs.BoolVar(&c.Strict, "strict", false, "strict mode: API versions must be matched") fs.StringVar(&c.workingDir, "working-dir", "", "path to Terraform configuration files") fs.StringVar(&c.varFile, "var-file", "", "path to the terraform variable file") fs.StringVar(&c.TargetProvider, "to", "", "Specify the provider to migrate to. The allowed values are: azurerm and azapi. Default is azurerm.") fs.Usage = func() { c.Ui.Error(c.Help()) } return fs } func (c *MigrateCommand) Run(args []string) int { // 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", "mig") // 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") f := c.flags() if err := f.Parse(args); err != nil { c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s", err)) return 1 } if c.TargetProvider == "" { c.TargetProvider = "azurerm" } if c.TargetProvider != "azapi" && c.TargetProvider != "azurerm" { c.Ui.Error("Invalid target provider. The allowed values are: azurerm and azapi.") return 1 } if c.workingDir == "" { c.workingDir, _ = os.Getwd() } log.Printf("[INFO] working directory: %s", c.workingDir) log.Printf("[INFO] initializing terraform...") terraform, err := tf.NewTerraform(c.workingDir, c.Verbose) if err != nil { log.Fatal(err) } planCommand := &PlanCommand{ //nolint Ui: c.Ui, Verbose: c.Verbose, Strict: c.Strict, workingDir: c.workingDir, varFile: c.varFile, TargetProvider: c.TargetProvider, } allResources := planCommand.Plan(terraform, false) c.MigrateResources(terraform, allResources) return 0 } func (c *MigrateCommand) Help() string { helpText := ` Usage: aztfmigrate migrate ` + c.Synopsis() + "\nThe Terraform addresses listed in file `aztfmigrate.ignore` will be ignored during migration.\n\n" + helpForFlags(c.flags()) return strings.TrimSpace(helpText) } func (c *MigrateCommand) Synopsis() string { return "Migrate azapi resources to azurerm resources in current working directory" } func (c *MigrateCommand) MigrateResources(terraform *tf.Terraform, resources []types.AzureResource) { if len(resources) == 0 { return } workingDirectory := terraform.GetWorkingDirectory() // write empty config to temp dir for import tempDir := filepath.Join(workingDirectory, tempFolderName) 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 { log.Printf("[ERROR] removing temp workspace %q: %+v", tempDir, err) } }() tempTerraform, err := tf.NewTerraform(tempDir, c.Verbose) if err != nil { log.Fatal(err) } log.Printf("[INFO] generating import config...") config := ImportConfig(resources, helper.FindHclBlock(workingDirectory, "terraform", nil)) if err = os.WriteFile(filepath.Join(tempDir, filenameImport), []byte(config), 0600); err != nil { log.Fatal(err) } log.Printf("[INFO] migrating resources...") for _, r := range resources { log.Printf("[INFO] generating new config for resource %s...", r.OldAddress(nil)) if err := r.GenerateNewConfig(tempTerraform); err != nil { log.Printf("[ERROR] %+v", err) } } log.Printf("[INFO] updating config...") updateResources := make([]types.AzapiUpdateResource, 0) for _, r := range resources { if updateResource, ok := r.(*types.AzapiUpdateResource); ok { updateResources = append(updateResources, *updateResource) } } if err := types.UpdateMigratedResourceBlock(workingDirectory, updateResources); err != nil { log.Fatal(err) } // migrate depends_on, lifecycle, provisioner for _, r := range resources { if existingBlock, err := types.GetResourceBlock(workingDirectory, r.OldAddress(nil)); err == nil && existingBlock != nil { 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) } } } } // remove from config for _, r := range resources { if r.IsMigrated() { log.Printf("[INFO] removing %s from config", r.OldAddress(nil)) stateUpdateBlocks := r.StateUpdateBlocks() newBlocks := make([]*hclwrite.Block, 0) newBlocks = append(newBlocks, stateUpdateBlocks...) newBlocks = append(newBlocks, r.MigratedBlock()) if err := types.ReplaceResourceBlock(workingDirectory, r.OldAddress(nil), newBlocks); err != nil { log.Printf("[ERROR] error removing %s from state: %+v", r.OldAddress(nil), err) } } } log.Printf("[INFO] replacing references with migrated resource...") outputs := make([]types.Output, 0) for _, r := range resources { if r.IsMigrated() { outputs = append(outputs, r.Outputs()...) } } if err := types.ReplaceGenericOutputs(workingDirectory, outputs); err != nil { log.Printf("[ERROR] replacing outputs: %+v", err) } } func ImportConfig(resources []types.AzureResource, terraformBlock *hclwrite.Block) string { config := `terraform { required_providers { azapi = { source = "Azure/azapi" } } }` if terraformBlock != nil { newFile := hclwrite.NewEmptyFile() newFile.Body().AppendBlock(terraformBlock) config = string(hclwrite.Format(newFile.Bytes())) } for _, r := range resources { config += r.EmptyImportConfig() } subscriptionId := "" for _, r := range resources { switch resource := r.(type) { case *types.AzapiResource: for _, instance := range resource.Instances { if strings.HasPrefix(instance.ResourceId, "/subscriptions/") { subscriptionId = strings.Split(instance.ResourceId, "/")[2] break } } case *types.AzapiUpdateResource: if strings.HasPrefix(resource.Id, "/subscriptions/") { subscriptionId = strings.Split(resource.Id, "/")[2] } case *types.AzurermResource: for _, instance := range resource.Instances { if strings.HasPrefix(instance.ResourceId, "/subscriptions/") { subscriptionId = strings.Split(instance.ResourceId, "/")[2] break } } } if subscriptionId != "" { break } } const providerConfig = ` provider "azurerm" { features {} subscription_id = "%s" } provider "azapi" { } ` return fmt.Sprintf(providerConfig, subscriptionId) + config }