| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: BUSL-1.1 |
| |
| package instances |
| |
| import ( |
| "fmt" |
| "strings" |
| "testing" |
| |
| "github.com/google/go-cmp/cmp" |
| "github.com/zclconf/go-cty-debug/ctydebug" |
| "github.com/zclconf/go-cty/cty" |
| |
| "github.com/hashicorp/terraform/internal/addrs" |
| "github.com/hashicorp/terraform/internal/configs" |
| "github.com/hashicorp/terraform/internal/moduletest/mocking" |
| ) |
| |
| func TestExpanderWithOverrides(t *testing.T) { |
| |
| mustModuleInstance := func(t *testing.T, s string) addrs.ModuleInstance { |
| if len(s) == 0 { |
| return addrs.RootModuleInstance |
| } |
| |
| addr, diags := addrs.ParseModuleInstanceStr(s) |
| if diags.HasErrors() { |
| t.Fatalf("unexpected error: %s", diags.Err()) |
| } |
| return addr |
| } |
| |
| tcs := map[string]struct { |
| // Hook to install chosen overrides. |
| overrides mocking.InitLocalOverrides |
| |
| // Hook to initialise the expander with the desired state. |
| expander func(*Expander) |
| |
| // The target module instance to inspect. |
| target string |
| |
| // Set to true to include overrides in the result. |
| includeOverrides bool |
| |
| // The expected result. |
| wantModules []addrs.ModuleInstance |
| |
| // The expected result for partial modules. |
| wantPartials map[string]bool |
| }{ |
| "root module": { |
| wantModules: singletonRootModule, |
| wantPartials: make(map[string]bool), |
| }, |
| "instanced child module not overridden": { |
| expander: func(expander *Expander) { |
| expander.SetModuleCount(addrs.RootModuleInstance, addrs.ModuleCall{Name: "double"}, 2) |
| }, |
| target: "module.double", |
| wantModules: []addrs.ModuleInstance{ |
| mustModuleInstance(t, "module.double[0]"), |
| mustModuleInstance(t, "module.double[1]"), |
| }, |
| wantPartials: make(map[string]bool), |
| }, |
| "instanced child module single instance overridden": { |
| overrides: func(overrides addrs.Map[addrs.Targetable, *configs.Override]) { |
| overrides.Put(mustModuleInstance(t, "module.double[0]"), &configs.Override{}) |
| }, |
| expander: func(expander *Expander) { |
| expander.SetModuleCount(addrs.RootModuleInstance, addrs.ModuleCall{Name: "double"}, 2) |
| }, |
| target: "module.double", |
| wantModules: []addrs.ModuleInstance{ |
| mustModuleInstance(t, "module.double[1]"), |
| }, |
| wantPartials: make(map[string]bool), |
| }, |
| "instanced child module single instance overridden includes overrides": { |
| overrides: func(overrides addrs.Map[addrs.Targetable, *configs.Override]) { |
| overrides.Put(mustModuleInstance(t, "module.double[0]"), &configs.Override{}) |
| }, |
| expander: func(expander *Expander) { |
| expander.SetModuleCount(addrs.RootModuleInstance, addrs.ModuleCall{Name: "double"}, 2) |
| }, |
| target: "module.double", |
| includeOverrides: true, |
| wantModules: []addrs.ModuleInstance{ |
| mustModuleInstance(t, "module.double[0]"), |
| mustModuleInstance(t, "module.double[1]"), |
| }, |
| wantPartials: make(map[string]bool), |
| }, |
| "deeply nested child module with parent overridden": { |
| overrides: func(overrides addrs.Map[addrs.Targetable, *configs.Override]) { |
| overrides.Put(mustModuleInstance(t, "module.double[0]"), &configs.Override{}) |
| }, |
| expander: func(expander *Expander) { |
| expander.SetModuleCount(addrs.RootModuleInstance, addrs.ModuleCall{Name: "double"}, 2) |
| expander.SetModuleSingle(mustModuleInstance(t, "module.double[1]"), addrs.ModuleCall{Name: "single"}) |
| }, |
| target: "module.double.module.single", |
| wantModules: []addrs.ModuleInstance{mustModuleInstance(t, "module.double[1].module.single")}, |
| wantPartials: make(map[string]bool), |
| }, |
| "unknown child module overridden by instanced module": { |
| overrides: func(overrides addrs.Map[addrs.Targetable, *configs.Override]) { |
| overrides.Put(mustModuleInstance(t, "module.unknown[0]"), &configs.Override{}) |
| }, |
| expander: func(expander *Expander) { |
| expander.SetModuleCountUnknown(addrs.RootModuleInstance, addrs.ModuleCall{Name: "unknown"}) |
| }, |
| target: "module.unknown", |
| wantPartials: map[string]bool{ |
| "module.unknown[*]": true, |
| }, |
| }, |
| "unknown child module overridden by instanced module includes overrides": { |
| overrides: func(overrides addrs.Map[addrs.Targetable, *configs.Override]) { |
| overrides.Put(mustModuleInstance(t, "module.unknown"), &configs.Override{}) |
| }, |
| expander: func(expander *Expander) { |
| expander.SetModuleCountUnknown(addrs.RootModuleInstance, addrs.ModuleCall{Name: "unknown"}) |
| }, |
| target: "module.unknown", |
| wantPartials: make(map[string]bool), // This time it's empty, as we overrode all instances. |
| }, |
| } |
| |
| for name, tc := range tcs { |
| t.Run(name, func(t *testing.T) { |
| overrides := mocking.OverridesForTesting(nil, tc.overrides) |
| |
| expander := NewExpander(overrides) |
| if tc.expander != nil { |
| tc.expander(expander) |
| } |
| |
| target := mustModuleInstance(t, tc.target).Module() |
| |
| gotModules := expander.ExpandModule(target, tc.includeOverrides) |
| gotPartials := expander.UnknownModuleInstances(target, tc.includeOverrides) |
| |
| if diff := cmp.Diff(tc.wantModules, gotModules); len(diff) > 0 { |
| t.Errorf("wrong result\n%s", diff) |
| } |
| |
| // Convert the gotPartials into strings to make cmp.Diff work. |
| gotPartialsStr := make(map[string]bool, len(gotPartials)) |
| for _, partial := range gotPartials { |
| gotPartialsStr[partial.String()] = true |
| } |
| |
| if diff := cmp.Diff(tc.wantPartials, gotPartialsStr, ctydebug.CmpOptions); len(diff) > 0 { |
| t.Errorf("wrong result\n%s", diff) |
| } |
| }) |
| } |
| |
| } |
| |
| func TestExpander(t *testing.T) { |
| // Some module and resource addresses and values we'll use repeatedly below. |
| singleModuleAddr := addrs.ModuleCall{Name: "single"} |
| count2ModuleAddr := addrs.ModuleCall{Name: "count2"} |
| count0ModuleAddr := addrs.ModuleCall{Name: "count0"} |
| forEachModuleAddr := addrs.ModuleCall{Name: "for_each"} |
| singleResourceAddr := addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "test", |
| Name: "single", |
| } |
| count2ResourceAddr := addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "test", |
| Name: "count2", |
| } |
| count0ResourceAddr := addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "test", |
| Name: "count0", |
| } |
| forEachResourceAddr := addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "test", |
| Name: "for_each", |
| } |
| eachMap := map[string]cty.Value{ |
| "a": cty.NumberIntVal(1), |
| "b": cty.NumberIntVal(2), |
| } |
| |
| // In normal use, Expander would be called in the context of a graph |
| // traversal to ensure that information is registered/requested in the |
| // correct sequence, but to keep this test self-contained we'll just |
| // manually write out the steps here. |
| // |
| // The steps below are assuming a configuration tree like the following: |
| // - root module |
| // - resource test.single with no count or for_each |
| // - resource test.count2 with count = 2 |
| // - resource test.count0 with count = 0 |
| // - resource test.for_each with for_each = { a = 1, b = 2 } |
| // - child module "single" with no count or for_each |
| // - resource test.single with no count or for_each |
| // - resource test.count2 with count = 2 |
| // - child module "count2" with count = 2 |
| // - resource test.single with no count or for_each |
| // - resource test.count2 with count = 2 |
| // - child module "count2" with count = 2 |
| // - resource test.count2 with count = 2 |
| // - child module "count0" with count = 0 |
| // - resource test.single with no count or for_each |
| // - child module for_each with for_each = { a = 1, b = 2 } |
| // - resource test.single with no count or for_each |
| // - resource test.count2 with count = 2 |
| |
| ex := NewExpander(nil) |
| |
| // We don't register the root module, because it's always implied to exist. |
| // |
| // Below we're going to use braces and indentation just to help visually |
| // reflect the tree structure from the tree in the above comment, in the |
| // hope that the following is easier to follow. |
| // |
| // The Expander API requires that we register containing modules before |
| // registering anything inside them, so we'll work through the above |
| // in a depth-first order in the registration steps that follow. |
| { |
| ex.SetResourceSingle(addrs.RootModuleInstance, singleResourceAddr) |
| ex.SetResourceCount(addrs.RootModuleInstance, count2ResourceAddr, 2) |
| ex.SetResourceCount(addrs.RootModuleInstance, count0ResourceAddr, 0) |
| ex.SetResourceForEach(addrs.RootModuleInstance, forEachResourceAddr, eachMap) |
| |
| ex.SetModuleSingle(addrs.RootModuleInstance, singleModuleAddr) |
| { |
| // The single instance of the module |
| moduleInstanceAddr := addrs.RootModuleInstance.Child("single", addrs.NoKey) |
| ex.SetResourceSingle(moduleInstanceAddr, singleResourceAddr) |
| ex.SetResourceCount(moduleInstanceAddr, count2ResourceAddr, 2) |
| } |
| |
| ex.SetModuleCount(addrs.RootModuleInstance, count2ModuleAddr, 2) |
| for i1 := 0; i1 < 2; i1++ { |
| moduleInstanceAddr := addrs.RootModuleInstance.Child("count2", addrs.IntKey(i1)) |
| ex.SetResourceSingle(moduleInstanceAddr, singleResourceAddr) |
| ex.SetResourceCount(moduleInstanceAddr, count2ResourceAddr, 2) |
| ex.SetModuleCount(moduleInstanceAddr, count2ModuleAddr, 2) |
| for i2 := 0; i2 < 2; i2++ { |
| moduleInstanceAddr := moduleInstanceAddr.Child("count2", addrs.IntKey(i2)) |
| ex.SetResourceCount(moduleInstanceAddr, count2ResourceAddr, 2) |
| } |
| } |
| |
| ex.SetModuleCount(addrs.RootModuleInstance, count0ModuleAddr, 0) |
| { |
| // There are no instances of module "count0", so our nested module |
| // would never actually get registered here: the expansion node |
| // for the resource would see that its containing module has no |
| // instances and so do nothing. |
| } |
| |
| ex.SetModuleForEach(addrs.RootModuleInstance, forEachModuleAddr, eachMap) |
| for k := range eachMap { |
| moduleInstanceAddr := addrs.RootModuleInstance.Child("for_each", addrs.StringKey(k)) |
| ex.SetResourceSingle(moduleInstanceAddr, singleResourceAddr) |
| ex.SetResourceCount(moduleInstanceAddr, count2ResourceAddr, 2) |
| } |
| } |
| |
| t.Run("root module", func(t *testing.T) { |
| // Requesting expansion of the root module doesn't really mean anything |
| // since it's always a singleton, but for consistency it should work. |
| got := ex.ExpandModule(addrs.RootModule, false) |
| want := []addrs.ModuleInstance{addrs.RootModuleInstance} |
| if diff := cmp.Diff(want, got); diff != "" { |
| t.Errorf("wrong result\n%s", diff) |
| } |
| }) |
| t.Run("resource single", func(t *testing.T) { |
| got := ex.ExpandModuleResource( |
| addrs.RootModule, |
| singleResourceAddr, |
| ) |
| want := []addrs.AbsResourceInstance{ |
| mustAbsResourceInstanceAddr(`test.single`), |
| } |
| if diff := cmp.Diff(want, got); diff != "" { |
| t.Errorf("wrong result\n%s", diff) |
| } |
| }) |
| t.Run("resource count2", func(t *testing.T) { |
| got := ex.ExpandModuleResource( |
| addrs.RootModule, |
| count2ResourceAddr, |
| ) |
| want := []addrs.AbsResourceInstance{ |
| mustAbsResourceInstanceAddr(`test.count2[0]`), |
| mustAbsResourceInstanceAddr(`test.count2[1]`), |
| } |
| if diff := cmp.Diff(want, got); diff != "" { |
| t.Errorf("wrong result\n%s", diff) |
| } |
| }) |
| t.Run("resource count0", func(t *testing.T) { |
| got := ex.ExpandModuleResource( |
| addrs.RootModule, |
| count0ResourceAddr, |
| ) |
| want := []addrs.AbsResourceInstance(nil) |
| if diff := cmp.Diff(want, got); diff != "" { |
| t.Errorf("wrong result\n%s", diff) |
| } |
| }) |
| t.Run("resource for_each", func(t *testing.T) { |
| got := ex.ExpandModuleResource( |
| addrs.RootModule, |
| forEachResourceAddr, |
| ) |
| want := []addrs.AbsResourceInstance{ |
| mustAbsResourceInstanceAddr(`test.for_each["a"]`), |
| mustAbsResourceInstanceAddr(`test.for_each["b"]`), |
| } |
| if diff := cmp.Diff(want, got); diff != "" { |
| t.Errorf("wrong result\n%s", diff) |
| } |
| }) |
| t.Run("module single", func(t *testing.T) { |
| got := ex.ExpandModule(addrs.RootModule.Child("single"), false) |
| want := []addrs.ModuleInstance{ |
| mustModuleInstanceAddr(`module.single`), |
| } |
| if diff := cmp.Diff(want, got); diff != "" { |
| t.Errorf("wrong result\n%s", diff) |
| } |
| }) |
| t.Run("module single resource single", func(t *testing.T) { |
| got := ex.ExpandModuleResource( |
| mustModuleAddr("single"), |
| singleResourceAddr, |
| ) |
| want := []addrs.AbsResourceInstance{ |
| mustAbsResourceInstanceAddr("module.single.test.single"), |
| } |
| if diff := cmp.Diff(want, got); diff != "" { |
| t.Errorf("wrong result\n%s", diff) |
| } |
| }) |
| t.Run("module single resource count2", func(t *testing.T) { |
| // Two different ways of asking the same question, which should |
| // both produce the same result. |
| // First: nested expansion of all instances of the resource across |
| // all instances of the module, but it's a single-instance module |
| // so the first level is a singleton. |
| got1 := ex.ExpandModuleResource( |
| mustModuleAddr(`single`), |
| count2ResourceAddr, |
| ) |
| // Second: expansion of only instances belonging to a specific |
| // instance of the module, but again it's a single-instance module |
| // so there's only one to ask about. |
| got2 := ex.ExpandResource( |
| count2ResourceAddr.Absolute( |
| addrs.RootModuleInstance.Child("single", addrs.NoKey), |
| ), |
| ) |
| want := []addrs.AbsResourceInstance{ |
| mustAbsResourceInstanceAddr(`module.single.test.count2[0]`), |
| mustAbsResourceInstanceAddr(`module.single.test.count2[1]`), |
| } |
| if diff := cmp.Diff(want, got1); diff != "" { |
| t.Errorf("wrong ExpandModuleResource result\n%s", diff) |
| } |
| if diff := cmp.Diff(want, got2); diff != "" { |
| t.Errorf("wrong ExpandResource result\n%s", diff) |
| } |
| }) |
| t.Run("module single resource count2 with non-existing module instance", func(t *testing.T) { |
| got := ex.ExpandResource( |
| count2ResourceAddr.Absolute( |
| // Note: This is intentionally an invalid instance key, |
| // so we're asking about module.single[1].test.count2 |
| // even though module.single doesn't have count set and |
| // therefore there is no module.single[1]. |
| addrs.RootModuleInstance.Child("single", addrs.IntKey(1)), |
| ), |
| ) |
| // If the containing module instance doesn't exist then it can't |
| // possibly have any resource instances inside it. |
| want := ([]addrs.AbsResourceInstance)(nil) |
| if diff := cmp.Diff(want, got); diff != "" { |
| t.Errorf("wrong result\n%s", diff) |
| } |
| }) |
| t.Run("module count2", func(t *testing.T) { |
| got := ex.ExpandModule(mustModuleAddr(`count2`), false) |
| want := []addrs.ModuleInstance{ |
| mustModuleInstanceAddr(`module.count2[0]`), |
| mustModuleInstanceAddr(`module.count2[1]`), |
| } |
| if diff := cmp.Diff(want, got); diff != "" { |
| t.Errorf("wrong result\n%s", diff) |
| } |
| }) |
| t.Run("module count2 resource single", func(t *testing.T) { |
| got := ex.ExpandModuleResource( |
| mustModuleAddr(`count2`), |
| singleResourceAddr, |
| ) |
| want := []addrs.AbsResourceInstance{ |
| mustAbsResourceInstanceAddr(`module.count2[0].test.single`), |
| mustAbsResourceInstanceAddr(`module.count2[1].test.single`), |
| } |
| if diff := cmp.Diff(want, got); diff != "" { |
| t.Errorf("wrong result\n%s", diff) |
| } |
| }) |
| t.Run("module count2 resource count2", func(t *testing.T) { |
| got := ex.ExpandModuleResource( |
| mustModuleAddr(`count2`), |
| count2ResourceAddr, |
| ) |
| want := []addrs.AbsResourceInstance{ |
| mustAbsResourceInstanceAddr(`module.count2[0].test.count2[0]`), |
| mustAbsResourceInstanceAddr(`module.count2[0].test.count2[1]`), |
| mustAbsResourceInstanceAddr(`module.count2[1].test.count2[0]`), |
| mustAbsResourceInstanceAddr(`module.count2[1].test.count2[1]`), |
| } |
| if diff := cmp.Diff(want, got); diff != "" { |
| t.Errorf("wrong result\n%s", diff) |
| } |
| }) |
| t.Run("module count2 module count2", func(t *testing.T) { |
| got := ex.ExpandModule(mustModuleAddr(`count2.count2`), false) |
| want := []addrs.ModuleInstance{ |
| mustModuleInstanceAddr(`module.count2[0].module.count2[0]`), |
| mustModuleInstanceAddr(`module.count2[0].module.count2[1]`), |
| mustModuleInstanceAddr(`module.count2[1].module.count2[0]`), |
| mustModuleInstanceAddr(`module.count2[1].module.count2[1]`), |
| } |
| if diff := cmp.Diff(want, got); diff != "" { |
| t.Errorf("wrong result\n%s", diff) |
| } |
| }) |
| t.Run("module count2[0] module count2 instances", func(t *testing.T) { |
| instAddr := mustModuleInstanceAddr(`module.count2[0].module.count2[0]`) |
| callAddr := instAddr.AbsCall() // discards the final [0] instance key from the above |
| keyType, got, known := ex.ExpandAbsModuleCall(callAddr) |
| if !known { |
| t.Fatal("expansion unknown; want known") |
| } |
| if keyType != addrs.IntKeyType { |
| t.Fatalf("wrong key type %#v; want %#v", keyType, addrs.IntKeyType) |
| } |
| want := []addrs.InstanceKey{ |
| addrs.IntKey(0), |
| addrs.IntKey(1), |
| } |
| if diff := cmp.Diff(want, got); diff != "" { |
| t.Errorf("wrong result\n%s", diff) |
| } |
| }) |
| t.Run("module count2 module count2 GetDeepestExistingModuleInstance", func(t *testing.T) { |
| t.Run("first step invalid", func(t *testing.T) { |
| got := ex.GetDeepestExistingModuleInstance(mustModuleInstanceAddr(`module.count2["nope"].module.count2[0]`)) |
| want := addrs.RootModuleInstance |
| if !want.Equal(got) { |
| t.Errorf("wrong result\ngot: %s\nwant: %s", got, want) |
| } |
| }) |
| t.Run("second step invalid", func(t *testing.T) { |
| got := ex.GetDeepestExistingModuleInstance(mustModuleInstanceAddr(`module.count2[1].module.count2`)) |
| want := mustModuleInstanceAddr(`module.count2[1]`) |
| if !want.Equal(got) { |
| t.Errorf("wrong result\ngot: %s\nwant: %s", got, want) |
| } |
| }) |
| t.Run("neither step valid", func(t *testing.T) { |
| got := ex.GetDeepestExistingModuleInstance(mustModuleInstanceAddr(`module.count2.module.count2["nope"]`)) |
| want := addrs.RootModuleInstance |
| if !want.Equal(got) { |
| t.Errorf("wrong result\ngot: %s\nwant: %s", got, want) |
| } |
| }) |
| t.Run("both steps valid", func(t *testing.T) { |
| got := ex.GetDeepestExistingModuleInstance(mustModuleInstanceAddr(`module.count2[1].module.count2[0]`)) |
| want := mustModuleInstanceAddr(`module.count2[1].module.count2[0]`) |
| if !want.Equal(got) { |
| t.Errorf("wrong result\ngot: %s\nwant: %s", got, want) |
| } |
| }) |
| }) |
| t.Run("module count2 resource count2 resource count2", func(t *testing.T) { |
| got := ex.ExpandModuleResource( |
| mustModuleAddr(`count2.count2`), |
| count2ResourceAddr, |
| ) |
| want := []addrs.AbsResourceInstance{ |
| mustAbsResourceInstanceAddr(`module.count2[0].module.count2[0].test.count2[0]`), |
| mustAbsResourceInstanceAddr(`module.count2[0].module.count2[0].test.count2[1]`), |
| mustAbsResourceInstanceAddr(`module.count2[0].module.count2[1].test.count2[0]`), |
| mustAbsResourceInstanceAddr(`module.count2[0].module.count2[1].test.count2[1]`), |
| mustAbsResourceInstanceAddr(`module.count2[1].module.count2[0].test.count2[0]`), |
| mustAbsResourceInstanceAddr(`module.count2[1].module.count2[0].test.count2[1]`), |
| mustAbsResourceInstanceAddr(`module.count2[1].module.count2[1].test.count2[0]`), |
| mustAbsResourceInstanceAddr(`module.count2[1].module.count2[1].test.count2[1]`), |
| } |
| if diff := cmp.Diff(want, got); diff != "" { |
| t.Errorf("wrong result\n%s", diff) |
| } |
| }) |
| t.Run("module count2 resource count2 resource count2", func(t *testing.T) { |
| got := ex.ExpandResource( |
| count2ResourceAddr.Absolute(mustModuleInstanceAddr(`module.count2[0].module.count2[1]`)), |
| ) |
| want := []addrs.AbsResourceInstance{ |
| mustAbsResourceInstanceAddr(`module.count2[0].module.count2[1].test.count2[0]`), |
| mustAbsResourceInstanceAddr(`module.count2[0].module.count2[1].test.count2[1]`), |
| } |
| if diff := cmp.Diff(want, got); diff != "" { |
| t.Errorf("wrong result\n%s", diff) |
| } |
| }) |
| t.Run("module count0", func(t *testing.T) { |
| got := ex.ExpandModule(mustModuleAddr(`count0`), false) |
| want := []addrs.ModuleInstance(nil) |
| if diff := cmp.Diff(want, got); diff != "" { |
| t.Errorf("wrong result\n%s", diff) |
| } |
| }) |
| t.Run("module count0 resource single", func(t *testing.T) { |
| got := ex.ExpandModuleResource( |
| mustModuleAddr(`count0`), |
| singleResourceAddr, |
| ) |
| // The containing module has zero instances, so therefore there |
| // are zero instances of this resource even though it doesn't have |
| // count = 0 set itself. |
| want := []addrs.AbsResourceInstance(nil) |
| if diff := cmp.Diff(want, got); diff != "" { |
| t.Errorf("wrong result\n%s", diff) |
| } |
| }) |
| t.Run("module for_each", func(t *testing.T) { |
| got := ex.ExpandModule(mustModuleAddr(`for_each`), false) |
| want := []addrs.ModuleInstance{ |
| mustModuleInstanceAddr(`module.for_each["a"]`), |
| mustModuleInstanceAddr(`module.for_each["b"]`), |
| } |
| if diff := cmp.Diff(want, got); diff != "" { |
| t.Errorf("wrong result\n%s", diff) |
| } |
| }) |
| t.Run("module for_each resource single", func(t *testing.T) { |
| got := ex.ExpandModuleResource( |
| mustModuleAddr(`for_each`), |
| singleResourceAddr, |
| ) |
| want := []addrs.AbsResourceInstance{ |
| mustAbsResourceInstanceAddr(`module.for_each["a"].test.single`), |
| mustAbsResourceInstanceAddr(`module.for_each["b"].test.single`), |
| } |
| if diff := cmp.Diff(want, got); diff != "" { |
| t.Errorf("wrong result\n%s", diff) |
| } |
| }) |
| t.Run("module for_each resource count2", func(t *testing.T) { |
| got := ex.ExpandModuleResource( |
| mustModuleAddr(`for_each`), |
| count2ResourceAddr, |
| ) |
| want := []addrs.AbsResourceInstance{ |
| mustAbsResourceInstanceAddr(`module.for_each["a"].test.count2[0]`), |
| mustAbsResourceInstanceAddr(`module.for_each["a"].test.count2[1]`), |
| mustAbsResourceInstanceAddr(`module.for_each["b"].test.count2[0]`), |
| mustAbsResourceInstanceAddr(`module.for_each["b"].test.count2[1]`), |
| } |
| if diff := cmp.Diff(want, got); diff != "" { |
| t.Errorf("wrong result\n%s", diff) |
| } |
| }) |
| t.Run("module for_each resource count2", func(t *testing.T) { |
| got := ex.ExpandResource( |
| count2ResourceAddr.Absolute(mustModuleInstanceAddr(`module.for_each["a"]`)), |
| ) |
| want := []addrs.AbsResourceInstance{ |
| mustAbsResourceInstanceAddr(`module.for_each["a"].test.count2[0]`), |
| mustAbsResourceInstanceAddr(`module.for_each["a"].test.count2[1]`), |
| } |
| if diff := cmp.Diff(want, got); diff != "" { |
| t.Errorf("wrong result\n%s", diff) |
| } |
| }) |
| |
| t.Run(`module.for_each["b"] repetitiondata`, func(t *testing.T) { |
| got := ex.GetModuleInstanceRepetitionData( |
| mustModuleInstanceAddr(`module.for_each["b"]`), |
| ) |
| want := RepetitionData{ |
| EachKey: cty.StringVal("b"), |
| EachValue: cty.NumberIntVal(2), |
| } |
| if diff := cmp.Diff(want, got, cmp.Comparer(valueEquals)); diff != "" { |
| t.Errorf("wrong result\n%s", diff) |
| } |
| }) |
| t.Run(`module.count2[0].module.count2[1] repetitiondata`, func(t *testing.T) { |
| got := ex.GetModuleInstanceRepetitionData( |
| mustModuleInstanceAddr(`module.count2[0].module.count2[1]`), |
| ) |
| want := RepetitionData{ |
| CountIndex: cty.NumberIntVal(1), |
| } |
| if diff := cmp.Diff(want, got, cmp.Comparer(valueEquals)); diff != "" { |
| t.Errorf("wrong result\n%s", diff) |
| } |
| }) |
| t.Run(`module.for_each["a"] repetitiondata`, func(t *testing.T) { |
| got := ex.GetModuleInstanceRepetitionData( |
| mustModuleInstanceAddr(`module.for_each["a"]`), |
| ) |
| want := RepetitionData{ |
| EachKey: cty.StringVal("a"), |
| EachValue: cty.NumberIntVal(1), |
| } |
| if diff := cmp.Diff(want, got, cmp.Comparer(valueEquals)); diff != "" { |
| t.Errorf("wrong result\n%s", diff) |
| } |
| }) |
| |
| t.Run(`test.for_each["a"] repetitiondata`, func(t *testing.T) { |
| got := ex.GetResourceInstanceRepetitionData( |
| mustAbsResourceInstanceAddr(`test.for_each["a"]`), |
| ) |
| want := RepetitionData{ |
| EachKey: cty.StringVal("a"), |
| EachValue: cty.NumberIntVal(1), |
| } |
| if diff := cmp.Diff(want, got, cmp.Comparer(valueEquals)); diff != "" { |
| t.Errorf("wrong result\n%s", diff) |
| } |
| }) |
| t.Run(`module.for_each["a"].test.single repetitiondata`, func(t *testing.T) { |
| got := ex.GetResourceInstanceRepetitionData( |
| mustAbsResourceInstanceAddr(`module.for_each["a"].test.single`), |
| ) |
| want := RepetitionData{} |
| if diff := cmp.Diff(want, got, cmp.Comparer(valueEquals)); diff != "" { |
| t.Errorf("wrong result\n%s", diff) |
| } |
| }) |
| t.Run(`module.for_each["a"].test.count2[1] repetitiondata`, func(t *testing.T) { |
| got := ex.GetResourceInstanceRepetitionData( |
| mustAbsResourceInstanceAddr(`module.for_each["a"].test.count2[1]`), |
| ) |
| want := RepetitionData{ |
| CountIndex: cty.NumberIntVal(1), |
| } |
| if diff := cmp.Diff(want, got, cmp.Comparer(valueEquals)); diff != "" { |
| t.Errorf("wrong result\n%s", diff) |
| } |
| }) |
| } |
| |
| func TestExpanderWithUnknowns(t *testing.T) { |
| t.Run("resource in root module with unknown for_each", func(t *testing.T) { |
| resourceAddr := addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "test", |
| Name: "foo", |
| } |
| ex := NewExpander(nil) |
| ex.SetResourceForEachUnknown(addrs.RootModuleInstance, resourceAddr) |
| |
| got := ex.ExpandModuleResource(addrs.RootModule, resourceAddr) |
| if len(got) != 0 { |
| t.Errorf("unexpected known addresses: %#v", got) |
| } |
| }) |
| t.Run("resource in root module with unknown count", func(t *testing.T) { |
| resourceAddr := addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "test", |
| Name: "foo", |
| } |
| ex := NewExpander(nil) |
| ex.SetResourceCountUnknown(addrs.RootModuleInstance, resourceAddr) |
| |
| got := ex.ExpandModuleResource(addrs.RootModule, resourceAddr) |
| if len(got) != 0 { |
| t.Errorf("unexpected known addresses: %#v", got) |
| } |
| }) |
| t.Run("module with unknown for_each", func(t *testing.T) { |
| moduleCallAddr := addrs.ModuleCall{Name: "foo"} |
| ex := NewExpander(nil) |
| ex.SetModuleForEachUnknown(addrs.RootModuleInstance, moduleCallAddr) |
| |
| got := ex.ExpandModule(addrs.Module{moduleCallAddr.Name}, false) |
| if len(got) != 0 { |
| t.Errorf("unexpected known addresses: %#v", got) |
| } |
| |
| gotUnknown := ex.UnknownModuleInstances(addrs.Module{moduleCallAddr.Name}, false) |
| if len(gotUnknown) != 1 { |
| t.Errorf("unexpected unknown addresses: %#v", gotUnknown) |
| } |
| wantUnknownCall := addrs.RootModuleInstance.UnexpandedChild(moduleCallAddr) |
| if !gotUnknown.Has(wantUnknownCall) { |
| t.Errorf("unknown should have %s, but it doesn't", wantUnknownCall) |
| } |
| }) |
| t.Run("module with unknown count", func(t *testing.T) { |
| moduleCallAddr := addrs.ModuleCall{Name: "foo"} |
| ex := NewExpander(nil) |
| ex.SetModuleCountUnknown(addrs.RootModuleInstance, moduleCallAddr) |
| |
| gotKnown := ex.ExpandModule(addrs.Module{moduleCallAddr.Name}, false) |
| if len(gotKnown) != 0 { |
| t.Errorf("unexpected known addresses: %#v", gotKnown) |
| } |
| |
| gotUnknown := ex.UnknownModuleInstances(addrs.Module{moduleCallAddr.Name}, false) |
| if len(gotUnknown) != 1 { |
| t.Errorf("unexpected unknown addresses: %#v", gotUnknown) |
| } |
| wantUnknownCall := addrs.RootModuleInstance.UnexpandedChild(moduleCallAddr) |
| if !gotUnknown.Has(wantUnknownCall) { |
| t.Errorf("unknown should have %s, but it doesn't", wantUnknownCall) |
| } |
| }) |
| t.Run("nested module with unknown count", func(t *testing.T) { |
| moduleCallAddr1 := addrs.ModuleCall{Name: "foo"} |
| moduleCallAddr2 := addrs.ModuleCall{Name: "bar"} |
| module1 := addrs.RootModule.Child(moduleCallAddr1.Name) |
| module2 := module1.Child(moduleCallAddr2.Name) |
| module1Inst0 := addrs.RootModuleInstance.Child("foo", addrs.IntKey(0)) |
| module1Inst1 := addrs.RootModuleInstance.Child("foo", addrs.IntKey(1)) |
| module1Inst2 := addrs.RootModuleInstance.Child("foo", addrs.IntKey(2)) |
| ex := NewExpander(nil) |
| ex.SetModuleCount(addrs.RootModuleInstance, moduleCallAddr1, 3) |
| ex.SetModuleCountUnknown(module1Inst0, moduleCallAddr2) |
| ex.SetModuleCount(module1Inst1, moduleCallAddr2, 1) |
| ex.SetModuleCountUnknown(module1Inst2, moduleCallAddr2) |
| |
| // We'll also put some resources inside module.foo[1].module.bar[0] |
| // so that we can test requesting unknown resource instance sets. |
| resourceAddrKnownExp := addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "test", |
| Name: "known_expansion", |
| } |
| resourceAddrUnknownExp := addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "test", |
| Name: "unknown_expansion", |
| } |
| module1Inst1Module2Inst0 := module1Inst1.Child("bar", addrs.IntKey(0)) |
| ex.SetResourceCount(module1Inst1Module2Inst0, resourceAddrKnownExp, 2) |
| ex.SetResourceCountUnknown(module1Inst1Module2Inst0, resourceAddrUnknownExp) |
| |
| module2Call := addrs.AbsModuleCall{ |
| Module: module1Inst0, |
| Call: moduleCallAddr2, |
| } |
| _, _, instsKnown := ex.ExpandAbsModuleCall(module2Call) |
| if instsKnown { |
| t.Fatalf("instances of %s are known; should be unknown", module2Call.String()) |
| } |
| |
| gotKnown := ex.ExpandModule(module2, false) |
| wantKnown := []addrs.ModuleInstance{ |
| module1Inst1.Child("bar", addrs.IntKey(0)), |
| } |
| if diff := cmp.Diff(wantKnown, gotKnown); diff != "" { |
| t.Errorf("unexpected known addresses\n%s", diff) |
| } |
| |
| gotUnknown := ex.UnknownModuleInstances(module2, false) |
| if len(gotUnknown) != 2 { |
| t.Errorf("unexpected unknown addresses: %#v", gotUnknown) |
| } |
| if wantUnknownCall := module1Inst0.UnexpandedChild(moduleCallAddr2); !gotUnknown.Has(wantUnknownCall) { |
| t.Errorf("unknown should have %s, but it doesn't", wantUnknownCall) |
| } |
| if unwantUnknownCall := module1Inst1.UnexpandedChild(moduleCallAddr2); gotUnknown.Has(unwantUnknownCall) { |
| t.Errorf("unknown should not have %s, but does", unwantUnknownCall) |
| } |
| if wantUnknownCall := module1Inst2.UnexpandedChild(moduleCallAddr2); !gotUnknown.Has(wantUnknownCall) { |
| t.Errorf("unknown should have %s, but it doesn't", wantUnknownCall) |
| } |
| |
| gotKnownResource := ex.ExpandResource(module1Inst1Module2Inst0.Resource( |
| resourceAddrKnownExp.Mode, resourceAddrKnownExp.Type, resourceAddrKnownExp.Name, |
| )) |
| wantKnownResource := []addrs.AbsResourceInstance{ |
| mustAbsResourceInstanceAddr("module.foo[1].module.bar[0].test.known_expansion[0]"), |
| mustAbsResourceInstanceAddr("module.foo[1].module.bar[0].test.known_expansion[1]"), |
| } |
| if diff := cmp.Diff(wantKnownResource, gotKnownResource); diff != "" { |
| t.Errorf("unexpected known addresses\n%s", diff) |
| } |
| |
| gotUnknownResource := ex.UnknownResourceInstances(module2.Resource( |
| resourceAddrUnknownExp.Mode, resourceAddrUnknownExp.Type, resourceAddrUnknownExp.Name, |
| )) |
| if len(gotUnknownResource) != 3 { |
| t.Errorf("unexpected unknown addresses: %#v", gotUnknownResource) |
| } |
| if wantResInst := module1Inst0.UnexpandedChild(moduleCallAddr2).Resource(resourceAddrUnknownExp); !gotUnknownResource.Has(wantResInst) { |
| t.Errorf("unknown should have %s, but it doesn't", wantResInst) |
| } |
| if wantResInst := module1Inst1Module2Inst0.UnexpandedResource(resourceAddrUnknownExp); !gotUnknownResource.Has(wantResInst) { |
| t.Errorf("unknown should have %s, but it doesn't", wantResInst) |
| } |
| if wantResInst := module1Inst2.UnexpandedChild(moduleCallAddr2).Resource(resourceAddrUnknownExp); !gotUnknownResource.Has(wantResInst) { |
| t.Errorf("unknown should have %s, but it doesn't", wantResInst) |
| } |
| }) |
| } |
| |
| func mustAbsResourceInstanceAddr(str string) addrs.AbsResourceInstance { |
| addr, diags := addrs.ParseAbsResourceInstanceStr(str) |
| if diags.HasErrors() { |
| panic(fmt.Sprintf("invalid absolute resource instance address: %s", diags.Err())) |
| } |
| return addr |
| } |
| |
| func mustModuleAddr(str string) addrs.Module { |
| if len(str) == 0 { |
| return addrs.RootModule |
| } |
| // We don't have a real parser for these because they don't appear in the |
| // language anywhere, but this interpretation mimics the format we |
| // produce from the String method on addrs.Module. |
| parts := strings.Split(str, ".") |
| return addrs.Module(parts) |
| } |
| |
| func mustModuleInstanceAddr(str string) addrs.ModuleInstance { |
| if len(str) == 0 { |
| return addrs.RootModuleInstance |
| } |
| addr, diags := addrs.ParseModuleInstanceStr(str) |
| if diags.HasErrors() { |
| panic(fmt.Sprintf("invalid module instance address: %s", diags.Err())) |
| } |
| return addr |
| } |
| |
| func valueEquals(a, b cty.Value) bool { |
| if a == cty.NilVal || b == cty.NilVal { |
| return a == b |
| } |
| return a.RawEquals(b) |
| } |