internal/meta/config_info.go (160 lines of code) (raw):
package meta
import (
"fmt"
"io"
"sort"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/magodo/armid"
"github.com/zclconf/go-cty/cty"
)
type ConfigInfos []ConfigInfo
type ConfigInfo struct {
ImportItem
DependsOn []Dependency
hcl *hclwrite.File
}
type Dependency struct {
Candidates []string
}
func (cfg ConfigInfo) DumpHCL(w io.Writer) (int, error) {
out := hclwrite.Format(cfg.hcl.Bytes())
return w.Write(out)
}
func (cfgs ConfigInfos) AddDependency() error {
cfgs.addParentChildDependency()
if err := cfgs.addReferenceDependency(); err != nil {
return err
}
// Disduplicate then sort the dependencies
for i, cfg := range cfgs {
if len(cfg.DependsOn) == 0 {
continue
}
// Disduplicate same resource ids that has exact one candidate
set := map[string]bool{}
duplicates := []Dependency{}
for _, dep := range cfg.DependsOn {
if len(dep.Candidates) == 1 {
set[dep.Candidates[0]] = true
} else {
duplicates = append(duplicates, dep)
}
}
// Disduplicate dependency that is parent of another dependency
var covlist []string
for dep := range set {
for odep := range set {
if dep == odep {
continue
}
if strings.HasPrefix(strings.ToUpper(odep), strings.ToUpper(dep)) {
covlist = append(covlist, dep)
break
}
}
}
for _, dep := range covlist {
delete(set, dep)
}
// If all duplicate candidates are child of this dependency, then also remove it.
covlist = []string{}
for dep := range set {
for _, odep := range duplicates {
allIsChild := true
for _, candidate := range odep.Candidates {
if !strings.HasPrefix(strings.ToUpper(candidate), strings.ToUpper(dep)) {
allIsChild = false
break
}
}
if allIsChild {
covlist = append(covlist, dep)
break
}
}
}
for _, dep := range covlist {
delete(set, dep)
}
// Sort the dependencies
cfg.DependsOn = duplicates
for id := range set {
id := id
cfg.DependsOn = append(cfg.DependsOn, Dependency{Candidates: []string{id}})
}
sort.Slice(cfg.DependsOn, func(i, j int) bool {
d1, d2 := cfg.DependsOn[i], cfg.DependsOn[j]
if len(d1.Candidates) != len(d2.Candidates) {
return len(d1.Candidates) < len(d2.Candidates)
}
return strings.Join(d1.Candidates, "") < strings.Join(d2.Candidates, "")
})
cfgs[i] = cfg
}
return nil
}
func (cfgs ConfigInfos) addParentChildDependency() {
for i, cfg := range cfgs {
parentId := cfg.AzureResourceID.Parent()
// This resource is either a root scope or a root scoped resource
if parentId == nil {
// Root scope: ignore as it has no parent
if cfg.AzureResourceID.ParentScope() == nil {
continue
}
// Root scoped resource: use its parent scope as its parent
parentId = cfg.AzureResourceID.ParentScope()
} else if parentId.Parent() == nil {
// The cfg reosurce is the RP 1st level resource, we regard its parent scope as its parent
parentId = cfg.AzureResourceID.ParentScope()
}
// Adding the direct parent resource as its dependency
for _, ocfg := range cfgs {
if cfg.AzureResourceID.Equal(ocfg.AzureResourceID) {
continue
}
if parentId.Equal(ocfg.AzureResourceID) {
id := ocfg.AzureResourceID.String()
cfg.DependsOn = []Dependency{{Candidates: []string{id}}}
cfgs[i] = cfg
break
}
}
}
}
func (cfgs ConfigInfos) addReferenceDependency() error {
// TF resource id to Azure resource ids.
// Typically, one TF resource id maps to one Azure resource id. However, there are cases that one one TF resource id maps to multiple Azure resource ids.
// E.g. A parent and child resources have the same TF id. Or the association resource's TF id is the same as the master resource's.
m := map[string][]armid.ResourceId{}
for _, cfg := range cfgs {
m[cfg.TFResourceId] = append(m[cfg.TFResourceId], cfg.AzureResourceID)
}
for i, cfg := range cfgs {
file, err := hclsyntax.ParseConfig(cfg.hcl.Bytes(), "main.tf", hcl.InitialPos)
if err != nil {
return fmt.Errorf("parsing hcl for %s: %v", cfg.AzureResourceID, err)
}
hclsyntax.VisitAll(file.Body.(*hclsyntax.Body), func(node hclsyntax.Node) hcl.Diagnostics {
expr, ok := node.(*hclsyntax.LiteralValueExpr)
if !ok {
return nil
}
val := expr.Val
if !expr.Val.IsKnown() || !val.Type().Equals(cty.String) {
return nil
}
maybeTFId := val.AsString()
// This is safe to match case sensitively given the TF id are consistent across the provider. Otherwise, it is a provider bug.
dependingResourceIds, ok := m[maybeTFId]
if !ok {
return nil
}
var dependingResourceIdsWithoutSelf []string
for _, id := range dependingResourceIds[:] {
if id.String() == cfg.AzureResourceID.String() {
continue
}
// if cfg is parent of `id` resource, we should skip, or it will cause circular dependency, so skip parent depends on sub resources
if cfg.AzureResourceID.Equal(id.Parent()) {
continue
}
dependingResourceIdsWithoutSelf = append(dependingResourceIdsWithoutSelf, id.String())
}
if len(dependingResourceIdsWithoutSelf) != 0 {
cfg.DependsOn = append(cfg.DependsOn, Dependency{Candidates: dependingResourceIdsWithoutSelf})
}
return nil
})
cfgs[i] = cfg
}
return nil
}