Skip to content

Commit 86b6750

Browse files
author
Liam Cervante
authored
Fix missing validation for count and for-each meta-arguments (#35432)
1 parent 634155f commit 86b6750

File tree

6 files changed

+208
-56
lines changed

6 files changed

+208
-56
lines changed

internal/terraform/context_plan2_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5783,3 +5783,67 @@ resource "test_object" "obj" {
57835783
t.Errorf("unexpected diags\n%s", diags)
57845784
}
57855785
}
5786+
5787+
func TestContext2Plan_selfReferences(t *testing.T) {
5788+
tcs := []struct {
5789+
attribute string
5790+
}{
5791+
// Note here, the type returned by the lookup doesn't really matter as
5792+
// we should safely fail before we even get to type checking.
5793+
{
5794+
attribute: "count = test_object.a[0].test_string",
5795+
},
5796+
{
5797+
attribute: "count = test_object.a[*].test_string",
5798+
},
5799+
{
5800+
attribute: "for_each = test_object.a[0].test_string",
5801+
},
5802+
{
5803+
attribute: "for_each = test_object.a[*].test_string",
5804+
},
5805+
// Even though the can and try functions might normally allow some
5806+
// fairly crazy things, we're still going to put a stop to a self
5807+
// reference since it is more akin to a compilation error than some kind
5808+
// of dynamic exception.
5809+
{
5810+
attribute: "for_each = can(test_object.a[0].test_string) ? 0 : 1",
5811+
},
5812+
{
5813+
attribute: "count = try(test_object.a[0].test_string, 0)",
5814+
},
5815+
}
5816+
for _, tc := range tcs {
5817+
t.Run(tc.attribute, func(t *testing.T) {
5818+
tmpl := `
5819+
resource "test_object" "a" {
5820+
%%attribute%%
5821+
}
5822+
`
5823+
module := strings.ReplaceAll(tmpl, "%%attribute%%", tc.attribute)
5824+
m := testModuleInline(t, map[string]string{
5825+
"main.tf": module,
5826+
})
5827+
5828+
p := simpleMockProvider()
5829+
ctx := testContext2(t, &ContextOpts{
5830+
Providers: map[addrs.Provider]providers.Factory{
5831+
// The providers never actually going to get called here, we should
5832+
// catch the error long before anything happens.
5833+
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
5834+
},
5835+
})
5836+
5837+
_, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
5838+
if len(diags) != 1 {
5839+
t.Fatalf("expected one diag, got %d: %s", len(diags), diags.ErrWithWarnings())
5840+
}
5841+
5842+
got, want := diags.Err().Error(), "Self-referential block: Configuration for test_object.a may not refer to itself."
5843+
if cmp.Diff(want, got) != "" {
5844+
t.Fatalf("unexpected error\n%s", cmp.Diff(want, got))
5845+
}
5846+
})
5847+
}
5848+
5849+
}

internal/terraform/context_plan_import_test.go

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1657,7 +1657,44 @@ resource "test_object" "a" {
16571657

16581658
// We're expecting exactly one diag, which is the self-reference error.
16591659
if len(diags) != 1 {
1660-
t.Fatalf("expected one diag, got %d", len(diags))
1660+
t.Fatalf("expected one diag, got %d: %s", len(diags), diags.ErrWithWarnings())
1661+
}
1662+
1663+
got, want := diags.Err().Error(), "Invalid import id argument: The import ID cannot reference the resource being imported."
1664+
if cmp.Diff(want, got) != "" {
1665+
t.Fatalf("unexpected error\n%s", cmp.Diff(want, got))
1666+
}
1667+
}
1668+
1669+
func TestContext2Plan_importSelfReferenceInstanceRef(t *testing.T) {
1670+
m := testModuleInline(t, map[string]string{
1671+
"main.tf": `
1672+
import {
1673+
to = test_object.a
1674+
id = test_object.a[0].test_string
1675+
}
1676+
1677+
resource "test_object" "a" {
1678+
count = 1
1679+
test_string = "foo"
1680+
}
1681+
`,
1682+
})
1683+
1684+
p := simpleMockProvider()
1685+
ctx := testContext2(t, &ContextOpts{
1686+
Providers: map[addrs.Provider]providers.Factory{
1687+
// The providers never actually going to get called here, we should
1688+
// catch the error long before anything happens.
1689+
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
1690+
},
1691+
})
1692+
1693+
_, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
1694+
1695+
// We're expecting exactly one diag, which is the self-reference error.
1696+
if len(diags) != 1 {
1697+
t.Fatalf("expected one diag, got %d: %s", len(diags), diags.ErrWithWarnings())
16611698
}
16621699

16631700
got, want := diags.Err().Error(), "Invalid import id argument: The import ID cannot reference the resource being imported."
@@ -1739,7 +1776,7 @@ output "foo" {
17391776

17401777
// We're expecting exactly one diag, which is the self-reference error.
17411778
if len(diags) != 1 {
1742-
t.Fatalf("expected one diag, got %d", len(diags))
1779+
t.Fatalf("expected one diag, got %d: %s", len(diags), diags.ErrWithWarnings())
17431780
}
17441781

17451782
// Terraform detects this case as a cycle, and the order of rendering the

internal/terraform/eval_import.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func evaluateImportIdExpression(expr hcl.Expression, target addrs.AbsResourceIns
2828
})
2929
}
3030

