types/hcl.go (375 lines of code) (raw):
package types
import (
"fmt"
"log"
"os"
"path/filepath"
"reflect"
"strings"
"github.com/Azure/aztfmigrate/helper"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/hcl/v2/hclwrite"
)
// GetResourceBlock searches tf files in working directory and return `targetAddress` block
func GetResourceBlock(workingDirectory, targetAddress string) (*hclwrite.Block, error) {
for _, file := range helper.ListHclFiles(workingDirectory) {
// #nosec G304
src, err := os.ReadFile(filepath.Join(workingDirectory, file.Name()))
if err != nil {
return nil, err
}
f, diag := hclwrite.ParseConfig(src, file.Name(), hcl.InitialPos)
if f == nil || diag != nil && diag.HasErrors() || f.Body() == nil {
continue
}
for _, block := range f.Body().Blocks() {
if block != nil && block.Type() == "resource" {
address := strings.Join(block.Labels(), ".")
if targetAddress == address {
return block, nil
}
}
}
}
return nil, nil
}
// ReplaceResourceBlock searches tf files in working directory and replace `targetAddress` block with `newBlock`
func ReplaceResourceBlock(workingDirectory, targetAddress string, newBlocks []*hclwrite.Block) error {
for _, file := range helper.ListHclFiles(workingDirectory) {
// #nosec G304
src, err := os.ReadFile(filepath.Join(workingDirectory, file.Name()))
if err != nil {
return err
}
f, diag := hclwrite.ParseConfig(src, file.Name(), hcl.InitialPos)
if f == nil || diag != nil && diag.HasErrors() || f.Body() == nil {
continue
}
blocks := f.Body().Blocks()
f.Body().Clear()
found := false
for _, block := range blocks {
if block != nil && block.Type() == "resource" {
address := strings.Join(block.Labels(), ".")
if targetAddress == address {
f.Body().AppendUnstructuredTokens(CommentOutBlock(block))
f.Body().AppendNewline()
for _, newBlock := range newBlocks {
if newBlock == nil {
continue
}
f.Body().AppendBlock(newBlock)
f.Body().AppendNewline()
}
found = true
continue
}
}
f.Body().AppendBlock(block)
f.Body().AppendNewline()
}
if found {
if err := os.WriteFile(filepath.Join(workingDirectory, file.Name()), hclwrite.Format(f.Bytes()), 0600); err != nil {
log.Printf("[Error] saving configuration %s: %+v", file.Name(), err)
}
return nil
}
}
return nil
}
// ReplaceGenericOutputs searches tf files in working directory and replace generic resource's output with new address
func ReplaceGenericOutputs(workingDirectory string, outputs []Output) error {
for _, file := range helper.ListHclFiles(workingDirectory) {
// #nosec G304
src, err := os.ReadFile(filepath.Join(workingDirectory, file.Name()))
if err != nil {
return err
}
f, diag := hclwrite.ParseConfig(src, file.Name(), hcl.InitialPos)
if f == nil || diag != nil && diag.HasErrors() || f.Body() == nil {
continue
}
for _, block := range f.Body().Blocks() {
if block.Type() == "removed" || block.Type() == "import" || block.Type() == "moved" {
continue
}
if block != nil {
ReplaceOutputs(block, outputs)
}
}
if err := os.WriteFile(filepath.Join(workingDirectory, file.Name()), hclwrite.Format(f.Bytes()), 0600); err != nil {
log.Printf("[Error] saving configuration %s: %+v", file.Name(), err)
}
}
return nil
}
func ReplaceOutputs(block *hclwrite.Block, outputs []Output) {
for attrName, attr := range block.Body().Attributes() {
attrValue := string(attr.Expr().BuildTokens(nil).Bytes())
for _, output := range outputs {
attrValue = strings.ReplaceAll(attrValue, output.OldName, output.NewName)
}
block.Body().SetAttributeRaw(attrName, helper.GetTokensForExpression(attrValue))
}
for index := range block.Body().Blocks() {
ReplaceOutputs(block.Body().Blocks()[index], outputs)
}
}
// UpdateMigratedResourceBlock searches tf files in working directory and update generic patch resource's target
func UpdateMigratedResourceBlock(workingDirectory string, resources []AzapiUpdateResource) error {
for _, file := range helper.ListHclFiles(workingDirectory) {
// #nosec G304
src, err := os.ReadFile(filepath.Join(workingDirectory, file.Name()))
if err != nil {
return err
}
f, diag := hclwrite.ParseConfig(src, file.Name(), hcl.InitialPos)
if f == nil || diag != nil && diag.HasErrors() || f.Body() == nil {
continue
}
for _, block := range f.Body().Blocks() {
if block != nil && block.Type() == "resource" {
address := strings.Join(block.Labels(), ".")
for _, r := range resources {
if r.NewAddress(nil) == address { // TODO: && r.Change.Action != no_op
recursiveUpdate(block, r.Block, r.Change.Before, r.Change.After)
break
}
}
}
}
if err := os.WriteFile(filepath.Join(workingDirectory, file.Name()), hclwrite.Format(f.Bytes()), 0600); err != nil {
log.Printf("[Error] saving configuration %s: %+v", file.Name(), err)
}
}
return nil
}
func recursiveUpdate(old *hclwrite.Block, new *hclwrite.Block, before interface{}, after interface{}) {
// user can't use patch resource to add item to some array, so we don't need to deal with before or after is an array
beforeMap, ok1 := before.(map[string]interface{})
afterMap, ok2 := after.(map[string]interface{})
if !ok1 || !ok2 {
return
}
attrs := make(map[string]bool)
for attrName := range new.Body().Attributes() {
attrs[attrName] = true
}
for attrName := range old.Body().Attributes() {
attrs[attrName] = true
}
for attrName := range attrs {
if !reflect.DeepEqual(beforeMap[attrName], afterMap[attrName]) {
// add
if beforeMap[attrName] != nil && afterMap[attrName] == nil {
old.Body().SetAttributeRaw(attrName, new.Body().GetAttribute(attrName).Expr().BuildTokens(nil))
continue
}
// delete
if beforeMap[attrName] == nil && afterMap[attrName] != nil {
old.Body().RemoveAttribute(attrName)
continue
}
// update
if new.Body().GetAttribute(attrName) == nil {
continue
}
old.Body().SetAttributeRaw(attrName, new.Body().GetAttribute(attrName).Expr().BuildTokens(nil))
}
}
blocks := make(map[string]bool)
for _, block := range new.Body().Blocks() {
blocks[block.Type()] = true
}
for _, block := range old.Body().Blocks() {
blocks[block.Type()] = true
}
for blockName := range blocks {
if !reflect.DeepEqual(beforeMap[blockName], afterMap[blockName]) {
oldBlocks := make([]*hclwrite.Block, 0)
for _, block := range old.Body().Blocks() {
if block.Type() == blockName {
oldBlocks = append(oldBlocks, block)
}
}
newBlocks := make([]*hclwrite.Block, 0)
for _, block := range new.Body().Blocks() {
if block.Type() == blockName {
newBlocks = append(newBlocks, block)
}
}
// add
if len(newBlocks) != 0 && len(oldBlocks) == 0 {
for _, block := range newBlocks {
old.Body().AppendBlock(block)
}
continue
}
// delete
if len(newBlocks) == 0 && len(oldBlocks) != 0 {
for _, block := range oldBlocks {
old.Body().RemoveBlock(block)
}
continue
}
// update
if len(newBlocks) == len(oldBlocks) {
beforeArr := make([]interface{}, 0)
afterArr := make([]interface{}, 0)
if beforeMap[blockName] != nil {
temp, _ := beforeMap[blockName].([]interface{})
beforeArr = temp
}
if afterMap[blockName] != nil {
temp, _ := afterMap[blockName].([]interface{})
afterArr = temp
}
if len(beforeArr) != len(afterArr) && len(beforeArr) != len(oldBlocks) {
log.Fatal()
}
for index := range newBlocks {
recursiveUpdate(oldBlocks[index], newBlocks[index], beforeArr[index], afterArr[index])
}
} else {
for _, block := range oldBlocks {
old.Body().RemoveBlock(block)
}
for _, block := range newBlocks {
old.Body().AppendBlock(block)
}
}
}
}
}
// InjectReference replaces `block`'s literal value with reference provided by `refs`
func InjectReference(block *hclwrite.Block, refs []Reference) *hclwrite.Block {
if block.Body() == nil {
return block
}
search := make([]string, 0)
replacement := make([]string, 0)
for _, ref := range refs {
if stringValue, ok := ref.Value.(string); ok {
search = append(search, stringValue)
replacement = append(replacement, ref.Name)
}
}
for attrName, attr := range block.Body().Attributes() {
if input := helper.GetValueFromExpression(attr.Expr().BuildTokens(nil)); input != nil {
if output, found := helper.ToHclSearchReplace(input, search, replacement); found {
block.Body().SetAttributeRaw(attrName, helper.GetTokensForExpression(output))
}
}
}
for index := range block.Body().Blocks() {
InjectReference(block.Body().Blocks()[index], refs)
}
return block
}
// GetValuePropMap returns a map from literal value to reference
func GetValuePropMap(block *hclwrite.Block, prefix string) map[string]string {
res := make(map[string]string)
if block == nil {
return res
}
for attrName, attr := range block.Body().Attributes() {
res[strings.TrimSpace(string(attr.Expr().BuildTokens(nil).Bytes()))] = prefix + "." + attrName
}
blocksMap := make(map[string][]*hclwrite.Block)
for _, block := range block.Body().Blocks() {
if len(blocksMap[block.Type()]) == 0 {
blocksMap[block.Type()] = make([]*hclwrite.Block, 0)
}
blocksMap[block.Type()] = append(blocksMap[block.Type()], block)
}
for blockType, arr := range blocksMap {
for index, block := range arr {
propValueMap := GetValuePropMap(block, fmt.Sprintf("%s.%s.%d", prefix, blockType, index))
for k, v := range propValueMap {
res[k] = v
}
}
}
return res
}
// CombineBlock combines `blocks` and update its result to `output` block, and return the difference in a map(key is attribute name, value is a list of attribute values)
func CombineBlock(blocks []*hclwrite.Block, output *hclwrite.Block, isForEach bool) map[string][]hclwrite.Tokens {
attrNameSet := make(map[string]bool)
for _, b := range blocks {
for attrName := range b.Body().Attributes() {
attrNameSet[attrName] = true
}
}
attrValueMap := make(map[string][]hclwrite.Tokens)
for attrName := range attrNameSet {
values := make([]string, len(blocks))
tokens := make([]hclwrite.Tokens, len(blocks))
for i, b := range blocks {
if b == nil {
values[i] = "null"
tokens[i] = nil
continue
}
attr := b.Body().GetAttribute(attrName)
if attr == nil {
values[i] = "null"
tokens[i] = nil
} else {
tokens[i] = b.Body().GetAttribute(attrName).Expr().BuildTokens(nil)
values[i] = strings.TrimSpace(string(tokens[i].Bytes()))
}
}
switch {
case helper.IsArrayWithSameValue(values):
output.Body().SetAttributeRaw(attrName, blocks[0].Body().GetAttribute(attrName).Expr().BuildTokens(nil))
case isForEach:
output.Body().SetAttributeRaw(attrName, helper.GetTokensForExpression("each.value."+attrName))
attrValueMap[attrName] = tokens
default:
output.Body().SetAttributeRaw(attrName, helper.GetTokensForExpression(fmt.Sprintf("%s${count.index}%s", helper.Prefix(values), helper.Suffix(values))))
attrValueMap[attrName] = tokens
}
}
blockNameSet := make(map[string]bool)
for _, b := range blocks {
for _, nb := range b.Body().Blocks() {
blockNameSet[nb.Type()] = true
}
}
for blockName := range blockNameSet {
nestedBlocks := make([]*hclwrite.Block, len(blocks))
for i, b := range blocks {
if nestedBlock := b.Body().FirstMatchingBlock(blockName, []string{}); nestedBlock != nil {
nestedBlocks[i] = nestedBlock
}
}
outputNestedBlock := output.Body().AppendNewBlock(blockName, []string{})
tempMap := CombineBlock(nestedBlocks, outputNestedBlock, isForEach)
for k, v := range tempMap {
attrValueMap[k] = v
}
}
return attrValueMap
}
// GetForEachConstants converts a map of difference to hcl object
func GetForEachConstants(instances []Instance, items map[string][]hclwrite.Tokens) string {
config := ""
i := 0
for _, instance := range instances {
item := ""
for key := range items {
item += fmt.Sprintf("%s = %s\n", quotedKey(key), string(items[key][i].Bytes()))
}
config += fmt.Sprintf("%s = {\n%s\n}\n", quotedKey(fmt.Sprintf("%v", instance.Index)), item)
i++
}
config = fmt.Sprintf("{\n%s}\n", config)
return config
}
func quotedKey(input string) string {
if len(input) == 0 {
return input
}
if strings.Contains(input, ".") || strings.Contains(input, "/") || input[0] == '$' || input[0] >= '0' && input[0] <= '9' {
return fmt.Sprintf("\"%s\"", input)
}
return input
}
func CommentOutBlock(block *hclwrite.Block) hclwrite.Tokens {
file := hclwrite.NewEmptyFile()
file.Body().AppendBlock(block)
content := string(file.Bytes())
lines := strings.Split(content, "\n")
for i, line := range lines {
lines[i] = fmt.Sprintf("# %s", line)
}
return hclwrite.Tokens{
&hclwrite.Token{
Type: hclsyntax.TokenComment,
Bytes: []byte(strings.Join(lines, "\n")),
SpacesBefore: 0,
},
}
}