opentofu/instances/expander.go
Martin Atkins 1dece66b10 instances: A package for module/resource reptition
This package aims to encapsulate the module/resource repetition problem
so that Terraform Core's graph node DynamicExpand implementations can be
simpler.

This is also a building block on the path towards module repetition, by
modelling the recursive expansion of modules and their contents. This will
allow the Terraform Core plan graph to have one node per configuration
construct, each of which will DynamicExpand into as many sub-nodes as
necessary to cover all of the recursive module instantiations.

For the moment this is just dead code, because Terraform Core isn't yet
updated to use it.
2020-02-14 15:20:07 -08:00

320 lines
13 KiB
Go

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
}