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 }