opentofu/terraform/transform_targets.go
James Bardin c99157c35b new targets transformer
This simplifies the initial targeting logic, and removes the complex
algorithm for finding descendants that result in output changes, which
hid bugs that failed with modules.

The targeting is handled in 2 phases. First we find all individual
resource nodes that are targeted, then add all their dependencies to the
set of targets. This in essence is all we need for targeting, and is
straightforward to understand.

The next phase is to add any root module outputs that can be solely
derived from the set of targeted resources. There is currently no way to
target outputs themselves, so this is how we can allow these to be
updated as part of a target.

Rather than attempting to backtrack through the graph to find candidate
outputs, requiring each node on the chain to properly advertise if it
could be traversed, then backtracking again to determine if the
candidate is valid (which often got "off course"), we can start directly
from the outputs themselves. The algorithm here is simpler: if all the
root output's resource dependencies are targeted, add that output and
its dependencies to the targeted set.
2020-06-24 10:27:52 -04:00

188 lines
5.9 KiB
Go

package terraform
import (
"log"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/dag"
)
// GraphNodeTargetable is an interface for graph nodes to implement when they
// need to be told about incoming targets. This is useful for nodes that need
// to respect targets as they dynamically expand. Note that the list of targets
// provided will contain every target provided, and each implementing graph
// node must filter this list to targets considered relevant.
type GraphNodeTargetable interface {
SetTargets([]addrs.Targetable)
}
// GraphNodeTargetDownstream is an interface for graph nodes that need to
// be remain present under targeting if any of their dependencies are targeted.
// TargetDownstream is called with the set of vertices that are direct
// dependencies for the node, and it should return true if the node must remain
// in the graph in support of those dependencies.
//
// This is used in situations where the dependency edges are representing an
// ordering relationship but the dependency must still be visited if its
// dependencies are visited. This is true for outputs, for example, since
// they must get updated if any of their dependent resources get updated,
// which would not normally be true if one of their dependencies were targeted.
type GraphNodeTargetDownstream interface {
TargetDownstream(targeted, untargeted dag.Set) bool
}
// TargetsTransformer is a GraphTransformer that, when the user specifies a
// list of resources to target, limits the graph to only those resources and
// their dependencies.
type TargetsTransformer struct {
// List of targeted resource names specified by the user
Targets []addrs.Targetable
// If set, the index portions of resource addresses will be ignored
// for comparison. This is used when transforming a graph where
// counted resources have not yet been expanded, since otherwise
// the unexpanded nodes (which never have indices) would not match.
IgnoreIndices bool
}
func (t *TargetsTransformer) Transform(g *Graph) error {
if len(t.Targets) > 0 {
targetedNodes, err := t.selectTargetedNodes(g, t.Targets)
if err != nil {
return err
}
for _, v := range g.Vertices() {
if !targetedNodes.Include(v) {
log.Printf("[DEBUG] Removing %q, filtered by targeting.", dag.VertexName(v))
g.Remove(v)
}
}
}
return nil
}
// Returns a set of targeted nodes. A targeted node is either addressed
// directly, address indirectly via its container, or it's a dependency of a
// targeted node.
func (t *TargetsTransformer) selectTargetedNodes(g *Graph, addrs []addrs.Targetable) (dag.Set, error) {
targetedNodes := make(dag.Set)
vertices := g.Vertices()
for _, v := range vertices {
if t.nodeIsTarget(v, addrs) {
targetedNodes.Add(v)
// We inform nodes that ask about the list of targets - helps for nodes
// that need to dynamically expand. Note that this only occurs for nodes
// that are already directly targeted.
if tn, ok := v.(GraphNodeTargetable); ok {
tn.SetTargets(addrs)
}
deps, _ := g.Ancestors(v)
for _, d := range deps {
targetedNodes.Add(d)
}
}
}
// It is expected that outputs which are only derived from targeted
// resources are also updated. While we don't include any other possible
// side effects from the targeted nodes, these are added because outputs
// cannot be targeted on their own.
// Start by finding the root module output nodes themselves
for _, v := range vertices {
// outputs are all temporary value types
tv, ok := v.(graphNodeTemporaryValue)
if !ok {
continue
}
// root module outputs indicate that while they are an output type,
// they not temporary and will return false here.
if tv.temporaryValue() {
continue
}
// If this output is descended only from targeted resources, then we
// will keep it
deps, _ := g.Ancestors(v)
found := 0
for _, d := range deps {
switch d.(type) {
case GraphNodeResourceInstance:
case GraphNodeConfigResource:
default:
continue
}
if !targetedNodes.Include(d) {
// this dependency isn't being targeted, so we can't process this
// output
found = 0
break
}
found++
}
if found > 0 {
// we found an output we can keep; add it, and all it's dependencies
targetedNodes.Add(v)
for _, d := range deps {
targetedNodes.Add(d)
}
}
}
return targetedNodes, nil
}
func (t *TargetsTransformer) nodeIsTarget(v dag.Vertex, targets []addrs.Targetable) bool {
var vertexAddr addrs.Targetable
switch r := v.(type) {
case GraphNodeResourceInstance:
vertexAddr = r.ResourceInstanceAddr()
case GraphNodeConfigResource:
vertexAddr = r.ResourceAddr()
default:
// Only resource and resource instance nodes can be targeted.
return false
}
for _, targetAddr := range targets {
if t.IgnoreIndices {
// If we're ignoring indices then we'll convert any resource instance
// addresses into resource addresses. We don't need to convert
// vertexAddr because instance addresses are contained within
// their associated resources, and so .TargetContains will take
// care of this for us.
switch instance := targetAddr.(type) {
case addrs.AbsResourceInstance:
targetAddr = instance.ContainingResource().Config()
case addrs.ModuleInstance:
targetAddr = instance.Module()
}
}
if targetAddr.TargetContains(vertexAddr) {
return true
}
}
return false
}
// RemovableIfNotTargeted is a special interface for graph nodes that
// aren't directly addressable, but need to be removed from the graph when they
// are not targeted. (Nodes that are not directly targeted end up in the set of
// targeted nodes because something that _is_ targeted depends on them.) The
// initial use case for this interface is GraphNodeConfigVariable, which was
// having trouble interpolating for module variables in targeted scenarios that
// filtered out the resource node being referenced.
type RemovableIfNotTargeted interface {
RemoveIfNotTargeted() bool
}