Skip to content

Commit 1e41449

Browse files
authored
evaluate: return diagnostics instead of unknown for uninitialised locals and resources (#37663)
* evaluate: return diagnostics instead of unknown for uninitialised locals and resources * changelog * also input variables
1 parent a750471 commit 1e41449

File tree

6 files changed

+184
-10
lines changed

6 files changed

+184
-10
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: BUG FIXES
2+
body: 'console and test: return explicit diagnostics when referencing resources that were not included in the most recent operation.'
3+
time: 2025-09-24T11:04:16.860364+02:00
4+
custom:
5+
Issue: "37663"

internal/command/test_test.go

Lines changed: 124 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -412,11 +412,6 @@ func TestTest_Runs(t *testing.T) {
412412
"no-tests": {
413413
code: 0,
414414
},
415-
"expect-failures-assertions": {
416-
expectedOut: []string{"0 passed, 1 failed."},
417-
expectedErr: []string{"Test assertion failed"},
418-
code: 1,
419-
},
420415
}
421416
for name, tc := range tcs {
422417
t.Run(name, func(t *testing.T) {
@@ -5454,6 +5449,130 @@ func TestTest_JUnitOutput(t *testing.T) {
54545449
}
54555450
}
54565451

5452+
func TestTest_ReferencesIntoIncompletePlan(t *testing.T) {
5453+
td := t.TempDir()
5454+
testCopyDir(t, testFixturePath(path.Join("test", "expect-failures-assertions")), td)
5455+
t.Chdir(td)
5456+
5457+
provider := testing_command.NewProvider(nil)
5458+
providerSource, close := newMockProviderSource(t, map[string][]string{
5459+
"test": {"1.0.0"},
5460+
})
5461+
defer close()
5462+
5463+
streams, done := terminal.StreamsForTesting(t)
5464+
view := views.NewView(streams)
5465+
ui := new(cli.MockUi)
5466+
5467+
meta := Meta{
5468+
testingOverrides: metaOverridesForProvider(provider.Provider),
5469+
Ui: ui,
5470+
View: view,
5471+
Streams: streams,
5472+
ProviderSource: providerSource,
5473+
}
5474+
5475+
init := &InitCommand{
5476+
Meta: meta,
5477+
}
5478+
5479+
if code := init.Run(nil); code != 0 {
5480+
output := done(t)
5481+
t.Fatalf("expected status code %d but got %d: %s", 0, code, output.All())
5482+
}
5483+
5484+
// Reset the streams for the next command.
5485+
streams, done = terminal.StreamsForTesting(t)
5486+
meta.Streams = streams
5487+
meta.View = views.NewView(streams)
5488+
5489+
c := &TestCommand{
5490+
Meta: meta,
5491+
}
5492+
5493+
code := c.Run([]string{"-no-color"})
5494+
if code != 1 {
5495+
t.Errorf("expected status code %d but got %d", 0, code)
5496+
}
5497+
output := done(t)
5498+
5499+
out, err := output.Stdout(), output.Stderr()
5500+
5501+
expectedOut := `main.tftest.hcl... in progress
5502+
run "fail"... fail
5503+
main.tftest.hcl... tearing down
5504+
main.tftest.hcl... fail
5505+
5506+
Failure! 0 passed, 1 failed.
5507+
`
5508+
5509+
if diff := cmp.Diff(out, expectedOut); len(diff) > 0 {
5510+
t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expectedOut, out, diff)
5511+
}
5512+
5513+
if !strings.Contains(err, "Reference to uninitialized resource") {
5514+
t.Errorf("missing reference to uninitialized resource error: \n%s", err)
5515+
}
5516+
5517+
if !strings.Contains(err, "Reference to uninitialized local") {
5518+
t.Errorf("missing reference to uninitialized local error: \n%s", err)
5519+
}
5520+
}
5521+
5522+
func TestTest_ReferencesIntoTargetedPlan(t *testing.T) {
5523+
td := t.TempDir()
5524+
testCopyDir(t, testFixturePath(path.Join("test", "invalid-reference-with-target")), td)
5525+
t.Chdir(td)
5526+
5527+
provider := testing_command.NewProvider(nil)
5528+
providerSource, close := newMockProviderSource(t, map[string][]string{
5529+
"test": {"1.0.0"},
5530+
})
5531+
defer close()
5532+
5533+
streams, done := terminal.StreamsForTesting(t)
5534+
view := views.NewView(streams)
5535+
ui := new(cli.MockUi)
5536+
5537+
meta := Meta{
5538+
testingOverrides: metaOverridesForProvider(provider.Provider),
5539+
Ui: ui,
5540+
View: view,
5541+
Streams: streams,
5542+
ProviderSource: providerSource,
5543+
}
5544+
5545+
init := &InitCommand{
5546+
Meta: meta,
5547+
}
5548+
5549+
if code := init.Run(nil); code != 0 {
5550+
output := done(t)
5551+
t.Fatalf("expected status code %d but got %d: %s", 0, code, output.All())
5552+
}
5553+
5554+
// Reset the streams for the next command.
5555+
streams, done = terminal.StreamsForTesting(t)
5556+
meta.Streams = streams
5557+
meta.View = views.NewView(streams)
5558+
5559+
c := &TestCommand{
5560+
Meta: meta,
5561+
}
5562+
5563+
code := c.Run([]string{"-no-color"})
5564+
if code != 1 {
5565+
t.Errorf("expected status code %d but got %d", 0, code)
5566+
}
5567+
output := done(t)
5568+
5569+
err := output.Stderr()
5570+
5571+
if !strings.Contains(err, "Reference to uninitialized variable") {
5572+
t.Errorf("missing reference to uninitialized variable error: \n%s", err)
5573+
}
5574+
}
5575+
54575576
// https://github.com/hashicorp/terraform/issues/37546
54585577
func TestTest_TeardownOrder(t *testing.T) {
54595578
td := t.TempDir()
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
variable "input" {
3+
type = string
4+
}
5+
6+
resource "test_resource" "one" {
7+
value = var.input
8+
}
9+
10+
resource "test_resource" "two" {}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
2+
run "test" {
3+
command = plan
4+
5+
plan_options {
6+
target = [test_resource.two]
7+
}
8+
9+
variables {
10+
input = "hello"
11+
}
12+
13+
assert {
14+
condition = var.input == "hello"
15+
error_message = "wrong input"
16+
}
17+
}

internal/terraform/context_plan_actions_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ list "test_resource" "test1" {
176176
},
177177
},
178178
"query run, action references resource": {
179+
toBeImplemented: true, // TODO: Fix the graph built by query operations.
179180
module: map[string]string{
180181
"main.tf": `
181182
action "test_action" "hello" {

internal/terraform/evaluate.go

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,18 @@ func (d *evaluationStateData) GetInputVariable(addr addrs.InputVariable, rng tfd
297297
return ret, diags
298298
}
299299

300-
val := d.Evaluator.NamedValues.GetInputVariableValue(d.ModulePath.InputVariable(addr.Name))
300+
var val cty.Value
301+
if target := d.ModulePath.InputVariable(addr.Name); !d.Evaluator.NamedValues.HasInputVariableValue(target) {
302+
val = cty.DynamicVal
303+
diags = diags.Append(&hcl.Diagnostic{
304+
Severity: hcl.DiagError,
305+
Summary: "Reference to uninitialized variable",
306+
Detail: fmt.Sprintf("The variable %s was not processed by the most recent operation, this likely means the previous operation either failed or was incomplete due to targeting.", addr),
307+
Subject: rng.ToHCL().Ptr(),
308+
})
309+
} else {
310+
val = d.Evaluator.NamedValues.GetInputVariableValue(target)
311+
}
301312

302313
// Mark if sensitive and/or ephemeral
303314
if config.Sensitive {
@@ -342,11 +353,17 @@ func (d *evaluationStateData) GetLocalValue(addr addrs.LocalValue, rng tfdiags.S
342353
return cty.DynamicVal, diags
343354
}
344355

345-
if target := addr.Absolute(d.ModulePath); d.Evaluator.NamedValues.HasLocalValue(target) {
346-
return d.Evaluator.NamedValues.GetLocalValue(addr.Absolute(d.ModulePath)), diags
356+
target := addr.Absolute(d.ModulePath)
357+
if !d.Evaluator.NamedValues.HasLocalValue(target) {
358+
return cty.DynamicVal, diags.Append(&hcl.Diagnostic{
359+
Severity: hcl.DiagError,
360+
Summary: "Reference to uninitialized local value",
361+
Detail: fmt.Sprintf("The local value %s was not processed by the most recent operation, this likely means the previous operation either failed or was incomplete due to targeting.", addr),
362+
Subject: rng.ToHCL().Ptr(),
363+
})
347364
}
348365

349-
return cty.DynamicVal, diags
366+
return d.Evaluator.NamedValues.GetLocalValue(target), diags
350367
}
351368

352369
func (d *evaluationStateData) GetModule(addr addrs.ModuleCall, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
@@ -556,7 +573,12 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc
556573
if addr.Mode == addrs.EphemeralResourceMode {
557574
unknownVal = unknownVal.Mark(marks.Ephemeral)
558575
}
559-
return unknownVal, diags
576+
return unknownVal, diags.Append(&hcl.Diagnostic{
577+
Severity: hcl.DiagError,
578+
Summary: "Reference to uninitialized resource",
579+
Detail: fmt.Sprintf("The resource %s was not processed by the most recent operation, this likely means the previous operation either failed or was incomplete due to targeting.", addr),
580+
Subject: rng.ToHCL().Ptr(),
581+
})
560582
}
561583

562584
if _, _, hasUnknownKeys := d.Evaluator.Instances.ResourceInstanceKeys(addr.Absolute(moduleAddr)); hasUnknownKeys {

0 commit comments

Comments
 (0)