Support for static variables used with encrypted plans

This starts to address a long standing quirk of how variables are
processed for plan files.  When we introduced state and plan encryption,
we opened up the door for variables to be used when applying an
encrypted plan file.  In opentofu#1922, we discussed a workaround
where the auto tfvars file or env variables could be used when
the -var and -var-file flags were forbidden.

The approach taken here is to compare any provided input variables (from
any source) against the variables in the plan file. If there are any
mismatches, we provide a clear error.

We could potentially detect if this plan file was encrypted and only
allow this additional functionality in that circumstance. This is a
complex interaction that will need to be discussed in the corresponding
PR.

Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
This commit is contained in:
Christian Mesh 2024-09-18 15:37:11 -04:00
parent 53130fa487
commit d490309360
5 changed files with 69 additions and 65 deletions

View File

@ -12,7 +12,6 @@ import (
"sort" "sort"
"strings" "strings"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/backend" "github.com/opentofu/opentofu/internal/backend"
@ -264,31 +263,7 @@ func (b *Local) localRunForPlanFile(op *backend.Operation, pf *planfile.Reader,
// we need to apply the plan. // we need to apply the plan.
run.Plan = plan run.Plan = plan
subCall := op.RootCall.WithVariables(func(variable *configs.Variable) (cty.Value, hcl.Diagnostics) { subCall := op.RootCall.WithVariables(plan.VariableMapper())
var diags hcl.Diagnostics
name := variable.Name
v, ok := plan.VariableValues[name]
if !ok {
if variable.Required() {
// This should not happen...
return cty.DynamicVal, diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Missing plan variable " + variable.Name,
})
}
return variable.Default, nil
}
parsed, parsedErr := v.Decode(cty.DynamicPseudoType)
if parsedErr != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: parsedErr.Error(),
})
}
return parsed, diags
})
loader := configload.NewLoaderFromSnapshot(snap) loader := configload.NewLoaderFromSnapshot(snap)
config, configDiags := loader.LoadConfig(snap.Modules[""].Dir, subCall) config, configDiags := loader.LoadConfig(snap.Modules[""].Dir, subCall)
@ -298,6 +273,37 @@ func (b *Local) localRunForPlanFile(op *backend.Operation, pf *planfile.Reader,
} }
run.Config = config run.Config = config
// Check that all provided variables are in the configuration
_, undeclaredDiags := backend.ParseUndeclaredVariableValues(op.Variables, config.Module.Variables)
diags = diags.Append(undeclaredDiags)
// Check that all variables provided match
for varName, varCfg := range config.Module.Variables {
if _, ok := op.Variables[varName]; ok {
// Variable provided via cli/files/env/etc...
inputValue, inputDiags := op.RootCall.Variables()(varCfg)
// Variable provided via the plan
planValue, planDiags := subCall.Variables()(varCfg)
diags = diags.Append(inputDiags).Append(planDiags)
if inputDiags.HasErrors() || planDiags.HasErrors() {
return nil, snap, diags
}
if inputValue.Equals(planValue).False() {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Mismatch between input and plan variable value",
// TODO this is not a great error message
fmt.Sprintf("Value %s for %s was provided, but does not match plan's value of %s", inputValue, varName, planValue),
))
}
}
}
if diags.HasErrors() {
return nil, snap, diags
}
// NOTE: We're intentionally comparing the current locks with the // NOTE: We're intentionally comparing the current locks with the
// configuration snapshot, rather than the lock snapshot in the plan file, // configuration snapshot, rather than the lock snapshot in the plan file,
// because it's the current locks which dictate our plugin selections // because it's the current locks which dictate our plugin selections

View File

