cmd/plan_command.go (163 lines of code) (raw):
package cmd
import (
"bufio"
"flag"
"fmt"
"log"
"os"
"path"
"strings"
"github.com/Azure/aztfmigrate/azurerm"
"github.com/Azure/aztfmigrate/tf"
"github.com/Azure/aztfmigrate/types"
"github.com/mitchellh/cli"
)
type PlanCommand struct {
Ui cli.Ui
Verbose bool
Strict bool
workingDir string
varFile string
TargetProvider string
}
func (c *PlanCommand) 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 *PlanCommand) 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")
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
}
log.Printf("[INFO] target provider: %s", c.TargetProvider)
log.Printf("[INFO] initializing terraform...")
if c.workingDir == "" {
c.workingDir, _ = os.Getwd()
}
terraform, err := tf.NewTerraform(c.workingDir, c.Verbose)
if err != nil {
log.Fatal(err)
}
c.Plan(terraform, true)
return 0
}
func (c *PlanCommand) Help() string {
helpText := `
Usage: aztfmigrate plan
` + 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 *PlanCommand) Synopsis() string {
return "Show terraform resources which can be migrated to azurerm or azapi resources in current working directory"
}
func (c *PlanCommand) Plan(terraform *tf.Terraform, isPlanOnly bool) []types.AzureResource {
// get azapi resource from state
log.Printf("[INFO] running terraform plan...")
p, err := terraform.Plan(&c.varFile)
if err != nil {
log.Fatal(err)
}
migrationMessage := "The following resources will be migrated:\n"
unsupportedMessage := "The following resources can't be migrated:\n"
ignoreMessage := "The following resources will be ignored in migration:\n"
ignoreSet := make(map[string]bool)
if file, err := os.ReadFile(path.Join(terraform.GetWorkingDirectory(), "aztfmigrate.ignore")); err == nil {
lines := strings.Split(string(file), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if len(line) == 0 {
continue
}
ignoreSet[line] = true
ignoreMessage += fmt.Sprintf("\t%s\n", line)
}
}
res := make([]types.AzureResource, 0)
for _, item := range types.ListResourcesFromPlan(p) {
if item.TargetProvider() != c.TargetProvider {
continue
}
if ignoreSet[item.OldAddress(nil)] {
continue
}
if err := item.CoverageCheck(c.Strict); err != nil {
unsupportedMessage += fmt.Sprintf("\t%s\n", err)
continue
}
switch resource := item.(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.Fatal(fmt.Errorf("failed to get resource type for %s: %w", resourceId, err))
}
if exact {
resource.ResourceType = resourceTypes[0]
} else if !isPlanOnly {
resource.ResourceType = c.getUserInputResourceType(resourceId, resourceTypes)
}
if resource.ResourceType != "" {
migrationMessage += fmt.Sprintf("\t%s will be replaced with %s\n", resource.OldAddress(nil), resource.NewAddress(nil))
} else {
migrationMessage += fmt.Sprintf("\t%s will be replaced with %v\n", resource.OldAddress(nil), strings.Join(resourceTypes, ", "))
}
res = append(res, resource)
case *types.AzapiUpdateResource:
resourceTypes, exact, err := azurerm.GetAzureRMResourceType(resource.Id)
if err != nil {
log.Fatal(fmt.Errorf("failed to get resource type for %s: %w", resource.Id, err))
}
if exact {
resource.ResourceType = resourceTypes[0]
} else if !isPlanOnly {
resource.ResourceType = c.getUserInputResourceType(resource.Id, resourceTypes)
}
if resource.ResourceType != "" {
migrationMessage += fmt.Sprintf("\t%s will be replaced with %s\n", resource.OldAddress(nil), resource.NewAddress(nil))
} else {
migrationMessage += fmt.Sprintf("\t%s will be replaced with %v\n", resource.OldAddress(nil), strings.Join(resourceTypes, ", "))
}
res = append(res, resource)
case *types.AzurermResource:
if len(resource.Instances) == 0 {
continue
}
migrationMessage += fmt.Sprintf("\t%s will be replaced with %s\n", resource.OldAddress(nil), resource.NewAddress(nil))
res = append(res, resource)
}
}
log.Printf("[INFO]\n\nThe tool will perform the following actions:\n\n%s\n%s\n%s\n", migrationMessage, unsupportedMessage, ignoreMessage)
return res
}
func (c *PlanCommand) getUserInputResourceType(resourceId string, values []string) string {
c.Ui.Warn(fmt.Sprintf("Couldn't find unique resource type for id: %s\nPossible values are [%s].\nPlease input an azurerm resource type:", resourceId, strings.Join(values, ", ")))
resourceType := ""
for {
reader := bufio.NewReader(os.Stdin)
resourceType, _ = reader.ReadString('\n')
resourceType = strings.Trim(resourceType, "\r\n")
for _, value := range values {
if value == resourceType {
return resourceType
}
}
c.Ui.Warn("Invalid input. Please input an azurerm resource type:")
}
}