ephemeral: allow setting non-ephemeral values during apply to same value as in plan

pull/35903/head
Daniel Schmidt 1 year ago
parent bf68ac8110
commit 0e632aae86
No known key found for this signature in database
GPG Key ID: 377C3A4D62FBBBE2

@ -11,6 +11,7 @@ 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"
@ -325,23 +326,41 @@ func (b *Local) opApply(
// 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.
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),
Subject: rng,
})
// If a non-ephemeral variable is set differently between plan and apply, we should emit a diagnostic.
value, ok := plan.VariableValues[varName]
if !ok {
if v.Value.IsNull() {
continue
} else {
// TODO: Add test for this case
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),
Subject: rng,
})
}
}
val, err := value.Decode(cty.DynamicPseudoType)
if err != nil {
// TODO: Reword error message
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Variable was set with a different type when applying a saved plan",
Detail: fmt.Sprintf("The variable %s was set to a different type of value during plan than during apply. Please either don't supply the value or supply the same value if the variable.", varName),
Subject: rng,
})
} else {
if v.Value.Equals(val) == cty.False {
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. The saved plan specifies %q as the value whereas during apply the value %q was %s. To declare an ephemeral variable which is not saved in the plan file, use ephemeral = true.", varName, v.Value.GoString(), val.GoString(), v.SourceType.DiagnosticLabel()),
Subject: rng,
})
}
}
}
}
applyOpts = &terraform.ApplyOpts{

@ -860,7 +860,7 @@ func TestApply_planWithVarFile(t *testing.T) {
t.Fatalf("err: %s", err)
}
planPath := applyFixturePlanFile(t)
planPath := applyFixturePlanFileWithVariableValue(t, "bar")
statePath := testTempFile(t)
cwd, err := os.Getwd()
@ -2407,6 +2407,53 @@ func applyFixturePlanFileMatchState(t *testing.T, stateMeta statemgr.SnapshotMet
)
}
// applyFixturePlanFileWithVariableValue creates a plan file at a temporary location containing
// a single change to create the test_instance.foo and a variable value that is included in the
// "apply" test fixture, returning the location of that plan file.
func applyFixturePlanFileWithVariableValue(t *testing.T, value string) string {
_, snap := testModuleWithSnapshot(t, "apply")
plannedVal := cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"ami": cty.StringVal("bar"),
})
priorValRaw, err := plans.NewDynamicValue(cty.NullVal(plannedVal.Type()), plannedVal.Type())
if err != nil {
t.Fatal(err)
}
plannedValRaw, err := plans.NewDynamicValue(plannedVal, plannedVal.Type())
if err != nil {
t.Fatal(err)
}
plan := testPlan(t)
plan.Changes.AppendResourceInstanceChange(&plans.ResourceInstanceChangeSrc{
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
ProviderAddr: addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: priorValRaw,
After: plannedValRaw,
},
})
plan.VariableValues = map[string]plans.DynamicValue{
"foo": mustNewDynamicValue(value, cty.DynamicPseudoType),
}
return testPlanFileMatchState(
t,
snap,
states.NewState(),
plan,
statemgr.SnapshotMeta{},
)
}
const applyVarFile = `
foo = "bar"
`
@ -2414,3 +2461,12 @@ foo = "bar"
const applyVarFileJSON = `
{ "foo": "bar" }
`
func mustNewDynamicValue(val string, ty cty.Type) plans.DynamicValue {
realVal := cty.StringVal(val)
ret, err := plans.NewDynamicValue(realVal, ty)
if err != nil {
panic(err)
}
return ret
}

@ -137,6 +137,27 @@ func (v ValueSourceType) GoString() string {
return fmt.Sprintf("terraform.%s", v)
}
func (v ValueSourceType) DiagnosticLabel() string {
switch v {
case ValueFromConfig:
return "set by the default value in configuration"
case ValueFromAutoFile:
return "set by an automatically loaded .tfvars file"
case ValueFromNamedFile:
return "set by a .tfvars file passed through -var-file argument"
case ValueFromCLIArg:
return "set by a CLI argument"
case ValueFromEnvVar:
return "set by an environment variable"
case ValueFromInput:
return "set by an interactive input"
case ValueFromPlan:
return "set by the plan"
default:
return "unknown"
}
}
//go:generate go run golang.org/x/tools/cmd/stringer -type ValueSourceType
// InputValues is a map of InputValue instances.

Loading…
Cancel
Save