31-
diags = diags.Append(validateSelfRefFromImport(target.Resource.Resource, expr))
31+
diags = diags.Append(validateImportSelfRef(target.Resource.Resource, expr))
3232
if diags.HasErrors() {
3333
return cty.NilVal, diags
3434
}

internal/terraform/node_resource_plan.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@ func (n *nodeExpandPlannableResource) DynamicExpand(ctx EvalContext) (*Graph, tf
103103
// resource. The config maybe nil if we are generating configuration, or
104104
// deleting a resource.
105105
if n.Config != nil {
106-
diags = diags.Append(validateSelfRefInExpr(n.Addr.Resource, n.Config.Count))
107-
diags = diags.Append(validateSelfRefInExpr(n.Addr.Resource, n.Config.ForEach))
106+
diags = diags.Append(validateMetaSelfRef(n.Addr.Resource, n.Config.Count))
107+
diags = diags.Append(validateMetaSelfRef(n.Addr.Resource, n.Config.ForEach))
108108
if diags.HasErrors() {
109109
return nil, diags
110110
}

internal/terraform/validate_selfref.go

Lines changed: 54 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -58,63 +58,69 @@ func validateSelfRef(addr addrs.Referenceable, config hcl.Body, providerSchema p
5858
return diags
5959
}
6060

61-
// validateSelfRefInExpr checks to ensure that a specific expression does not
62-
// reference the same block that it is contained within.
63-
func validateSelfRefInExpr(addr addrs.Referenceable, expr hcl.Expression) tfdiags.Diagnostics {
64-
var diags tfdiags.Diagnostics
65-
66-
addrStrs := make([]string, 0, 1)
67-
addrStrs = append(addrStrs, addr.String())
68-
switch tAddr := addr.(type) {
69-
case addrs.ResourceInstance:
70-
// A resource instance may not refer to its containing resource either.
71-
addrStrs = append(addrStrs, tAddr.ContainingResource().String())
72-
}
73-
74-
refs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, expr)
75-
for _, ref := range refs {
76-
77-
for _, addrStr := range addrStrs {
78-
if ref.Subject.String() == addrStr {
79-
diags = diags.Append(&hcl.Diagnostic{
80-
Severity: hcl.DiagError,
81-
Summary: "Self-referential block",
82-
Detail: fmt.Sprintf("Configuration for %s may not refer to itself.", addrStr),
83-
Subject: ref.SourceRange.ToHCL().Ptr(),
84-
})
85-
}
61+
// validateMetaSelfRef checks to ensure that a specific meta expression (count /
62+
// for_each) does not reference the resource it is attached to. The behaviour
63+
// is slightly different from validateSelfRef in that this function is only ever
64+
// called from static contexts (ie. before expansion) and as such the address is
65+
// always a Resource.
66+
//
67+
// This also means that often the references will be to instances of the
68+
// resource, so we need to unpack these to the containing resource to compare
69+
// against the static resource. From the perspective of this function
70+
// `test_resource.foo[4]` is considered to be a self reference to
71+
// `test_resource.foo`, in which is a significant behaviour change to
72+
// validateSelfRef.
73+
func validateMetaSelfRef(addr addrs.Resource, expr hcl.Expression) tfdiags.Diagnostics {
74+
return validateSelfRefFromExprInner(addr, expr, func(ref *addrs.Reference) *hcl.Diagnostic {
75+
return &hcl.Diagnostic{
76+
Severity: hcl.DiagError,
77+
Summary: "Self-referential block",
78+
Detail: fmt.Sprintf("Configuration for %s may not refer to itself.", addr.String()),
79+
Subject: ref.SourceRange.ToHCL().Ptr(),
8680
}
87-
}
88-
89-
return diags
81+
})
9082
}
9183

92-
// validateSelfRefFromImport is similar to validateSelfRefInExpr except it
84+
// validateImportSelfRef is similar to validateMetaSelfRef except it
9385
// tweaks the error message slightly to reflect the self-reference is coming
94-
// from an import block instead of directly from the resource.
95-
func validateSelfRefFromImport(addr addrs.Referenceable, expr hcl.Expression) tfdiags.Diagnostics {
96-
var diags tfdiags.Diagnostics
86+
// from an import block instead of directly from the resource. All the same
87+
// caveats apply as validateMetaSelfRef.
88+
func validateImportSelfRef(addr addrs.Resource, expr hcl.Expression) tfdiags.Diagnostics {
89+
return validateSelfRefFromExprInner(addr, expr, func(ref *addrs.Reference) *hcl.Diagnostic {
90+
return &hcl.Diagnostic{
91+
Severity: hcl.DiagError,
92+
Summary: "Invalid import id argument",
93+
Detail: "The import ID cannot reference the resource being imported.",
94+
Subject: ref.SourceRange.ToHCL().Ptr(),
95+
}
96+
})
97+
}
9798

98-
addrStrs := make([]string, 0, 1)
99-
addrStrs = append(addrStrs, addr.String())
100-
switch tAddr := addr.(type) {
101-
case addrs.ResourceInstance:
102-
// A resource instance may not refer to its containing resource either.
103-
addrStrs = append(addrStrs, tAddr.ContainingResource().String())
104-
}
99+
// validateSelfRefFromExprInner is a helper function that takes an address and
100+
// an expression and returns diagnostics for self-references in the expression.
101+
//
102+
// This should only be called via validateMetaSelfRef and validateImportSelfRef,
103+
// do not access this function directly.
104+
func validateSelfRefFromExprInner(addr addrs.Resource, expr hcl.Expression, diag func(ref *addrs.Reference) *hcl.Diagnostic) tfdiags.Diagnostics {
105+
var diags tfdiags.Diagnostics
105106

106107
refs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, expr)
107108
for _, ref := range refs {
109+
var target addrs.Resource
110+
switch t := ref.Subject.(type) {
111+
case addrs.ResourceInstance:
112+
// Automatically unpack an instance reference to its containing
113+
// resource, since we're only comparing against the static resource.
114+
target = t.Resource
115+
case addrs.Resource:
116+
target = t
117+
default:
118+
// Anything else cannot be a self-reference.
119+
continue
120+
}
108121

109-
for _, addrStr := range addrStrs {
110-
if ref.Subject.String() == addrStr {
111-
diags = diags.Append(&hcl.Diagnostic{
112-
Severity: hcl.DiagError,
113-
Summary: "Invalid import id argument",
114-
Detail: "The import ID cannot reference the resource being imported.",
115-
Subject: ref.SourceRange.ToHCL().Ptr(),
116-
})
117-
}
122+
if target.Equal(addr) {
123+
diags = diags.Append(diag(ref))
118124
}
119125
}
120126

internal/terraform/validate_selfref_test.go

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,55 @@ func TestValidateSelfRef(t *testing.T) {
109109
t.Errorf("unexpected error\n\n%s", diags.Err())
110110
}
111111
}
112+
})
113+
}
114+
}
115+
116+
func TestValidateSelfInExpr(t *testing.T) {
117+
rAddr := addrs.Resource{
118+
Mode: addrs.ManagedResourceMode,
119+
Type: "aws_instance",
120+
Name: "foo",
121+
}
112122

113-
// We can use the same expressions to test the expression
114-
// validation.
115-
diags = validateSelfRefInExpr(test.Addr, test.Expr)
123+
tests := []struct {
124+
Name string
125+
Addr addrs.Resource
126+
Expr hcl.Expression
127+
Err bool
128+
}{
129+
{
130+
"no references at all",
131+
rAddr,
132+
hcltest.MockExprLiteral(cty.StringVal("bar")),
133+
false,
134+
},
135+
136+
{
137+
"non self reference",
138+
rAddr,
139+
hcltest.MockExprTraversalSrc("aws_instance.bar.id"),
140+
false,
141+
},
142+
143+
{
144+
"self reference",
145+
rAddr,
146+
hcltest.MockExprTraversalSrc("aws_instance.foo.id"),
147+
true,
148+
},
149+
150+
{
151+
"self reference other index",
152+
rAddr,
153+
hcltest.MockExprTraversalSrc("aws_instance.foo[4].id"),
154+
true,
155+
},
156+
}
157+
158+
for i, test := range tests {
159+
t.Run(fmt.Sprintf("%d-%s", i, test.Name), func(t *testing.T) {
160+
diags := validateMetaSelfRef(test.Addr, test.Expr)
116161
if diags.HasErrors() != test.Err {
117162
if test.Err {
118163
t.Errorf("unexpected success; want error")

0 commit comments

Comments
 (0)