Skip to content

Commit 412e58d

Browse files
committed
simplify handling of apply time input vars
As the final step in stabilising the ephemeral values experiment, we can remove the separate code path for handling input variables supplied via -var and -var-file during apply. The intent here is conveyed in the tests: only ephemeral variables may be supplied via -var and -var-file during apply. As per the TODO copied from the prototype, there is some more work to be done here in making the handling of undeclared variables during apply as sophisticated as that during plan, emitting helpful warnings (for example) when input variables are supplied unnecessarily via environment variables.
1 parent 32ed0f7 commit 412e58d

File tree

3 files changed

+247
-167
lines changed

3 files changed

+247
-167
lines changed

internal/backend/local/backend_apply.go

Lines changed: 111 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,11 @@ import (
1111
"time"
1212

1313
"github.com/hashicorp/hcl/v2"
14-
"github.com/zclconf/go-cty/cty"
1514

1615
"github.com/hashicorp/terraform/internal/addrs"
1716
"github.com/hashicorp/terraform/internal/backend/backendrun"
18-
"github.com/hashicorp/terraform/internal/collections"
1917
"github.com/hashicorp/terraform/internal/command/views"
2018
"github.com/hashicorp/terraform/internal/configs"
21-
"github.com/hashicorp/terraform/internal/experiments"
2219
"github.com/hashicorp/terraform/internal/logging"
2320
"github.com/hashicorp/terraform/internal/plans"
2421
"github.com/hashicorp/terraform/internal/states"
@@ -236,57 +233,122 @@ func (b *Local) opApply(
236233
stateHook.StateMgr = opState
237234

238235
var applyOpts *terraform.ApplyOpts
239-
if lr.Config.Module.ActiveExperiments.Has(experiments.EphemeralValues) {
240-
// We only try to handle apply-time input variables if the root module
241-
// has opted into the ephemeral_values language experiment, because
242-
// otherwise there can't possibly be any input variables required
243-
// in the apply phase and this reduces the risk of the experimental
244-
// code impacting non-experimental usage.
245-
// If we stablize something like this experiment, we should find a
246-
// less clunky way to introduce this extra step.
247-
applyTimeValues, applyVarDiags := applyTimeInputValues(
248-
plan.ApplyTimeVariables,
249-
lr.Config.Module.Variables,
250-
op.Variables,
251-
lr.PlanOpts.SetVariables,
252-
combinedPlanApply,
253-
)
254-
diags = diags.Append(applyVarDiags)
255-
if diags.HasErrors() {
256-
op.ReportResult(runningOp, diags)
257-
return
258-
}
259-
applyOpts = &terraform.ApplyOpts{
260-
SetVariables: applyTimeValues,
261-
}
262-
} else {
263-
// When the ephemeral values experiment isn't enabled, no variables
264-
// may be _explicitly_ set during the apply phase at all, but it's
265-
// valid for variable to show up from more implicit locations like
266-
// environment variables and .auto.tfvars files.
267-
if len(op.Variables) != 0 && !combinedPlanApply {
268-
for _, rawV := range op.Variables {
269-
// We're "parsing" only to get the resulting value's SourceType,
270-
// so we'll use configs.VariableParseLiteral just because it's
271-
// the most liberal interpretation and so least likely to
272-
// fail with an unrelated error.
273-
v, _ := rawV.ParseVariableValue(configs.VariableParseLiteral)
274-
if v == nil {
275-
// We'll ignore any that don't parse at all, because
276-
// they'll fail elsewhere in this process anyway.
236+
if len(op.Variables) != 0 && !combinedPlanApply {
237+
applyTimeValues := make(terraform.InputValues, plan.ApplyTimeVariables.Len())
238+
for varName, rawV := range op.Variables {
239+
// We're "parsing" only to get the resulting value's SourceType,
240+
// so we'll use configs.VariableParseLiteral just because it's
241+
// the most liberal interpretation and so least likely to
242+
// fail with an unrelated error.
243+
v, _ := rawV.ParseVariableValue(configs.VariableParseLiteral)
244+
if v == nil {
245+
// We'll ignore any that don't parse at all, because
246+
// they'll fail elsewhere in this process anyway.
247+
continue
248+
}
249+
250+
if v.SourceType == terraform.ValueFromCLIArg || v.SourceType == terraform.ValueFromNamedFile {
251+
var rng *hcl.Range
252+
if v.HasSourceRange() {
253+
rng = v.SourceRange.ToHCL().Ptr()
254+
}
255+
256+
// If the variable isn't declared in config at all, take
257+
// this opportunity to give the user a helpful error,
258+
// rather than waiting for a less helpful one later.
259+
decl, ok := lr.Config.Module.Variables[varName]
260+
if !ok {
261+
diags = diags.Append(&hcl.Diagnostic{
262+
Severity: hcl.DiagError,
263+
Summary: "Value for undeclared variable",
264+
Detail: fmt.Sprintf("The variable %s cannot be set using the -var and -var-file options because it is not declared in configuration.", varName),
265+
Subject: rng,
266+
})
277267
continue
278268
}
279-
if v.SourceType == terraform.ValueFromCLIArg || v.SourceType == terraform.ValueFromNamedFile {
280-
diags = diags.Append(tfdiags.Sourceless(
281-
tfdiags.Error,
282-
"Can't set variables when applying a saved plan",
283-
"The -var and -var-file options cannot be used when applying a saved plan file, because a saved plan includes the variable values that were set when it was created.",
284-
))
285-
op.ReportResult(runningOp, diags)
286-
return
269+
270+
// If the var is declared as ephemeral in config, go ahead and handle it
271+
if decl.Ephemeral {
272+
// Determine whether this is an apply-time variable, i.e. an
273+
// ephemeral variable that was set (non-null) during the
274+
// planning phase.
275+
applyTimeVar := false
276+
for _, avName := range plan.ApplyTimeVariables.Elems() {
277+
if varName == avName {
278+
applyTimeVar = true
279+
}
280+
}
281+
282+
// If this isn't an apply-time variable, it's not valid to
283+
// set it during apply.
284+
if !applyTimeVar {
285+
diags = diags.Append(&hcl.Diagnostic{
286+
Severity: hcl.DiagError,
287+
Summary: "Ephemeral variable was not set during planning",
288+
Detail: fmt.Sprintf(
289+
"The ephemeral input variable %q was not set during the planning phase, and so must remain unset during the apply phase.",
290+
varName,
291+
),
292+
Subject: rng,
293+
})
294+
continue
295+
}
296+
297+
// Get the value of the variable, because we'll need it for
298+
// the next two steps.
299+
val, valDiags := rawV.ParseVariableValue(decl.ParsingMode)
300+
diags = diags.Append(valDiags)
301+
if valDiags.HasErrors() {
302+
continue
303+
}
304+
305+
// If this is an apply-time variable, the user must supply a
306+
// value during apply: it can't be null.
307+
if applyTimeVar && val.Value.IsNull() {
308+
diags = diags.Append(&hcl.Diagnostic{
309+
Severity: hcl.DiagError,
310+
Summary: "Ephemeral variable must be set for apply",
311+
Detail: fmt.Sprintf(
312+
"The ephemeral input variable %q was set during the planning phase, and so must be set again during the apply phase.",
313+
varName,
314+
),
315+
})
316+
continue
317+
}
318+
319+
// If we get here, we are in possession of a non-null
320+
// ephemeral apply-time input variable, and need only pass
321+
// its value on to the ApplyOpts.
322+
applyTimeValues[varName] = val
323+
} else {
324+
// TODO: We should probably actually tolerate this if the new
325+
// value is equal to the value that was saved in the plan, since
326+
// that'd make it possible to, for example, reuse a .tfvars file
327+
// containing a mixture of ephemeral and non-ephemeral definitions
328+
// during the apply phase, rather than having to split ephemeral
329+
// and non-ephemeral definitions into separate files. For initial
330+
// experiment we'll keep things a little simpler, though, and
331+
// just skip this check if we're doing a combined plan/apply where
332+
// the apply phase will therefore always have exactly the same
333+
// inputs as the plan phase.
334+
335+
diags = diags.Append(&hcl.Diagnostic{
336+
Severity: hcl.DiagError,
337+
Summary: "Can't set variable when applying a saved plan",
338+
Detail: fmt.Sprintf("The variable %s cannot be set using the -var and -var-file options when applying a saved plan file, because a saved plan includes the variable values that were set when it was created. To declare an ephemeral variable which is not saved in the plan file, use ephemeral = true.", varName),
339+
Subject: rng,
340+
})
287341
}
342+
288343
}
289344
}
345+
applyOpts = &terraform.ApplyOpts{
346+
SetVariables: applyTimeValues,
347+
}
348+
if diags.HasErrors() {
349+
op.ReportResult(runningOp, diags)
350+
return
351+
}
290352
}
291353

292354
// Start the apply in a goroutine so that we can be interrupted.
@@ -395,122 +457,6 @@ func (b *Local) backupStateForError(stateFile *statefile.File, err error, view v
395457
return diags
396458
}
397459

398-
func applyTimeInputValues(needVars collections.Set[string], decls map[string]*configs.Variable,
399-
unparsedVars map[string]backendrun.UnparsedVariableValue,
400-
setVars terraform.InputValues,
401-
ignoreExtras bool) (terraform.InputValues, tfdiags.Diagnostics) {
402-
// TEMP: This function is here to deal with the currently-experimental
403-
// possibility of certain input variables being required during an apply
404-
// phase because they were set during planning but declared as being
405-
// ephemeral.
406-
//
407-
// To reduce the disruption to existing code caused by this language
408-
// experiment the following is implemented by lightly misusing some
409-
// existing functions that were designed for interpreting variable values
410-
// during the planning phase. If we move forward with something like this
411-
// design for ephemeral input variables then we should consider revisiting
412-
// this to see if we can share the relevant parts of this logic in a less
413-
// clunky way.
414-
415-
// As a way to trick the functions we built for plan-time variable
416-
// processing into dealing with apply-time variables, we'll construct
417-
// a copy of the variable configurations map with only the needed
418-
// variables in it.
419-
filteredDecls := make(map[string]*configs.Variable, len(decls))
420-
for name, config := range decls {
421-
if needVars.Has(name) {
422-
filteredDecls[name] = config
423-
}
424-
}
425-
ret, diags := backendrun.ParseDeclaredVariableValues(unparsedVars, filteredDecls)
426-
427-
// reflect any user-set values
428-
ret = ret.Override(setVars)
429-
430-
undeclared, _ := backendrun.ParseUndeclaredVariableValues(unparsedVars, filteredDecls)
431-
// The diagnostics returned by ParseUndeclaredVariableValues are written
432-
// to make sense for the plan phase, so we'll ignore them and produce
433-
// our own diagnostics here.
434-
for name, defn := range undeclared {
435-
// Something can get in here either by being not declared at all,
436-
// by being a non-ephemeral variable which should therefore have been
437-
// set during the planning phase, or by being an ephemeral value that
438-
// wasn't set during planning and must therefore stay unset during
439-
// apply. We'll distinguish those cases below.
440-
decl, declared := decls[name]
441-
if !declared {
442-
// FIXME: Ideally we should treat this situation similarly to how
443-
// we would during planning, raising an error if defined in an
444-
// "explicit-ish" way but a warning if set in an ambient way such
445-
// as an environment variable. But for now we'll just ignore
446-
// undeclared input variables in all cases for simplicity's sake.
447-
continue
448-
}
449-
450-
var rng *hcl.Range
451-
if defn.HasSourceRange() {
452-
rng = defn.SourceRange.ToHCL().Ptr()
453-
}
454-
455-
if decl.Ephemeral {
456-
// An ephemeral variable that appears as "undeclared" is one that
457-
// wasn't set during planning and must therefore remain unset
458-
// during apply.
459-
diags = diags.Append(&hcl.Diagnostic{
460-
Severity: hcl.DiagError,
461-
Summary: "Ephemeral variable was not set during planning",
462-
Detail: fmt.Sprintf(
463-
"The ephemeral input variable %q was not set during the planning phase, and so must remain unset during the apply phase.",
464-
name,
465-
),
466-
Subject: rng,
467-
})
468-
} else {
469-
// TODO: We should probably actually tolerate this if the new
470-
// value is equal to the value that was saved in the plan, since
471-
// that'd make it possible to, for example, reuse a .tfvars file
472-
// containing a mixture of ephemeral and non-ephemeral definitions
473-
// during the apply phase, rather than having to split ephemeral
474-
// and non-ephemeral definitions into separate files. For initial
475-
// experiment we'll keep things a little simpler, though, and
476-
// just skip this check if we're doing a combined plan/apply where
477-
// the apply phase will therefore always have exactly the same
478-
// inputs as the plan phase.
479-
if !ignoreExtras {
480-
diags = diags.Append(&hcl.Diagnostic{
481-
Severity: hcl.DiagError,
482-
Summary: "Cannot change value for non-ephemeral variable",
483-
Detail: fmt.Sprintf(
484-
"Input variable %q is non-ephemeral, so its value was decided during the planning phase and cannot be reset for the apply phase.",
485-
name,
486-
),
487-
Subject: rng,
488-
})
489-
}
490-
}
491-
}
492-
493-
// We should now have a non-null value for each of the variables in needVars
494-
for _, name := range needVars.Elems() {
495-
val := cty.NullVal(cty.DynamicPseudoType)
496-
if defn, ok := ret[name]; ok {
497-
val = defn.Value
498-
}
499-
if val.IsNull() {
500-
diags = diags.Append(&hcl.Diagnostic{
501-
Severity: hcl.DiagError,
502-
Summary: "Ephemeral variable must be set for apply",
503-
Detail: fmt.Sprintf(
504-
"The ephemeral input variable %q was set during the planning phase, and so must be set again during the apply phase.",
505-
name,
506-
),
507-
})
508-
}
509-
}
510-
511-
return ret, diags
512-
}
513-
514460
const stateWriteBackedUpError = `The error shown above has prevented Terraform from writing the updated state to the configured backend. To allow for recovery, the state has been written to the file "errored.tfstate" in the current working directory.
515461
516462
Running "terraform apply" again at this point will create a forked state, making it harder to recover.

0 commit comments

Comments
 (0)