opentofu/internal/instances/expander_test.go

538 lines
19 KiB
Go

// 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)
}