268 lines
8.4 KiB
Go
268 lines
8.4 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
|
|
|
|
// 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 {
|
|
targetedNodes, err := t.selectTargetedNodes(g, t.Targets)
|
|
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
|
|
}
|
|
|
|
// 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. Destroy mode keeps dependents instead of dependencies.
|
|
func (t *TargetsTransformer) selectTargetedNodes(g *Graph, addrs []addrs.Targetable) (*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 {
|
|
// providers don't cause transitive dependencies, so don't target
|
|
// downstream from them.
|
|
if _, ok := v.(GraphNodeProvider); ok {
|
|
continue
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
if !targetedNodes.Include(d) {
|
|
// this one is going to be removed, so it doesn't count
|
|
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, targets []addrs.Targetable) bool {
|
|
var vertexAddr addrs.Targetable
|
|
switch r := v.(type) {
|
|
case GraphNodeResourceInstance:
|
|
vertexAddr = r.ResourceInstanceAddr()
|
|
case GraphNodeResource:
|
|
vertexAddr = r.ResourceAddr()
|
|
default:
|
|
// Only resource and resource instance nodes can be targeted.
|
|
return false
|
|
}
|
|
_, ok := v.(GraphNodeResource)
|
|
if !ok {
|
|
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.
|
|
if instance, isInstance := targetAddr.(addrs.AbsResourceInstance); isInstance {
|
|
targetAddr = instance.ContainingResource()
|
|
}
|
|
}
|
|
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
|
|
}
|