terraform/terraform/transform_targets.go

269 lines
8.1 KiB
Go

package terraform
import (
"log"
"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([]ResourceAddress)
}
// 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 []string
// List of parsed targets, provided by callers like ResourceCountTransform
// that already have the targets parsed
ParsedTargets []ResourceAddress
// 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
// Set to true when we're in a `terraform destroy` or a
// `terraform plan -destroy`
Destroy bool
}
func (t *TargetsTransformer) Transform(g *Graph) error {
if len(t.Targets) > 0 && len(t.ParsedTargets) == 0 {
addrs, err := t.parseTargetAddresses()
if err != nil {
return err
}
t.ParsedTargets = addrs
}
if len(t.ParsedTargets) > 0 {
targetedNodes, err := t.selectTargetedNodes(g, t.ParsedTargets)
if err != nil {
return err
}
for _, v := range g.Vertices() {
removable := false
if _, ok := v.(GraphNodeResource); ok {
removable = true
}
if vr, ok := v.(RemovableIfNotTargeted); ok {
removable = vr.RemoveIfNotTargeted()
}
if removable && !targetedNodes.Include(v) {
log.Printf("[DEBUG] Removing %q, filtered by targeting.", dag.VertexName(v))
g.Remove(v)
}
}
}
return nil
}
func (t *TargetsTransformer) parseTargetAddresses() ([]ResourceAddress, error) {
addrs := make([]ResourceAddress, len(t.Targets))
for i, target := range t.Targets {
ta, err := ParseResourceAddress(target)
if err != nil {
return nil, err
}
addrs[i] = *ta
}
return addrs, nil
}
// Returns the list of targeted nodes. A targeted node is either addressed
// directly, or is an Ancestor of a targeted node. Destroy mode keeps
// Descendents instead of Ancestors.
func (t *TargetsTransformer) selectTargetedNodes(
g *Graph, addrs []ResourceAddress) (*dag.Set, error) {
targetedNodes := new(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)
}
var deps *dag.Set
var err error
if t.Destroy {
deps, err = g.Descendents(v)
} else {
deps, err = g.Ancestors(v)
}
if err != nil {
return nil, err
}
for _, d := range deps.List() {
targetedNodes.Add(d)
}
}
}
return t.addDependencies(targetedNodes, g)
}
func (t *TargetsTransformer) addDependencies(targetedNodes *dag.Set, g *Graph) (*dag.Set, error) {
// Handle nodes that need to be included if their dependencies are included.
// This requires multiple passes since we need to catch transitive
// dependencies if and only if they are via other nodes that also
// support TargetDownstream. For example:
// output -> output -> targeted-resource: both outputs need to be targeted
// output -> non-targeted-resource -> targeted-resource: output not targeted
//
// We'll keep looping until we stop targeting more nodes.
queue := targetedNodes.List()
for len(queue) > 0 {
vertices := queue
queue = nil // ready to append for next iteration if neccessary
for _, v := range vertices {
dependers := g.UpEdges(v)
if dependers == nil {
// indicates that there are no up edges for this node, so
// we have nothing to do here.
continue
}
dependers = dependers.Filter(func(dv interface{}) bool {
_, ok := dv.(GraphNodeTargetDownstream)
return ok
})
if dependers.Len() == 0 {
continue
}
for _, dv := range dependers.List() {
if targetedNodes.Include(dv) {
// Already present, so nothing to do
continue
}
// We'll give the node some information about what it's
// depending on in case that informs its decision about whether
// it is safe to be targeted.
deps := g.DownEdges(v)
depsTargeted := deps.Intersection(targetedNodes)
depsUntargeted := deps.Difference(depsTargeted)
if dv.(GraphNodeTargetDownstream).TargetDownstream(depsTargeted, depsUntargeted) {
targetedNodes.Add(dv)
// Need to visit this node on the next pass to see if it
// has any transitive dependers.
queue = append(queue, dv)
}
}
}
}
return targetedNodes.Filter(func(dv interface{}) bool {
return filterPartialOutputs(dv, targetedNodes, g)
}), nil
}
// Outputs may have been included transitively, but if any of their
// dependencies have been pruned they won't be resolvable.
// If nothing depends on the output, and the output is missing any
// dependencies, remove it from the graph.
// This essentially maintains the previous behavior where interpolation in
// outputs would fail silently, but can now surface errors where the output
// is required.
func filterPartialOutputs(v interface{}, targetedNodes *dag.Set, g *Graph) bool {
// should this just be done with TargetDownstream?
if _, ok := v.(*NodeApplyableOutput); !ok {
return true
}
dependers := g.UpEdges(v)
for _, d := range dependers.List() {
if _, ok := d.(*NodeCountBoundary); ok {
continue
}
// as soon as we see a real dependency, we mark this as
// non-removable
return true
}
depends := g.DownEdges(v)
for _, d := range depends.List() {
if !targetedNodes.Include(d) {
log.Printf("[WARN] %s missing targeted dependency %s, removing from the graph",
dag.VertexName(v), dag.VertexName(d))
return false
}
}
return true
}
func (t *TargetsTransformer) nodeIsTarget(
v dag.Vertex, addrs []ResourceAddress) bool {
r, ok := v.(GraphNodeResource)
if !ok {
return false
}
addr := r.ResourceAddr()
for _, targetAddr := range addrs {
if t.IgnoreIndices {
// targetAddr is not a pointer, so we can safely mutate it without
// interfering with references elsewhere.
targetAddr.Index = -1
}
if targetAddr.Contains(addr) {
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
}