Repository: opentofu/opentofu
File: internal/backend/local/backend_local.go (Go)

Code before:

	// 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
	// in coreOpts below. However, we'll also separately check that the
	// plan file has identical locked plugins below, and thus we're effectively
	// checking consistency with both here.
	if errs := config.VerifyDependencySelections(op.DependencyLocks); len(errs) > 0 {
		var buf strings.Builder
		for _, err := range errs {
			fmt.Fprintf(&buf, "\n  - %s", err.Error())
		}
		diags = diags.Append(tfdiags.Sourceless(
			tfdiags.Error,
			"Inconsistent dependency lock file",
			fmt.Sprintf(
				"The following dependency selections recorded in the lock file are inconsistent with the configuration in the saved plan:%s\n\nA saved plan can be applied only to the same configuration it was created from. Create a new plan from the updated configuration.",
				buf.String(),
			),
		))

Review comment (security):
We don't really have a good, general-purpose way to include arbitrary values in error messages, because they could be anything from a single string to a huge nested object structure.

For that reason, typically we've just avoided putting values in error messages at all. That also neatly avoids the problem of potentially disclosing sensitive values. Unfortunately since this is a "sourceless" diagnostic it won't have the usual context about which variables were in scope for its evaluation, but I'd suggest that for now we just accept this error message being kinda generic and iterate on it if we get feedback about it.

Specifically then, I'd suggest something like this for now:

> If %s is set during the apply phase then its value must be equal to the one used during the planning phase.

If we do end up wanting to improve this later then perhaps we can find a way to generalized [newDiagnosticSnippetValueDescription](https://github.com/opentofu/opentofu/blob/85bfb19577733f6f9636c92b6db0bdc1da454ffa/internal/command/views/json/diagnostic.go#L422-L480) for use in other contexts, but I'd want to know that this error message arises often enough and needs that context enough to justify it, since that code is already quite annoying to extend for ongoing new requirements even for its current singular purpose of the automatic context in information diagnostics.



Code after:
		return nil, snap, diags
	}
	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",
					fmt.Sprintf("Value saved in the plan file for variable %q is different from the one given to the current command.", varName),
				))
			}
		}
	}

	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
	// in coreOpts below. However, we'll also separately check that the
	// plan file has identical locked plugins below, and thus we're effectively
	// checking consistency with both here.
	if errs := config.VerifyDependencySelections(op.DependencyLocks); len(errs) > 0 {
		var buf strings.Builder
		for _, err := range errs {
			fmt.Fprintf(&buf, "\n  - %s", err.Error())
		}
		diags = diags.Append(tfdiags.Sourceless(
			tfdiags.Error,
			"Inconsistent dependency lock file",
			fmt.Sprintf(
				"The following dependency selections recorded in the lock file are inconsistent with the configuration in the saved plan:%s\n\nA saved plan can be applied only to the same configuration it was created from. Create a new plan from the updated configuration.",
				buf.String(),