diff --git a/instances/expander.go b/instances/expander.go new file mode 100644 index 000000000..357d0f9fd --- /dev/null +++ b/instances/expander.go @@ -0,0 +1,319 @@ +package instances + +import ( + "fmt" + "sort" + "sync" + + "github.com/hashicorp/terraform/addrs" + "github.com/zclconf/go-cty/cty" +) + +// Expander instances serve as a coordination point for gathering object +// repetition values (count and for_each in configuration) and then later +// making use of them to fully enumerate all of the instances of an object. +// +// The two repeatable object types in Terraform are modules and resources. +// Because resources belong to modules and modules can nest inside other +// modules, module expansion in particular has a recursive effect that can +// cause deep objects to expand exponentially. Expander assumes that all +// instances of a module have the same static objects inside, and that they +// differ only in the repetition count for some of those objects. +// +// Expander is a synchronized object whose methods can be safely called +// from concurrent threads of execution. However, it does expect a certain +// sequence of operations which is normally obtained by the caller traversing +// a dependency graph: each object must have its repetition mode set exactly +// once, and this must be done before any calls that depend on the repetition +// mode. In other words, the count or for_each expression value for a module +// must be provided before any object nested directly or indirectly inside +// that module can be expanded. If this ordering is violated, the methods +// will panic to enforce internal consistency. +// +// The Expand* methods of Expander only work directly with modules and with +// resources. Addresses for other objects that nest within modules but +// do not themselves support repetition can be obtained by calling ExpandModule +// with the containing module path and then producing one absolute instance +// address per module instance address returned. +type Expander struct { + mu sync.RWMutex + exps *expanderModule +} + +// NewExpander initializes and returns a new Expander, empty and ready to use. +func NewExpander() *Expander { + return &Expander{ + exps: newExpanderModule(), + } +} + +// SetModuleSingle records that the given module call inside the given parent +// module does not use any repetition arguments and is therefore a singleton. +func (e *Expander) SetModuleSingle(parentAddr addrs.ModuleInstance, callAddr addrs.ModuleCall) { + e.setModuleExpansion(parentAddr, callAddr, expansionSingleVal) +} + +// SetModuleCount records that the given module call inside the given parent +// module instance uses the "count" repetition argument, with the given value. +func (e *Expander) SetModuleCount(parentAddr addrs.ModuleInstance, callAddr addrs.ModuleCall, count int) { + e.setModuleExpansion(parentAddr, callAddr, expansionCount(count)) +} + +// SetModuleForEach records that the given module call inside the given parent +// module instance uses the "for_each" repetition argument, with the given +// map value. +// +// In the configuration language the for_each argument can also accept a set. +// It's the caller's responsibility to convert that into an identity map before +// calling this method. +func (e *Expander) SetModuleForEach(parentAddr addrs.ModuleInstance, callAddr addrs.ModuleCall, mapping map[string]cty.Value) { + e.setModuleExpansion(parentAddr, callAddr, expansionForEach(mapping)) +} + +// SetResourceSingle records that the given module inside the given parent +// module does not use any repetition arguments and is therefore a singleton. +func (e *Expander) SetResourceSingle(parentAddr addrs.ModuleInstance, resourceAddr addrs.Resource) { + e.setResourceExpansion(parentAddr, resourceAddr, expansionSingleVal) +} + +// SetResourceCount records that the given module inside the given parent +// module uses the "count" repetition argument, with the given value. +func (e *Expander) SetResourceCount(parentAddr addrs.ModuleInstance, resourceAddr addrs.Resource, count int) { + e.setResourceExpansion(parentAddr, resourceAddr, expansionCount(count)) +} + +// SetResourceForEach records that the given module inside the given parent +// module uses the "for_each" repetition argument, with the given map value. +// +// In the configuration language the for_each argument can also accept a set. +// It's the caller's responsibility to convert that into an identity map before +// calling this method. +func (e *Expander) SetResourceForEach(parentAddr addrs.ModuleInstance, resourceAddr addrs.Resource, mapping map[string]cty.Value) { + e.setResourceExpansion(parentAddr, resourceAddr, expansionForEach(mapping)) +} + +// ExpandModule finds the exhaustive set of module instances resulting from +// the expansion of the given module and all of its ancestor modules. +// +// All of the modules on the path to the identified module must already have +// had their expansion registered using one of the SetModule* methods before +// calling, or this method will panic. +func (e *Expander) ExpandModule(addr addrs.Module) []addrs.ModuleInstance { + if len(addr) == 0 { + // Root module is always a singleton. + return singletonRootModule + } + + e.mu.RLock() + defer e.mu.RUnlock() + + // We're going to be dynamically growing ModuleInstance addresses, so + // we'll preallocate some space to do it so that for typical shallow + // module trees we won't need to reallocate this. + // (moduleInstances does plenty of allocations itself, so the benefit of + // pre-allocating this is marginal but it's not hard to do.) + parentAddr := make(addrs.ModuleInstance, 0, 4) + ret := e.exps.moduleInstances(addr, parentAddr) + sort.SliceStable(ret, func(i, j int) bool { + return ret[i].Less(ret[j]) + }) + return ret +} + +// ExpandResource finds the exhaustive set of resource instances resulting from +// the expansion of the given resource and all of its containing modules. +// +// All of the modules on the path to the identified resource and the resource +// itself must already have had their expansion registered using one of the +// SetModule*/SetResource* methods before calling, or this method will panic. +func (e *Expander) ExpandResource(parentAddr addrs.Module, resourceAddr addrs.Resource) []addrs.AbsResourceInstance { + e.mu.RLock() + defer e.mu.RUnlock() + + // We're going to be dynamically growing ModuleInstance addresses, so + // we'll preallocate some space to do it so that for typical shallow + // module trees we won't need to reallocate this. + // (moduleInstances does plenty of allocations itself, so the benefit of + // pre-allocating this is marginal but it's not hard to do.) + moduleInstanceAddr := make(addrs.ModuleInstance, 0, 4) + ret := e.exps.resourceInstances(parentAddr, resourceAddr, moduleInstanceAddr) + sort.SliceStable(ret, func(i, j int) bool { + return ret[i].Less(ret[j]) + }) + return ret +} + +// GetModuleInstanceRepetitionData returns an object describing the values +// that should be available for each.key, each.value, and count.index within +// the call block for the given module instance. +func (e *Expander) GetModuleInstanceRepetitionData(addr addrs.ModuleInstance) RepetitionData { + if len(addr) == 0 { + // The root module is always a singleton, so it has no repetition data. + return RepetitionData{} + } + + e.mu.RLock() + defer e.mu.RUnlock() + + parentMod := e.findModule(addr[:len(addr)-1]) + lastStep := addr[len(addr)-1] + exp, ok := parentMod.moduleCalls[addrs.ModuleCall{Name: lastStep.Name}] + if !ok { + panic(fmt.Sprintf("no expansion has been registered for %s", addr)) + } + return exp.repetitionData(lastStep.InstanceKey) +} + +// GetResourceInstanceRepetitionData returns an object describing the values +// that should be available for each.key, each.value, and count.index within +// the definition block for the given resource instance. +func (e *Expander) GetResourceInstanceRepetitionData(addr addrs.AbsResourceInstance) RepetitionData { + e.mu.RLock() + defer e.mu.RUnlock() + + parentMod := e.findModule(addr.Module) + exp, ok := parentMod.resources[addr.Resource.Resource] + if !ok { + panic(fmt.Sprintf("no expansion has been registered for %s", addr.ContainingResource())) + } + return exp.repetitionData(addr.Resource.Key) +} + +func (e *Expander) findModule(moduleInstAddr addrs.ModuleInstance) *expanderModule { + // We expect that all of the modules on the path to our module instance + // should already have expansions registered. + mod := e.exps + for i, step := range moduleInstAddr { + next, ok := mod.childInstances[step] + if !ok { + // Top-down ordering of registration is part of the contract of + // Expander, so this is always indicative of a bug in the caller. + panic(fmt.Sprintf("no expansion has been registered for ancestor module %s", moduleInstAddr[:i+1])) + } + mod = next + } + return mod +} + +func (e *Expander) setModuleExpansion(parentAddr addrs.ModuleInstance, callAddr addrs.ModuleCall, exp expansion) { + e.mu.Lock() + defer e.mu.Unlock() + + mod := e.findModule(parentAddr) + if _, exists := mod.moduleCalls[callAddr]; exists { + panic(fmt.Sprintf("expansion already registered for %s", parentAddr.Child(callAddr.Name, addrs.NoKey))) + } + // We'll also pre-register the child instances so that later calls can + // populate them as the caller traverses the configuration tree. + for _, key := range exp.instanceKeys() { + step := addrs.ModuleInstanceStep{Name: callAddr.Name, InstanceKey: key} + mod.childInstances[step] = newExpanderModule() + } + mod.moduleCalls[callAddr] = exp +} + +func (e *Expander) setResourceExpansion(parentAddr addrs.ModuleInstance, resourceAddr addrs.Resource, exp expansion) { + e.mu.Lock() + defer e.mu.Unlock() + + mod := e.findModule(parentAddr) + if _, exists := mod.resources[resourceAddr]; exists { + panic(fmt.Sprintf("expansion already registered for %s", resourceAddr.Absolute(parentAddr))) + } + mod.resources[resourceAddr] = exp +} + +type expanderModule struct { + moduleCalls map[addrs.ModuleCall]expansion + resources map[addrs.Resource]expansion + childInstances map[addrs.ModuleInstanceStep]*expanderModule +} + +func newExpanderModule() *expanderModule { + return &expanderModule{ + moduleCalls: make(map[addrs.ModuleCall]expansion), + resources: make(map[addrs.Resource]expansion), + childInstances: make(map[addrs.ModuleInstanceStep]*expanderModule), + } +} + +var singletonRootModule = []addrs.ModuleInstance{addrs.RootModuleInstance} + +func (m *expanderModule) moduleInstances(addr addrs.Module, parentAddr addrs.ModuleInstance) []addrs.ModuleInstance { + callName := addr[0] + exp, ok := m.moduleCalls[addrs.ModuleCall{Name: callName}] + if !ok { + // This is a bug in the caller, because it should always register + // expansions for an object and all of its ancestors before requesting + // expansion of it. + panic(fmt.Sprintf("no expansion has been registered for %s", parentAddr.Child(callName, addrs.NoKey))) + } + + var ret []addrs.ModuleInstance + + // If there's more than one step remaining then we need to traverse deeper. + if len(addr) > 1 { + for step, inst := range m.childInstances { + if step.Name != callName { + continue + } + instAddr := append(parentAddr, step) + ret = append(ret, inst.moduleInstances(addr[1:], instAddr)...) + } + return ret + } + + // Otherwise, we'll use the expansion from the final step to produce + // a sequence of addresses under this prefix. + for _, k := range exp.instanceKeys() { + // We're reusing the buffer under parentAddr as we recurse through + // the structure, so we need to copy it here to produce a final + // immutable slice to return. + full := make(addrs.ModuleInstance, 0, len(parentAddr)+1) + full = append(full, parentAddr...) + full = full.Child(callName, k) + ret = append(ret, full) + } + return ret +} + +func (m *expanderModule) resourceInstances(moduleAddr addrs.Module, resourceAddr addrs.Resource, parentAddr addrs.ModuleInstance) []addrs.AbsResourceInstance { + var ret []addrs.AbsResourceInstance + + if len(moduleAddr) > 0 { + // We need to traverse through the module levels first, so we can + // then iterate resource expansions in the context of each module + // path leading to them. + callName := moduleAddr[0] + if _, ok := m.moduleCalls[addrs.ModuleCall{Name: callName}]; !ok { + // This is a bug in the caller, because it should always register + // expansions for an object and all of its ancestors before requesting + // expansion of it. + panic(fmt.Sprintf("no expansion has been registered for %s", parentAddr.Child(callName, addrs.NoKey))) + } + + for step, inst := range m.childInstances { + if step.Name != callName { + continue + } + moduleInstAddr := append(parentAddr, step) + ret = append(ret, inst.resourceInstances(moduleAddr[1:], resourceAddr, moduleInstAddr)...) + } + return ret + } + + exp, ok := m.resources[resourceAddr] + if !ok { + panic(fmt.Sprintf("no expansion has been registered for %s", resourceAddr.Absolute(parentAddr))) + } + + for _, k := range exp.instanceKeys() { + // We're reusing the buffer under parentAddr as we recurse through + // the structure, so we need to copy it here to produce a final + // immutable slice to return. + moduleAddr := make(addrs.ModuleInstance, len(parentAddr)) + copy(moduleAddr, parentAddr) + ret = append(ret, resourceAddr.Instance(k).Absolute(moduleAddr)) + } + return ret +} diff --git a/instances/expander_test.go b/instances/expander_test.go new file mode 100644 index 000000000..63dd35113 --- /dev/null +++ b/instances/expander_test.go @@ -0,0 +1,458 @@ +package instances + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/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.ExpandResource( + 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.ExpandResource( + 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.ExpandResource( + 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.ExpandResource( + 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.ExpandResource( + 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) { + got := ex.ExpandResource( + mustModuleAddr(`single`), + count2ResourceAddr, + ) + want := []addrs.AbsResourceInstance{ + mustAbsResourceInstanceAddr(`module.single.test.count2[0]`), + mustAbsResourceInstanceAddr(`module.single.test.count2[1]`), + } + 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.ExpandResource( + 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.ExpandResource( + 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 resource count2 resource count2", func(t *testing.T) { + got := ex.ExpandResource( + 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 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.ExpandResource( + 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.ExpandResource( + 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.ExpandResource( + 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["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 mustResourceAddr(str string) addrs.Resource { + addr, diags := addrs.ParseAbsResourceStr(str) + if diags.HasErrors() { + panic(fmt.Sprintf("invalid resource address: %s", diags.Err())) + } + if !addr.Module.IsRoot() { + panic("invalid resource address: includes module path") + } + return addr.Resource +} + +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) +} diff --git a/instances/expansion_mode.go b/instances/expansion_mode.go new file mode 100644 index 000000000..be3393432 --- /dev/null +++ b/instances/expansion_mode.go @@ -0,0 +1,85 @@ +package instances + +import ( + "fmt" + "sort" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/addrs" +) + +// expansion is an internal interface used to represent the different +// ways expansion can operate depending on how repetition is configured for +// an object. +type expansion interface { + instanceKeys() []addrs.InstanceKey + repetitionData(addrs.InstanceKey) RepetitionData +} + +// expansionSingle is the expansion corresponding to no repetition arguments +// at all, producing a single object with no key. +// +// expansionSingleVal is the only valid value of this type. +type expansionSingle uintptr + +var singleKeys = []addrs.InstanceKey{addrs.NoKey} +var expansionSingleVal expansionSingle + +func (e expansionSingle) instanceKeys() []addrs.InstanceKey { + return singleKeys +} + +func (e expansionSingle) repetitionData(key addrs.InstanceKey) RepetitionData { + if key != addrs.NoKey { + panic("cannot use instance key with non-repeating object") + } + return RepetitionData{} +} + +// expansionCount is the expansion corresponding to the "count" argument. +type expansionCount int + +func (e expansionCount) instanceKeys() []addrs.InstanceKey { + ret := make([]addrs.InstanceKey, int(e)) + for i := range ret { + ret[i] = addrs.IntKey(i) + } + return ret +} + +func (e expansionCount) repetitionData(key addrs.InstanceKey) RepetitionData { + i := int(key.(addrs.IntKey)) + if i < 0 || i >= int(e) { + panic(fmt.Sprintf("instance key %d out of range for count %d", i, e)) + } + return RepetitionData{ + CountIndex: cty.NumberIntVal(int64(i)), + } +} + +// expansionForEach is the expansion corresponding to the "for_each" argument. +type expansionForEach map[string]cty.Value + +func (e expansionForEach) instanceKeys() []addrs.InstanceKey { + ret := make([]addrs.InstanceKey, 0, len(e)) + for k := range e { + ret = append(ret, addrs.StringKey(k)) + } + sort.Slice(ret, func(i, j int) bool { + return ret[i].(addrs.StringKey) < ret[j].(addrs.StringKey) + }) + return ret +} + +func (e expansionForEach) repetitionData(key addrs.InstanceKey) RepetitionData { + k := string(key.(addrs.StringKey)) + v, ok := e[k] + if !ok { + panic(fmt.Sprintf("instance key %q does not match any instance", k)) + } + return RepetitionData{ + EachKey: cty.StringVal(k), + EachValue: v, + } +} diff --git a/instances/instance_key_data.go b/instances/instance_key_data.go new file mode 100644 index 000000000..9ada5253c --- /dev/null +++ b/instances/instance_key_data.go @@ -0,0 +1,28 @@ +package instances + +import ( + "github.com/zclconf/go-cty/cty" +) + +// RepetitionData represents the values available to identify individual +// repetitions of a particular object. +// +// This corresponds to the each.key, each.value, and count.index symbols in +// the configuration language. +type RepetitionData struct { + // CountIndex is the value for count.index, or cty.NilVal if evaluating + // in a context where the "count" argument is not active. + // + // For correct operation, this should always be of type cty.Number if not + // nil. + CountIndex cty.Value + + // EachKey and EachValue are the values for each.key and each.value + // respectively, or cty.NilVal if evaluating in a context where the + // "for_each" argument is not active. These must either both be set + // or neither set. + // + // For correct operation, EachKey must always be either of type cty.String + // or cty.Number if not nil. + EachKey, EachValue cty.Value +} diff --git a/terraform/eval_context.go b/terraform/eval_context.go index 05e272600..d8dd55b4c 100644 --- a/terraform/eval_context.go +++ b/terraform/eval_context.go @@ -4,6 +4,7 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/configs/configschema" + "github.com/hashicorp/terraform/instances" "github.com/hashicorp/terraform/lang" "github.com/hashicorp/terraform/plans" "github.com/hashicorp/terraform/providers" @@ -152,4 +153,12 @@ type EvalContext interface { // State returns a wrapper object that provides safe concurrent access to // the global state. State() *states.SyncState + + // InstanceExpander returns a helper object for tracking the expansion of + // graph nodes during the plan phase in response to "count" and "for_each" + // arguments. + // + // The InstanceExpander is a global object that is shared across all of the + // EvalContext objects for a given configuration. + InstanceExpander() *instances.Expander }