mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
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:
parent
53130fa487
commit
d490309360
@ -12,7 +12,6 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"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.
|
||||
run.Plan = plan
|
||||
|
||||
subCall := op.RootCall.WithVariables(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
|
||||
})
|
||||
subCall := op.RootCall.WithVariables(plan.VariableMapper())
|
||||
|
||||
loader := configload.NewLoaderFromSnapshot(snap)
|
||||
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
|
||||
|
||||
// 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
|
||||
// configuration snapshot, rather than the lock snapshot in the plan file,
|
||||
// because it's the current locks which dictate our plugin selections
|
||||
|
@ -87,17 +87,6 @@ func (c *ApplyCommand) Run(rawArgs []string) int {
|
||||
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
|
||||
// operation, but there is no clear path to pass this value down, so we
|
||||
// continue to mutate the Meta object state for now.
|
||||
|
@ -12,9 +12,6 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/backend"
|
||||
"github.com/opentofu/opentofu/internal/cloud"
|
||||
"github.com/opentofu/opentofu/internal/cloud/cloudplan"
|
||||
@ -371,31 +368,7 @@ func getDataFromPlanfileReader(planReader *planfile.Reader, rootCall configs.Sta
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
subCall := rootCall.WithVariables(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
|
||||
})
|
||||
subCall := rootCall.WithVariables(plan.VariableMapper())
|
||||
|
||||
// Get config
|
||||
config, diags := planReader.ReadConfig(subCall)
|
||||
|
@ -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 {
|
||||
return StaticModuleCall{
|
||||
addr: s.addr,
|
||||
|
@ -9,9 +9,11 @@ import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/configs"
|
||||
"github.com/opentofu/opentofu/internal/configs/configschema"
|
||||
"github.com/opentofu/opentofu/internal/lang/globalref"
|
||||
"github.com/opentofu/opentofu/internal/states"
|
||||
@ -197,6 +199,36 @@ func (p *Plan) ProviderAddrs() []addrs.AbsProviderConfig {
|
||||
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
|
||||
// existed when a plan was created.
|
||||
type Backend struct {
|
||||
|
Loading…
Reference in New Issue
Block a user