internal/langserver/handlers/command/arm_template_converter.go (171 lines of code) (raw):

package command import ( "context" "encoding/json" "fmt" "strings" "github.com/Azure/azapi-lsp/internal/telemetry" "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclwrite" "github.com/zclconf/go-cty/cty" ) type Context struct { File *hclwrite.File typeLabelMap map[string]bool idRefMap map[string]string } func NewContext() *Context { file, _ := hclwrite.ParseConfig([]byte(hclTemplate), "main.tf", hcl.InitialPos) return &Context{ File: file, typeLabelMap: make(map[string]bool), idRefMap: make(map[string]string), } } func (c *Context) AppendBlock(block *hclwrite.Block) { if block.Type() == "resource" { typeValue := string(block.Body().GetAttribute("type").Expr().BuildTokens(nil).Bytes()) typeValue = strings.Trim(typeValue, " \"") tfLabel := block.Labels()[1] if typeLabel := fmt.Sprintf("%s-%s", typeValue, tfLabel); c.typeLabelMap[typeLabel] { for i := 1; ; i++ { tfLabel = fmt.Sprintf("%s%d", block.Labels()[1], i) newTypeLabel := fmt.Sprintf("%s-%s", typeValue, tfLabel) if !c.typeLabelMap[newTypeLabel] { newLabels := []string{block.Labels()[0], tfLabel} block.SetLabels(newLabels) c.typeLabelMap[newTypeLabel] = true break } } } else { c.typeLabelMap[typeLabel] = true } nameValue := string(block.Body().GetAttribute("name").Expr().BuildTokens(nil).Bytes()) nameValue = strings.Trim(nameValue, " \"") parentIdValue := string(block.Body().GetAttribute("parent_id").Expr().BuildTokens(nil).Bytes()) parentIdValue = strings.Trim(parentIdValue, " \"") c.idRefMap[buildResourceId(nameValue, parentIdValue, typeValue)] = fmt.Sprintf("azapi_resource.%s.id", tfLabel) } c.File.Body().AppendBlock(block) c.File.Body().AppendNewline() } func (c *Context) String() string { result := string(c.File.Bytes()) for id, ref := range c.idRefMap { result = strings.ReplaceAll(result, fmt.Sprintf(`"%s"`, id), ref) } result = string(hclwrite.Format([]byte(result))) // TODO: improve it result = strings.ReplaceAll(result, "$${", "${") return result } func convertARMTemplate(ctx context.Context, input string, telemetrySender telemetry.Sender) (string, error) { var model ARMTemplateModel err := json.Unmarshal([]byte(input), &model) if err != nil { return "", err } c := NewContext() for key, parameter := range model.Parameters { varBlock := hclwrite.NewBlock("variable", []string{key}) switch strings.ToLower(parameter.Type) { case "string": varBlock.Body().SetAttributeTraversal("type", hcl.Traversal{hcl.TraverseRoot{Name: "string"}}) case "securestring": varBlock.Body().SetAttributeTraversal("type", hcl.Traversal{hcl.TraverseRoot{Name: "string"}}) varBlock.Body().SetAttributeValue("sensitive", cty.True) default: // Todo: support other types varBlock.Body().SetAttributeTraversal("type", hcl.Traversal{hcl.TraverseRoot{Name: strings.ToLower(parameter.Type)}}) } varBlock.Body().SetAttributeValue("default", cty.StringVal(parameter.DefaultValue)) c.AppendBlock(varBlock) } typeSet := make(map[string]bool) for _, resource := range model.Resources { typeValue := "" if resource["type"] != nil { typeValue = resource["type"].(string) } if resource["apiVersion"] != nil { typeValue = fmt.Sprintf("%s@%s", typeValue, resource["apiVersion"].(string)) } typeSet[typeValue] = true res := flattenARMExpression(resource) data, err := json.MarshalIndent(res, "", " ") if err != nil { return "", fmt.Errorf("unable to marshal JSON content: %v", err) } resourceJson := string(data) resourceBlock, err := ParseResourceJson(resourceJson) if err != nil { return "", fmt.Errorf("unable to parse resource JSON content: %v", err) } if resourceBlock == nil { return "", fmt.Errorf("resource block is nil") } c.AppendBlock(resourceBlock) } types := make([]string, 0) for t := range typeSet { types = append(types, t) } telemetrySender.SendEvent(ctx, "ConvertJsonToAzapi", map[string]interface{}{ "status": "completed", "kind": "arm-template", "type": strings.Join(types, ","), }) return c.String(), nil } const hclTemplate = ` variable "subscriptionId" { type = string description = "The subscription id" } variable "resourceGroupName" { type = string description = "The resource group name" } ` type ARMTemplateParameterModel struct { DefaultValue string `json:"defaultValue"` Type string `json:"type"` } type ARMTemplateModel struct { Schema string `json:"$schema"` ContentVersion string `json:"contentVersion"` Parameters map[string]ARMTemplateParameterModel `json:"parameters"` Variables interface{} `json:"variables"` Resources []map[string]interface{} `json:"resources"` } func buildResourceId(name, parentId, resourceType string) string { azureResourceType := resourceType[:strings.Index(resourceType, "@")] azureResourceId := "" switch { case strings.Count(azureResourceType, "/") == 1: // build azure resource id switch azureResourceType { case arm.ResourceGroupResourceType.String(): azureResourceId = fmt.Sprintf("%s/resourceGroups/%s", parentId, name) case arm.SubscriptionResourceType.String(): azureResourceId = fmt.Sprintf("/subscriptions/%s", name) case arm.TenantResourceType.String(): azureResourceId = "/" case arm.ProviderResourceType.String(): // avoid duplicated `/` if parent_id is tenant scope scopeId := parentId if parentId == "/" { scopeId = "" } azureResourceId = fmt.Sprintf("%s/providers/%s", scopeId, name) default: // avoid duplicated `/` if parent_id is tenant scope scopeId := parentId if parentId == "/" { scopeId = "" } azureResourceId = fmt.Sprintf("%s/providers/%s/%s", scopeId, azureResourceType, name) } default: // build azure resource id lastType := azureResourceType[strings.LastIndex(azureResourceType, "/")+1:] azureResourceId = fmt.Sprintf("%s/%s/%s", parentId, lastType, name) } return azureResourceId }