Skip to content

Commit

Permalink
Merge pull request #35964 from hashicorp/backport/pass-ephemeral-vari…
Browse files Browse the repository at this point in the history
…ables-to-terraform-apply/nicely-daring-shepherd

Backport of Pass ephemeral variables to terraform apply into v1.10
  • Loading branch information
DanielMSchmidt authored Nov 8, 2024
2 parents f5e035c + d216ebe commit b51f730
Show file tree
Hide file tree
Showing 7 changed files with 531 additions and 173 deletions.
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.
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

0 comments on commit b51f730

Please sign in to comment.