diff --git a/internal/meta/append_lifecycle.go b/internal/meta/append_lifecycle.go new file mode 100644 index 0000000..e28fb5a --- /dev/null +++ b/internal/meta/append_lifecycle.go @@ -0,0 +1,34 @@ +package meta + +import ( + "fmt" + "strings" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclwrite" +) + +func hclBlockAppendLifecycle(body *hclwrite.Body, ignoreChanges []string) error { + srcs := map[string][]byte{} + if len(ignoreChanges) > 0 { + for i := range ignoreChanges { + ignoreChanges[i] = ignoreChanges[i] + "," + } + srcs["ignore_changes"] = []byte("ignore_changes = [\n" + strings.Join(ignoreChanges, "\n") + "\n]\n") + } + + if len(srcs) == 0 { + return nil + } + + b := hclwrite.NewBlock("lifecycle", nil) + for name, src := range srcs { + expr, diags := hclwrite.ParseConfig(src, "f", hcl.InitialPos) + if diags.HasErrors() { + return fmt.Errorf(`building "lifecycle.%s" attribute: %s`, name, diags.Error()) + } + b.Body().SetAttributeRaw(name, expr.Body().GetAttribute(name).Expr().BuildTokens(nil)) + } + body.AppendBlock(b) + return nil +} diff --git a/internal/meta/base_meta.go b/internal/meta/base_meta.go index 6596721..a41d9fd 100644 --- a/internal/meta/base_meta.go +++ b/internal/meta/base_meta.go @@ -1092,11 +1092,10 @@ func (meta baseMeta) stateToConfig(ctx context.Context, list ImportList) (Config } out = append(out, ConfigInfo{ ImportItem: importedList[i], - hcl: f, - dependencies: Dependencies{ - refDeps: make(map[string]Dependency), - parentChildDeps: make(map[Dependency]bool), - ambiguousRefDeps: make(map[string][]Dependency), + HCL: f, + Dependencies: Dependencies{ + ByIdRef: make(map[string]Dependency), + ByIdRefAmbiguous: make(map[string][]Dependency), }, }) } @@ -1148,7 +1147,7 @@ func (meta baseMeta) lifecycleAddon(configs ConfigInfos) (ConfigInfos, error) { for i, cfg := range configs { switch cfg.TFAddr.Type { case "azurerm_application_insights_web_test": - if err := hclBlockAppendLifecycle(cfg.hcl.Body().Blocks()[0].Body(), []string{"tags"}); err != nil { + if err := hclBlockAppendLifecycle(cfg.HCL.Body().Blocks()[0].Body(), []string{"tags"}); err != nil { return nil, fmt.Errorf("azurerm_application_insights_web_test: %v", err) } } @@ -1158,12 +1157,12 @@ func (meta baseMeta) lifecycleAddon(configs ConfigInfos) (ConfigInfos, error) { } func (meta baseMeta) addDependency(configs ConfigInfos) (ConfigInfos, error) { - if err := configs.PopulateReferenceDependencies(); err != nil { + if err := configs.PopulateReferenceDeps(); err != nil { return nil, fmt.Errorf("populating reference dependencies: %v", err) } - configs.populateParentChildDependency() + configs.PopulateRelationDeps() - if err := configs.applyDependenciesToHclBlock(); err != nil { + if err := configs.ApplyDepsToHCL(); err != nil { return nil, fmt.Errorf("applying dependencies to HCL blocks: %v", err) } diff --git a/internal/meta/config_info.go b/internal/meta/config_info.go index fc74589..e482ec5 100644 --- a/internal/meta/config_info.go +++ b/internal/meta/config_info.go @@ -1,10 +1,17 @@ package meta import ( + "fmt" "io" + "sort" + "strings" "github.com/Azure/aztfexport/internal/tfaddr" + "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 @@ -12,29 +19,28 @@ type ConfigInfos []ConfigInfo type ConfigInfo struct { ImportItem - dependencies Dependencies + Dependencies Dependencies - hcl *hclwrite.File -} - -func (cfg ConfigInfo) DumpHCL(w io.Writer) (int, error) { - out := hclwrite.Format(cfg.hcl.Bytes()) - return w.Write(out) + HCL *hclwrite.File } type Dependencies struct { - // Dependencies inferred by scanning for resource id values, will be applied by substituting with TF address - // Key is TFResourceId - refDeps map[string]Dependency + // Dependencies inferred by scanning for resource id values + // The key is TFResourceId. + ByIdRef map[string]Dependency - // Similar to refDeps, but due to multiple Azure resources can map to a same TF resource id, we can't decide which Azure resource - // is depended on. Hence these will end up as comments inside "depends_on" block for the user to manually resolve. + // Similar to ByIdRef, but due to multiple Azure resources can map to a same TF resource id (being referenced), + // this is regarded as ambiguous references. // The key is TFResourceId. - ambiguousRefDeps map[string][]Dependency + ByIdRefAmbiguous map[string][]Dependency - // Dependencies inferred via Azure resource id parent lookup, and will be applied in the "depends_on" block. - // Especially, any dependency that is (transitively) present via refDepds will be filtered. - parentChildDeps map[Dependency]bool + // Dependencies inferred by resource group name reference. + // NOTE: This holds since the azurerm/azapi provider is guaranteed to work for a single subscription. + ByRgNameRef *Dependency + + // Dependencies inferred via Azure resource id parent lookup. + // At most one such dependency can exist. + ByRelation *Dependency } type Dependency struct { @@ -42,3 +48,267 @@ type Dependency struct { AzureResourceId string TFAddr tfaddr.TFAddr } + +func (cfg ConfigInfo) DumpHCL(w io.Writer) (int, error) { + out := hclwrite.Format(cfg.HCL.Bytes()) + return w.Write(out) +} + +func (cfg *ConfigInfo) applyRefDepsToHCL() { + var applyF func(*hclwrite.Body, map[string]Dependency, *Dependency) + applyF = func(body *hclwrite.Body, idDeps map[string]Dependency, rgDep *Dependency) { + // Apply the rg name reference + if rgDep != nil { + if _, ok := body.Attributes()["resource_group_name"]; ok { + body.SetAttributeTraversal("resource_group_name", hcl.Traversal{ + hcl.TraverseRoot{Name: rgDep.TFAddr.Type}, + hcl.TraverseAttr{Name: rgDep.TFAddr.Name}, + hcl.TraverseAttr{Name: "name"}, + }) + } + } + + // Apply the id reference + if len(idDeps) != 0 { + for name, attr := range body.Attributes() { + tokens := attr.Expr().BuildTokens(nil) + newTokens := hclwrite.Tokens{} + toApply := false + for i := 0; i < len(tokens); i++ { + refDep, refDepExists := idDeps[string(tokens[i].Bytes)] + // Parsing process guaranteed QuotedLit is surrounded by Opening and Closing quote + if tokens[i].Type == hclsyntax.TokenQuotedLit && refDepExists { + newTokens[len(newTokens)-1] = &hclwrite.Token{ + Type: hclsyntax.TokenIdent, + Bytes: fmt.Appendf(nil, "%s.id", refDep.TFAddr), + SpacesBefore: tokens[i-1].SpacesBefore, + } + toApply = true + i += 1 // Skip the next token as it was already processed + } else { + newTokens = append(newTokens, tokens[i]) + } + } + if toApply { + body.SetAttributeRaw(name, newTokens) + } + for _, nestedBlock := range body.Blocks() { + applyF(nestedBlock.Body(), idDeps, nil) + } + } + } + } + applyF(cfg.HCL.Body().Blocks()[0].Body(), cfg.Dependencies.ByIdRef, cfg.Dependencies.ByRgNameRef) +} + +func (cfg *ConfigInfo) applyExplicitDepsToHCL() error { + body := cfg.HCL.Body().Blocks()[0].Body() + + relationDep := cfg.Dependencies.ByRelation + if relationDep != nil { + // Skip this relation dependency if it's already covered by any of the other applied dependencies. + var appliedDepIds []string + for _, dep := range cfg.Dependencies.ByIdRef { + appliedDepIds = append(appliedDepIds, dep.AzureResourceId) + } + if dep := cfg.Dependencies.ByRgNameRef; dep != nil { + appliedDepIds = append(appliedDepIds, dep.AzureResourceId) + } + var covered bool + for _, id := range appliedDepIds { + if isParentOf(relationDep.AzureResourceId, id) { + covered = true + break + } + } + if covered { + relationDep = nil + } + } + + // There isn't a case that other applied dependencies will cover all the possible ambiguous dependencies. + // Whilst if in the future, there are more dependencies being added that can cover the ambiguous deps, + // we shall do the same skip check as above for the relation dependencies. + ambiguousDeps := cfg.Dependencies.ByIdRefAmbiguous + + if len(ambiguousDeps) == 0 && relationDep == nil { + return nil + } + + src := "depends_on = [\n" + if relationDep != nil { + src += relationDep.TFAddr.String() + ",\n" + } + if len(ambiguousDeps) > 0 { + ambiguousDepsComments := make([]string, 0, len(ambiguousDeps)) + for _, deps := range ambiguousDeps { + tfAddrs := make([]string, 0, len(deps)) + for _, dep := range deps { + tfAddrs = append(tfAddrs, dep.TFAddr.String()) + } + sort.Strings(tfAddrs) + ambiguousDepsComments = append(ambiguousDepsComments, fmt.Sprintf("# One of %s (can't auto-resolve as their ids are identical)", strings.Join(tfAddrs, ","))) + } + sort.Strings(ambiguousDepsComments) + src += strings.Join(ambiguousDepsComments, "\n") + "\n" + } + src += "]\n" + expr, diags := hclwrite.ParseConfig([]byte(src), "f", hcl.InitialPos) + if diags.HasErrors() { + return fmt.Errorf(`building "depends_on" attribute: %s`, diags.Error()) + } + body.SetAttributeRaw("depends_on", expr.Body().GetAttribute("depends_on").Expr().BuildTokens(nil)) + + return nil +} + +// Look at the Azure resource id and determine if parent dependency exist. +// For example, /subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foos/foo1 +// has a parent /subscriptions/123/resourceGroups/rg1, which is the resource group. +func (cfgs ConfigInfos) PopulateRelationDeps() { + for i, cfg := range cfgs { + parentId := cfg.AzureResourceID.Parent() + + // This resource is either a root scope or a RP id (which doesn't exist) + if parentId == nil { + continue + } + + // This resource is the first level resource under the (root) scope. + // E.g. + // - (root scoped) /subscriptions/sub1/foos/foo + // - (scoped) /subscriptions/sub1/providers/Microsoft.Foo/foos/foo1 + // Regard the parent scope as its parent. + if parentId.Parent() == nil { + parentId = cfg.AzureResourceID.ParentScope() + } + + // Adding the direct parent resource as its dependency + for _, ocfg := range cfgs { + if parentId.Equal(ocfg.AzureResourceID) { + cfg.Dependencies.ByRelation = &Dependency{ + TFResourceId: ocfg.TFResourceId, + AzureResourceId: ocfg.AzureResourceID.String(), + TFAddr: ocfg.TFAddr, + } + break + } + } + cfgs[i] = cfg + } +} + +// Scan the HCL files for references to other resources. There are two references will be detected: +// 1. Reference by (TF) resource id. This can be detected any where in the expression. +// Especially, a single TF resource id can map to multiple resources, in which case the dependencies is regarded as ambiguous. +// 2. Reference by resoruce group name. This only applies to the top level attribute named `resource_group_name`. +func (cfgs ConfigInfos) PopulateReferenceDeps() error { + // key: TFResourceId + allResMap := map[string][]*ConfigInfo{} + // key: resource group name + allRgMap := map[string]*ConfigInfo{} + for _, cfg := range cfgs { + allResMap[cfg.TFResourceId] = append(allResMap[cfg.TFResourceId], &cfg) + if id, ok := cfg.AzureResourceID.(*armid.ResourceGroup); ok && len(id.AttrTypes) == 0 { + allRgMap[id.Name] = &cfg + } + } + 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) + } + // Scan for the top level resource group name reference + if attr, ok := file.Body.(*hclsyntax.Body).Blocks[0].Body.Attributes["resource_group_name"]; ok { + if tplExpr, ok := attr.Expr.(*hclsyntax.TemplateExpr); ok && tplExpr.IsStringLiteral() { + val, _ := tplExpr.Value(nil) + rgName := val.AsString() + if rgCfg, ok := allRgMap[rgName]; ok { + // Ensure the referenced resource group is really the parent resource group of the current resource. + // This is to avoid the case that the referenced resource group is from another subscription. + // Since the resource group name is equal, we only need to further check its subscription id. + if isParentOf(rgCfg.AzureResourceID.String(), cfg.AzureResourceID.String()) { + cfg.Dependencies.ByRgNameRef = &Dependency{ + AzureResourceId: rgCfg.ImportItem.AzureResourceID.String(), + TFResourceId: rgCfg.ImportItem.TFResourceId, + TFAddr: rgCfg.ImportItem.TFAddr, + } + } + } + } + } + + // Scan for resource id reference + 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() + + // Try to look up this string attribute from the TF id map. If there is a match, we regard it as a valid TF resource id. + // This is safe to match case sensitively given the TF id are consistent across the provider. Otherwise, it is a provider bug. + dependingConfigsRaw, ok := allResMap[maybeTFId] + if !ok { + return nil + } + depTFResId := maybeTFId + + var dependingConfigs []*ConfigInfo + for _, depCfg := range dependingConfigsRaw[:] { + // Ignore the self dependency + if cfg.AzureResourceID.String() == depCfg.AzureResourceID.String() { + continue + } + // Ignore the dependency on the child resource (which will cause circular dependency) + if cfg.AzureResourceID.Equal(depCfg.AzureResourceID.Parent()) { + continue + } + dependingConfigs = append(dependingConfigs, depCfg) + } + + if len(dependingConfigs) == 1 { + cfg.Dependencies.ByIdRef[depTFResId] = Dependency{ + TFResourceId: depTFResId, + AzureResourceId: dependingConfigs[0].AzureResourceID.String(), + TFAddr: dependingConfigs[0].TFAddr, + } + } else if len(dependingConfigs) > 1 { + deps := make([]Dependency, 0, len(dependingConfigs)) + for _, depCfg := range dependingConfigs { + deps = append(deps, Dependency{ + TFResourceId: depTFResId, + AzureResourceId: depCfg.AzureResourceID.String(), + TFAddr: depCfg.TFAddr, + }) + } + cfg.Dependencies.ByIdRefAmbiguous[depTFResId] = deps + } + + return nil + }) + cfgs[i] = cfg + } + return nil +} + +func (configs ConfigInfos) ApplyDepsToHCL() error { + for i, cfg := range configs { + cfg.applyRefDepsToHCL() + if err := cfg.applyExplicitDepsToHCL(); err != nil { + return fmt.Errorf("applying explicit dependencies to %s: %w", cfg.TFResourceId, err) + } + configs[i] = cfg + } + return nil +} + +// isParentOf is a utility to tell whether the "pid" is a top level of the "id". +// Given both "pid" and "id" are Azure resource ids. +func isParentOf(pid, id string) bool { + return strings.HasPrefix(strings.ToLower(id), strings.ToLower(pid)) +} diff --git a/internal/meta/config_info_dependencies_utils_test.go b/internal/meta/config_info_dependencies_utils_test.go deleted file mode 100644 index ef1f35b..0000000 --- a/internal/meta/config_info_dependencies_utils_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package meta - -import ( - "fmt" - - "github.com/Azure/aztfexport/internal/tfaddr" - "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/hcl/v2/hclwrite" - "github.com/magodo/armid" -) - -func mustParseTFAddr(s string) tfaddr.TFAddr { - tfAddr, err := tfaddr.ParseTFResourceAddr(s) - if err != nil { - panic(fmt.Sprintf("failed to parse TF resource address %s: %v", s, err)) - } - return *tfAddr -} - -func configInfoWithDeps( - azureResourceIdStr string, - tFResourceId string, - tfAddr tfaddr.TFAddr, - hclStr string, - refDeps map[string]Dependency, - ambiguousDeps map[string][]Dependency, -) ConfigInfo { - azureResourceId, err := armid.ParseResourceId(string(azureResourceIdStr)) - if err != nil { - panic(fmt.Sprintf("failed to parse Azure resource ID %s: %v", azureResourceIdStr, err)) - } - hcl, diag := hclwrite.ParseConfig([]byte(hclStr), "main.tf", hcl.InitialPos) - if diag.HasErrors() { - panic(fmt.Sprintf("failed to parse HCL for Azure resource ID %s: %v", azureResourceIdStr, diag)) - } - return ConfigInfo{ - ImportItem: ImportItem{ - AzureResourceID: azureResourceId, - TFResourceId: tFResourceId, - TFAddr: tfAddr, - }, - dependencies: Dependencies{ - refDeps: refDeps, - parentChildDeps: make(map[Dependency]bool), - ambiguousRefDeps: ambiguousDeps, - }, - hcl: hcl, - } -} - -func configInfo( - azureResourceIdStr string, - tFResourceId string, - tfAddr tfaddr.TFAddr, - hclStr string, -) ConfigInfo { - return configInfoWithDeps( - azureResourceIdStr, - tFResourceId, - tfAddr, - hclStr, - make(map[string]Dependency), - make(map[string][]Dependency), - ) -} diff --git a/internal/meta/config_info_populate_parent_child_dependencies.go b/internal/meta/config_info_populate_parent_child_dependencies.go deleted file mode 100644 index 1815aa6..0000000 --- a/internal/meta/config_info_populate_parent_child_dependencies.go +++ /dev/null @@ -1,58 +0,0 @@ -package meta - -import ( - "strings" - - "github.com/magodo/armid" -) - -// Look at the resource id and determine if parent dependency exist. -// For example, /subscriptions/123/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/vm1 -// has a parent /subscriptions/123/resourceGroups/rg1, which is the resource group. -// Unless present as reference dependency (maybe transitively), this parent will be added as an explicit dependency -// (via depends_on meta arg). -func (cfgs ConfigInfos) populateParentChildDependency() { - 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 resource 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) && - // Only add parent dependency if it is not already (maybe transitively) a reference dependency. - !hasReferenceDepWithPrefix(cfg.dependencies.refDeps, ocfg.AzureResourceID) { - cfg.dependencies.parentChildDeps[Dependency{ - TFResourceId: ocfg.TFResourceId, - AzureResourceId: ocfg.AzureResourceID.String(), - TFAddr: ocfg.TFAddr, - }] = true - break - } - } - cfgs[i] = cfg - } -} - -func hasReferenceDepWithPrefix(refDeps map[string]Dependency, prefix armid.ResourceId) bool { - for _, dep := range refDeps { - if strings.HasPrefix(dep.AzureResourceId, prefix.String()) { - return true - } - } - return false -} diff --git a/internal/meta/config_info_populate_parent_child_dependencies_test.go b/internal/meta/config_info_populate_parent_child_dependencies_test.go deleted file mode 100644 index 51f39c4..0000000 --- a/internal/meta/config_info_populate_parent_child_dependencies_test.go +++ /dev/null @@ -1,216 +0,0 @@ -package meta - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestPopulateParentChildDependencies(t *testing.T) { - testCases := []struct { - name string - inputConfigs ConfigInfos - expectedParentChildDeps map[string]map[Dependency]bool // key: AzureResourceId - }{ - { - name: "no parent-child relationships", - inputConfigs: []ConfigInfo{ - configInfo( - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", - mustParseTFAddr("azurerm_foo_resource.res-0"), - ` -resource "azurerm_foo_resource" "res-0" { - name = "foo1" -} -`, - ), - configInfo( - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Bar/bar/bar1", - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Bar/bar/bar1", - mustParseTFAddr("azurerm_bar_resource.res-1"), - ` -resource "azurerm_bar_resource" "res-1" { - name = "bar1" -} -`, - ), - }, - expectedParentChildDeps: map[string]map[Dependency]bool{ - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1": {}, - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Bar/bar/bar1": {}, - }, - }, - { - name: "res-0 is a parent of res-1: expect explicit dep from res-1 to res-0", - inputConfigs: []ConfigInfo{ - configInfo( - "/subscriptions/123/resourceGroups/rg1", - "/subscriptions/123/resourceGroups/rg1", - mustParseTFAddr("azurerm_resource_group.res-0"), - ` -resource "azurerm_resource_group" "res-0" { - name = "rg1" - location = "West Europe" -} -`, - ), - configInfo( - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", - mustParseTFAddr("azurerm_foo_resource.res-1"), - ` -resource "azurerm_foo_resource" "res-1" { - name = "foo1" - resource_group_name = "rg1" -} -`, - ), - }, - expectedParentChildDeps: map[string]map[Dependency]bool{ - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1": { - { - TFAddr: mustParseTFAddr("azurerm_resource_group.res-0"), - AzureResourceId: "/subscriptions/123/resourceGroups/rg1", - TFResourceId: "/subscriptions/123/resourceGroups/rg1", - }: true, - }, - "/subscriptions/123/resourceGroups/rg1": {}, - }, - }, - { - name: "res-2 -> res-1 -> res-0 connected by reference dependency, res-2 is child of res-0: expect no explicit dep because it has been satisfied transitively by reference dep", - inputConfigs: []ConfigInfo{ - configInfo( - "/subscriptions/123/resourceGroups/rg1", - "/subscriptions/123/resourceGroups/rg1", - mustParseTFAddr("azurerm_resource_group.res-0"), - ` -resource "azurerm_resource_group" "res-0" { - name = "rg1" - location = "West Europe" -} -`, - ), - configInfoWithDeps( - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", - mustParseTFAddr("azurerm_foo_resource.res-1"), - ` -resource "azurerm_foo_resource" "res-1" { - name = "foo1" - resource_group_id = "/subscriptions/123/resourceGroups/rg1" -} -`, - map[string]Dependency{ - "/subscriptions/123/resourceGroups/rg1": { - TFAddr: mustParseTFAddr("azurerm_resource_group.res-0"), - AzureResourceId: "/subscriptions/123/resourceGroups/rg1", - TFResourceId: "/subscriptions/123/resourceGroups/rg1", - }, - }, - make(map[string][]Dependency), - ), - configInfoWithDeps( - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Bar/bar/bar1", - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Bar/bar/bar1", - mustParseTFAddr("azurerm_bar_resource.res-2"), - ` -resource "azurerm_bar_resource" "res-2" { - name = "bar1" - foo_id = "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1" -} -`, - map[string]Dependency{ - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Bar/bar/bar1": { - TFAddr: mustParseTFAddr("azurerm_resource_group.res-1"), - AzureResourceId: "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", - TFResourceId: "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", - }, - }, - make(map[string][]Dependency), - ), - }, - expectedParentChildDeps: map[string]map[Dependency]bool{ - "/subscriptions/123/resourceGroups/rg1": {}, - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1": {}, - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Bar/bar/bar1": {}, - }, - }, - { - name: "res-0 and res-1 are ambiguous (different azureResourceId, same tfResourceId), res-2 is child of res-0, res-2 has ambiguous refDep to res-0 and res-1: expect parentChildDep to be added to res-2", - inputConfigs: []ConfigInfo{ - configInfo( - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1/sub1/sub1", - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", - mustParseTFAddr("azurerm_foo_sub1_resource.res-0"), - ` -resource "azurerm_foo_sub1_resource" "res-0" { - name = "res0" - location = "West Europe" -} -`, - ), - configInfo( - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1/sub2/sub2", - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", - mustParseTFAddr("azurerm_foo_sub2_resource.res-1"), - ` -resource "azurerm_foo_sub2_resource" "res-1" { - name = "res1" - location = "West Europe" -} -`, - ), - configInfoWithDeps( - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1/sub1/sub1/deep1/deep1", - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1/sub1/sub1/deep1/deep1", - mustParseTFAddr("azurerm_foo_sub1_deep1_resource.res-2"), - ` -resource "azurerm_foo_sub1_deep1_resource" "res-2" { - name = "res2" - foo_id = "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1" -} -`, - map[string]Dependency{}, - map[string][]Dependency{ - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1": { - { - TFAddr: mustParseTFAddr("azurerm_foo_sub1_resource.res-0"), - AzureResourceId: "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1/sub1/sub1", - TFResourceId: "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", - }, - { - TFAddr: mustParseTFAddr("azurerm_foo_sub1_resource.res-1"), - AzureResourceId: "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1/sub2/sub2", - TFResourceId: "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", - }, - }, - }, - ), - }, - expectedParentChildDeps: map[string]map[Dependency]bool{ - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1/sub1/sub1": {}, - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1/sub2/sub2": {}, - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1/sub1/sub1/deep1/deep1": { - { - TFAddr: mustParseTFAddr("azurerm_foo_sub1_resource.res-0"), - AzureResourceId: "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1/sub1/sub1", - TFResourceId: "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", - }: true, - }, - }, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - testCase.inputConfigs.populateParentChildDependency() - for _, cfg := range testCase.inputConfigs { - azureResourceId := cfg.AzureResourceID.String() - expectedExplicitDeps := testCase.expectedParentChildDeps[azureResourceId] - assert.Equal(t, expectedExplicitDeps, cfg.dependencies.parentChildDeps, "parentChildDeps matches expectation, azureResourceId: %s", azureResourceId) - } - }) - } -} diff --git a/internal/meta/config_info_populate_reference_dependencies.go b/internal/meta/config_info_populate_reference_dependencies.go deleted file mode 100644 index 0065053..0000000 --- a/internal/meta/config_info_populate_reference_dependencies.go +++ /dev/null @@ -1,83 +0,0 @@ -package meta - -import ( - "fmt" - - "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/hcl/v2/hclsyntax" - "github.com/zclconf/go-cty/cty" -) - -// Scan the HCL files for references to other resources. -// For example the HCL attribute `key_vault_id = "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.KeyVault/vaults/vault1"` -// will yield a dependency to the TF resource with address `azurerm_key_vault.vault1`. -// Note that a single TF resource id can map to multiple resources -- in which case the dependencies will be categorised -// as ambiguous. -func (cfgs ConfigInfos) PopulateReferenceDependencies() error { - // key: TFResourceId - m := map[string][]*ConfigInfo{} - for _, cfg := range cfgs { - m[cfg.TFResourceId] = append(m[cfg.TFResourceId], &cfg) - } - - 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. - dependingConfigsRaw, ok := m[maybeTFId] - if !ok { - return nil - } - - depTFResId := maybeTFId - - var dependingConfigs []*ConfigInfo - for _, depCfg := range dependingConfigsRaw[:] { - // Ignore the self dependency - if cfg.AzureResourceID.String() == depCfg.AzureResourceID.String() { - continue - } - // Ignore the dependency on the child resource (which will cause circular dependency) - if cfg.AzureResourceID.Equal(depCfg.AzureResourceID.Parent()) { - continue - } - dependingConfigs = append(dependingConfigs, depCfg) - } - - if len(dependingConfigs) == 1 { - cfg.dependencies.refDeps[depTFResId] = Dependency{ - TFResourceId: depTFResId, - AzureResourceId: dependingConfigs[0].AzureResourceID.String(), - TFAddr: dependingConfigs[0].TFAddr, - } - } else if len(dependingConfigs) > 1 { - deps := make([]Dependency, 0, len(dependingConfigs)) - for _, depCfg := range dependingConfigs { - deps = append(deps, Dependency{ - TFResourceId: depTFResId, - AzureResourceId: depCfg.AzureResourceID.String(), - TFAddr: depCfg.TFAddr, - }) - } - cfg.dependencies.ambiguousRefDeps[depTFResId] = deps - } - - return nil - }) - cfgs[i] = cfg - } - return nil -} diff --git a/internal/meta/config_info_populate_reference_dependencies_test.go b/internal/meta/config_info_populate_reference_dependencies_test.go deleted file mode 100644 index a3dcf58..0000000 --- a/internal/meta/config_info_populate_reference_dependencies_test.go +++ /dev/null @@ -1,166 +0,0 @@ -package meta - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestPopulateReferenceDependencies(t *testing.T) { - testCases := []struct { - name string - inputConfigs ConfigInfos - expectedReferenceDeps map[string]map[string]Dependency // key: AzureResourceId, inner key: TFResourceId - expectedAmbiguousDeps map[string]map[string][]Dependency // key: AzureResourceId, inner key: TFResourceId - }{ - { - name: "no dependencies between resources", - inputConfigs: []ConfigInfo{ - configInfo( - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", - mustParseTFAddr("azurerm_foo_resource.res-0"), - ` -resource "azurerm_foo_resource" "res-0" { - name = "foo1" -} -`, - ), - configInfo( - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Bar/bar/bar1", - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Bar/bar/bar1", - mustParseTFAddr("azurerm_bar_resource.res-1"), - ` -resource "azurerm_bar_resource" "res-1" { - name = "bar1" -} -`, - ), - }, - expectedReferenceDeps: map[string]map[string]Dependency{ - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1": {}, - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Bar/bar/bar1": {}, - }, - expectedAmbiguousDeps: map[string]map[string][]Dependency{ - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1": {}, - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Bar/bar/bar1": {}, - }, - }, - { - name: "res-0 is a resource group, rites-1 refer to it by id: expect reference dep from res-1 to res-0", - inputConfigs: []ConfigInfo{ - configInfo( - "/subscriptions/123/resourceGroups/rg1", - "/subscriptions/123/resourceGroups/rg1", - mustParseTFAddr("azurerm_resource_group.res-0"), - ` -resource "azurerm_resource_group" "res-0" { - name = "rg1" - location = "West Europe" -} -`, - ), - configInfo( - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", - mustParseTFAddr("azurerm_foo_resource.res-1"), - ` -resource "azurerm_foo_resource" "res-1" { - name = "foo1" - resource_group_id = "/subscriptions/123/resourceGroups/rg1" -} -`, - ), - }, - expectedReferenceDeps: map[string]map[string]Dependency{ - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1": { - "/subscriptions/123/resourceGroups/rg1": { - TFAddr: mustParseTFAddr("azurerm_resource_group.res-0"), - AzureResourceId: "/subscriptions/123/resourceGroups/rg1", - TFResourceId: "/subscriptions/123/resourceGroups/rg1", - }, - }, - "/subscriptions/123/resourceGroups/rg1": make(map[string]Dependency), - }, - expectedAmbiguousDeps: map[string]map[string][]Dependency{ - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1": {}, - "/subscriptions/123/resourceGroups/rg1": {}, - }, - }, - { - name: "res-0 and res-1 have different azure resource id, but same TF resource id, res-2 refer to this TF resource id: expect res-2 to have ambiguous dep to the TF resource id", - inputConfigs: []ConfigInfo{ - configInfo( - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1/sub1/sub1", - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", - mustParseTFAddr("azurerm_foo_resource.res-0"), - ` -resource "azurerm_foo_sub1_resource" "res-0" { - name = "foo1_sub1" -} -`, - ), - configInfo( - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1/sub2/sub2", - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", - mustParseTFAddr("azurerm_foo_resource.res-1"), - ` -resource "azurerm_foo_sub2_resource" "res-1" { - name = "foo1_sub2" -} -`, - ), - configInfo( - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Bar/bar/bar1", - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Bar/bar/bar1", - mustParseTFAddr("azurerm_bar_resource.res-2"), - ` -resource "azurerm_bar_resource" "res-2" { - name = "bar1" - foo_resource_id = "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1" -} -`, - ), - }, - expectedReferenceDeps: map[string]map[string]Dependency{ - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1/sub1/sub1": {}, - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1/sub2/sub2": {}, - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Bar/bar/bar1": {}, - }, - expectedAmbiguousDeps: map[string]map[string][]Dependency{ - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1/sub1/sub1": {}, - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1/sub2/sub2": {}, - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Bar/bar/bar1": { - "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1": { - { - TFAddr: mustParseTFAddr("azurerm_foo_resource.res-0"), - AzureResourceId: "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1/sub1/sub1", - TFResourceId: "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", - }, - { - TFAddr: mustParseTFAddr("azurerm_foo_resource.res-1"), - AzureResourceId: "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1/sub2/sub2", - TFResourceId: "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", - }, - }, - }, - }, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - err := testCase.inputConfigs.PopulateReferenceDependencies() - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - for _, cfg := range testCase.inputConfigs { - expectedRefDeps := testCase.expectedReferenceDeps[cfg.AzureResourceID.String()] - assert.Equal(t, expectedRefDeps, cfg.dependencies.refDeps, "referenceDeps matches expectation, azureResourceId: %s", cfg.AzureResourceID.String()) - expectedAmbiguousRefDeps := testCase.expectedAmbiguousDeps[cfg.AzureResourceID.String()] - assert.Equal(t, expectedAmbiguousRefDeps, cfg.dependencies.ambiguousRefDeps, "ambiguousDeps matches expectation, azureResourceId: %s", cfg.AzureResourceID.String()) - } - }) - } -} diff --git a/internal/meta/config_info_test.go b/internal/meta/config_info_test.go new file mode 100644 index 0000000..6cfec24 --- /dev/null +++ b/internal/meta/config_info_test.go @@ -0,0 +1,737 @@ +package meta + +import ( + "fmt" + "testing" + + "github.com/Azure/aztfexport/internal/tfaddr" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclwrite" + "github.com/stretchr/testify/assert" +) + +func newConfigInfo(azid, tfid, tfaddr, hcl string, deps *Dependencies) ConfigInfo { + cinfo := ConfigInfo{ + ImportItem: ImportItem{ + AzureResourceID: mustParseResourceId(azid), + TFResourceId: tfid, + TFAddr: mustParseTFAddr(tfaddr), + }, + HCL: mustHclWriteParse(hcl), + Dependencies: Dependencies{ + ByIdRef: make(map[string]Dependency), + ByIdRefAmbiguous: make(map[string][]Dependency), + }, + } + if deps != nil { + cinfo.Dependencies = *deps + } + return cinfo +} + +func TestConfigInfos_PopulateReferenceDeps(t *testing.T) { + testCases := []struct { + name string + inputConfigs ConfigInfos + expectedRgNameReferenceDeps map[string]*Dependency // key: AzureResourceId + expectedIdReferenceDeps map[string]map[string]Dependency // key: AzureResourceId, inner key: TFResourceId + expectedIdReferenceAmbiguousDeps map[string]map[string][]Dependency // key: AzureResourceId, inner key: TFResourceId + }{ + { + name: "no dependencies between resources", + inputConfigs: []ConfigInfo{ + newConfigInfo( + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", + "azurerm_foo_resource.res-0", + ` +resource "azurerm_foo_resource" "res-0" { + name = "foo1" +} +`, + nil, + ), + newConfigInfo( + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Bar/bar/bar1", + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Bar/bar/bar1", + "azurerm_bar_resource.res-1", + ` +resource "azurerm_bar_resource" "res-1" { + name = "bar1" +} +`, + nil, + ), + }, + expectedIdReferenceDeps: map[string]map[string]Dependency{ + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1": {}, + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Bar/bar/bar1": {}, + }, + expectedIdReferenceAmbiguousDeps: map[string]map[string][]Dependency{ + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1": {}, + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Bar/bar/bar1": {}, + }, + }, + { + name: "res-1 reference res-2 by id", + inputConfigs: []ConfigInfo{ + newConfigInfo( + "/subscriptions/123/resourceGroups/rg1", + "/subscriptions/123/resourceGroups/rg1", + "azurerm_resource_group.res-0", + ` +resource "azurerm_resource_group" "res-0" { + name = "rg1" + location = "West Europe" +} +`, + nil, + ), + newConfigInfo( + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", + "azurerm_foo_resource.res-1", + ` +resource "azurerm_foo_resource" "res-1" { + name = "foo1" + resource_group_id = "/subscriptions/123/resourceGroups/rg1" +} +`, + nil, + ), + }, + expectedIdReferenceDeps: map[string]map[string]Dependency{ + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1": { + "/subscriptions/123/resourceGroups/rg1": { + TFAddr: mustParseTFAddr("azurerm_resource_group.res-0"), + AzureResourceId: "/subscriptions/123/resourceGroups/rg1", + TFResourceId: "/subscriptions/123/resourceGroups/rg1", + }, + }, + "/subscriptions/123/resourceGroups/rg1": make(map[string]Dependency), + }, + expectedIdReferenceAmbiguousDeps: map[string]map[string][]Dependency{ + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1": {}, + "/subscriptions/123/resourceGroups/rg1": {}, + }, + }, + { + name: "res-0 and res-1 have different azure resource id, but same TF resource id, res-2 refer to this TF resource id: expect res-2 to have ambiguous id dep to the TF resource id", + inputConfigs: []ConfigInfo{ + newConfigInfo( + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1/sub1/sub1", + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", + "azurerm_foo_resource.res-0", + `resource "azurerm_foo_sub1_resource" "res-0" { + name = "foo1_sub1" +}`, + nil, + ), + newConfigInfo( + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1/sub2/sub2", + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", + "azurerm_foo_resource.res-1", + ` +resource "azurerm_foo_sub2_resource" "res-1" { + name = "foo1_sub2" +} +`, + nil, + ), + newConfigInfo( + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Bar/bar/bar1", + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Bar/bar/bar1", + "azurerm_bar_resource.res-2", + ` +resource "azurerm_bar_resource" "res-2" { + name = "bar1" + foo_resource_id = "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1" +} +`, + nil, + ), + }, + expectedIdReferenceDeps: map[string]map[string]Dependency{ + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1/sub1/sub1": {}, + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1/sub2/sub2": {}, + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Bar/bar/bar1": {}, + }, + expectedIdReferenceAmbiguousDeps: map[string]map[string][]Dependency{ + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1/sub1/sub1": {}, + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1/sub2/sub2": {}, + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Bar/bar/bar1": { + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1": { + { + TFAddr: mustParseTFAddr("azurerm_foo_resource.res-0"), + AzureResourceId: "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1/sub1/sub1", + TFResourceId: "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", + }, + { + TFAddr: mustParseTFAddr("azurerm_foo_resource.res-1"), + AzureResourceId: "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1/sub2/sub2", + TFResourceId: "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", + }, + }, + }, + }, + }, + { + name: "res-1 reference resource group by `resource_group_name`", + inputConfigs: []ConfigInfo{ + newConfigInfo( + "/subscriptions/123/resourceGroups/rg1", + "/subscriptions/123/resourceGroups/rg1", + "azurerm_resource_group.res-0", + ` +resource "azurerm_resource_group" "res-0" { + name = "rg1" + location = "West Europe" +} +`, + nil, + ), + newConfigInfo( + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", + "azurerm_foo_resource.res-1", + ` +resource "azurerm_foo_resource" "res-1" { + name = "foo1" + resource_group_name = "rg1" +} +`, + nil, + ), + }, + expectedRgNameReferenceDeps: map[string]*Dependency{ + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1": { + AzureResourceId: "/subscriptions/123/resourceGroups/rg1", + TFResourceId: "/subscriptions/123/resourceGroups/rg1", + TFAddr: mustParseTFAddr("azurerm_resource_group.res-0"), + }, + "/subscriptions/123/resourceGroups/rg1": nil, + }, + expectedIdReferenceDeps: map[string]map[string]Dependency{ + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1": {}, + "/subscriptions/123/resourceGroups/rg1": {}, + }, + expectedIdReferenceAmbiguousDeps: map[string]map[string][]Dependency{ + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1": {}, + "/subscriptions/123/resourceGroups/rg1": {}, + }, + }, + { + name: "res-1 reference resource group by `resource_group_name`, but in a different subscription", + inputConfigs: []ConfigInfo{ + newConfigInfo( + "/subscriptions/123/resourceGroups/rg1", + "/subscriptions/123/resourceGroups/rg1", + "azurerm_resource_group.res-0", + ` +resource "azurerm_resource_group" "res-0" { + name = "rg1" + location = "West Europe" +} +`, + nil, + ), + newConfigInfo( + "/subscriptions/456/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", + "/subscriptions/456/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", + "azurerm_foo_resource.res-1", + ` +resource "azurerm_foo_resource" "res-1" { + name = "foo1" + resource_group_name = "rg1" +} +`, + nil, + ), + }, + expectedRgNameReferenceDeps: map[string]*Dependency{ + "/subscriptions/456/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1": nil, + "/subscriptions/123/resourceGroups/rg1": nil, + }, + expectedIdReferenceDeps: map[string]map[string]Dependency{ + "/subscriptions/456/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1": {}, + "/subscriptions/123/resourceGroups/rg1": {}, + }, + expectedIdReferenceAmbiguousDeps: map[string]map[string][]Dependency{ + "/subscriptions/456/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1": {}, + "/subscriptions/123/resourceGroups/rg1": {}, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + err := testCase.inputConfigs.PopulateReferenceDeps() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + for _, cfg := range testCase.inputConfigs { + expectedIdRefDeps := testCase.expectedIdReferenceDeps[cfg.AzureResourceID.String()] + assert.Equal(t, expectedIdRefDeps, cfg.Dependencies.ByIdRef, "idReferenceDeps matches expectation, azureResourceId: %s", cfg.AzureResourceID.String()) + + expectedAmbiguousIdRefDeps := testCase.expectedIdReferenceAmbiguousDeps[cfg.AzureResourceID.String()] + assert.Equal(t, expectedAmbiguousIdRefDeps, cfg.Dependencies.ByIdRefAmbiguous, "ambiguousIdReferenceDeps matches expectation, azureResourceId: %s", cfg.AzureResourceID.String()) + + expectedRgNameRefDeps := testCase.expectedRgNameReferenceDeps[cfg.AzureResourceID.String()] + assert.Equal(t, expectedRgNameRefDeps, cfg.Dependencies.ByRgNameRef, "nameReferenceDeps matches expectation, azureResourceId: %s", cfg.AzureResourceID.String()) + } + }) + } +} + +func TestConfigInfos_PopulateRelationDeps(t *testing.T) { + testCases := []struct { + name string + inputConfigs ConfigInfos + expectedRelationDep map[string]*Dependency // key: AzureResourceId + }{ + { + name: "no parent-child relationships", + inputConfigs: []ConfigInfo{ + newConfigInfo( + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", + "azurerm_foo_resource.res-0", + ` +resource "azurerm_foo_resource" "res-0" { + name = "foo1" +} +`, + nil, + ), + newConfigInfo( + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Bar/bar/bar1", + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Bar/bar/bar1", + "azurerm_bar_resource.res-1", + ` +resource "azurerm_bar_resource" "res-1" { + name = "bar1" +} +`, + nil, + ), + }, + expectedRelationDep: map[string]*Dependency{ + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1": nil, + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Bar/bar/bar1": nil, + }, + }, + { + name: "res-0 is a parent of res-1: expect explicit dep from res-1 to res-0", + inputConfigs: []ConfigInfo{ + newConfigInfo( + "/subscriptions/123/resourceGroups/rg1", + "/subscriptions/123/resourceGroups/rg1", + "azurerm_resource_group.res-0", + ` +resource "azurerm_resource_group" "res-0" { + name = "rg1" + location = "West Europe" +} +`, + nil, + ), + newConfigInfo( + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1", + "azurerm_foo_resource.res-1", + ` +resource "azurerm_foo_resource" "res-1" { + name = "foo1" + resource_group_name = "rg1" +} +`, + nil, + ), + }, + expectedRelationDep: map[string]*Dependency{ + "/subscriptions/123/resourceGroups/rg1/providers/Microsoft.Foo/foo/foo1": { + TFAddr: mustParseTFAddr("azurerm_resource_group.res-0"), + AzureResourceId: "/subscriptions/123/resourceGroups/rg1", + TFResourceId: "/subscriptions/123/resourceGroups/rg1", + }, + "/subscriptions/123/resourceGroups/rg1": nil, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + testCase.inputConfigs.PopulateRelationDeps() + for _, cfg := range testCase.inputConfigs { + azureResourceId := cfg.AzureResourceID.String() + expectedExplicitDeps := testCase.expectedRelationDep[azureResourceId] + assert.Equal(t, expectedExplicitDeps, cfg.Dependencies.ByRelation, "parentChildDeps matches expectation, azureResourceId: %s", azureResourceId) + } + }) + } +} + +func TestConfigInfo_ApplyReferenceDepsToHCL(t *testing.T) { + testCases := []struct { + name string + inputHcl string + depsByIdRef map[string]Dependency // key: TFResourceId + depsByRgNameRef *Dependency + expectedHcl string + }{ + { + name: "no reference dependencies", + inputHcl: ` +resource azurerm_resource_foo self { + name = "test" + resource_group_name = "test" +} +`, + depsByIdRef: make(map[string]Dependency), + expectedHcl: ` +resource azurerm_resource_foo self { + name = "test" + resource_group_name = "test" +} +`, + }, + { + name: "single reference dependency in top level attribute", + inputHcl: ` +resource azurerm_resource_bar self { + name = "test" + foo_id = "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123" +} +`, + depsByIdRef: map[string]Dependency{ + "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123": { + TFResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123", + AzureResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123", + TFAddr: mustParseTFAddr("azurerm_foo_resource.res-1"), + }, + }, + expectedHcl: ` +resource azurerm_resource_bar self { + name = "test" + foo_id = azurerm_foo_resource.res-1.id +} +`, + }, + { + name: "single rg name dependency in top level attribute", + inputHcl: ` +resource azurerm_resource_foo self { + name = "test" + resource_group_name = "rg1" +} +`, + depsByRgNameRef: &Dependency{ + TFResourceId: "/subscriptions/123/resourceGroups/rg1", + AzureResourceId: "/subscriptions/123/resourceGroups/rg1", + TFAddr: mustParseTFAddr("azurerm_resource_group.res-0"), + }, + expectedHcl: ` +resource azurerm_resource_foo self { + name = "test" + resource_group_name =azurerm_resource_group.res-0.name +} +`, + }, + { + name: "Mixed rg name dependency and id dependencies", + inputHcl: ` +resource azurerm_resource_foo self { + name = "test" + resource_group_name = "rg1" + bar_id = "/subscriptions/123/providers/Microsoft.Bar/bars/bar1" +} +`, + depsByIdRef: map[string]Dependency{ + "/subscriptions/123/providers/Microsoft.Bar/bars/bar1": { + TFResourceId: "/subscriptions/123/providers/Microsoft.Bar/bars/bar1", + AzureResourceId: "/subscriptions/123/providers/Microsoft.Bar/bars/bar1", + TFAddr: mustParseTFAddr("azurerm_resource_bar.res-0"), + }, + }, + depsByRgNameRef: &Dependency{ + TFResourceId: "/subscriptions/123/resourceGroups/rg1", + AzureResourceId: "/subscriptions/123/resourceGroups/rg1", + TFAddr: mustParseTFAddr("azurerm_resource_group.res-0"), + }, + expectedHcl: ` +resource azurerm_resource_foo self { + name = "test" + resource_group_name =azurerm_resource_group.res-0.name + bar_id = azurerm_resource_bar.res-0.id +} +`, + }, + { + name: "multiple reference dependency in top level and nested block", + inputHcl: ` +resource azurerm_resource_foo self { + name = "test" + foo_id = "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123" + + some_block { + bar_id = "/subscriptions/123/resourceGroups/123/providers/Microsoft.Bar/bar/456" + } +} +`, + depsByIdRef: map[string]Dependency{ + "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123": { + TFAddr: mustParseTFAddr("azurerm_foo_resource.res-1"), + AzureResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123", + TFResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123", + }, + "/subscriptions/123/resourceGroups/123/providers/Microsoft.Bar/bar/456": { + TFAddr: mustParseTFAddr("azurerm_bar_resource.res-2"), + AzureResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Bar/bar/456", + TFResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Bar/bar/456", + }, + }, + expectedHcl: ` +resource azurerm_resource_foo self { + name = "test" + foo_id = azurerm_foo_resource.res-1.id + + some_block { + bar_id = azurerm_bar_resource.res-2.id + } +} +`, + }, + { + name: "multiple reference dependency in array and maps", + inputHcl: ` +resource azurerm_resource_foo self { + name = "test" + foo_ids = ["/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123"] + + bar_ids_map = { + bar_id = "/subscriptions/123/resourceGroups/123/providers/Microsoft.Bar/bar/456" + } +} +`, + depsByIdRef: map[string]Dependency{ + "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123": { + TFAddr: mustParseTFAddr("azurerm_foo_resource.res-1"), + AzureResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123", + TFResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123", + }, + "/subscriptions/123/resourceGroups/123/providers/Microsoft.Bar/bar/456": { + TFAddr: mustParseTFAddr("azurerm_bar_resource.res-2"), + AzureResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Bar/bar/456", + TFResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Bar/bar/456", + }, + }, + expectedHcl: ` +resource azurerm_resource_foo self { + name = "test" + foo_ids = [azurerm_foo_resource.res-1.id] + + bar_ids_map = { + bar_id = azurerm_bar_resource.res-2.id + } +} +`, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + cinfo := ConfigInfo{ + Dependencies: Dependencies{ + ByIdRef: testCase.depsByIdRef, + ByRgNameRef: testCase.depsByRgNameRef, + }, + HCL: mustHclWriteParse(testCase.inputHcl), + } + cinfo.applyRefDepsToHCL() + assert.Equal(t, testCase.expectedHcl, string(cinfo.HCL.BuildTokens(nil).Bytes())) + }) + } +} + +func TestConfigInfo_ApplyExplicitDepsToHCL(t *testing.T) { + testCases := []struct { + name string + inputHcl string + depsByByRelation *Dependency + depsByIdRefAmbiguous map[string][]Dependency // key: TFResourceId + depsByIdRef map[string]Dependency + depsByRgNameRef *Dependency + expectedHcl string + }{ + { + name: "no explicit or ambiguous dependencies", + inputHcl: ` +resource azurerm_resource_foo self { + name = "test" + foo_id = azurerm_foo_resource.res-1.id +} +`, + expectedHcl: ` +resource azurerm_resource_foo self { + name = "test" + foo_id = azurerm_foo_resource.res-1.id +} +`, + }, + { + name: "single parent child dependency", + inputHcl: ` +resource azurerm_resource_foo self { + name = "test" + resource_group_name = "test" +} +`, + depsByByRelation: &Dependency{ + TFAddr: mustParseTFAddr("azurerm_resource_group.res-0"), + AzureResourceId: "/subscriptions/123/resourceGroups/123", + TFResourceId: "/subscriptions/123/resourceGroups/123", + }, + expectedHcl: ` +resource azurerm_resource_foo self { + name = "test" + resource_group_name = "test" +depends_on= [ +azurerm_resource_group.res-0, +] +} +`, + }, + { + name: "single parent child dependency, but is covered by an id reference dependency", + inputHcl: ` +resource azurerm_resource_foo self { + name = "test" + resource_group_name = "test" +} +`, + depsByByRelation: &Dependency{ + TFAddr: mustParseTFAddr("azurerm_resource_group.res-0"), + AzureResourceId: "/subscriptions/123/resourceGroups/123", + TFResourceId: "/subscriptions/123/resourceGroups/123", + }, + depsByIdRef: map[string]Dependency{ + "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foos/foo1": { + TFAddr: mustParseTFAddr("azurerm_resource_foo.res-0"), + AzureResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foos/foo1", + TFResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foos/foo1", + }, + }, + expectedHcl: ` +resource azurerm_resource_foo self { + name = "test" + resource_group_name = "test" +} +`, + }, + { + name: "single parent child dependency, but is covered by a resource group name reference dependency", + inputHcl: ` +resource azurerm_resource_foo self { + name = "test" + resource_group_name = "test" +} +`, + depsByByRelation: &Dependency{ + TFAddr: mustParseTFAddr("azurerm_resource_group.res-0"), + AzureResourceId: "/subscriptions/123/resourceGroups/test", + TFResourceId: "/subscriptions/123/resourceGroups/test", + }, + depsByRgNameRef: &Dependency{ + TFAddr: mustParseTFAddr("azurerm_resource_group.res-0"), + AzureResourceId: "/subscriptions/123/resourceGroups/test", + TFResourceId: "/subscriptions/123/resourceGroups/test", + }, + expectedHcl: ` +resource azurerm_resource_foo self { + name = "test" + resource_group_name = "test" +} +`, + }, + { + name: "multiple ambiguous dependencies", + inputHcl: ` +resource azurerm_resource_foo self { + name = "test" + resource_group_name = "test" +} +`, + depsByIdRefAmbiguous: map[string][]Dependency{ + "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123": { + { + TFAddr: mustParseTFAddr("azurerm_foo_sub1_resource.res-1"), + AzureResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123/sub1/sub1", + TFResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123", + }, + { + TFAddr: mustParseTFAddr("azurerm_foo_sub2_resource.res-2"), + AzureResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123/sub2/sub2", + TFResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123", + }, + }, + "/subscriptions/123/resourceGroups/123/providers/Microsoft.Bar/bar/456": { + { + TFAddr: mustParseTFAddr("azurerm_bar_sub1_resource.res-3"), + AzureResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Bar/bar/456/sub1/sub1", + TFResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Bar/bar/456", + }, + { + TFAddr: mustParseTFAddr("azurerm_bar_sub2_resource.res-4"), + AzureResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Bar/bar/456/sub2/sub2", + TFResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Bar/bar/456", + }, + }, + }, + expectedHcl: ` +resource azurerm_resource_foo self { + name = "test" + resource_group_name = "test" +depends_on= [ +# One of azurerm_bar_sub1_resource.res-3,azurerm_bar_sub2_resource.res-4 (can't auto-resolve as their ids are identical) +# One of azurerm_foo_sub1_resource.res-1,azurerm_foo_sub2_resource.res-2 (can't auto-resolve as their ids are identical) +] +} +`, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + cinfo := ConfigInfo{ + Dependencies: Dependencies{ + ByRelation: testCase.depsByByRelation, + ByIdRef: testCase.depsByIdRef, + ByIdRefAmbiguous: testCase.depsByIdRefAmbiguous, + ByRgNameRef: testCase.depsByRgNameRef, + }, + HCL: mustHclWriteParse(testCase.inputHcl), + } + + assert.NoError(t, cinfo.applyExplicitDepsToHCL()) + actualHcl := string(cinfo.HCL.BuildTokens(nil).Bytes()) + assert.Equal(t, testCase.expectedHcl, actualHcl) + }) + } +} + +func mustHclWriteParse(input string) *hclwrite.File { + file, diag := hclwrite.ParseConfig([]byte(input), "input.hcl", hcl.InitialPos) + if diag.HasErrors() { + panic(fmt.Sprintf("failed to parse HCL: %v", diag)) + } + return file +} + +func mustParseTFAddr(s string) tfaddr.TFAddr { + tfAddr, err := tfaddr.ParseTFResourceAddr(s) + if err != nil { + panic(fmt.Sprintf("failed to parse TF resource address %s: %v", s, err)) + } + return *tfAddr +} diff --git a/internal/meta/hcl_edit.go b/internal/meta/hcl_edit.go deleted file mode 100644 index c1cfbfe..0000000 --- a/internal/meta/hcl_edit.go +++ /dev/null @@ -1,127 +0,0 @@ -package meta - -import ( - "fmt" - "sort" - "strings" - - "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/hcl/v2/hclsyntax" - "github.com/hashicorp/hcl/v2/hclwrite" -) - -func (configs ConfigInfos) applyDependenciesToHclBlock() error { - for i, cfg := range configs { - applyReferenceDependenciesToHcl(cfg.hcl.Body().Blocks()[0].Body(), cfg.dependencies.refDeps) - if err := applyParentChildAndAmbiguousDepsToHclBlock( - cfg.hcl.Body().Blocks()[0].Body(), - cfg.dependencies.parentChildDeps, - cfg.dependencies.ambiguousRefDeps); err != nil { - return fmt.Errorf("applying explicit and ambiguous dependencies to %s: %w", cfg.TFResourceId, err) - } - configs[i] = cfg - } - return nil -} - -func applyReferenceDependenciesToHcl(body *hclwrite.Body, refDeps map[string]Dependency) { - if len(refDeps) == 0 { - return - } - - for name, attr := range body.Attributes() { - tokens := attr.Expr().BuildTokens(nil) - newTokens := hclwrite.Tokens{} - tokensModified := false - - for i := 0; i < len(tokens); i++ { - refDep, refDepExists := refDeps[string(tokens[i].Bytes)] - // Parsing process guaranteed QuotedLit is surrounded by Opening and Closing quote - if tokens[i].Type == hclsyntax.TokenQuotedLit && refDepExists { - newTokens[len(newTokens)-1] = &hclwrite.Token{ - Type: hclsyntax.TokenIdent, - Bytes: fmt.Appendf(nil, "%s.id", refDep.TFAddr), - SpacesBefore: tokens[i-1].SpacesBefore, - } - tokensModified = true - i += 1 // Skip the next token as it was already processed - } else { - newTokens = append(newTokens, tokens[i]) - } - } - - if tokensModified { - body.SetAttributeRaw(name, newTokens) - } - - for _, nestedBlock := range body.Blocks() { - applyReferenceDependenciesToHcl(nestedBlock.Body(), refDeps) - } - } -} - -func applyParentChildAndAmbiguousDepsToHclBlock( - body *hclwrite.Body, - parentChildDeps map[Dependency]bool, - ambiguousDeps map[string][]Dependency, -) error { - if len(parentChildDeps)+len(ambiguousDeps) == 0 { - return nil - } - - src := "depends_on = [\n" - if len(parentChildDeps) > 0 { - tfAddrs := make([]string, 0, len(parentChildDeps)) - for dep := range parentChildDeps { - tfAddrs = append(tfAddrs, dep.TFAddr.String()) - } - sort.Strings(tfAddrs) - src += strings.Join(tfAddrs, ",\n") + "\n" - } - if len(ambiguousDeps) > 0 { - ambiguousDepsComments := make([]string, 0, len(ambiguousDeps)) - for _, deps := range ambiguousDeps { - tfAddrs := make([]string, 0, len(deps)) - for _, dep := range deps { - tfAddrs = append(tfAddrs, dep.TFAddr.String()) - } - sort.Strings(tfAddrs) - ambiguousDepsComments = append(ambiguousDepsComments, fmt.Sprintf("# One of %s (can't auto-resolve as their ids are identical)", strings.Join(tfAddrs, ","))) - } - sort.Strings(ambiguousDepsComments) - src += strings.Join(ambiguousDepsComments, "\n") + "\n" - } - src += "]\n" - expr, diags := hclwrite.ParseConfig([]byte(src), "f", hcl.InitialPos) - if diags.HasErrors() { - return fmt.Errorf(`building "depends_on" attribute: %s`, diags.Error()) - } - body.SetAttributeRaw("depends_on", expr.Body().GetAttribute("depends_on").Expr().BuildTokens(nil)) - - return nil -} - -func hclBlockAppendLifecycle(body *hclwrite.Body, ignoreChanges []string) error { - srcs := map[string][]byte{} - if len(ignoreChanges) > 0 { - for i := range ignoreChanges { - ignoreChanges[i] = ignoreChanges[i] + "," - } - srcs["ignore_changes"] = []byte("ignore_changes = [\n" + strings.Join(ignoreChanges, "\n") + "\n]\n") - } - - if len(srcs) == 0 { - return nil - } - - b := hclwrite.NewBlock("lifecycle", nil) - for name, src := range srcs { - expr, diags := hclwrite.ParseConfig(src, "f", hcl.InitialPos) - if diags.HasErrors() { - return fmt.Errorf(`building "lifecycle.%s" attribute: %s`, name, diags.Error()) - } - b.Body().SetAttributeRaw(name, expr.Body().GetAttribute(name).Expr().BuildTokens(nil)) - } - body.AppendBlock(b) - return nil -} diff --git a/internal/meta/hcl_edit_test.go b/internal/meta/hcl_edit_test.go deleted file mode 100644 index 7fd448f..0000000 --- a/internal/meta/hcl_edit_test.go +++ /dev/null @@ -1,259 +0,0 @@ -package meta - -import ( - "fmt" - "testing" - - "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/hcl/v2/hclwrite" - "github.com/stretchr/testify/assert" -) - -func TestApplyReferenceDependenciesToHcl(t *testing.T) { - testCases := []struct { - name string - inputHcl string - refDeps map[string]Dependency // key: TFResourceId - expectedHcl string - }{ - { - name: "no reference dependencies", - inputHcl: ` - name = "test" - resource_group_name = "test" -`, - refDeps: make(map[string]Dependency), - expectedHcl: ` - name = "test" - resource_group_name = "test" -`, - }, - { - name: "single reference dependency in top level attribute", - inputHcl: ` - name = "test" - foo_id = "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123" -`, - refDeps: map[string]Dependency{ - "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123": { - TFResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123", - AzureResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123", - TFAddr: mustParseTFAddr("azurerm_foo_resource.res-1"), - }, - }, - expectedHcl: ` - name = "test" - foo_id = azurerm_foo_resource.res-1.id -`, - }, - { - name: "multiple reference dependency in top level and nested block", - inputHcl: ` - name = "test" - foo_id = "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123" - - some_block { - bar_id = "/subscriptions/123/resourceGroups/123/providers/Microsoft.Bar/bar/456" - } -`, - refDeps: map[string]Dependency{ - "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123": { - TFAddr: mustParseTFAddr("azurerm_foo_resource.res-1"), - AzureResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123", - TFResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123", - }, - "/subscriptions/123/resourceGroups/123/providers/Microsoft.Bar/bar/456": { - TFAddr: mustParseTFAddr("azurerm_bar_resource.res-2"), - AzureResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Bar/bar/456", - TFResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Bar/bar/456", - }, - }, - expectedHcl: ` - name = "test" - foo_id = azurerm_foo_resource.res-1.id - - some_block { - bar_id = azurerm_bar_resource.res-2.id - } -`, - }, - { - name: "multiple reference dependency in array and maps", - inputHcl: ` - name = "test" - foo_ids = ["/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123"] - - bar_ids_map = { - bar_id = "/subscriptions/123/resourceGroups/123/providers/Microsoft.Bar/bar/456" - } -`, - refDeps: map[string]Dependency{ - "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123": { - TFAddr: mustParseTFAddr("azurerm_foo_resource.res-1"), - AzureResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123", - TFResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123", - }, - "/subscriptions/123/resourceGroups/123/providers/Microsoft.Bar/bar/456": { - TFAddr: mustParseTFAddr("azurerm_bar_resource.res-2"), - AzureResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Bar/bar/456", - TFResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Bar/bar/456", - }, - }, - expectedHcl: ` - name = "test" - foo_ids = [azurerm_foo_resource.res-1.id] - - bar_ids_map = { - bar_id = azurerm_bar_resource.res-2.id - } -`, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - body := mustHclWriteParse(testCase.inputHcl) - applyReferenceDependenciesToHcl(body, testCase.refDeps) - assert.Equal(t, testCase.expectedHcl, string(body.BuildTokens(nil).Bytes())) - }) - } -} - -func TestApplyParentChildAndAmbiguousDepsToHclBlock(t *testing.T) { - testCases := []struct { - name string - inputHcl string - parentChildDeps map[Dependency]bool - ambiguousDeps map[string][]Dependency // key: TFResourceId - expectedHcl string - }{ - { - name: "no explicit or ambiguous dependencies", - inputHcl: ` - name = "test" - foo_id = azurerm_foo_resource.res-1.id -`, - parentChildDeps: make(map[Dependency]bool), - ambiguousDeps: make(map[string][]Dependency), - expectedHcl: ` - name = "test" - foo_id = azurerm_foo_resource.res-1.id -`, - }, - { - name: "single parent child dependency", - inputHcl: ` - name = "test" - resource_group_name = "test" -`, - parentChildDeps: map[Dependency]bool{ - { - TFAddr: mustParseTFAddr("azurerm_resource_group.res-0"), - AzureResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.ResourceGroup/resourceGroup/123", - TFResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.ResourceGroup/resourceGroup/123", - }: true, - }, - ambiguousDeps: make(map[string][]Dependency), - expectedHcl: ` - name = "test" - resource_group_name = "test" -depends_on= [ -azurerm_resource_group.res-0 -] -`, - }, - { - name: "multiple parent child dependencies", - inputHcl: ` - name = "test" - resource_group_name = "test" -`, - parentChildDeps: map[Dependency]bool{ - { - TFAddr: mustParseTFAddr("azurerm_resource_group.res-0"), - AzureResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.ResourceGroup/resourceGroup/123", - TFResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.ResourceGroup/resourceGroup/123", - }: true, - { - TFAddr: mustParseTFAddr("azurerm_resource_group.res-1"), - AzureResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.ResourceGroup/resourceGroup/124", - TFResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.ResourceGroup/resourceGroup/124", - }: true, - }, - ambiguousDeps: make(map[string][]Dependency), - expectedHcl: ` - name = "test" - resource_group_name = "test" -depends_on= [ -azurerm_resource_group.res-0, -azurerm_resource_group.res-1 -] -`, - }, - { - name: "multiple ambiguous dependencies", - inputHcl: ` - name = "test" - resource_group_name = "test" -`, - parentChildDeps: make(map[Dependency]bool), - ambiguousDeps: map[string][]Dependency{ - "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123": { - { - TFAddr: mustParseTFAddr("azurerm_foo_sub1_resource.res-1"), - AzureResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123/sub1/sub1", - TFResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123", - }, - { - TFAddr: mustParseTFAddr("azurerm_foo_sub2_resource.res-2"), - AzureResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123/sub2/sub2", - TFResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Foo/foo/123", - }, - }, - "/subscriptions/123/resourceGroups/123/providers/Microsoft.Bar/bar/456": { - { - TFAddr: mustParseTFAddr("azurerm_bar_sub1_resource.res-3"), - AzureResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Bar/bar/456/sub1/sub1", - TFResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Bar/bar/456", - }, - { - TFAddr: mustParseTFAddr("azurerm_bar_sub2_resource.res-4"), - AzureResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Bar/bar/456/sub2/sub2", - TFResourceId: "/subscriptions/123/resourceGroups/123/providers/Microsoft.Bar/bar/456", - }, - }, - }, - expectedHcl: ` - name = "test" - resource_group_name = "test" -depends_on= [ -# One of azurerm_bar_sub1_resource.res-3,azurerm_bar_sub2_resource.res-4 (can't auto-resolve as their ids are identical) -# One of azurerm_foo_sub1_resource.res-1,azurerm_foo_sub2_resource.res-2 (can't auto-resolve as their ids are identical) -] -`, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - body := mustHclWriteParse(testCase.inputHcl) - err := applyParentChildAndAmbiguousDepsToHclBlock( - body, - testCase.parentChildDeps, - testCase.ambiguousDeps, - ) - assert.NoError(t, err) - - actualHcl := string(body.BuildTokens(nil).Bytes()) - assert.Equal(t, testCase.expectedHcl, actualHcl) - }) - } -} - -func mustHclWriteParse(input string) *hclwrite.Body { - file, diag := hclwrite.ParseConfig([]byte(input), "input.hcl", hcl.InitialPos) - if diag.HasErrors() { - panic(fmt.Sprintf("failed to parse HCL: %v", diag)) - } - return file.Body() -}