types/azurerm_resource.go (261 lines of code) (raw):

package types import ( "fmt" "log" "net/url" "strings" "github.com/Azure/aztfmigrate/helper" "github.com/Azure/aztfmigrate/tf" "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" "github.com/gertd/go-pluralize" _ "github.com/gertd/go-pluralize" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclwrite" ) var _ AzureResource = &AzurermResource{} type AzurermResource struct { OldLabel string NewLabel string OldResourceType string NewResourceType string Block *hclwrite.Block Instances []Instance References []Reference Migrated bool } func (r *AzurermResource) StateUpdateBlocks() []*hclwrite.Block { movedBlock := hclwrite.NewBlock("moved", nil) movedBlock.Body().SetAttributeTraversal("from", hcl.Traversal{hcl.TraverseRoot{Name: r.OldResourceType}, hcl.TraverseAttr{Name: r.OldLabel}}) movedBlock.Body().SetAttributeTraversal("to", hcl.Traversal{hcl.TraverseRoot{Name: r.NewResourceType}, hcl.TraverseAttr{Name: r.NewLabel}}) blocks := make([]*hclwrite.Block, 0) blocks = append(blocks, movedBlock) return blocks } func (r *AzurermResource) Outputs() []Output { res := make([]Output, 0) for _, instance := range r.Instances { res = append(res, instance.Outputs...) } res = append(res, Output{ OldName: r.OldAddress(nil), NewName: r.NewAddress(nil), }) return res } func (r *AzurermResource) MigratedBlock() *hclwrite.Block { return r.Block } func (r *AzurermResource) IsMigrated() bool { return r.Migrated } func (r *AzurermResource) GenerateNewConfig(terraform *tf.Terraform) error { if !r.IsMultipleResources() { instance := r.Instances[0] log.Printf("[INFO] importing %s to %s and generating config...", instance.ResourceId, r.NewAddress(nil)) block, err := importAndGenerateConfig(terraform, r.NewAddress(nil), instance.ResourceId, "", true) if err != nil { return err } r.Block = block valuePropMap := GetValuePropMap(r.Block, r.NewAddress(nil)) for i, output := range r.Instances[0].Outputs { r.Instances[0].Outputs[i].NewName = valuePropMap[output.GetStringValue()] } r.Migrated = true log.Printf("[INFO] resource %s has migrated to %s", r.OldAddress(nil), r.NewAddress(nil)) r.Block = InjectReference(r.Block, r.References) } else { // import and build combined block log.Printf("[INFO] generating config...") blocks := make([]*hclwrite.Block, 0) for _, instance := range r.Instances { 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) } } combinedBlock := hclwrite.NewBlock("resource", []string{r.NewResourceType, r.NewLabel}) if r.IsForEach() { foreachItems := CombineBlock(blocks, combinedBlock, true) foreachConfig := GetForEachConstants(r.Instances, foreachItems) combinedBlock.Body().SetAttributeRaw("for_each", helper.GetTokensForExpression(foreachConfig)) } else { _ = CombineBlock(blocks, combinedBlock, false) combinedBlock.Body().SetAttributeRaw("count", helper.GetTokensForExpression(fmt.Sprintf("%d", len(r.Instances)))) } r.Block = combinedBlock for i, instance := range r.Instances { valuePropMap := GetValuePropMap(blocks[i], r.NewAddress(instance.Index)) for j, output := range r.Instances[i].Outputs { r.Instances[i].Outputs[j].NewName = valuePropMap[output.GetStringValue()] } } for i, instance := range r.Instances { // TODO: improve this, azapi resource should use .output.xxx to access the output properties r.Instances[i].Outputs = append(r.Instances[i].Outputs, Output{ OldName: r.OldAddress(instance.Index), NewName: r.NewAddress(instance.Index), }) props := []string{"location", "tags", "identity", "identity.0", "identity.0.type", "identity.0.identity_ids", "id"} for _, prop := range props { r.Instances[i].Outputs = append(r.Instances[i].Outputs, Output{ OldName: r.OldAddress(instance.Index) + "." + prop, NewName: r.NewAddress(instance.Index) + "." + prop, }) } } r.Block = InjectReference(r.Block, r.References) } r.Block = sortAttributes(r.Block) r.Migrated = true return nil } func (r *AzurermResource) TargetProvider() string { return "azapi" } func (r *AzurermResource) CoverageCheck(_ bool) error { // all properties are supported by azapi resource, so no need to check return nil } func (r *AzurermResource) OldAddress(index interface{}) string { oldAddress := fmt.Sprintf("%s.%s", r.OldResourceType, r.OldLabel) if index == nil { return oldAddress } switch i := index.(type) { case int, int32, int64, float32, float64: return fmt.Sprintf(`%s[%v]`, oldAddress, i) case string: return fmt.Sprintf(`%s["%s"]`, oldAddress, i) default: return oldAddress } } func (r *AzurermResource) NewAddress(index interface{}) string { newAddress := fmt.Sprintf("azapi_resource.%s", r.NewLabel) if index == nil { return newAddress } switch i := index.(type) { case int, int32, int64, float32, float64: return fmt.Sprintf(`%s[%v]`, newAddress, i) case string: return fmt.Sprintf(`%s["%s"]`, newAddress, i) default: return newAddress } } func (r *AzurermResource) EmptyImportConfig() string { config := "" if r.IsMultipleResources() { for _, instance := range r.Instances { 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) } return config } func (r *AzurermResource) IsMultipleResources() bool { return len(r.Instances) != 0 && r.Instances[0].Index != nil } func (r *AzurermResource) IsForEach() bool { if len(r.Instances) != 0 && r.Instances[0].Index != nil { if _, ok := r.Instances[0].Index.(string); ok { return true } } return false } var pluralizeClient = pluralize.NewClient() func NewLabel(id string, oldLabel string) string { resourceType := ResourceTypeOfResourceId(id) lastSegment := LastSegment(resourceType) // #nosec G404 return fmt.Sprintf("%s_%s", pluralizeClient.Singular(lastSegment), oldLabel) } func LastSegment(input string) string { id := strings.Trim(input, "/") components := strings.Split(id, "/") if len(components) == 0 { return "" } return components[len(components)-1] } func ResourceTypeOfResourceId(input string) string { if input == "/" { return arm.TenantResourceType.String() } id := input if resourceType, err := arm.ParseResourceType(id); err == nil { if resourceType.Type != arm.ProviderResourceType.Type { return resourceType.String() } } idURL, err := url.ParseRequestURI(id) if err != nil { return "" } path := idURL.Path path = strings.TrimPrefix(path, "/") path = strings.TrimSuffix(path, "/") components := strings.Split(path, "/") resourceType := "" provider := "" for current := 0; current < len(components)-1; current += 2 { key := components[current] value := components[current+1] // Check key/value for empty strings. if key == "" || value == "" { return "" } if key == "providers" { provider = value resourceType = provider } else if len(provider) > 0 { resourceType += "/" + key } } return resourceType } func sortAttributes(input *hclwrite.Block) *hclwrite.Block { output := hclwrite.NewBlock(input.Type(), input.Labels()) attrList := []string{"count", "for_each", "type", "parent_id", "name", "location", "identity", "body", "tags"} usedAttr := make(map[string]bool) for _, attr := range attrList { if attribute := input.Body().GetAttribute(attr); attribute != nil { output.Body().SetAttributeRaw(attr, attribute.Expr().BuildTokens(nil)) usedAttr[attr] = true } else { for _, block := range input.Body().Blocks() { if block.Type() == attr { output.Body().AppendBlock(block) usedAttr[attr] = true } } } } for attrName, attribute := range input.Body().Attributes() { if _, ok := usedAttr[attrName]; !ok { output.Body().SetAttributeRaw(attrName, attribute.Expr().BuildTokens(nil)) } } for _, block := range input.Body().Blocks() { if _, ok := usedAttr[block.Type()]; !ok { output.Body().AppendBlock(block) } } return output } func AzurermIdToAzureId(azurermResourceType string, azurermId string) (string, error) { switch azurermResourceType { case "azurerm_monitor_diagnostic_setting": // input: <target id>|<diagnostic setting name> // output: <target id>/providers/Microsoft.Insights/diagnosticSettings/<diagnostic setting name> azurermIdSplit := strings.Split(azurermId, "|") if len(azurermIdSplit) != 2 { return "", fmt.Errorf("invalid id: %s, expected format: <target id>|<diagnostic setting name>", azurermId) } return fmt.Sprintf("%s/providers/Microsoft.Insights/diagnosticSettings/%s", azurermIdSplit[0], azurermIdSplit[1]), nil case "azurerm_role_definition": // input: <role definition id>|<scope> // output: <role definition id> azurermIdSplit := strings.Split(azurermId, "|") if len(azurermIdSplit) != 2 { return "", fmt.Errorf("invalid id: %s, expected format: <role definition id>|<scope>", azurermId) } return azurermIdSplit[0], nil // add more cases here as needed } // return azure id return azurermId, nil }