Skip to content

Commit 24c45fc

Browse files
committed
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.
1 parent 70999b1 commit 24c45fc

5 files changed

Lines changed: 82 additions & 4 deletions

File tree

terraform/context_plan_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2114,6 +2114,43 @@ module.child:
21142114
}
21152115
}
21162116

2117+
func TestContext2Plan_targetedModuleUntargetedVariable(t *testing.T) {
2118+
m := testModule(t, "plan-targeted-module-untargeted-variable")
2119+
p := testProvider("aws")
2120+
p.DiffFn = testDiffFn
2121+
ctx := testContext2(t, &ContextOpts{
2122+
Module: m,
2123+
Providers: map[string]ResourceProviderFactory{
2124+
"aws": testProviderFuncFixed(p),
2125+
},
2126+
Targets: []string{"aws_instance.blue", "module.blue_mod"},
2127+
})
2128+
2129+
plan, err := ctx.Plan()
2130+
if err != nil {
2131+
t.Fatalf("err: %s", err)
2132+
}
2133+
2134+
actual := strings.TrimSpace(plan.String())
2135+
expected := strings.TrimSpace(`
2136+
DIFF:
2137+
2138+
CREATE: aws_instance.blue
2139+
2140+
module.blue_mod:
2141+
CREATE: aws_instance.mod
2142+
type: "" => "aws_instance"
2143+
value: "" => "<computed>"
2144+
2145+
STATE:
2146+
2147+
<no state>
2148+
`)
2149+
if actual != expected {
2150+
t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, actual)
2151+
}
2152+
}
2153+
21172154
// https://github.com/hashicorp/terraform/issues/4515
21182155
func TestContext2Plan_targetedOverTen(t *testing.T) {
21192156
m := testModule(t, "plan-targeted-over-ten")

terraform/graph_config_node_variable.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ func (n *GraphNodeConfigVariable) DependableName() []string {
3636
return []string{n.Name()}
3737
}
3838

39+
// RemoveIfNotTargeted implements RemovableIfNotTargeted.
40+
// When targeting is active, variables that are not targeted should be removed
41+
// from the graph, because otherwise module variables trying to interpolate
42+
// their references can fail when they're missing the referent resource node.
43+
func (n *GraphNodeConfigVariable) RemoveIfNotTargeted() bool {
44+
return true
45+
}
46+
3947
func (n *GraphNodeConfigVariable) DependentOn() []string {
4048
// If we don't have any value set, we don't depend on anything
4149
if n.Value == nil {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
variable "id" {}
2+
3+
resource "aws_instance" "mod" {
4+
value = "${var.id}"
5+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
resource "aws_instance" "blue" { }
2+
resource "aws_instance" "green" { }
3+
4+
module "blue_mod" {
5+
source = "./child"
6+
id = "${aws_instance.blue.id}"
7+
}
8+
9+
module "green_mod" {
10+
source = "./child"
11+
id = "${aws_instance.green.id}"
12+
}

terraform/transform_targets.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,16 @@ func (t *TargetsTransformer) Transform(g *Graph) error {
3737
}
3838

3939
for _, v := range g.Vertices() {
40+
removable := false
4041
if _, ok := v.(GraphNodeAddressable); ok {
41-
if !targetedNodes.Include(v) {
42-
log.Printf("[DEBUG] Removing %q, filtered by targeting.", dag.VertexName(v))
43-
g.Remove(v)
44-
}
42+
removable = true
43+
}
44+
if vr, ok := v.(RemovableIfNotTargeted); ok {
45+
removable = vr.RemoveIfNotTargeted()
46+
}
47+
if removable && !targetedNodes.Include(v) {
48+
log.Printf("[DEBUG] Removing %q, filtered by targeting.", dag.VertexName(v))
49+
g.Remove(v)
4550
}
4651
}
4752
}
@@ -110,3 +115,14 @@ func (t *TargetsTransformer) nodeIsTarget(
110115
}
111116
return false
112117
}
118+
119+
// RemovableIfNotTargeted is a special interface for graph nodes that
120+
// aren't directly addressable, but need to be removed from the graph when they
121+
// are not targeted. (Nodes that are not directly targeted end up in the set of
122+
// targeted nodes because something that _is_ targeted depends on them.) The
123+
// initial use case for this interface is GraphNodeConfigVariable, which was
124+
// having trouble interpolating for module variables in targeted scenarios that
125+
// filtered out the resource node being referenced.
126+
type RemovableIfNotTargeted interface {
127+
RemoveIfNotTargeted() bool
128+
}

0 commit comments

Comments
 (0)