internal/langserver/handlers/command/resource_json_converter.go (225 lines of code) (raw):

package command import ( "context" "encoding/json" "fmt" "log" "strings" "github.com/Azure/azapi-lsp/internal/azure" "github.com/Azure/azapi-lsp/internal/azure/types" "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 resourceModel struct { Type string `json:"type"` Name string `json:"name"` Location string `json:"location"` ID string `json:"id"` Tags map[string]string `json:"tags"` Identity *identityModel `json:"identity"` DependsOn []string `json:"dependsOn"` } type identityModel struct { Type string `json:"type"` UserAssignedIdentities map[string]interface{} `json:"userAssignedIdentities"` } func convertResourceJson(ctx context.Context, input string, telemetrySender telemetry.Sender) (string, error) { var model resourceModel err := json.Unmarshal([]byte(input), &model) if err != nil { return "", fmt.Errorf("unable to unmarshal JSON content: %w", err) } block, err := ParseResourceJson(input) if err != nil { return "", err } label := "" if len(block.Labels()) == 2 { label = block.Labels()[1] } apiVersion := "" typeValue := string(block.Body().GetAttribute("type").Expr().BuildTokens(nil).Bytes()) typeValue = strings.Trim(typeValue, " \"") if parts := strings.Split(typeValue, "@"); len(parts) == 2 { apiVersion = parts[1] } telemetrySender.SendEvent(ctx, "ConvertJsonToAzapi", map[string]interface{}{ "status": "completed", "kind": "resource-json", "type": typeValue, }) importBlock := hclwrite.NewBlock("import", nil) importBlock.Body().SetAttributeValue("id", cty.StringVal(fmt.Sprintf("%s?api-version=%s", model.ID, apiVersion))) importBlock.Body().SetAttributeTraversal("to", hcl.Traversal{hcl.TraverseRoot{Name: "azapi_resource"}, hcl.TraverseAttr{Name: label}}) result := fmt.Sprintf(`/* Note: This is a generated HCL content from the JSON input which is based on the latest API version available. To import the resource, please run the following command: terraform import azapi_resource.%s %s?api-version=%s Or add the below config: %s*/ %s`, label, model.ID, apiVersion, string(hclwrite.Format(importBlock.BuildTokens(nil).Bytes())), string(hclwrite.Format(block.BuildTokens(nil).Bytes()))) return result, nil } func ParseResourceJson(content string) (*hclwrite.Block, error) { var model resourceModel err := json.Unmarshal([]byte(content), &model) if err != nil { return nil, fmt.Errorf("unable to unmarshal JSON content: %w", err) } block := hclwrite.NewBlock("resource", []string{}) parentId := "" if armId, err := arm.ParseResourceID(model.ID); err == nil { // use the resource type from the ID model.Type = armId.ResourceType.String() parentId = armId.Parent.String() } if parentId == "" { for _, v := range model.DependsOn { armId, err := arm.ParseResourceID(v) if err != nil { continue } if armId.ResourceType.String() == GetParentType(model.Type) { parentId = armId.String() break } } } if parentId == "" { parentId = "/subscriptions/${var.subscriptionId}/resourceGroups/${var.resourceGroupName}" } apiVersions := azure.GetApiVersions(model.Type) apiVersion := "TODO" if len(apiVersions) > 0 { apiVersion = apiVersions[len(apiVersions)-1] } label := pluralizeClient.Singular(LastSegment(model.Type)) block.SetLabels([]string{"azapi_resource", label}) block.Body().SetAttributeValue("type", cty.StringVal(fmt.Sprintf("%s@%s", model.Type, apiVersion))) block.Body().SetAttributeValue("parent_id", cty.StringVal(parentId)) nameValue := model.Name[strings.LastIndex(model.Name, "/")+1:] block.Body().SetAttributeValue("name", cty.StringVal(nameValue)) def, _ := azure.GetResourceDefinition(model.Type, apiVersion) if model.Location != "" && (def == nil || canResourceHaveProperty(def, "location")) { block.Body().SetAttributeValue("location", cty.StringVal(model.Location)) } if model.Identity != nil && !strings.EqualFold(model.Identity.Type, "None") { identityBlock := hclwrite.NewBlock("identity", nil) identityBlock.Body().SetAttributeValue("type", cty.StringVal(model.Identity.Type)) if len(model.Identity.UserAssignedIdentities) > 0 { identityIds := make([]cty.Value, 0) for k := range model.Identity.UserAssignedIdentities { identityIds = append(identityIds, cty.StringVal(k)) } identityBlock.Body().SetAttributeValue("identity_ids", cty.ListVal(identityIds)) } else { identityBlock.Body().SetAttributeValue("identity_ids", cty.ListValEmpty(cty.String)) } block.Body().AppendBlock(identityBlock) } var bodyMap map[string]interface{} err = json.Unmarshal([]byte(content), &bodyMap) if err != nil { return nil, fmt.Errorf("unable to unmarshal JSON content: %w", err) } delete(bodyMap, "type") delete(bodyMap, "location") delete(bodyMap, "id") delete(bodyMap, "tags") delete(bodyMap, "identity") bodyMap["name"] = nameValue if label == "basicPublishingCredentialsPolicy" { fmt.Println(bodyMap) } if def != nil { writeOnlyBody := def.GetWriteOnly(NormalizeObject(bodyMap)) ok := false bodyMap, ok = writeOnlyBody.(map[string]interface{}) if !ok { log.Printf("[ERROR] unable to get write only body, result: %v", writeOnlyBody) } } delete(bodyMap, "name") block.Body().SetAttributeValue("body", toCtyValue(bodyMap)) if len(model.Tags) > 0 { tags := map[string]cty.Value{} for k, v := range model.Tags { tags[k] = cty.StringVal(v) } block.Body().SetAttributeValue("tags", cty.MapVal(tags)) } return block, nil } func toCtyValue(input interface{}) cty.Value { if input == nil { return cty.NullVal(cty.DynamicPseudoType) } switch v := input.(type) { case map[string]interface{}: m := map[string]cty.Value{} for k, v := range v { m[k] = toCtyValue(v) } return cty.ObjectVal(m) case []interface{}: l := make([]cty.Value, len(v)) for i, e := range v { l[i] = toCtyValue(e) } return cty.TupleVal(l) case string: return cty.StringVal(v) case bool: return cty.BoolVal(v) case float64: return cty.NumberFloatVal(v) case float32: return cty.NumberFloatVal(float64(v)) case int: return cty.NumberIntVal(int64(v)) case int64: return cty.NumberIntVal(v) case int32: return cty.NumberIntVal(int64(v)) default: return cty.NullVal(cty.DynamicPseudoType) } } func LastSegment(input string) string { id := strings.Trim(input, "/") components := strings.Split(id, "/") if len(components) == 0 { return "" } return components[len(components)-1] } func GetParentType(resourceType string) string { parts := strings.Split(resourceType, "/") if len(parts) <= 2 { return "" } return strings.Join(parts[0:len(parts)-1], "/") } func NormalizeObject(input interface{}) interface{} { jsonString, _ := json.Marshal(input) var output interface{} _ = json.Unmarshal(jsonString, &output) return output } func canResourceHaveProperty(resourceDef *types.ResourceType, property string) bool { if resourceDef == nil || resourceDef.Body == nil || resourceDef.Body.Type == nil { return false } objectType, ok := (*resourceDef.Body.Type).(*types.ObjectType) if !ok { return false } if prop, ok := objectType.Properties[property]; ok { if !prop.IsReadOnly() { return true } } return false }