@@ -11,10 +11,12 @@ import (
1111 "time"
1212
1313 "github.com/hashicorp/hcl/v2"
14+ "github.com/zclconf/go-cty/cty"
1415
1516 "github.com/hashicorp/terraform/internal/addrs"
1617 "github.com/hashicorp/terraform/internal/backend/backendrun"
1718 "github.com/hashicorp/terraform/internal/command/views"
19+ viewsjson "github.com/hashicorp/terraform/internal/command/views/json"
1820 "github.com/hashicorp/terraform/internal/configs"
1921 "github.com/hashicorp/terraform/internal/logging"
2022 "github.com/hashicorp/terraform/internal/plans"
@@ -232,9 +234,24 @@ func (b *Local) opApply(
232234 // Set up our hook for continuous state updates
233235 stateHook .StateMgr = opState
234236
235- var applyOpts * terraform.ApplyOpts
236- if len (op .Variables ) != 0 && ! combinedPlanApply {
237- applyTimeValues := make (terraform.InputValues , plan .ApplyTimeVariables .Len ())
237+ applyTimeValues := make (terraform.InputValues , plan .ApplyTimeVariables .Len ())
238+
239+ // In a combined plan/apply run, getting the context already gathers the interactive
240+ // input, therefore we need to make sure to pass the ephemeral variables to the applyOpts.
241+ if combinedPlanApply {
242+ for varName , v := range lr .PlanOpts .SetVariables {
243+ decl , ok := lr .Config .Module .Variables [varName ]
244+ if ! ok {
245+ continue // This should never happen, but we'll ignore it if it does.
246+ }
247+
248+ if v .SourceType == terraform .ValueFromInput && decl .Ephemeral {
249+ applyTimeValues [varName ] = v
250+ }
251+ }
252+ }
253+
254+ if len (op .Variables ) != 0 {
238255 for varName , rawV := range op .Variables {
239256 // We're "parsing" only to get the resulting value's SourceType,
240257 // so we'll use configs.VariableParseLiteral just because it's
@@ -247,16 +264,19 @@ func (b *Local) opApply(
247264 continue
248265 }
249266
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- }
267+ var rng * hcl.Range
268+ if v .HasSourceRange () {
269+ rng = v .SourceRange .ToHCL ().Ptr ()
270+ }
255271
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 ]
272+ decl , ok := lr .Config .Module .Variables [varName ]
273+
274+ // If the variable isn't declared in config at all, take
275+ // this opportunity to give the user a helpful error,
276+ // rather than waiting for a less helpful one later.
277+ // We are ok with over-supplying variables through environment variables
278+ // since it would be a breaking change to disallow it.
279+ if v .SourceType == terraform .ValueFromCLIArg || v .SourceType == terraform .ValueFromNamedFile {
260280 if ! ok {
261281 diags = diags .Append (& hcl.Diagnostic {
262282 Severity : hcl .DiagError ,
@@ -266,85 +286,94 @@ func (b *Local) opApply(
266286 })
267287 continue
268288 }
289+ }
269290
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 .All () {
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
291+ // If the var is declared as ephemeral in config, go ahead and handle it
292+ if ok && decl .Ephemeral {
293+ // Determine whether this is an apply-time variable, i.e. an
294+ // ephemeral variable that was set (non-null) during the
295+ // planning phase.
296+ applyTimeVar := false
297+ for avName := range plan .ApplyTimeVariables .All () {
298+ if varName == avName {
299+ applyTimeVar = true
295300 }
301+ }
296302
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- }
303+ // If this isn't an apply-time variable, it's not valid to
304+ // set it during apply.
305+ if ! applyTimeVar {
306+ diags = diags .Append (& hcl.Diagnostic {
307+ Severity : hcl .DiagError ,
308+ Summary : "Ephemeral variable was not set during planning" ,
309+ Detail : fmt .Sprintf (
310+ "The ephemeral input variable %q was not set during the planning phase, and so must remain unset during the apply phase." ,
311+ varName ,
312+ ),
313+ Subject : rng ,
314+ })
315+ continue
316+ }
304317
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+ // Get the value of the variable, because we'll need it for
319+ // the next two steps.
320+ val , valDiags := rawV .ParseVariableValue (decl .ParsingMode )
321+ diags = diags .Append (valDiags )
322+ if valDiags .HasErrors () {
323+ continue
324+ }
318325
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.
326+ // If this is an apply-time variable, the user must supply a
327+ // value during apply: it can't be null.
328+ if applyTimeVar && val .Value .IsNull () {
329+ diags = diags .Append (& hcl.Diagnostic {
330+ Severity : hcl .DiagError ,
331+ Summary : "Ephemeral variable must be set for apply" ,
332+ Detail : fmt .Sprintf (
333+ "The ephemeral input variable %q was set during the planning phase, and so must be set again during the apply phase." ,
334+ varName ,
335+ ),
336+ })
337+ continue
338+ }
334339
340+ // If we get here, we are in possession of a non-null
341+ // ephemeral apply-time input variable, and need only pass
342+ // its value on to the ApplyOpts.
343+ applyTimeValues [varName ] = val
344+ } else {
345+ // If a non-ephemeral variable is set differently between plan and apply, we should emit a diagnostic.
346+ plannedVariableValue , ok := plan .VariableValues [varName ]
347+ if ! ok {
335348 diags = diags .Append (& hcl.Diagnostic {
336349 Severity : hcl .DiagError ,
337350 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 ),
351+ Detail : fmt .Sprintf ("The variable %s cannot be set using the -var and -var-file options when applying a saved plan file, because it is neither ephemeral nor has it been declared during the plan operation . To declare an ephemeral variable which is not saved in the plan file, use ephemeral = true." , varName ),
339352 Subject : rng ,
340353 })
354+ continue
341355 }
342356
357+ val , err := plannedVariableValue .Decode (cty .DynamicPseudoType )
358+ if err != nil {
359+ diags = diags .Append (& hcl.Diagnostic {
360+ Severity : hcl .DiagError ,
361+ Summary : "Could not decode variable value from plan" ,
362+ Detail : fmt .Sprintf ("The variable %s could not be decoded from the plan. %s. This is a bug in Terraform, please report it." , varName , err ),
363+ Subject : rng ,
364+ })
365+ } else {
366+ if v .Value .Equals (val ).False () {
367+ diags = diags .Append (& hcl.Diagnostic {
368+ Severity : hcl .DiagError ,
369+ Summary : "Can't change variable when applying a saved plan" ,
370+ 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. The saved plan specifies %s as the value whereas during apply the value %s was %s. To declare an ephemeral variable which is not saved in the plan file, use ephemeral = true." , varName , viewsjson .CompactValueStr (v .Value ), viewsjson .CompactValueStr (val ), v .SourceType .DiagnosticLabel ()),
371+ Subject : rng ,
372+ })
373+ }
374+ }
343375 }
344376 }
345- applyOpts = & terraform.ApplyOpts {
346- SetVariables : applyTimeValues ,
347- }
348377 if diags .HasErrors () {
349378 op .ReportResult (runningOp , diags )
350379 return
@@ -360,7 +389,9 @@ func (b *Local) opApply(
360389 defer close (doneCh )
361390
362391 log .Printf ("[INFO] backend/local: apply calling Apply" )
363- applyState , applyDiags = lr .Core .Apply (plan , lr .Config , applyOpts )
392+ applyState , applyDiags = lr .Core .Apply (plan , lr .Config , & terraform.ApplyOpts {
393+ SetVariables : applyTimeValues ,
394+ })
364395 }()
365396
366397 if b .opWait (doneCh , stopCtx , cancelCtx , lr .Core , opState , op .View ) {
0 commit comments