Skip to content

Commit 54739db

Browse files
author
Liam Cervante
authored
import: fix crash when referencing the to-be-imported resource from the 'id' field (#35420)
1 parent 5e3a44f commit 54739db

File tree

4 files changed

+176
-11
lines changed

4 files changed

+176
-11
lines changed

internal/terraform/context_plan_import_test.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1564,6 +1564,7 @@ import {
15641564
t.Fatalf("unexpected errors\n%s", diags.Err().Error())
15651565
}
15661566
}
1567+
15671568
func TestContext2Plan_importDuringDestroy(t *testing.T) {
15681569
m := testModuleInline(t, map[string]string{
15691570
"main.tf": `
@@ -1628,3 +1629,130 @@ func TestContext2Plan_importDuringDestroy(t *testing.T) {
16281629
t.Fatalf("unexpected errors\n%s", diags.Err().Error())
16291630
}
16301631
}
1632+
1633+
func TestContext2Plan_importSelfReference(t *testing.T) {
1634+
m := testModuleInline(t, map[string]string{
1635+
"main.tf": `
1636+
import {
1637+
to = test_object.a
1638+
id = test_object.a.test_string
1639+
}
1640+
1641+
resource "test_object" "a" {
1642+
test_string = "foo"
1643+
}
1644+
`,
1645+
})
1646+
1647+
p := simpleMockProvider()
1648+
ctx := testContext2(t, &ContextOpts{
1649+
Providers: map[addrs.Provider]providers.Factory{
1650+
// The providers never actually going to get called here, we should
1651+
// catch the error long before anything happens.
1652+
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
1653+
},
1654+
})
1655+
1656+
_, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
1657+
1658+
// We're expecting exactly one diag, which is the self-reference error.
1659+
if len(diags) != 1 {
1660+
t.Fatalf("expected one diag, got %d", len(diags))
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_importSelfReferenceInst(t *testing.T) {
1670+
m := testModuleInline(t, map[string]string{
1671+
"main.tf": `
1672+
import {
1673+
to = test_object.a[0]
1674+
id = test_object.a.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())
1698+
}
1699+
1700+
got, want := diags.Err().Error(), "Invalid import id argument: The import ID cannot reference the resource being imported."
1701+
if cmp.Diff(want, got) != "" {
1702+
t.Fatalf("unexpected error\n%s", cmp.Diff(want, got))
1703+
}
1704+
}
1705+
1706+
func TestContext2Plan_importSelfReferenceInModule(t *testing.T) {
1707+
m := testModuleInline(t, map[string]string{
1708+
"main.tf": `
1709+
import {
1710+
to = module.mod.test_object.a
1711+
id = module.mod.foo
1712+
}
1713+
1714+
module "mod" {
1715+
source = "./mod"
1716+
}
1717+
`,
1718+
"mod/main.tf": `
1719+
resource "test_object" "a" {
1720+
test_string = "foo"
1721+
}
1722+
1723+
output "foo" {
1724+
value = test_object.a.test_string
1725+
}
1726+
`,
1727+
})
1728+
1729+
p := simpleMockProvider()
1730+
ctx := testContext2(t, &ContextOpts{
1731+
Providers: map[addrs.Provider]providers.Factory{
1732+
// The providers never actually going to get called here, we should
1733+
// catch the error long before anything happens.
1734+
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
1735+
},
1736+
})
1737+
1738+
_, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
1739+
1740+
// We're expecting exactly one diag, which is the self-reference error.
1741+
if len(diags) != 1 {
1742+
t.Fatalf("expected one diag, got %d", len(diags))
1743+
}
1744+
1745+
// Terraform detects this case as a cycle, and the order of rendering the
1746+
// cycle if non-deterministic, so we can't do a straight string match.
1747+
1748+
got := diags.Err().Error()
1749+
if !strings.Contains(got, "Cycle:") {
1750+
t.Errorf("should have reported a cycle error, but got %s", got)
1751+
}
1752+
if !strings.Contains(got, "module.mod.output.foo (expand)") {
1753+
t.Errorf("should have reported the cycle to contain the module output, but got %s", got)
1754+
}
1755+
if !strings.Contains(got, "module.mod.test_object.a (expand)") {
1756+
t.Errorf("should have reported the cycle to contain the target resource, but got %s", got)
1757+
}
1758+
}

internal/terraform/eval_import.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,9 @@ import (
1616
"github.com/hashicorp/terraform/internal/tfdiags"
1717
)
1818

19-
func evaluateImportIdExpression(expr hcl.Expression, ctx EvalContext, keyData instances.RepetitionData, allowUnknown bool) (cty.Value, tfdiags.Diagnostics) {
19+
func evaluateImportIdExpression(expr hcl.Expression, target addrs.AbsResourceInstance, ctx EvalContext, keyData instances.RepetitionData, allowUnknown bool) (cty.Value, tfdiags.Diagnostics) {
2020
var diags tfdiags.Diagnostics
2121

22-
// import blocks only exist in the root module, and must be evaluated in
23-
// that context.
24-
ctx = evalContextForModuleInstance(ctx, addrs.RootModuleInstance)
25-
2622
if expr == nil {
2723
return cty.NilVal, diags.Append(&hcl.Diagnostic{
2824
Severity: hcl.DiagError,
@@ -32,6 +28,14 @@ func evaluateImportIdExpression(expr hcl.Expression, ctx EvalContext, keyData in
3228
})
3329
}
3430

31+
diags = diags.Append(validateSelfRefFromImport(target.Resource.Resource, expr))
32+
if diags.HasErrors() {
33+
return cty.NilVal, diags
34+
}
35+
36+
// import blocks only exist in the root module, and must be evaluated in
37+
// that context.
38+
ctx = evalContextForModuleInstance(ctx, addrs.RootModuleInstance)
3539
scope := ctx.EvaluationScope(nil, nil, keyData)
3640
importIdVal, evalDiags := scope.EvalExpr(expr, cty.String)
3741
diags = diags.Append(evalDiags)

internal/terraform/node_resource_plan.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,6 @@ func (n *nodeExpandPlannableResource) expandResourceImports(ctx EvalContext, all
177177
}
178178

179179
if imp.Config.ForEach == nil {
180-
importID, evalDiags := evaluateImportIdExpression(imp.Config.ID, ctx, EvalDataForNoInstanceKey, allowUnknown)
181-
diags = diags.Append(evalDiags)
182-
if diags.HasErrors() {
183-
return knownImports, unknownImports, diags
184-
}
185180

186181
traversal, hds := hcl.AbsTraversalForExpr(imp.Config.To)
187182
diags = diags.Append(hds)
@@ -191,6 +186,12 @@ func (n *nodeExpandPlannableResource) expandResourceImports(ctx EvalContext, all
191186
return knownImports, unknownImports, diags
192187
}
193188

189+
importID, evalDiags := evaluateImportIdExpression(imp.Config.ID, to, ctx, EvalDataForNoInstanceKey, allowUnknown)
190+
diags = diags.Append(evalDiags)
191+
if diags.HasErrors() {
192+
return knownImports, unknownImports, diags
193+
}
194+
194195
knownImports.Put(to, importID)
195196

196197
log.Printf("[TRACE] expandResourceImports: found single import target %s", to)
@@ -242,7 +243,7 @@ func (n *nodeExpandPlannableResource) expandResourceImports(ctx EvalContext, all
242243
return knownImports, unknownImports, diags
243244
}
244245

245-
importID, evalDiags := evaluateImportIdExpression(imp.Config.ID, ctx, keyData, allowUnknown)
246+
importID, evalDiags := evaluateImportIdExpression(imp.Config.ID, res, ctx, keyData, allowUnknown)
246247
diags = diags.Append(evalDiags)
247248
if diags.HasErrors() {
248249
return knownImports, unknownImports, diags

internal/terraform/validate_selfref.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,38 @@ func validateSelfRefInExpr(addr addrs.Referenceable, expr hcl.Expression) tfdiag
8989
return diags
9090
}
9191

92+
// validateSelfRefFromImport is similar to validateSelfRefInExpr except it
93+
// 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
97+
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+
}
105+
106+
refs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, expr)
107+
for _, ref := range refs {
108+
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+
}
118+
}
119+
}
120+
121+
return diags
122+
}
123+
92124
// Legacy provisioner configurations may refer to single instances using the
93125
// resource address. We need to filter these out from the reported references
94126
// to prevent cycles.

0 commit comments

Comments
 (0)