Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pass ephemeral variables to terraform apply #35903

Merged
merged 14 commits into from
Nov 7, 2024
Merged
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
185 changes: 108 additions & 77 deletions internal/backend/local/backend_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ import (
"time"

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

"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/backend/backendrun"
"github.com/hashicorp/terraform/internal/command/views"
viewsjson "github.com/hashicorp/terraform/internal/command/views/json"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/logging"
"github.com/hashicorp/terraform/internal/plans"
Expand Down Expand Up @@ -232,9 +234,24 @@ func (b *Local) opApply(
// Set up our hook for continuous state updates
stateHook.StateMgr = opState

var applyOpts *terraform.ApplyOpts
if len(op.Variables) != 0 && !combinedPlanApply {
applyTimeValues := make(terraform.InputValues, plan.ApplyTimeVariables.Len())
applyTimeValues := make(terraform.InputValues, plan.ApplyTimeVariables.Len())

// In a combined plan/apply run, getting the context already gathers the interactive
// input, therefore we need to make sure to pass the ephemeral variables to the applyOpts.
if combinedPlanApply {
for varName, v := range lr.PlanOpts.SetVariables {
decl, ok := lr.Config.Module.Variables[varName]
if !ok {
continue // This should never happen, but we'll ignore it if it does.
}

if v.SourceType == terraform.ValueFromInput && decl.Ephemeral {
applyTimeValues[varName] = v
}
}
}

if len(op.Variables) != 0 {
for varName, rawV := range op.Variables {
// We're "parsing" only to get the resulting value's SourceType,
// so we'll use configs.VariableParseLiteral just because it's
Expand All @@ -247,16 +264,19 @@ func (b *Local) opApply(
continue
}

if v.SourceType == terraform.ValueFromCLIArg || v.SourceType == terraform.ValueFromNamedFile {
var rng *hcl.Range
if v.HasSourceRange() {
rng = v.SourceRange.ToHCL().Ptr()
}
var rng *hcl.Range
if v.HasSourceRange() {
rng = v.SourceRange.ToHCL().Ptr()
}

// If the variable isn't declared in config at all, take
// this opportunity to give the user a helpful error,
// rather than waiting for a less helpful one later.
decl, ok := lr.Config.Module.Variables[varName]
decl, ok := lr.Config.Module.Variables[varName]

// If the variable isn't declared in config at all, take
// this opportunity to give the user a helpful error,
// rather than waiting for a less helpful one later.
// We are ok with over-supplying variables through environment variables
// since it would be a breaking change to disallow it.
if v.SourceType == terraform.ValueFromCLIArg || v.SourceType == terraform.ValueFromNamedFile {
if !ok {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Expand All @@ -266,85 +286,94 @@ func (b *Local) opApply(
})
continue
}
}

// If the var is declared as ephemeral in config, go ahead and handle it
if decl.Ephemeral {
// Determine whether this is an apply-time variable, i.e. an
// ephemeral variable that was set (non-null) during the
// planning phase.
applyTimeVar := false
for avName := range plan.ApplyTimeVariables.All() {
if varName == avName {
applyTimeVar = true
}
}

// If this isn't an apply-time variable, it's not valid to
// set it during apply.
if !applyTimeVar {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Ephemeral variable was not set during planning",
Detail: fmt.Sprintf(
"The ephemeral input variable %q was not set during the planning phase, and so must remain unset during the apply phase.",
varName,
),
Subject: rng,
})
continue
// If the var is declared as ephemeral in config, go ahead and handle it
if ok && decl.Ephemeral {
// Determine whether this is an apply-time variable, i.e. an
// ephemeral variable that was set (non-null) during the
// planning phase.
applyTimeVar := false
for avName := range plan.ApplyTimeVariables.All() {
if varName == avName {
applyTimeVar = true
}
}

// Get the value of the variable, because we'll need it for
// the next two steps.
val, valDiags := rawV.ParseVariableValue(decl.ParsingMode)
diags = diags.Append(valDiags)
if valDiags.HasErrors() {
continue
}
// If this isn't an apply-time variable, it's not valid to
// set it during apply.
if !applyTimeVar {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Ephemeral variable was not set during planning",
Detail: fmt.Sprintf(
"The ephemeral input variable %q was not set during the planning phase, and so must remain unset during the apply phase.",
varName,
),
Subject: rng,
})
continue
}

// If this is an apply-time variable, the user must supply a
// value during apply: it can't be null.
if applyTimeVar && val.Value.IsNull() {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Ephemeral variable must be set for apply",
Detail: fmt.Sprintf(
"The ephemeral input variable %q was set during the planning phase, and so must be set again during the apply phase.",
varName,
),
})
continue
}
// Get the value of the variable, because we'll need it for
// the next two steps.
val, valDiags := rawV.ParseVariableValue(decl.ParsingMode)
diags = diags.Append(valDiags)
if valDiags.HasErrors() {
continue
}

// If we get here, we are in possession of a non-null
// ephemeral apply-time input variable, and need only pass
// its value on to the ApplyOpts.
applyTimeValues[varName] = val
} else {
// TODO: We should probably actually tolerate this if the new
// value is equal to the value that was saved in the plan, since
// that'd make it possible to, for example, reuse a .tfvars file
// containing a mixture of ephemeral and non-ephemeral definitions
// during the apply phase, rather than having to split ephemeral
// and non-ephemeral definitions into separate files. For initial
// experiment we'll keep things a little simpler, though, and
// just skip this check if we're doing a combined plan/apply where
// the apply phase will therefore always have exactly the same
// inputs as the plan phase.
// If this is an apply-time variable, the user must supply a
// value during apply: it can't be null.
if applyTimeVar && val.Value.IsNull() {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Ephemeral variable must be set for apply",
Detail: fmt.Sprintf(
"The ephemeral input variable %q was set during the planning phase, and so must be set again during the apply phase.",
varName,
),
})
continue
}

