Skip to content

Commit

Permalink
Feature arm templates to azapi (#105)
Browse files Browse the repository at this point in the history
* support paste arm template as azapi config

* go mod vendor

* fix golint
  • Loading branch information
ms-henglu authored Aug 7, 2024
1 parent e1f003e commit 9173b25
Show file tree
Hide file tree
Showing 71 changed files with 15,133 additions and 159 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0
github.com/apparentlymart/go-textseg v1.0.0
github.com/creachadair/jrpc2 v0.32.0
github.com/expr-lang/expr v1.16.9
github.com/gertd/go-pluralize v0.2.1
github.com/google/go-cmp v0.5.8
github.com/hashicorp/go-version v1.3.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ github.com/creachadair/jrpc2 v0.32.0/go.mod h1:w+GXZGc+NwsH0xsUOgeLBIIRM0jBOSTXh
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/expr-lang/expr v1.16.9 h1:WUAzmR0JNI9JCiF0/ewwHB1gmcGw5wW7nWt8gc6PpCI=
github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/gertd/go-pluralize v0.2.1 h1:M3uASbVjMnTsPb0PNqg+E/24Vwigyo/tvyMTtAlLgiA=
Expand Down
19 changes: 13 additions & 6 deletions internal/azure/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"strconv"
"strings"
"sync"

"github.com/Azure/azapi-lsp/internal/azure/types"
)
Expand All @@ -15,23 +16,25 @@ type Schema struct {
}

type Resource struct {
Definitions []ResourceDefinition
Definitions []*ResourceDefinition
}

type Function struct {
Definitions []FunctionDefinition
Definitions []*FunctionDefinition
}

type ResourceDefinition struct {
Definition *types.ResourceType
Location TypeLocation
ApiVersion string
mutex sync.Mutex
}

type FunctionDefinition struct {
Definition *types.ResourceFunctionType
Location TypeLocation
ApiVersion string
mutex sync.Mutex
}

type TypeLocation struct {
Expand Down Expand Up @@ -128,11 +131,11 @@ func (o *Schema) UnmarshalJSON(body []byte) error {
resource := o.Resources[resourceType]
if resource == nil {
o.Resources[resourceType] = &Resource{
Definitions: make([]ResourceDefinition, 0),
Definitions: make([]*ResourceDefinition, 0),
}
resource = o.Resources[resourceType]
}
resource.Definitions = append(resource.Definitions, ResourceDefinition{
resource.Definitions = append(resource.Definitions, &ResourceDefinition{
Definition: nil,
Location: v,
ApiVersion: k[index+1:],
Expand All @@ -143,12 +146,12 @@ func (o *Schema) UnmarshalJSON(body []byte) error {
function := o.Functions[k]
if function == nil {
o.Functions[k] = &Function{
Definitions: make([]FunctionDefinition, 0),
Definitions: make([]*FunctionDefinition, 0),
}
function = o.Functions[k]
}
for _, item := range arr {
function.Definitions = append(function.Definitions, FunctionDefinition{
function.Definitions = append(function.Definitions, &FunctionDefinition{
Definition: nil,
Location: item,
ApiVersion: apiVersion,
Expand All @@ -164,6 +167,8 @@ func (o *ResourceDefinition) GetDefinition() (*types.ResourceType, error) {
if o == nil {
return nil, nil
}
o.mutex.Lock()
defer o.mutex.Unlock()
if o.Definition != nil {
return o.Definition, nil
}
Expand All @@ -179,6 +184,8 @@ func (o *FunctionDefinition) GetDefinition() (*types.ResourceFunctionType, error
if o == nil {
return nil, nil
}
o.mutex.Lock()
defer o.mutex.Unlock()
if o.Definition != nil {
return o.Definition, nil
}
Expand Down
4 changes: 2 additions & 2 deletions internal/azure/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ func GetResourceDefinition(resourceType, apiVersion string) (*types.ResourceType
return nil, fmt.Errorf("failed to find resource type %s api-version %s in azure schema index", resourceType, apiVersion)
}

func ListResourceFunctions(resourceType, apiVersion string) ([]FunctionDefinition, error) {
res := make([]FunctionDefinition, 0)
func ListResourceFunctions(resourceType, apiVersion string) ([]*FunctionDefinition, error) {
res := make([]*FunctionDefinition, 0)
azureSchema := GetAzureSchema()
if azureSchema == nil {
return nil, fmt.Errorf("failed to load azure schema index")
Expand Down
178 changes: 178 additions & 0 deletions internal/langserver/handlers/command/arm_template_converter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package command

import (
"encoding/json"
"fmt"
"strings"

"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(input string) (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)
}

for _, resource := range model.Resources {
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)
}

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
}
109 changes: 109 additions & 0 deletions internal/langserver/handlers/command/arm_template_converter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package command

import (
"testing"

"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/zclconf/go-cty/cty"
)

func Test_Context(t *testing.T) {
res1 := hclwrite.NewBlock("resource", []string{"azapi_resource", "managedCluster"})
res1.Body().SetAttributeValue("type", cty.StringVal("Microsoft.ContainerService/managedClusters@2021-03-01"))
res1.Body().SetAttributeValue("name", cty.StringVal("myCluster"))
res1.Body().SetAttributeValue("parent_id", cty.StringVal("myParentId"))

res2 := hclwrite.NewBlock("resource", []string{"azapi_resource", "managedCluster"})
res2.Body().SetAttributeValue("type", cty.StringVal("Microsoft.ContainerService/managedClusters@2021-03-01"))
res2.Body().SetAttributeValue("name", cty.StringVal("myCluster"))
res2.Body().SetAttributeValue("parent_id", cty.StringVal("myParentId"))

var1 := hclwrite.NewBlock("variable", []string{"foo"})

tests := []struct {
name string
input []*hclwrite.Block
expect string
}{
{
name: "empty",
input: []*hclwrite.Block{},
expect: `
variable "subscriptionId" {
type = string
description = "The subscription id"
}
variable "resourceGroupName" {
type = string
description = "The resource group name"
}
`,
},
{
name: "with variables",
input: []*hclwrite.Block{
var1,
},
expect: `
variable "subscriptionId" {
type = string
description = "The subscription id"
}
variable "resourceGroupName" {
type = string
description = "The resource group name"
}
variable "foo" {
}
`,
},
{
name: "with resources",
input: []*hclwrite.Block{
res1,
res2,
},
expect: `
variable "subscriptionId" {
type = string
description = "The subscription id"
}
variable "resourceGroupName" {
type = string
description = "The resource group name"
}
resource "azapi_resource" "managedCluster" {
type = "Microsoft.ContainerService/managedClusters@2021-03-01"
name = "myCluster"
parent_id = "myParentId"
}
resource "azapi_resource" "managedCluster1" {
type = "Microsoft.ContainerService/managedClusters@2021-03-01"
name = "myCluster"
parent_id = "myParentId"
}
`,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := NewContext()
for _, b := range tt.input {
ctx.AppendBlock(b)
}
if got := ctx.String(); got != tt.expect {
t.Errorf("unexpected result, got: %s, expect: %s", got, tt.expect)
}
})
}
}
Loading

0 comments on commit 9173b25

Please sign in to comment.