github.com/kanishk98/terraform@v1.3.0-dev.0.20220917174235-661ca8088a6a/internal/terraform/eval_conditions.go (about)

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