types/azapi_resource.go (241 lines of code) (raw):
package types
import (
"fmt"
"os"
"strconv"
"strings"
"github.com/Azure/aztfmigrate/azurerm"
"github.com/Azure/aztfmigrate/azurerm/coverage"
"github.com/Azure/aztfmigrate/azurerm/schema"
"github.com/Azure/aztfmigrate/helper"
"github.com/Azure/aztfmigrate/tf"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/zclconf/go-cty/cty"
)
var _ AzureResource = &AzapiResource{}
type AzapiResource struct {
Label string
Instances []Instance
ResourceType string
Block *hclwrite.Block
References []Reference
InputProperties []string
OutputProperties []string
Migrated bool
}
func (r *AzapiResource) StateUpdateBlocks() []*hclwrite.Block {
blocks := make([]*hclwrite.Block, 0)
blocks = append(blocks, r.removedBlock())
blocks = append(blocks, r.importBlock())
return blocks
}
func (r *AzapiResource) 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 *AzapiResource) MigratedBlock() *hclwrite.Block {
return r.Block
}
func (r *AzapiResource) IsMigrated() bool {
return r.Migrated
}
func (r *AzapiResource) importBlock() *hclwrite.Block {
importBlock := hclwrite.NewBlock("import", nil)
if r.IsMultipleResources() {
forEachMap := make(map[string]cty.Value)
for _, instance := range r.Instances {
switch v := instance.Index.(type) {
case string:
forEachMap[instance.ResourceId] = cty.StringVal(v)
default:
value, _ := strconv.ParseInt(fmt.Sprintf("%v", v), 10, 64)
forEachMap[instance.ResourceId] = cty.NumberIntVal(value)
}
}
importBlock.Body().SetAttributeValue("for_each", cty.MapVal(forEachMap))
importBlock.Body().SetAttributeTraversal("id", hcl.Traversal{hcl.TraverseRoot{Name: "each"}, hcl.TraverseAttr{Name: "key"}})
importBlock.Body().SetAttributeTraversal("to", hcl.Traversal{hcl.TraverseRoot{Name: r.ResourceType}, hcl.TraverseAttr{Name: fmt.Sprintf("%s[each.value]", r.Label)}})
} else {
importBlock.Body().SetAttributeValue("id", cty.StringVal(r.Instances[0].ResourceId))
importBlock.Body().SetAttributeTraversal("to", hcl.Traversal{hcl.TraverseRoot{Name: r.ResourceType}, hcl.TraverseAttr{Name: r.Label}})
}
return importBlock
}
func (r *AzapiResource) removedBlock() *hclwrite.Block {
removedBlock := hclwrite.NewBlock("removed", nil)
removedBlock.Body().SetAttributeTraversal("from", hcl.Traversal{hcl.TraverseRoot{Name: "azapi_resource"}, hcl.TraverseAttr{Name: r.Label}})
removedLifecycleBlock := hclwrite.NewBlock("lifecycle", nil)
removedLifecycleBlock.Body().SetAttributeValue("destroy", cty.BoolVal(false))
removedBlock.Body().AppendBlock(removedLifecycleBlock)
return removedBlock
}
func (r *AzapiResource) GenerateNewConfig(terraform *tf.Terraform) error {
if !r.IsMultipleResources() {
instance := r.Instances[0]
block, err := importAndGenerateConfig(terraform, r.NewAddress(nil), instance.ResourceId, r.ResourceType, false)
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()]
}
for i, instance := range r.Instances {
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)
} else {
// import and build combined block
blocks := make([]*hclwrite.Block, 0)
for _, instance := range r.Instances {
instanceAddress := fmt.Sprintf("%s.%s_%v", r.ResourceType, r.Label, strings.ReplaceAll(fmt.Sprintf("%v", instance.Index), "/", "_"))
if block, err := importAndGenerateConfig(terraform, instanceAddress, instance.ResourceId, r.ResourceType, false); err == nil {
blocks = append(blocks, block)
}
}
combinedBlock := hclwrite.NewBlock("resource", []string{r.ResourceType, r.Label})
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 {
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.Migrated = true
return nil
}
func (r *AzapiResource) TargetProvider() string {
return "azurerm"
}
func (r *AzapiResource) CoverageCheck(strictMode bool) error {
if os.Getenv("AZTF_MIGRATE_SKIP_COVERAGE_CHECK") == "true" {
return nil
}
resourceId := r.Instances[0].ResourceId
idPattern, _ := GetIdPattern(resourceId)
if strictMode {
azurermApiVersion := coverage.GetApiVersion(idPattern)
if azurermApiVersion != r.Instances[0].ApiVersion {
return fmt.Errorf("%s: api-versions are not matched, expect %s, got %s",
r.OldAddress(nil), r.Instances[0].ApiVersion, azurermApiVersion)
}
}
_, uncoveredPut := coverage.GetPutCoverage(r.InputProperties, idPattern)
_, uncoveredGet := coverage.GetGetCoverage(r.OutputProperties, idPattern)
if len(uncoveredGet)+len(uncoveredPut) != 0 {
return fmt.Errorf("%s: input properties not supported: [%v], output properties not supported: [%v]",
r.OldAddress(nil), strings.Join(uncoveredPut, ", "), strings.Join(uncoveredGet, ", "))
}
return nil
}
func (r *AzapiResource) OldAddress(index interface{}) string {
oldAddress := fmt.Sprintf("azapi_resource.%s", r.Label)
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 *AzapiResource) NewAddress(index interface{}) string {
newAddress := fmt.Sprintf("%s.%s", r.ResourceType, r.Label)
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 *AzapiResource) EmptyImportConfig() string {
config := ""
for _, instance := range r.Instances {
if !r.IsMultipleResources() {
config += fmt.Sprintf("resource \"%s\" \"%s\" {}\n", r.ResourceType, r.Label)
} else {
config += fmt.Sprintf("resource \"%s\" \"%s_%s\" {}\n", r.ResourceType, r.Label, strings.ReplaceAll(fmt.Sprintf("%v", instance.Index), "/", "_"))
}
}
return config
}
func (r *AzapiResource) IsMultipleResources() bool {
return len(r.Instances) != 0 && r.Instances[0].Index != nil
}
func (r *AzapiResource) IsForEach() bool {
if len(r.Instances) != 0 && r.Instances[0].Index != nil {
if _, ok := r.Instances[0].Index.(string); ok {
return true
}
}
return false
}
type Instance struct {
Index interface{}
ApiVersion string
ResourceId string
Outputs []Output
}
func importAndGenerateConfig(terraform *tf.Terraform, address string, id string, resourceType string, skipTune bool) (*hclwrite.Block, error) {
tpl, err := terraform.ImportAdd(address, id)
if err != nil {
return nil, err
}
f, diag := hclwrite.ParseConfig([]byte(tpl), "", hcl.InitialPos)
if (diag != nil && diag.HasErrors()) || f == nil {
return nil, fmt.Errorf("parsing the HCL generated by \"terraform add\" of %s: %s", address, diag.Error())
}
if !skipTune {
rb := f.Body().Blocks()[0].Body()
sch := schema.ProviderSchemaInfo.ResourceSchemas[resourceType]
if err := azurerm.TuneHCLSchemaForResource(rb, sch); err != nil {
return nil, fmt.Errorf("tuning hcl config base on schema: %+v", err)
}
}
return f.Body().Blocks()[0], nil
}