github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/internal/durgaform/eval_conditions.go (about)

     1  package durgaform
     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/eliastor/durgaform/internal/addrs"
    13  	"github.com/eliastor/durgaform/internal/configs"
    14  	"github.com/eliastor/durgaform/internal/instances"
    15  	"github.com/eliastor/durgaform/internal/lang"
    16  	"github.com/eliastor/durgaform/internal/lang/marks"
    17  	"github.com/eliastor/durgaform/internal/plans"
    18  	"github.com/eliastor/durgaform/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  	if len(rules) == 0 {
    34  		// Nothing to do
    35  		return nil
    36  	}
    37  
    38  	severity := diagSeverity.ToHCL()
    39  
    40  	for i, rule := range rules {
    41  		checkAddr := self.Check(typ, i)
    42  
    43  		conditionResult, ruleDiags := evalCheckRule(typ, rule, ctx, self, keyData, severity)
    44  		diags = diags.Append(ruleDiags)
    45  		ctx.Conditions().SetResult(checkAddr, conditionResult)
    46  	}
    47  
    48  	return diags
    49  }
    50  
    51  func evalCheckRule(typ addrs.CheckType, rule *configs.CheckRule, ctx EvalContext, self addrs.Checkable, keyData instances.RepetitionData, severity hcl.DiagnosticSeverity) (*plans.ConditionResult, tfdiags.Diagnostics) {
    52  	var diags tfdiags.Diagnostics
    53  	const errInvalidCondition = "Invalid condition result"
    54  
    55  	refs, moreDiags := lang.ReferencesInExpr(rule.Condition)
    56  	diags = diags.Append(moreDiags)
    57  	moreRefs, moreDiags := lang.ReferencesInExpr(rule.ErrorMessage)
    58  	diags = diags.Append(moreDiags)
    59  	refs = append(refs, moreRefs...)
    60  
    61  	conditionResult := &plans.ConditionResult{
    62  		Address: self,
    63  		Result:  cty.UnknownVal(cty.Bool),
    64  		Type:    typ,
    65  	}
    66  
    67  	var selfReference addrs.Referenceable
    68  	// Only resource postconditions can refer to self
    69  	if typ == addrs.ResourcePostcondition {
    70  		switch s := self.(type) {
    71  		case addrs.AbsResourceInstance:
    72  			selfReference = s.Resource
    73  		default:
    74  			panic(fmt.Sprintf("Invalid self reference type %t", self))
    75  		}
    76  	}
    77  	scope := ctx.EvaluationScope(selfReference, keyData)
    78  
    79  	hclCtx, moreDiags := scope.EvalContext(refs)
    80  	diags = diags.Append(moreDiags)
    81  
    82  	result, hclDiags := rule.Condition.Value(hclCtx)
    83  	diags = diags.Append(hclDiags)
    84  
    85  	errorValue, errorDiags := rule.ErrorMessage.Value(hclCtx)
    86  	diags = diags.Append(errorDiags)
    87  
    88  	if diags.HasErrors() {
    89  		log.Printf("[TRACE] evalCheckRule: %s: %s", typ, diags.Err().Error())
    90  	}
    91  
    92  	if !result.IsKnown() {
    93  		// We'll wait until we've learned more, then.
    94  		return conditionResult, diags
    95  	}
    96  
    97  	if result.IsNull() {
    98  		diags = diags.Append(&hcl.Diagnostic{
    99  			Severity:    severity,
   100  			Summary:     errInvalidCondition,
   101  			Detail:      "Condition expression must return either true or false, not null.",
   102  			Subject:     rule.Condition.Range().Ptr(),
   103  			Expression:  rule.Condition,
   104  			EvalContext: hclCtx,
   105  		})
   106  		conditionResult.Result = cty.False
   107  		conditionResult.ErrorMessage = "Condition expression must return either true or false, not null."
   108  		return conditionResult, diags
   109  	}
   110  	var err error
   111  	result, err = convert.Convert(result, cty.Bool)
   112  	if err != nil {
   113  		detail := fmt.Sprintf("Invalid condition result value: %s.", tfdiags.FormatError(err))
   114  		diags = diags.Append(&hcl.Diagnostic{
   115  			Severity:    severity,
   116  			Summary:     errInvalidCondition,
   117  			Detail:      detail,
   118  			Subject:     rule.Condition.Range().Ptr(),
   119  			Expression:  rule.Condition,
   120  			EvalContext: hclCtx,
   121  		})
   122  		conditionResult.Result = cty.False
   123  		conditionResult.ErrorMessage = detail
   124  		return conditionResult, diags
   125  	}
   126  
   127  	// The condition result may be marked if the expression refers to a
   128  	// sensitive value.
   129  	result, _ = result.Unmark()
   130  	conditionResult.Result = result
   131  
   132  	if result.True() {
   133  		return conditionResult, diags
   134  	}
   135  
   136  	var errorMessage string
   137  	if !errorDiags.HasErrors() && errorValue.IsKnown() && !errorValue.IsNull() {
   138  		var err error
   139  		errorValue, err = convert.Convert(errorValue, cty.String)
   140  		if err != nil {
   141  			diags = diags.Append(&hcl.Diagnostic{
   142  				Severity:    severity,
   143  				Summary:     "Invalid error message",
   144  				Detail:      fmt.Sprintf("Unsuitable value for error message: %s.", tfdiags.FormatError(err)),
   145  				Subject:     rule.ErrorMessage.Range().Ptr(),
   146  				Expression:  rule.ErrorMessage,
   147  				EvalContext: hclCtx,
   148  			})
   149  		} else {
   150  			if marks.Has(errorValue, marks.Sensitive) {
   151  				diags = diags.Append(&hcl.Diagnostic{
   152  					Severity: severity,
   153  
   154  					Summary: "Error message refers to sensitive values",
   155  					Detail: `The error expression used to explain this condition refers to sensitive values. Durgaform will not display the resulting message.
   156  
   157  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.`,
   158  
   159  					Subject:     rule.ErrorMessage.Range().Ptr(),
   160  					Expression:  rule.ErrorMessage,
   161  					EvalContext: hclCtx,
   162  				})
   163  				errorMessage = "The error message included a sensitive value, so it will not be displayed."
   164  			} else {
   165  				errorMessage = strings.TrimSpace(errorValue.AsString())
   166  			}
   167  		}
   168  	}
   169  	if errorMessage == "" {
   170  		errorMessage = "Failed to evaluate condition error message."
   171  	}
   172  	diags = diags.Append(&hcl.Diagnostic{
   173  		Severity:    severity,
   174  		Summary:     fmt.Sprintf("%s failed", typ.Description()),
   175  		Detail:      errorMessage,
   176  		Subject:     rule.Condition.Range().Ptr(),
   177  		Expression:  rule.Condition,
   178  		EvalContext: hclCtx,
   179  	})
   180  	conditionResult.ErrorMessage = errorMessage
   181  	return conditionResult, diags
   182  }