terraform: Filter untargeted variable nodes

When targeting, only Addressable untargeted nodes were being removed
from the graph. Variable nodes are not directly Addressable, so they
were hanging around. This caused problems with module variables that
referred to Resource nodes. The Resource node would be filtered out of
the graph, but the module Variable node would not, so it would try to
interpolate during the graph walk and be unable to find it's referent.

This would present itself as strange "cannot find variable" errors for
variables that were uninvolved with the currently targeted set of
resources.

Here, we introduce a new interface that can be implemented by graph
nodes to indicate they should be filtered out from targeting even though
they are not directly addressable themselves.
This commit is contained in:
Paul Hinze 2016-07-29 10:53:13 -05:00
parent 70999b1a64
commit 24c45fcd5d
No known key found for this signature in database
GPG Key ID: B69DEDF2D55501C0
5 changed files with 82 additions and 4 deletions

View File

@ -2114,6 +2114,43 @@ module.child:
} }
} }
func TestContext2Plan_targetedModuleUntargetedVariable(t *testing.T) {
m := testModule(t, "plan-targeted-module-untargeted-variable")
p := testProvider("aws")
p.DiffFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
Targets: []string{"aws_instance.blue", "module.blue_mod"},
})
plan, err := ctx.Plan()
if err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(plan.String())
expected := strings.TrimSpace(`
DIFF:
CREATE: aws_instance.blue
module.blue_mod:
CREATE: aws_instance.mod
type: "" => "aws_instance"
value: "" => "<computed>"
STATE:
<no state>
`)
if actual != expected {
t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, actual)
}
}
// https://github.com/hashicorp/terraform/issues/4515 // https://github.com/hashicorp/terraform/issues/4515
func TestContext2Plan_targetedOverTen(t *testing.T) { func TestContext2Plan_targetedOverTen(t *testing.T) {
m := testModule(t, "plan-targeted-over-ten") m := testModule(t, "plan-targeted-over-ten")

View File

@ -36,6 +36,14 @@ func (n *GraphNodeConfigVariable) DependableName() []string {
return []string{n.Name()} return []string{n.Name()}
} }
// RemoveIfNotTargeted implements RemovableIfNotTargeted.
// When targeting is active, variables that are not targeted should be removed
// from the graph, because otherwise module variables trying to interpolate
// their references can fail when they're missing the referent resource node.
func (n *GraphNodeConfigVariable) RemoveIfNotTargeted() bool {
return true
}
func (n *GraphNodeConfigVariable) DependentOn() []string { func (n *GraphNodeConfigVariable) DependentOn() []string {
// If we don't have any value set, we don't depend on anything // If we don't have any value set, we don't depend on anything
if n.Value == nil { if n.Value == nil {

View File

@ -0,0 +1,5 @@
variable "id" {}
resource "aws_instance" "mod" {
value = "${var.id}"
}

View File

@ -0,0 +1,12 @@
resource "aws_instance" "blue" { }
resource "aws_instance" "green" { }
module "blue_mod" {
source = "./child"
id = "${aws_instance.blue.id}"
}
module "green_mod" {
source = "./child"
id = "${aws_instance.green.id}"
}

View File

@ -37,14 +37,19 @@ func (t *TargetsTransformer) Transform(g *Graph) error {
} }
for _, v := range g.Vertices() { for _, v := range g.Vertices() {
removable := false
if _, ok := v.(GraphNodeAddressable); ok { if _, ok := v.(GraphNodeAddressable); ok {
if !targetedNodes.Include(v) { 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)) log.Printf("[DEBUG] Removing %q, filtered by targeting.", dag.VertexName(v))
g.Remove(v) g.Remove(v)
} }
} }
} }
}
return nil return nil
} }
@ -110,3 +115,14 @@ func (t *TargetsTransformer) nodeIsTarget(
} }
return false 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
}