github.com/opentofu/opentofu@v1.7.1/internal/tofu/eval_conditions.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/zclconf/go-cty/cty"
    15  	"github.com/zclconf/go-cty/cty/convert"
    16  
    17  	"github.com/opentofu/opentofu/internal/addrs"
    18  	"github.com/opentofu/opentofu/internal/checks"
    19  	"github.com/opentofu/opentofu/internal/configs"
    20  	"github.com/opentofu/opentofu/internal/instances"
    21  	"github.com/opentofu/opentofu/internal/lang"
    22  	"github.com/opentofu/opentofu/internal/lang/marks"
    23  	"github.com/opentofu/opentofu/internal/tfdiags"
    24  )
    25  
    26  // evalCheckRules ensures that all of the given check rules pass against
    27  // the given HCL evaluation context.
    28  //
    29  // If any check rules produce an unknown result then they will be silently
    30  // ignored on the assumption that the same checks will be run again later
    31  // with fewer unknown values in the EvalContext.
    32  //
    33  // If any of the rules do not pass, the returned diagnostics will contain
    34  // errors. Otherwise, it will either be empty or contain only warnings.
    35  func evalCheckRules(typ addrs.CheckRuleType, rules []*configs.CheckRule, ctx EvalContext, self addrs.Checkable, keyData instances.RepetitionData, diagSeverity tfdiags.Severity) tfdiags.Diagnostics {
    36  	var diags tfdiags.Diagnostics
    37  
    38  	checkState := ctx.Checks()
    39  	if !checkState.ConfigHasChecks(self.ConfigCheckable()) {
    40  		// We have nothing to do if this object doesn't have any checks,
    41  		// but the "rules" slice should agree that we don't.
    42  		if ct := len(rules); ct != 0 {
    43  			panic(fmt.Sprintf("check state says that %s should have no rules, but it has %d", self, ct))
    44  		}
    45  		return diags
    46  	}
    47  
    48  	if len(rules) == 0 {
    49  		// Nothing to do
    50  		return nil
    51  	}
    52  
    53  	severity := diagSeverity.ToHCL()
    54  
    55  	for i, rule := range rules {
    56  		result, ruleDiags := evalCheckRule(addrs.NewCheckRule(self, typ, i), rule, ctx, keyData, severity)
    57  		diags = diags.Append(ruleDiags)
    58  
    59  		log.Printf("[TRACE] evalCheckRules: %s status is now %s", self, result.Status)
    60  		if result.Status == checks.StatusFail {
    61  			checkState.ReportCheckFailure(self, typ, i, result.FailureMessage)
    62  		} else {
    63  			checkState.ReportCheckResult(self, typ, i, result.Status)
    64  		}
    65  	}
    66  
    67  	return diags
    68  }
    69  
    70  type checkResult struct {
    71  	Status         checks.Status
    72  	FailureMessage string
    73  }
    74  
    75  func validateCheckRule(addr addrs.CheckRule, rule *configs.CheckRule, ctx EvalContext, keyData instances.RepetitionData) (string, *hcl.EvalContext, tfdiags.Diagnostics) {
    76  	var diags tfdiags.Diagnostics
    77  
    78  	refs, moreDiags := lang.ReferencesInExpr(addrs.ParseRef, rule.Condition)
    79  	diags = diags.Append(moreDiags)
    80  	moreRefs, moreDiags := lang.ReferencesInExpr(addrs.ParseRef, rule.ErrorMessage)
    81  	diags = diags.Append(moreDiags)
    82  	refs = append(refs, moreRefs...)
    83  
    84  	var selfReference, sourceReference addrs.Referenceable
    85  	switch addr.Type {
    86  	case addrs.ResourcePostcondition:
    87  		switch s := addr.Container.(type) {
    88  		case addrs.AbsResourceInstance:
    89  			// Only resource postconditions can refer to self
    90  			selfReference = s.Resource
    91  		default:
    92  			panic(fmt.Sprintf("Invalid self reference type %t", addr.Container))
    93  		}
    94  	case addrs.CheckAssertion:
    95  		switch s := addr.Container.(type) {
    96  		case addrs.AbsCheck:
    97  			// Only check blocks have scoped resources so need to specify their
    98  			// source.
    99  			sourceReference = s.Check
   100  		default:
   101  			panic(fmt.Sprintf("Invalid source reference type %t", addr.Container))
   102  		}
   103  	}
   104  	scope := ctx.EvaluationScope(selfReference, sourceReference, keyData)
   105  
   106  	hclCtx, moreDiags := scope.EvalContext(refs)
   107  	diags = diags.Append(moreDiags)
   108  
   109  	errorMessage, moreDiags := evalCheckErrorMessage(rule.ErrorMessage, hclCtx)
   110  	diags = diags.Append(moreDiags)
   111  
   112  	return errorMessage, hclCtx, diags
   113  }
   114  
   115  func evalCheckRule(addr addrs.CheckRule, rule *configs.CheckRule, ctx EvalContext, keyData instances.RepetitionData, severity hcl.DiagnosticSeverity) (checkResult, tfdiags.Diagnostics) {
   116  	// NOTE: Intentionally not passing the caller's selected severity in here,
   117  	// because this reports errors in the configuration itself, not the failure
   118  	// of an otherwise-valid condition.
   119  	errorMessage, hclCtx, diags := validateCheckRule(addr, rule, ctx, keyData)
   120  
   121  	const errInvalidCondition = "Invalid condition result"
   122  
   123  	resultVal, hclDiags := rule.Condition.Value(hclCtx)
   124  	diags = diags.Append(hclDiags)
   125  
   126  	if diags.HasErrors() {
   127  		log.Printf("[TRACE] evalCheckRule: %s: %s", addr.Type, diags.Err().Error())
   128  		return checkResult{Status: checks.StatusError}, diags
   129  	}
   130  
   131  	if !resultVal.IsKnown() {
   132  
   133  		// Check assertions warn if a status is unknown.
   134  		if addr.Type == addrs.CheckAssertion {
   135  			diags = diags.Append(&hcl.Diagnostic{
   136  				Severity:    hcl.DiagWarning,
   137  				Summary:     fmt.Sprintf("%s known after apply", addr.Type.Description()),
   138  				Detail:      "The condition could not be evaluated at this time, a result will be known when this plan is applied.",
   139  				Subject:     rule.Condition.Range().Ptr(),
   140  				Expression:  rule.Condition,
   141  				EvalContext: hclCtx,
   142  				Extra: &addrs.CheckRuleDiagnosticExtra{
   143  					CheckRule: addr,
   144  				},
   145  			})
   146  		}
   147  
   148  		// We'll wait until we've learned more, then.
   149  		return checkResult{Status: checks.StatusUnknown}, diags
   150  	}
   151  	if resultVal.IsNull() {
   152  		// NOTE: Intentionally not passing the caller's selected severity in here,
   153  		// because this reports errors in the configuration itself, not the failure
   154  		// of an otherwise-valid condition.
   155  		diags = diags.Append(&hcl.Diagnostic{
   156  			Severity:    hcl.DiagError,
   157  			Summary:     errInvalidCondition,
   158  			Detail:      "Condition expression must return either true or false, not null.",
   159  			Subject:     rule.Condition.Range().Ptr(),
   160  			Expression:  rule.Condition,
   161  			EvalContext: hclCtx,
   162  		})
   163  		return checkResult{Status: checks.StatusError}, diags
   164  	}
   165  	var err error
   166  	resultVal, err = convert.Convert(resultVal, cty.Bool)
   167  	if err != nil {
   168  		// NOTE: Intentionally not passing the caller's selected severity in here,
   169  		// because this reports errors in the configuration itself, not the failure
   170  		// of an otherwise-valid condition.
   171  		detail := fmt.Sprintf("Invalid condition result value: %s.", tfdiags.FormatError(err))
   172  		diags = diags.Append(&hcl.Diagnostic{
   173  			Severity:    hcl.DiagError,
   174  			Summary:     errInvalidCondition,
   175  			Detail:      detail,
   176  			Subject:     rule.Condition.Range().Ptr(),
   177  			Expression:  rule.Condition,
   178  			EvalContext: hclCtx,
   179  		})
   180  		return checkResult{Status: checks.StatusError}, diags
   181  	}
   182  
   183  	// The condition result may be marked if the expression refers to a
   184  	// sensitive value.
   185  	resultVal, _ = resultVal.Unmark()
   186  
   187  	status := checks.StatusForCtyValue(resultVal)
   188  
   189  	if status != checks.StatusFail {
   190  		return checkResult{Status: status}, diags
   191  	}
   192  
   193  	errorMessageForDiags := errorMessage
   194  	if errorMessageForDiags == "" {
   195  		errorMessageForDiags = "This check failed, but has an invalid error message as described in the other accompanying messages."
   196  	}
   197  	diags = diags.Append(&hcl.Diagnostic{
   198  		// The caller gets to choose the severity of this one, because we
   199  		// treat condition failures as warnings in the presence of
   200  		// certain special planning options.
   201  		Severity:    severity,
   202  		Summary:     fmt.Sprintf("%s failed", addr.Type.Description()),
   203  		Detail:      errorMessageForDiags,
   204  		Subject:     rule.Condition.Range().Ptr(),
   205  		Expression:  rule.Condition,
   206  		EvalContext: hclCtx,
   207  		Extra: &addrs.CheckRuleDiagnosticExtra{
   208  			CheckRule: addr,
   209  		},
   210  	})
   211  
   212  	return checkResult{
   213  		Status:         status,
   214  		FailureMessage: errorMessage,
   215  	}, diags
   216  }
   217  
   218  // evalCheckErrorMessage makes a best effort to evaluate the given expression,
   219  // as an error message string.
   220  //
   221  // It will either return a non-empty message string or it'll return diagnostics
   222  // with either errors or warnings that explain why the given expression isn't
   223  // acceptable.
   224  func evalCheckErrorMessage(expr hcl.Expression, hclCtx *hcl.EvalContext) (string, tfdiags.Diagnostics) {
   225  	var diags tfdiags.Diagnostics
   226  
   227  	val, hclDiags := expr.Value(hclCtx)
   228  	diags = diags.Append(hclDiags)
   229  	if hclDiags.HasErrors() {
   230  		return "", diags
   231  	}
   232  
   233  	val, err := convert.Convert(val, cty.String)
   234  	if err != nil {
   235  		diags = diags.Append(&hcl.Diagnostic{
   236  			Severity:    hcl.DiagError,
   237  			Summary:     "Invalid error message",
   238  			Detail:      fmt.Sprintf("Unsuitable value for error message: %s.", tfdiags.FormatError(err)),
   239  			Subject:     expr.Range().Ptr(),
   240  			Expression:  expr,
   241  			EvalContext: hclCtx,
   242  		})
   243  		return "", diags
   244  	}
   245  	if !val.IsKnown() {
   246  		return "", diags
   247  	}
   248  	if val.IsNull() {
   249  		diags = diags.Append(&hcl.Diagnostic{
   250  			Severity:    hcl.DiagError,
   251  			Summary:     "Invalid error message",
   252  			Detail:      "Unsuitable value for error message: must not be null.",
   253  			Subject:     expr.Range().Ptr(),
   254  			Expression:  expr,
   255  			EvalContext: hclCtx,
   256  		})
   257  		return "", diags
   258  	}
   259  
   260  	val, valMarks := val.Unmark()
   261  	if _, sensitive := valMarks[marks.Sensitive]; sensitive {
   262  		diags = diags.Append(&hcl.Diagnostic{
   263  			Severity: hcl.DiagWarning,
   264  			Summary:  "Error message refers to sensitive values",
   265  			Detail: `The error expression used to explain this condition refers to sensitive values, so OpenTofu will not display the resulting message.
   266  
   267  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.`,
   268  			Subject:     expr.Range().Ptr(),
   269  			Expression:  expr,
   270  			EvalContext: hclCtx,
   271  		})
   272  		return "", diags
   273  	}
   274  
   275  	// NOTE: We've discarded any other marks the string might have been carrying,
   276  	// aside from the sensitive mark.
   277  
   278  	return strings.TrimSpace(val.AsString()), diags
   279  }