Skip to content

Commit 4a36348

Browse files
Merge pull request #35903 from hashicorp/pass-ephemeral-variables-to-terraform-apply
Pass ephemeral variables to terraform apply
2 parents eeacced + 9097369 commit 4a36348

File tree

7 files changed

+531
-173
lines changed

7 files changed

+531
-173
lines changed

internal/backend/local/backend_apply.go

Lines changed: 108 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -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) {

internal/command/apply.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,15 @@ Options:
385385
-state-out=path Path to write state to that is different than
386386
"-state". This can be used to preserve the old
387387
state.
388+
389+
-var 'foo=bar' Set a value for one of the input variables in the root
390+
module of the configuration. Use this option more than
391+
once to set more than one variable.
392+
393+
-var-file=filename Load variable values from the given file, in addition
394+
to the default files terraform.tfvars and *.auto.tfvars.
395+
Use this option more than once to include more than one
396+
variables file.
388397
389398
If you don't provide a saved plan file then this command will also accept
390399
all of the plan-customization options accepted by the terraform plan command.

0 commit comments

Comments
 (0)