github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/internal/configs/checks.go (about) 1 package configs 2 3 import ( 4 "fmt" 5 6 "github.com/hashicorp/hcl/v2" 7 "github.com/eliastor/durgaform/internal/addrs" 8 "github.com/eliastor/durgaform/internal/lang" 9 ) 10 11 // CheckRule represents a configuration-defined validation rule, precondition, 12 // or postcondition. Blocks of this sort can appear in a few different places 13 // in configuration, including "validation" blocks for variables, 14 // and "precondition" and "postcondition" blocks for resources. 15 type CheckRule struct { 16 // Condition is an expression that must evaluate to true if the condition 17 // holds or false if it does not. If the expression produces an error then 18 // that's considered to be a bug in the module defining the check. 19 // 20 // The available variables in a condition expression vary depending on what 21 // a check is attached to. For example, validation rules attached to 22 // input variables can only refer to the variable that is being validated. 23 Condition hcl.Expression 24 25 // ErrorMessage should be one or more full sentences, which should be in 26 // English for consistency with the rest of the error message output but 27 // can in practice be in any language. The message should describe what is 28 // required for the condition to return true in a way that would make sense 29 // to a caller of the module. 30 // 31 // The error message expression has the same variables available for 32 // interpolation as the corresponding condition. 33 ErrorMessage hcl.Expression 34 35 DeclRange hcl.Range 36 } 37 38 // validateSelfReferences looks for references in the check rule matching the 39 // specified resource address, returning error diagnostics if such a reference 40 // is found. 41 func (cr *CheckRule) validateSelfReferences(checkType string, addr addrs.Resource) hcl.Diagnostics { 42 var diags hcl.Diagnostics 43 exprs := []hcl.Expression{ 44 cr.Condition, 45 cr.ErrorMessage, 46 } 47 for _, expr := range exprs { 48 if expr == nil { 49 continue 50 } 51 refs, _ := lang.References(expr.Variables()) 52 for _, ref := range refs { 53 var refAddr addrs.Resource 54 55 switch rs := ref.Subject.(type) { 56 case addrs.Resource: 57 refAddr = rs 58 case addrs.ResourceInstance: 59 refAddr = rs.Resource 60 default: 61 continue 62 } 63 64 if refAddr.Equal(addr) { 65 diags = diags.Append(&hcl.Diagnostic{ 66 Severity: hcl.DiagError, 67 Summary: fmt.Sprintf("Invalid reference in %s", checkType), 68 Detail: fmt.Sprintf("Configuration for %s may not refer to itself.", addr.String()), 69 Subject: expr.Range().Ptr(), 70 }) 71 break 72 } 73 } 74 } 75 return diags 76 } 77 78 // decodeCheckRuleBlock decodes the contents of the given block as a check rule. 79 // 80 // Unlike most of our "decode..." functions, this one can be applied to blocks 81 // of various types as long as their body structures are "check-shaped". The 82 // function takes the containing block only because some error messages will 83 // refer to its location, and the returned object's DeclRange will be the 84 // block's header. 85 func decodeCheckRuleBlock(block *hcl.Block, override bool) (*CheckRule, hcl.Diagnostics) { 86 var diags hcl.Diagnostics 87 cr := &CheckRule{ 88 DeclRange: block.DefRange, 89 } 90 91 if override { 92 // For now we'll just forbid overriding check blocks, to simplify 93 // the initial design. If we can find a clear use-case for overriding 94 // checks in override files and there's a way to define it that 95 // isn't confusing then we could relax this. 96 diags = diags.Append(&hcl.Diagnostic{ 97 Severity: hcl.DiagError, 98 Summary: fmt.Sprintf("Can't override %s blocks", block.Type), 99 Detail: fmt.Sprintf("Override files cannot override %q blocks.", block.Type), 100 Subject: cr.DeclRange.Ptr(), 101 }) 102 return cr, diags 103 } 104 105 content, moreDiags := block.Body.Content(checkRuleBlockSchema) 106 diags = append(diags, moreDiags...) 107 108 if attr, exists := content.Attributes["condition"]; exists { 109 cr.Condition = attr.Expr 110 111 if len(cr.Condition.Variables()) == 0 { 112 // A condition expression that doesn't refer to any variable is 113 // pointless, because its result would always be a constant. 114 diags = diags.Append(&hcl.Diagnostic{ 115 Severity: hcl.DiagError, 116 Summary: fmt.Sprintf("Invalid %s expression", block.Type), 117 Detail: "The condition expression must refer to at least one object from elsewhere in the configuration, or else its result would not be checking anything.", 118 Subject: cr.Condition.Range().Ptr(), 119 }) 120 } 121 } 122 123 if attr, exists := content.Attributes["error_message"]; exists { 124 cr.ErrorMessage = attr.Expr 125 } 126 127 return cr, diags 128 } 129 130 var checkRuleBlockSchema = &hcl.BodySchema{ 131 Attributes: []hcl.AttributeSchema{ 132 { 133 Name: "condition", 134 Required: true, 135 }, 136 { 137 Name: "error_message", 138 Required: true, 139 }, 140 }, 141 }