@ -87,17 +87,6 @@ func (c *ApplyCommand) Run(rawArgs []string) int {
return 1 return 1
} }
// Check for invalid combination of plan file and variable overrides
if planFile != nil && !args.Vars.Empty() {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Can't set variables when applying a saved plan",
"The -var and -var-file options cannot be used when applying a saved plan file, because a saved plan includes the variable values that were set when it was created.",
))
view.Diagnostics(diags)
return 1
}
// FIXME: the -input flag value is needed to initialize the backend and the // FIXME: the -input flag value is needed to initialize the backend and the
// operation, but there is no clear path to pass this value down, so we // operation, but there is no clear path to pass this value down, so we
// continue to mutate the Meta object state for now. // continue to mutate the Meta object state for now.

View File

@ -12,9 +12,6 @@ import (
"os" "os"
"strings" "strings"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/backend" "github.com/opentofu/opentofu/internal/backend"
"github.com/opentofu/opentofu/internal/cloud" "github.com/opentofu/opentofu/internal/cloud"
"github.com/opentofu/opentofu/internal/cloud/cloudplan" "github.com/opentofu/opentofu/internal/cloud/cloudplan"
@ -371,31 +368,7 @@ func getDataFromPlanfileReader(planReader *planfile.Reader, rootCall configs.Sta
return nil, nil, nil, err return nil, nil, nil, err
} }
subCall := rootCall.WithVariables(func(variable *configs.Variable) (cty.Value, hcl.Diagnostics) { subCall := rootCall.WithVariables(plan.VariableMapper())
var diags hcl.Diagnostics
name := variable.Name
v, ok := plan.VariableValues[name]
if !ok {
if variable.Required() {
// This should not happen...
return cty.DynamicVal, diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Missing plan variable " + variable.Name,
})
}
return variable.Default, nil
}
parsed, parsedErr := v.Decode(cty.DynamicPseudoType)
if parsedErr != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: parsedErr.Error(),
})
}
return parsed, diags
})
// Get config // Get config
config, diags := planReader.ReadConfig(subCall) config, diags := planReader.ReadConfig(subCall)

View File

@ -48,6 +48,10 @@ func NewStaticModuleCall(addr addrs.Module, vars StaticModuleVariables, rootPath
} }
} }
func (s StaticModuleCall) Variables() StaticModuleVariables {
return s.vars
}
func (s StaticModuleCall) WithVariables(vars StaticModuleVariables) StaticModuleCall { func (s StaticModuleCall) WithVariables(vars StaticModuleVariables) StaticModuleCall {
return StaticModuleCall{ return StaticModuleCall{
addr: s.addr, addr: s.addr,

View File

@ -9,9 +9,11 @@ import (
"sort" "sort"
"time" "time"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/configs/configschema" "github.com/opentofu/opentofu/internal/configs/configschema"
"github.com/opentofu/opentofu/internal/lang/globalref" "github.com/opentofu/opentofu/internal/lang/globalref"
"github.com/opentofu/opentofu/internal/states" "github.com/opentofu/opentofu/internal/states"
@ -197,6 +199,36 @@ func (p *Plan) ProviderAddrs() []addrs.AbsProviderConfig {
return ret return ret
} }
// Variable mapper checks that all of the provided variables match what has been provided in the plan
// They may be sourced from the environment, from cli args, and autoloaded tfvars files
func (plan *Plan) VariableMapper() configs.StaticModuleVariables {
return func(variable *configs.Variable) (cty.Value, hcl.Diagnostics) {
var diags hcl.Diagnostics
name := variable.Name
v, ok := plan.VariableValues[name]
if !ok {
if variable.Required() {
// This should not happen...
return cty.DynamicVal, diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Missing plan variable " + variable.Name,
})
}
return variable.Default, nil
}
parsed, parsedErr := v.Decode(cty.DynamicPseudoType)
if parsedErr != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: parsedErr.Error(),
})
}
return parsed, diags
}
}
// Backend represents the backend-related configuration and other data as it // Backend represents the backend-related configuration and other data as it
// existed when a plan was created. // existed when a plan was created.
type Backend struct { type Backend struct {