blob: d3b21c51bbc1fe925d26462e59406e97c9eada9c [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package graph
import (
"slices"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/dag"
"github.com/hashicorp/terraform/internal/moduletest"
"github.com/hashicorp/terraform/internal/terraform"
)
var (
_ GraphNodeExecutable = &TeardownSubgraph{}
_ Subgrapher = &TeardownSubgraph{}
)
type Subgrapher interface {
isSubGrapher()
}
// TeardownSubgraph is a subgraph for cleaning up the state of
// resources defined in the state files created by the test runs.
type TeardownSubgraph struct {
opts *graphOptions
parent *terraform.Graph
}
func (b *TeardownSubgraph) Execute(ctx *EvalContext) {
ctx.Renderer().File(b.opts.File, moduletest.TearDown)
// work out the transitive state dependencies for each run node in the parent graph
runRefMap := make(map[addrs.Run][]string)
for runNode := range dag.SelectSeq[*NodeTestRun](b.parent.VerticesSeq()) {
refs := b.parent.Ancestors(runNode)
for _, ref := range refs {
if ref, ok := ref.(*NodeTestRun); ok && ref.run.GetStateKey() != runNode.run.GetStateKey() {
runRefMap[runNode.run.Addr()] = append(runRefMap[runNode.run.Addr()], ref.run.GetStateKey())
}
}
}
// Create a new graph for the cleanup nodes
g, diags := (&terraform.BasicGraphBuilder{
Steps: []terraform.GraphTransformer{
&TestStateCleanupTransformer{opts: b.opts, runStateRefs: runRefMap},
&CloseTestGraphTransformer{},
&terraform.TransitiveReductionTransformer{},
},
Name: "TeardownSubgraph",
}).Build(addrs.RootModuleInstance)
b.opts.File.AppendDiagnostics(diags)
if diags.HasErrors() {
return
}
diags = Walk(g, ctx)
b.opts.File.AppendDiagnostics(diags)
}
func (b *TeardownSubgraph) isSubGrapher() {}
// TestStateCleanupTransformer is a GraphTransformer that adds a cleanup node
// for each state that is created by the test runs.
type TestStateCleanupTransformer struct {
opts *graphOptions
runStateRefs map[addrs.Run][]string
}
func (t *TestStateCleanupTransformer) Transform(g *terraform.Graph) error {
cleanupMap := make(map[string]*NodeStateCleanup)
arr := make([]*NodeStateCleanup, 0, len(t.opts.File.Runs))
// dependency map for state keys, which will be used to traverse
// the cleanup nodes in a depth-first manner.
depStateKeys := make(map[string][]string)
// iterate in reverse order of the run index, so that the last run for each state key
// is attached to the cleanup node.
for _, run := range slices.Backward(t.opts.File.Runs) {
key := run.GetStateKey()
if _, exists := cleanupMap[key]; !exists {
node := &NodeStateCleanup{
stateKey: key,
opts: t.opts,
parallel: run.Config.Parallel,
}
cleanupMap[key] = node
arr = append(arr, node)
g.Add(node)
// The dependency map for the state's last run will be used for the cleanup node.
depStateKeys[key] = t.runStateRefs[run.Addr()]
continue
}
// if one of the runs for this state key is not parallel, then
// the cleanup node should not be parallel either.
cleanupMap[key].parallel = cleanupMap[key].parallel && run.Config.Parallel
}
// Depth-first traversal to connect the cleanup nodes based on their dependencies.
// If an edge would create a cycle, we skip it.
visited := make(map[string]bool)
for _, node := range arr {
t.depthFirstTraverse(g, node, visited, cleanupMap, depStateKeys)
}
ControlParallelism(g, arr)
return nil
}
func (t *TestStateCleanupTransformer) depthFirstTraverse(g *terraform.Graph, node *NodeStateCleanup, visited map[string]bool, cleanupNodes map[string]*NodeStateCleanup, depStateKeys map[string][]string) {
// return if node is leaf or has already been visited
if visited[node.stateKey] || len(depStateKeys[node.stateKey]) == 0 {
return
}
// don't mark the node as visited if it's a leaf node, this ensures that other dependencies are still added to it
if len(depStateKeys[node.stateKey]) == 0 {
return
}
visited[node.stateKey] = true
for _, refStateKey := range depStateKeys[node.stateKey] {
// If the reference node has already been visited, skip it.
if visited[refStateKey] {
continue
}
refNode := cleanupNodes[refStateKey]
// leave non-parallel nodes out of this. Their sequential connections
// will be handled later.
if node.parallel && refNode.parallel {
g.Connect(dag.BasicEdge(refNode, node))
}
t.depthFirstTraverse(g, refNode, visited, cleanupNodes, depStateKeys)
}
}