Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/v1.17/BUG FIXES-20260812-172344.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: BUG FIXES
body: 'test: Optional ephemeral values do not have to be set at plan time'
time: 2026-08-12T17:23:44.947966+02:00
custom:
Issue: "38974"
11 changes: 9 additions & 2 deletions internal/command/test_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,8 @@ func TestTest_Runs(t *testing.T) {
},
"no_state": {
expectedOut: []string{"0 passed, 1 failed"},
expectedErr: []string{"No value for required variable"},
description: "the run apply fails, causing it to produce a nil state.",
expectedErr: []string{"apply_fail is set to true"},
description: "the run apply fails, causing it to produce a nil state. We expect a failure, but no panic",
code: 1,
},
"variables": {
Expand Down Expand Up @@ -359,6 +359,12 @@ func TestTest_Runs(t *testing.T) {
expectedOut: []string{"2 passed, 0 failed."},
code: 0,
},
"ephemeral_input_optional_unset": {
override: "ephemeral_input",
expectedOut: []string{"2 passed, 0 failed."},
args: []string{"-var=foo=\"triple\""},
code: 0,
},
"ephemeral_input_with_error": {
expectedOut: []string{"Error message refers to ephemeral values", "1 passed, 1 failed."},
expectedErr: []string{"Test assertion failed",
Expand Down Expand Up @@ -5156,6 +5162,7 @@ test_resource_id = %s`, resourceId, resourceId)
"write_only": cty.NullVal(cty.String),
"create_wait_seconds": cty.NullVal(cty.Number),
"destroy_fail": cty.False,
"apply_fail": cty.False,
"destroy_wait_seconds": cty.NullVal(cty.Number),
"defer": cty.NullVal(cty.Bool),
})},
Expand Down
11 changes: 11 additions & 0 deletions internal/command/testdata/test/ephemeral_input/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@ variable "foo" {
ephemeral = true
type = string
}
variable "bar" {
ephemeral = true
default = null
type = string
}

resource "test_resource" "bar" {
write_only = var.bar
}


output "value" {
value = "Hello, World!"
}
8 changes: 3 additions & 5 deletions internal/command/testdata/test/no_state/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@ variable "input" {
type = number
}

variable "input2" {
type = number
ephemeral = true
default = 0
resource "test_resource" "my_resource" {
apply_fail = true
}

output "output" {
value = var.input > 5 ? var.input : null
value = var.input
}
6 changes: 2 additions & 4 deletions internal/command/testdata/test/no_state/main.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@ run "first" {
input = 2
}

// var.input2 is ephemeral, and this would cause it to not be set during the plan phase,
// but will be set to default during the apply phase. This leads to the apply update being null
assert {
condition = output.output == var.input2
error_message = "output should have been null"
condition = output.output != var.input
error_message = "condition should fail"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"attributes": {
"create_wait_seconds": null,
"destroy_fail": false,
"apply_fail": false,
"destroy_wait_seconds": null,
"id": "53d69028-477d-7ba0-83c3-ff3807e3756f",
"interrupt_count": null,
Expand All @@ -38,4 +39,4 @@
}
],
"check_results": null
}
}
11 changes: 11 additions & 0 deletions internal/command/testing/test_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ var (
"value": {Type: cty.String, Optional: true},
"interrupt_count": {Type: cty.Number, Optional: true},
"destroy_fail": {Type: cty.Bool, Optional: true, Computed: true},
"apply_fail": {Type: cty.Bool, Optional: true},
"create_wait_seconds": {Type: cty.Number, Optional: true},
"destroy_wait_seconds": {Type: cty.Number, Optional: true},
"write_only": {Type: cty.String, Optional: true, WriteOnly: true},
Expand All @@ -60,6 +61,7 @@ var (

"interrupt_count": {Type: cty.Number, Computed: true},
"destroy_fail": {Type: cty.Bool, Computed: true},
"apply_fail": {Type: cty.Bool, Optional: true},
"create_wait_seconds": {Type: cty.Number, Computed: true},
"destroy_wait_seconds": {Type: cty.Number, Computed: true},
"defer": {Type: cty.Bool, Computed: true},
Expand Down Expand Up @@ -337,6 +339,15 @@ func (provider *TestProvider) ApplyResourceChange(request providers.ApplyResourc
resource = cty.ObjectVal(vals)
}

if applyFail := resource.GetAttr("apply_fail"); !applyFail.IsNull() && applyFail.IsKnown() && applyFail.True() {
var diags tfdiags.Diagnostics
diags = diags.Append(fmt.Errorf("apply_fail is set to true"))
return providers.ApplyResourceChangeResponse{
NewState: cty.NilVal,
Diagnostics: diags,
}
}

provider.Store.Put(provider.GetResourceKey(id.AsString()), resource)
return providers.ApplyResourceChangeResponse{
NewState: resource,
Expand Down
11 changes: 9 additions & 2 deletions internal/moduletest/graph/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,15 +142,22 @@ func apply(tfCtx *terraform.Context, run *configs.TestRun, module *configs.Confi
created = append(created, change)
}

// We only need to pass ephemeral variables to the apply operation, as the
// We only need to pass ephemeral variables that are needed to the apply operation, as the
// plan has already been evaluated with the full set of variables.
ephemeralVariables := make(terraform.InputValues)
for k, v := range module.Root.Module.Variables {
if v.EphemeralSet {
// [plan.ApplyTimeVariables] only contains ephemeral variables whose value are non-null.
// These are the only ones we need to set during the apply operation.
// The only way for such a variable to obtain values beyond this point
// is via defaults, and for that, we can count on the defaults being
// resolved in the core graph later on, so we do not need to include
// them here.
if v.EphemeralSet && plan.ApplyTimeVariables.Has(k) {
if value, ok := variables[k]; ok {
ephemeralVariables[k] = value
}
}

}

applyOpts := &terraform.ApplyOpts{
Expand Down
19 changes: 13 additions & 6 deletions internal/terraform/context_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,8 @@ Note that the -target option is not suitable for routine use, and is provided on
return newState, evalScope, diags
}

// checkApplyTimeVariables checks that the ephemeral variables needed in the configuration
// are also set during apply. Variables that are not needed should not be set at all.
func checkApplyTimeVariables(needed collections.Set[string], gotValues InputValues, config *configs.Config) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
for name := range needed.All() {
Expand All @@ -294,16 +296,21 @@ func checkApplyTimeVariables(needed collections.Set[string], gotValues InputValu
))
}
}
for name := range gotValues {
for name, value := range gotValues {
if !needed.Has(name) {
// We'll treat this a little differently depending on whether
// the variable is declared as ephemeral or not.
if vc, ok := config.Module.Variables[name]; ok && vc.Ephemeral {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"No value for required variable",
fmt.Sprintf("The ephemeral input variable %q was not set during the plan phase, and so must remain unset during the apply phase.", name),
))

// Only non-null ephemeral variables are recorded in the plan as needed,
// therefore we can treat a supplied null value here if it was not set
if !value.Value.IsNull() {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"No value for required variable",
fmt.Sprintf("The ephemeral input variable %q was not set during the plan phase, and so must remain unset during the apply phase.", name),
))
}
} else {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
Expand Down
4 changes: 4 additions & 0 deletions internal/terraform/context_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ type PlanOpts struct {
// by the user who is requesting the run, prior to any normalization or
// substitution of defaults. See the documentation for the InputValue
// type for more information on how to correctly populate this.
// Variables that are declared but not set are also included here,
// but their values are set to nil, so that Terraform Core
// can later subst substitute a default if available, or generate an error
// if not.
SetVariables InputValues

// If Targets has a non-zero length then it activates targeted planning
Expand Down