Files
opentf/internal/tofu/transform_reference_test.go
Andrei Ciobanu 4077c3d84f Feature branch: Ephemeral resources (#2852)
Signed-off-by: Andrei Ciobanu <andrei.ciobanu@opentofu.org>
2025-08-04 16:39:12 +03:00

424 lines
11 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package tofu
import (
"reflect"
"sort"
"strings"
"testing"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/configs/configschema"
"github.com/opentofu/opentofu/internal/dag"
)
func TestReferenceTransformer_simple(t *testing.T) {
g := Graph{Path: addrs.RootModuleInstance}
g.Add(&graphNodeRefParentTest{
NameValue: "A",
Names: []string{"A"},
})
g.Add(&graphNodeRefChildTest{
NameValue: "B",
Refs: []string{"A"},
})
tf := &ReferenceTransformer{}
if err := tf.Transform(t.Context(), &g); err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTransformRefBasicStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestReferenceTransformer_self(t *testing.T) {
g := Graph{Path: addrs.RootModuleInstance}
g.Add(&graphNodeRefParentTest{
NameValue: "A",
Names: []string{"A"},
})
g.Add(&graphNodeRefChildTest{
NameValue: "B",
Refs: []string{"A", "B"},
})
tf := &ReferenceTransformer{}
if err := tf.Transform(t.Context(), &g); err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTransformRefBasicStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestReferenceTransformer_path(t *testing.T) {
g := Graph{Path: addrs.RootModuleInstance}
g.Add(&graphNodeRefParentTest{
NameValue: "A",
Names: []string{"A"},
})
g.Add(&graphNodeRefChildTest{
NameValue: "B",
Refs: []string{"A"},
})
g.Add(&graphNodeRefParentTest{
NameValue: "child.A",
PathValue: addrs.ModuleInstance{addrs.ModuleInstanceStep{Name: "child"}},
Names: []string{"A"},
})
g.Add(&graphNodeRefChildTest{
NameValue: "child.B",
PathValue: addrs.ModuleInstance{addrs.ModuleInstanceStep{Name: "child"}},
Refs: []string{"A"},
})
tf := &ReferenceTransformer{}
if err := tf.Transform(t.Context(), &g); err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTransformRefPathStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestReferenceTransformer_resourceInstances(t *testing.T) {
// Our reference analyses are all done based on unexpanded addresses
// so that we can use this transformer both in the plan graph (where things
// are not expanded yet) and the apply graph (where resource instances are
// pre-expanded but nothing else is.)
// However, that would make the result too conservative about instances
// of the same resource in different instances of the same module, so we
// make an exception for that situation in particular, keeping references
// between resource instances segregated by their containing module
// instance.
g := Graph{Path: addrs.RootModuleInstance}
moduleInsts := []addrs.ModuleInstance{
{
{
Name: "foo", InstanceKey: addrs.IntKey(0),
},
},
{
{
Name: "foo", InstanceKey: addrs.IntKey(1),
},
},
}
resourceAs := make([]addrs.AbsResourceInstance, len(moduleInsts))
for i, moduleInst := range moduleInsts {
resourceAs[i] = addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "thing",
Name: "a",
}.Instance(addrs.NoKey).Absolute(moduleInst)
}
resourceBs := make([]addrs.AbsResourceInstance, len(moduleInsts))
for i, moduleInst := range moduleInsts {
resourceBs[i] = addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "thing",
Name: "b",
}.Instance(addrs.NoKey).Absolute(moduleInst)
}
g.Add(&graphNodeFakeResourceInstance{
Addr: resourceAs[0],
})
g.Add(&graphNodeFakeResourceInstance{
Addr: resourceBs[0],
Refs: []*addrs.Reference{
{
Subject: resourceAs[0].Resource,
},
},
})
g.Add(&graphNodeFakeResourceInstance{
Addr: resourceAs[1],
})
g.Add(&graphNodeFakeResourceInstance{
Addr: resourceBs[1],
Refs: []*addrs.Reference{
{
Subject: resourceAs[1].Resource,
},
},
})
tf := &ReferenceTransformer{}
if err := tf.Transform(t.Context(), &g); err != nil {
t.Fatalf("unexpected error: %s", err)
}
// Resource B should be connected to resource A in each module instance,
// but there should be no connections between the two module instances.
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(`
module.foo[0].thing.a
module.foo[0].thing.b
module.foo[0].thing.a
module.foo[1].thing.a
module.foo[1].thing.b
module.foo[1].thing.a
`)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
// TestAttachResourceDependsOnTransformer performs a sanity check on the happy path
// of attachResourceDependsOnTransformer.
// This expects to find the dependency injected into the graph nodes that this
// transformer is working with (data sources and ephemeral resource).
func TestAttachResourceDependsOnTransformer(t *testing.T) {
cfg := testModuleInline(t, map[string]string{
"main.tf": `
resource "null_instance" "write" {
foo = "attribute"
}
data "null_data_source" "read" {
depends_on = ["null_instance.write"]
}
ephemeral "null_ephemeral" "open" {
depends_on = ["null_instance.write"]
}
`,
})
g := Graph{Path: addrs.RootModuleInstance}
resCfg := cfg.Module.ResourceByAddr(addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "null_instance",
Name: "write",
})
resAddr := resCfg.Addr().InModule(addrs.RootModule)
g.Add(&NodeAbstractResourceInstance{
NodeAbstractResource: NodeAbstractResource{Addr: resAddr, Config: resCfg},
Addr: addrs.AbsResourceInstance{
Module: addrs.ModuleInstance{},
Resource: addrs.ResourceInstance{
Resource: resCfg.Addr(),
Key: addrs.NoKey,
},
}})
dataCfg := cfg.Module.ResourceByAddr(addrs.Resource{
Mode: addrs.DataResourceMode,
Type: "null_data_source",
Name: "read",
})
dataAddr := dataCfg.Addr().InModule(addrs.RootModule)
dataNode := &NodeAbstractResourceInstance{
NodeAbstractResource: NodeAbstractResource{Addr: dataAddr, Config: dataCfg,
Schema: &configschema.Block{}, // Setting this strictly to force references processing (check GraphNodeReferencer)
},
Addr: addrs.AbsResourceInstance{
Module: addrs.ModuleInstance{},
Resource: addrs.ResourceInstance{
Resource: dataCfg.Addr(),
Key: addrs.NoKey,
},
},
}
g.Add(dataNode)
ephemeralCfg := cfg.Module.ResourceByAddr(addrs.Resource{
Mode: addrs.EphemeralResourceMode,
Type: "null_ephemeral",
Name: "open",
})
ephemeralAddr := ephemeralCfg.Addr().InModule(addrs.RootModule)
ephemeralNode := &NodeAbstractResourceInstance{
NodeAbstractResource: NodeAbstractResource{Addr: ephemeralAddr, Config: ephemeralCfg,
Schema: &configschema.Block{}, // Setting this strictly to force references processing (check GraphNodeReferencer)
},
Addr: addrs.AbsResourceInstance{
Module: addrs.ModuleInstance{},
Resource: addrs.ResourceInstance{
Resource: ephemeralCfg.Addr(),
Key: addrs.NoKey,
},
},
}
g.Add(ephemeralNode)
tr := &attachResourceDependsOnTransformer{}
err := tr.Transform(t.Context(), &g)
if err != nil {
t.Fatalf("expected no error. got %s", err)
}
if got, want := len(dataNode.dependsOn), 1; got != want {
t.Fatalf("wrong number of deps on the data source graph node. expected %d but got %d", want, got)
}
if got, want := dataNode.dependsOn[0], resAddr; !got.Equal(want) {
t.Fatalf("wrong reference registered as dependency. expected %+v but got %+v", want, got)
}
if got, want := len(ephemeralNode.dependsOn), 1; got != want {
t.Fatalf("wrong number of deps on the data source graph node. expected %d but got %d", want, got)
}
if got, want := ephemeralNode.dependsOn[0], resAddr; !got.Equal(want) {
t.Fatalf("wrong reference registered as dependency. expected %+v but got %+v", want, got)
}
}
func TestReferenceMapReferences(t *testing.T) {
cases := map[string]struct {
Nodes []dag.Vertex
Check dag.Vertex
Result []string
}{
"simple": {
Nodes: []dag.Vertex{
&graphNodeRefParentTest{
NameValue: "A",
Names: []string{"A"},
},
},
Check: &graphNodeRefChildTest{
NameValue: "foo",
Refs: []string{"A"},
},
Result: []string{"A"},
},
}
for tn, tc := range cases {
t.Run(tn, func(t *testing.T) {
rm := NewReferenceMap(tc.Nodes)
result := rm.References(tc.Check)
var resultStr []string
for _, v := range result {
resultStr = append(resultStr, dag.VertexName(v))
}
sort.Strings(resultStr)
sort.Strings(tc.Result)
if !reflect.DeepEqual(resultStr, tc.Result) {
t.Fatalf("bad: %#v", resultStr)
}
})
}
}
type graphNodeRefParentTest struct {
NameValue string
PathValue addrs.ModuleInstance
Names []string
}
var _ GraphNodeReferenceable = (*graphNodeRefParentTest)(nil)
func (n *graphNodeRefParentTest) Name() string {
return n.NameValue
}
func (n *graphNodeRefParentTest) ReferenceableAddrs() []addrs.Referenceable {
ret := make([]addrs.Referenceable, len(n.Names))
for i, name := range n.Names {
ret[i] = addrs.LocalValue{Name: name}
}
return ret
}
func (n *graphNodeRefParentTest) Path() addrs.ModuleInstance {
return n.PathValue
}
func (n *graphNodeRefParentTest) ModulePath() addrs.Module {
return n.PathValue.Module()
}
type graphNodeRefChildTest struct {
NameValue string
PathValue addrs.ModuleInstance
Refs []string
}
var _ GraphNodeReferencer = (*graphNodeRefChildTest)(nil)
func (n *graphNodeRefChildTest) Name() string {
return n.NameValue
}
func (n *graphNodeRefChildTest) References() []*addrs.Reference {
ret := make([]*addrs.Reference, len(n.Refs))
for i, name := range n.Refs {
ret[i] = &addrs.Reference{
Subject: addrs.LocalValue{Name: name},
}
}
return ret
}
func (n *graphNodeRefChildTest) Path() addrs.ModuleInstance {
return n.PathValue
}
func (n *graphNodeRefChildTest) ModulePath() addrs.Module {
return n.PathValue.Module()
}
type graphNodeFakeResourceInstance struct {
Addr addrs.AbsResourceInstance
Refs []*addrs.Reference
}
var _ GraphNodeResourceInstance = (*graphNodeFakeResourceInstance)(nil)
var _ GraphNodeReferenceable = (*graphNodeFakeResourceInstance)(nil)
var _ GraphNodeReferencer = (*graphNodeFakeResourceInstance)(nil)
func (n *graphNodeFakeResourceInstance) ResourceInstanceAddr() addrs.AbsResourceInstance {
return n.Addr
}
func (n *graphNodeFakeResourceInstance) ModulePath() addrs.Module {
return n.Addr.Module.Module()
}
func (n *graphNodeFakeResourceInstance) ReferenceableAddrs() []addrs.Referenceable {
return []addrs.Referenceable{n.Addr.Resource}
}
func (n *graphNodeFakeResourceInstance) References() []*addrs.Reference {
return n.Refs
}
func (n *graphNodeFakeResourceInstance) StateDependencies() []addrs.ConfigResource {
return nil
}
func (n *graphNodeFakeResourceInstance) String() string {
return n.Addr.String()
}
const testTransformRefBasicStr = `
A
B
A
`
const testTransformRefPathStr = `
A
B
A
child.A
child.B
child.A
`