// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package instances import ( "fmt" "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/zclconf/go-cty/cty" "github.com/opentofu/opentofu/internal/addrs" ) 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() // 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) 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")) 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`)) 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`)) 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 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`)) 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`)) 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 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) }