// If we get here, we are in possession of a non-null
// ephemeral apply-time input variable, and need only pass
// its value on to the ApplyOpts.
applyTimeValues[varName] = val
} else {
// If a non-ephemeral variable is set differently between plan and apply, we should emit a diagnostic.
plannedVariableValue, ok := plan.VariableValues[varName]
if !ok {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Can't set variable when applying a saved plan",
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),
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),
Subject: rng,
})
continue
}

val, err := plannedVariableValue.Decode(cty.DynamicPseudoType)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Could not decode variable value from plan",
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),
Subject: rng,
})
} else {
if v.Value.Equals(val).False() {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Can't change variable when applying a saved plan",
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()),
Subject: rng,
})
}
}
}
}
applyOpts = &terraform.ApplyOpts{
SetVariables: applyTimeValues,
}
if diags.HasErrors() {
op.ReportResult(runningOp, diags)
return
Expand All @@ -360,7 +389,9 @@ func (b *Local) opApply(
defer close(doneCh)

log.Printf("[INFO] backend/local: apply calling Apply")
applyState, applyDiags = lr.Core.Apply(plan, lr.Config, applyOpts)
applyState, applyDiags = lr.Core.Apply(plan, lr.Config, &terraform.ApplyOpts{
SetVariables: applyTimeValues,
})
}()

if b.opWait(doneCh, stopCtx, cancelCtx, lr.Core, opState, op.View) {
Expand Down
9 changes: 9 additions & 0 deletions internal/command/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,15 @@ Options:
-state-out=path Path to write state to that is different than
"-state". This can be used to preserve the old
state.

-var 'foo=bar' Set a value for one of the input variables in the root
module of the configuration. Use this option more than
once to set more than one variable.

-var-file=filename Load variable values from the given file, in addition
to the default files terraform.tfvars and *.auto.tfvars.
Use this option more than once to include more than one
variables file.
DanielMSchmidt marked this conversation as resolved.
Show resolved Hide resolved

If you don't provide a saved plan file then this command will also accept
all of the plan-customization options accepted by the terraform plan command.
Expand Down
Loading
Loading