Merge pull request #36121 from hashicorp/jbardin/apply-env-vars

Fully parse input variables from `TF_VAR_` when validating during apply
pull/36139/head
James Bardin 1 year ago committed by GitHub
commit cbc837c4ed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -259,33 +259,26 @@ func (b *Local) opApply(
// same parsing logic from the plan to generate the diagnostics.
undeclaredVariables := map[string]backendrun.UnparsedVariableValue{}
for varName, rawV := range op.Variables {
parsedVars, _ := backendrun.ParseVariableValues(op.Variables, lr.Config.Module.Variables)
for varName := range op.Variables {
parsedVar, parsed := parsedVars[varName]
decl, ok := lr.Config.Module.Variables[varName]
if !ok {
if !ok || !parsed {
// We'll try to parse this and handle diagnostics for missing
// variables with ParseUndeclaredVariableValues after.
undeclaredVariables[varName] = rawV
continue
}
// We're "parsing" only to get the resulting value's SourceType,
// so we'll use configs.VariableParseLiteral just because it's
// the most liberal interpretation and so least likely to
// fail with an unrelated error.
v, _ := rawV.ParseVariableValue(configs.VariableParseLiteral)
if v == nil {
// We'll ignore any that don't parse at all, because
// they'll fail elsewhere in this process anyway.
undeclaredVariables[varName] = op.Variables[varName]
continue
}
var rng *hcl.Range
if v.HasSourceRange() {
rng = v.SourceRange.ToHCL().Ptr()
if parsedVar.HasSourceRange() {
rng = parsedVar.SourceRange.ToHCL().Ptr()
}
// If the var is declared as ephemeral in config, go ahead and handle it
if ok && decl.Ephemeral {
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.
@ -311,17 +304,9 @@ func (b *Local) opApply(
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 this is an apply-time variable, the user must supply a
// value during apply: it can't be null.
if applyTimeVar && val.Value.IsNull() {
if applyTimeVar && parsedVar.Value.IsNull() {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Ephemeral variable must be set for apply",
@ -336,17 +321,17 @@ func (b *Local) opApply(
// 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
applyTimeValues[varName] = parsedVar
} 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 {
// We'll catch this with ParseUndeclaredVariableValues after
undeclaredVariables[varName] = rawV
undeclaredVariables[varName] = op.Variables[varName]
continue
}
val, err := plannedVariableValue.Decode(cty.DynamicPseudoType)
plannedVar, err := plannedVariableValue.Decode(cty.DynamicPseudoType)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
@ -355,11 +340,11 @@ func (b *Local) opApply(
Subject: rng,
})
} else {
if v.Value.Equals(val).False() {
if parsedVar.Value.Equals(plannedVar).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()),
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(parsedVar.Value), viewsjson.CompactValueStr(plannedVar), parsedVar.SourceType.DiagnosticLabel()),
Subject: rng,
})
}

@ -964,16 +964,10 @@ func TestApply_planWithVarFileChangingVariableValue(t *testing.T) {
}
}
func TestApply_planVars(t *testing.T) {
// This test ensures that it isn't allowed to set non-ephemeral input
// variables when applying from a saved plan file, since in that case the
// variable values come from the saved plan file.
//
// This situation was originally checked by the apply command itself,
// and that's what this test was originally exercising. This rule
// is now enforced by the "local" backend instead, but this test
// is still valid since the command instance delegates to the
// local backend.
func TestApply_planUndeclaredVars(t *testing.T) {
// This test ensures that it isn't allowed to set undeclared input variables
// when applying from a saved plan file, since in that case the variable
// values come from the saved plan file.
planPath := applyFixturePlanFile(t)
statePath := testTempFile(t)
@ -1076,7 +1070,6 @@ foo = "bar"
"with planfile passing ephemeral variable through environment variable": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) {
t.Setenv("TF_VAR_foo", "bar")
defer t.Setenv("TF_VAR_foo", "")
args := []string{
"-state", statePath,
@ -1168,7 +1161,7 @@ foo = "bar"
"without planfile passing ephemeral variable through environment variable": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) {
t.Setenv("TF_VAR_foo", "bar")
defer t.Setenv("TF_VAR_foo", "")
t.Setenv("TF_VAR_unused", `{key:"val"}`)
args := []string{
"-state", statePath,

@ -9,6 +9,11 @@ variable "bar" {
ephemeral = true
}
variable "unused" {
type = map(string)
default = null
}
resource "test_instance" "foo" {
ami = "bar"
}

Loading…
Cancel
Save