From d490309360982e3ca95a46d153240b1458be539f Mon Sep 17 00:00:00 2001 From: Christian Mesh Date: Wed, 18 Sep 2024 15:37:11 -0400 Subject: [PATCH] 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 --- internal/backend/local/backend_local.go | 58 ++++++++++++++----------- internal/command/apply.go | 11 ----- internal/command/show.go | 29 +------------ internal/configs/static_evaluator.go | 4 ++ internal/plans/plan.go | 32 ++++++++++++++ 5 files changed, 69 insertions(+), 65 deletions(-) diff --git a/internal/backend/local/backend_local.go b/internal/backend/local/backend_local.go index 9ae703eb4e..fdf2aad8af 100644 --- a/internal/backend/local/backend_local.go +++ b/internal/backend/local/backend_local.go @@ -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 diff --git a/internal/command/apply.go b/internal/command/apply.go index f827b02d72..133c006e20 100644 --- a/internal/command/apply.go +++ b/internal/command/apply.go @@ -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. diff --git a/internal/command/show.go b/internal/command/show.go index c2a7467a61..2d1fe9acad 100644 --- a/internal/command/show.go +++ b/internal/command/show.go @@ -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) diff --git a/internal/configs/static_evaluator.go b/internal/configs/static_evaluator.go index 2c5a298d20..7d0a7f284a 100644 --- a/internal/configs/static_evaluator.go +++ b/internal/configs/static_evaluator.go @@ -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, diff --git a/internal/plans/plan.go b/internal/plans/plan.go index 6043613e2f..0a98626404 100644 --- a/internal/plans/plan.go +++ b/internal/plans/plan.go @@ -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 {