github.com/opentofu/opentofu@v1.7.1/internal/tofu/eval_variable.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package tofu
     7  
     8  import (
     9  	"fmt"
    10  	"log"
    11  	"strings"
    12  
    13  	"github.com/hashicorp/hcl/v2"
    14  	"github.com/hashicorp/hcl/v2/gohcl"
    15  	"github.com/zclconf/go-cty/cty"
    16  	"github.com/zclconf/go-cty/cty/convert"
    17  
    18  	"github.com/opentofu/opentofu/internal/addrs"
    19  	"github.com/opentofu/opentofu/internal/checks"
    20  	"github.com/opentofu/opentofu/internal/configs"
    21  	"github.com/opentofu/opentofu/internal/lang/marks"
    22  	"github.com/opentofu/opentofu/internal/tfdiags"
    23  )
    24  
    25  func prepareFinalInputVariableValue(addr addrs.AbsInputVariableInstance, raw *InputValue, cfg *configs.Variable) (cty.Value, tfdiags.Diagnostics) {
    26  	var diags tfdiags.Diagnostics
    27  
    28  	convertTy := cfg.ConstraintType
    29  	log.Printf("[TRACE] prepareFinalInputVariableValue: preparing %s", addr)
    30  
    31  	var defaultVal cty.Value
    32  	if cfg.Default != cty.NilVal {
    33  		log.Printf("[TRACE] prepareFinalInputVariableValue: %s has a default value", addr)
    34  		var err error
    35  		defaultVal, err = convert.Convert(cfg.Default, convertTy)
    36  		if err != nil {
    37  			// Validation of the declaration should typically catch this,
    38  			// but we'll check it here too to be robust.
    39  			diags = diags.Append(&hcl.Diagnostic{
    40  				Severity: hcl.DiagError,
    41  				Summary:  "Invalid default value for module argument",
    42  				Detail: fmt.Sprintf(
    43  					"The default value for variable %q is incompatible with its type constraint: %s.",
    44  					cfg.Name, err,
    45  				),
    46  				Subject: &cfg.DeclRange,
    47  			})
    48  			// We'll return a placeholder unknown value to avoid producing
    49  			// redundant downstream errors.
    50  			return cty.UnknownVal(cfg.Type), diags
    51  		}
    52  	}
    53  
    54  	var sourceRange tfdiags.SourceRange
    55  	var nonFileSource string
    56  	if raw.HasSourceRange() {
    57  		sourceRange = raw.SourceRange
    58  	} else {
    59  		// If the value came from a place that isn't a file and thus doesn't
    60  		// have its own source range, we'll use the declaration range as
    61  		// our source range and generate some slightly different error
    62  		// messages.
    63  		sourceRange = tfdiags.SourceRangeFromHCL(cfg.DeclRange)
    64  		switch raw.SourceType {
    65  		case ValueFromCLIArg:
    66  			nonFileSource = fmt.Sprintf("set using -var=\"%s=...\"", addr.Variable.Name)
    67  		case ValueFromEnvVar:
    68  			nonFileSource = fmt.Sprintf("set using the TF_VAR_%s environment variable", addr.Variable.Name)
    69  		case ValueFromInput:
    70  			nonFileSource = "set using an interactive prompt"
    71  		default:
    72  			nonFileSource = "set from outside of the configuration"
    73  		}
    74  	}
    75  
    76  	given := raw.Value
    77  	if given == cty.NilVal { // The variable wasn't set at all (even to null)
    78  		log.Printf("[TRACE] prepareFinalInputVariableValue: %s has no defined value", addr)
    79  		if cfg.Required() {
    80  			// NOTE: The CLI layer typically checks for itself whether all of
    81  			// the required _root_ module variables are set, which would
    82  			// mask this error with a more specific one that refers to the
    83  			// CLI features for setting such variables. We can get here for
    84  			// child module variables, though.
    85  			log.Printf("[ERROR] prepareFinalInputVariableValue: %s is required but is not set", addr)
    86  			diags = diags.Append(&hcl.Diagnostic{
    87  				Severity: hcl.DiagError,
    88  				Summary:  `Required variable not set`,
    89  				Detail:   fmt.Sprintf(`The variable %q is required, but is not set.`, addr.Variable.Name),
    90  				Subject:  cfg.DeclRange.Ptr(),
    91  			})
    92  			// We'll return a placeholder unknown value to avoid producing
    93  			// redundant downstream errors.
    94  			return cty.UnknownVal(cfg.Type), diags
    95  		}
    96  
    97  		given = defaultVal // must be set, because we checked above that the variable isn't required
    98  	}
    99  
   100  	// Apply defaults from the variable's type constraint to the converted value,
   101  	// unless the converted value is null. We do not apply defaults to top-level
   102  	// null values, as doing so could prevent assigning null to a nullable
   103  	// variable.
   104  	if cfg.TypeDefaults != nil && !given.IsNull() {
   105  		given = cfg.TypeDefaults.Apply(given)
   106  	}
   107  
   108  	val, err := convert.Convert(given, convertTy)
   109  	if err != nil {
   110  		log.Printf("[ERROR] prepareFinalInputVariableValue: %s has unsuitable type\n  got:  %s\n  want: %s", addr, given.Type(), convertTy)
   111  		var detail string
   112  		var subject *hcl.Range
   113  		if nonFileSource != "" {
   114  			detail = fmt.Sprintf(
   115  				"Unsuitable value for %s %s: %s.",
   116  				addr, nonFileSource, err,
   117  			)
   118  			subject = cfg.DeclRange.Ptr()
   119  		} else {
   120  			detail = fmt.Sprintf(
   121  				"The given value is not suitable for %s declared at %s: %s.",
   122  				addr, cfg.DeclRange.String(), err,
   123  			)
   124  			subject = sourceRange.ToHCL().Ptr()
   125  
   126  			// In some workflows, the operator running tofu does not have access to the variables
   127  			// themselves. They are for example stored in encrypted files that will be used by the CI toolset
   128  			// and not by the operator directly. In such a case, the failing secret value should not be
   129  			// displayed to the operator
   130  			if cfg.Sensitive {
   131  				detail = fmt.Sprintf(
   132  					"The given value is not suitable for %s, which is sensitive: %s. Invalid value defined at %s.",
   133  					addr, err, sourceRange.ToHCL(),
   134  				)
   135  				subject = cfg.DeclRange.Ptr()
   136  			}
   137  		}
   138  
   139  		diags = diags.Append(&hcl.Diagnostic{
   140  			Severity: hcl.DiagError,
   141  			Summary:  "Invalid value for input variable",
   142  			Detail:   detail,
   143  			Subject:  subject,
   144  		})
   145  		// We'll return a placeholder unknown value to avoid producing
   146  		// redundant downstream errors.
   147  		return cty.UnknownVal(cfg.Type), diags
   148  	}
   149  
   150  	// By the time we get here, we know:
   151  	// - val matches the variable's type constraint
   152  	// - val is definitely not cty.NilVal, but might be a null value if the given was already null.
   153  	//
   154  	// That means we just need to handle the case where the value is null,
   155  	// which might mean we need to use the default value, or produce an error.
   156  	//
   157  	// For historical reasons we do this only for a "non-nullable" variable.
   158  	// Nullable variables just appear as null if they were set to null,
   159  	// regardless of any default value.
   160  	if val.IsNull() && !cfg.Nullable {
   161  		log.Printf("[TRACE] prepareFinalInputVariableValue: %s is defined as null", addr)
   162  		if defaultVal != cty.NilVal {
   163  			val = defaultVal
   164  		} else {
   165  			log.Printf("[ERROR] prepareFinalInputVariableValue: %s is non-nullable but set to null, and is required", addr)
   166  			if nonFileSource != "" {
   167  				diags = diags.Append(&hcl.Diagnostic{
   168  					Severity: hcl.DiagError,
   169  					Summary:  `Required variable not set`,
   170  					Detail: fmt.Sprintf(
   171  						"Unsuitable value for %s %s: required variable may not be set to null.",
   172  						addr, nonFileSource,
   173  					),
   174  					Subject: cfg.DeclRange.Ptr(),
   175  				})
   176  			} else {
   177  				diags = diags.Append(&hcl.Diagnostic{
   178  					Severity: hcl.DiagError,
   179  					Summary:  `Required variable not set`,
   180  					Detail: fmt.Sprintf(
   181  						"The given value is not suitable for %s defined at %s: required variable may not be set to null.",
   182  						addr, cfg.DeclRange.String(),
   183  					),
   184  					Subject: sourceRange.ToHCL().Ptr(),
   185  				})
   186  			}
   187  			// Stub out our return value so that the semantic checker doesn't
   188  			// produce redundant downstream errors.
   189  			val = cty.UnknownVal(cfg.Type)
   190  		}
   191  	}
   192  
   193  	return val, diags
   194  }
   195  
   196  // evalVariableValidations ensures that all of the configured custom validations
   197  // for a variable are passing.
   198  //
   199  // This must be used only after any side-effects that make the value of the
   200  // variable available for use in expression evaluation, such as
   201  // EvalModuleCallArgument for variables in descendent modules.
   202  func evalVariableValidations(addr addrs.AbsInputVariableInstance, config *configs.Variable, expr hcl.Expression, ctx EvalContext) (diags tfdiags.Diagnostics) {
   203  	if config == nil || len(config.Validations) == 0 {
   204  		log.Printf("[TRACE] evalVariableValidations: no validation rules declared for %s, so skipping", addr)
   205  		return nil
   206  	}
   207  	log.Printf("[TRACE] evalVariableValidations: validating %s", addr)
   208  
   209  	checkState := ctx.Checks()
   210  	if !checkState.ConfigHasChecks(addr.ConfigCheckable()) {
   211  		// We have nothing to do if this object doesn't have any checks,
   212  		// but the "rules" slice should agree that we don't.
   213  		if ct := len(config.Validations); ct != 0 {
   214  			panic(fmt.Sprintf("check state says that %s should have no rules, but it has %d", addr, ct))
   215  		}
   216  		return diags
   217  	}
   218  
   219  	// Variable nodes evaluate in the parent module to where they were declared
   220  	// because the value expression (n.Expr, if set) comes from the calling
   221  	// "module" block in the parent module.
   222  	//
   223  	// Validation expressions are statically validated (during configuration
   224  	// loading) to refer only to the variable being validated, so we can
   225  	// bypass our usual evaluation machinery here and just produce a minimal
   226  	// evaluation context containing just the required value, and thus avoid
   227  	// the problem that ctx's evaluation functions refer to the wrong module.
   228  	val := ctx.GetVariableValue(addr)
   229  	if val == cty.NilVal {
   230  		diags = diags.Append(&hcl.Diagnostic{
   231  			Severity: hcl.DiagError,
   232  			Summary:  "No final value for variable",
   233  			Detail:   fmt.Sprintf("OpenTofu doesn't have a final value for %s during validation. This is a bug in OpenTofu; please report it!", addr),
   234  		})
   235  		return diags
   236  	}
   237  	hclCtx := &hcl.EvalContext{
   238  		Variables: map[string]cty.Value{
   239  			"var": cty.ObjectVal(map[string]cty.Value{
   240  				config.Name: val,
   241  			}),
   242  		},
   243  		Functions: ctx.EvaluationScope(nil, nil, EvalDataForNoInstanceKey).Functions(),
   244  	}
   245  
   246  	for ix, validation := range config.Validations {
   247  		result, ruleDiags := evalVariableValidation(validation, hclCtx, addr, config, expr, ix)
   248  		diags = diags.Append(ruleDiags)
   249  
   250  		log.Printf("[TRACE] evalVariableValidations: %s status is now %s", addr, result.Status)
   251  		if result.Status == checks.StatusFail {
   252  			checkState.ReportCheckFailure(addr, addrs.InputValidation, ix, result.FailureMessage)
   253  		} else {
   254  			checkState.ReportCheckResult(addr, addrs.InputValidation, ix, result.Status)
   255  		}
   256  	}
   257  
   258  	return diags
   259  }
   260  
   261  func evalVariableValidation(validation *configs.CheckRule, hclCtx *hcl.EvalContext, addr addrs.AbsInputVariableInstance, config *configs.Variable, expr hcl.Expression, ix int) (checkResult, tfdiags.Diagnostics) {
   262  	const errInvalidCondition = "Invalid variable validation result"
   263  	const errInvalidValue = "Invalid value for variable"
   264  	var diags tfdiags.Diagnostics
   265  
   266  	result, moreDiags := validation.Condition.Value(hclCtx)
   267  	diags = diags.Append(moreDiags)
   268  	errorValue, errorDiags := validation.ErrorMessage.Value(hclCtx)
   269  
   270  	// The following error handling is a workaround to preserve backwards
   271  	// compatibility. Due to an implementation quirk, all prior versions of
   272  	// Terraform would treat error messages specified using JSON
   273  	// configuration syntax (.tf.json) as string literals, even if they
   274  	// contained the "${" template expression operator. This behaviour did
   275  	// not match that of HCL configuration syntax, where a template
   276  	// expression would result in a validation error.
   277  	//
   278  	// As a result, users writing or generating JSON configuration syntax
   279  	// may have specified error messages which are invalid template
   280  	// expressions. As we add support for error message expressions, we are
   281  	// unable to perfectly distinguish between these two cases.
   282  	//
   283  	// To ensure that we don't break backwards compatibility, we have the
   284  	// below fallback logic if the error message fails to evaluate. This
   285  	// should only have any effect for JSON configurations. The gohcl
   286  	// DecodeExpression function behaves differently when the source of the
   287  	// expression is a JSON configuration file and a nil context is passed.
   288  	if errorDiags.HasErrors() {
   289  		// Attempt to decode the expression as a string literal. Passing
   290  		// nil as the context forces a JSON syntax string value to be
   291  		// interpreted as a string literal.
   292  		var errorString string
   293  		moreErrorDiags := gohcl.DecodeExpression(validation.ErrorMessage, nil, &errorString)
   294  		if !moreErrorDiags.HasErrors() {
   295  			// Decoding succeeded, meaning that this is a JSON syntax
   296  			// string value. We rewrap that as a cty value to allow later
   297  			// decoding to succeed.
   298  			errorValue = cty.StringVal(errorString)
   299  
   300  			// This warning diagnostic explains this odd behaviour, while
   301  			// giving us an escape hatch to change this to a hard failure
   302  			// in some future OpenTofu 1.x version.
   303  			errorDiags = hcl.Diagnostics{
   304  				&hcl.Diagnostic{
   305  					Severity:    hcl.DiagWarning,
   306  					Summary:     "Validation error message expression is invalid",
   307  					Detail:      fmt.Sprintf("The error message provided could not be evaluated as an expression, so OpenTofu is interpreting it as a string literal.\n\nIn future versions of OpenTofu, this will be considered an error. Please file a GitHub issue if this would break your workflow.\n\n%s", errorDiags.Error()),
   308  					Subject:     validation.ErrorMessage.Range().Ptr(),
   309  					Context:     validation.DeclRange.Ptr(),
   310  					Expression:  validation.ErrorMessage,
   311  					EvalContext: hclCtx,
   312  				},
   313  			}
   314  		}
   315  
   316  		// We want to either report the original diagnostics if the
   317  		// fallback failed, or the warning generated above if it succeeded.
   318  		diags = diags.Append(errorDiags)
   319  	}
   320  
   321  	if diags.HasErrors() {
   322  		log.Printf("[TRACE] evalVariableValidations: %s rule %s check rule evaluation failed: %s", addr, validation.DeclRange, diags.Err().Error())
   323  	}
   324  	if !result.IsKnown() {
   325  		log.Printf("[TRACE] evalVariableValidations: %s rule %s condition value is unknown, so skipping validation for now", addr, validation.DeclRange)
   326  
   327  		return checkResult{Status: checks.StatusUnknown}, diags // We'll wait until we've learned more, then.
   328  	}
   329  	if result.IsNull() {
   330  		diags = diags.Append(&hcl.Diagnostic{
   331  			Severity:    hcl.DiagError,
   332  			Summary:     errInvalidCondition,
   333  			Detail:      "Validation condition expression must return either true or false, not null.",
   334  			Subject:     validation.Condition.Range().Ptr(),
   335  			Expression:  validation.Condition,
   336  			EvalContext: hclCtx,
   337  		})
   338  		return checkResult{Status: checks.StatusError}, diags
   339  	}
   340  	var err error
   341  	result, err = convert.Convert(result, cty.Bool)
   342  	if err != nil {
   343  		diags = diags.Append(&hcl.Diagnostic{
   344  			Severity:    hcl.DiagError,
   345  			Summary:     errInvalidCondition,
   346  			Detail:      fmt.Sprintf("Invalid validation condition result value: %s.", tfdiags.FormatError(err)),
   347  			Subject:     validation.Condition.Range().Ptr(),
   348  			Expression:  validation.Condition,
   349  			EvalContext: hclCtx,
   350  		})
   351  		return checkResult{Status: checks.StatusError}, diags
   352  	}
   353  
   354  	// Validation condition may be marked if the input variable is bound to
   355  	// a sensitive value. This is irrelevant to the validation process, so
   356  	// we discard the marks now.
   357  	result, _ = result.Unmark()
   358  	status := checks.StatusForCtyValue(result)
   359  
   360  	if status != checks.StatusFail {
   361  		return checkResult{Status: status}, diags
   362  	}
   363  
   364  	var errorMessage string
   365  	if !errorDiags.HasErrors() && errorValue.IsKnown() && !errorValue.IsNull() {
   366  		var err error
   367  		errorValue, err = convert.Convert(errorValue, cty.String)
   368  		if err != nil {
   369  			diags = diags.Append(&hcl.Diagnostic{
   370  				Severity:    hcl.DiagError,
   371  				Summary:     "Invalid error message",
   372  				Detail:      fmt.Sprintf("Unsuitable value for error message: %s.", tfdiags.FormatError(err)),
   373  				Subject:     validation.ErrorMessage.Range().Ptr(),
   374  				Expression:  validation.ErrorMessage,
   375  				EvalContext: hclCtx,
   376  			})
   377  		} else {
   378  			if marks.Has(errorValue, marks.Sensitive) {
   379  				diags = diags.Append(&hcl.Diagnostic{
   380  					Severity: hcl.DiagError,
   381  
   382  					Summary: "Error message refers to sensitive values",
   383  					Detail: `The error expression used to explain this condition refers to sensitive values. OpenTofu will not display the resulting message.
   384  
   385  You can correct this by removing references to sensitive values, or by carefully using the nonsensitive() function if the expression will not reveal the sensitive data.`,
   386  
   387  					Subject:     validation.ErrorMessage.Range().Ptr(),
   388  					Expression:  validation.ErrorMessage,
   389  					EvalContext: hclCtx,
   390  				})
   391  				errorMessage = "The error message included a sensitive value, so it will not be displayed."
   392  			} else {
   393  				errorMessage = strings.TrimSpace(errorValue.AsString())
   394  			}
   395  		}
   396  	}
   397  	if errorMessage == "" {
   398  		errorMessage = "Failed to evaluate condition error message."
   399  	}
   400  
   401  	if expr != nil {
   402  		diags = diags.Append(&hcl.Diagnostic{
   403  			Severity:    hcl.DiagError,
   404  			Summary:     errInvalidValue,
   405  			Detail:      fmt.Sprintf("%s\n\nThis was checked by the validation rule at %s.", errorMessage, validation.DeclRange.String()),
   406  			Subject:     expr.Range().Ptr(),
   407  			Expression:  validation.Condition,
   408  			EvalContext: hclCtx,
   409  			Extra: &addrs.CheckRuleDiagnosticExtra{
   410  				CheckRule: addr.CheckRule(addrs.InputValidation, ix),
   411  			},
   412  		})
   413  	} else {
   414  		// Since we don't have a source expression for a root module
   415  		// variable, we'll just report the error from the perspective
   416  		// of the variable declaration itself.
   417  		diags = diags.Append(&hcl.Diagnostic{
   418  			Severity:    hcl.DiagError,
   419  			Summary:     errInvalidValue,
   420  			Detail:      fmt.Sprintf("%s\n\nThis was checked by the validation rule at %s.", errorMessage, validation.DeclRange.String()),
   421  			Subject:     config.DeclRange.Ptr(),
   422  			Expression:  validation.Condition,
   423  			EvalContext: hclCtx,
   424  			Extra: &addrs.CheckRuleDiagnosticExtra{
   425  				CheckRule: addr.CheckRule(addrs.InputValidation, ix),
   426  			},
   427  		})
   428  	}
   429  
   430  	return checkResult{
   431  		Status:         status,
   432  		FailureMessage: errorMessage,
   433  	}, diags
   434  }