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 }