github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/states/checks.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package states 5 6 import ( 7 "github.com/terramate-io/tf/addrs" 8 "github.com/terramate-io/tf/checks" 9 ) 10 11 // CheckResults represents a summary snapshot of the status of a set of checks 12 // declared in configuration, updated after each Terraform Core run that 13 // changes the state or remote system in a way that might impact the check 14 // results. 15 // 16 // Unlike a checks.State, this type only tracks the overall results for 17 // each checkable object and doesn't aim to preserve the identity of individual 18 // checks in the configuration. For our UI reporting purposes, it is entire 19 // objects that pass or fail based on their declared checks; the individual 20 // checks have no durable identity between runs, and so are only a language 21 // design convenience to help authors describe various independent conditions 22 // with different failure messages each. 23 // 24 // CheckResults should typically be considered immutable once constructed: 25 // instead of updating it in-place,instead construct an entirely new 26 // CheckResults object based on a fresh checks.State. 27 type CheckResults struct { 28 // ConfigResults has all of the individual check results grouped by the 29 // configuration object they relate to. 30 // 31 // The top-level map here will always have a key for every configuration 32 // object that includes checks at the time of evaluating the results, 33 // even if there turned out to be no instances of that object and 34 // therefore no individual check results. 35 ConfigResults addrs.Map[addrs.ConfigCheckable, *CheckResultAggregate] 36 } 37 38 // CheckResultAggregate represents both the overall result for a particular 39 // configured object that has checks and the individual checkable objects 40 // it declared, if any. 41 type CheckResultAggregate struct { 42 // Status is the aggregate status across all objects. 43 // 44 // Sometimes an error or check failure during planning will prevent 45 // Terraform Core from even determining the individual checkable objects 46 // associated with a downstream configuration object, and that situation is 47 // described here by this Status being checks.StatusUnknown and there being 48 // no elements in the ObjectResults field. 49 // 50 // That's different than Terraform Core explicitly reporting that there are 51 // no instances of the config object (e.g. a resource with count = 0), 52 // which leads to the aggregate status being checks.StatusPass while 53 // ObjectResults is still empty. 54 Status checks.Status 55 56 ObjectResults addrs.Map[addrs.Checkable, *CheckResultObject] 57 } 58 59 // CheckResultObject is the check status for a single checkable object. 60 // 61 // This aggregates together all of the checks associated with a particular 62 // object into a single pass/fail/error/unknown result, because checkable 63 // objects have durable addresses that can survive between runs, but their 64 // individual checks do not. (Module authors are free to reorder their checks 65 // for a particular object in the configuration with no change in meaning.) 66 type CheckResultObject struct { 67 // Status is the check status of the checkable object, derived from the 68 // results of all of its individual checks. 69 Status checks.Status 70 71 // FailureMessages is an optional set of module-author-defined messages 72 // describing the problems that the checks detected, for objects whose 73 // status is checks.StatusFail. 74 // 75 // (checks.StatusError problems get reported as normal diagnostics during 76 // evaluation instead, and so will not appear here.) 77 FailureMessages []string 78 } 79 80 // NewCheckResults constructs a new states.CheckResults object that is a 81 // snapshot of the check statuses recorded in the given checks.State object. 82 // 83 // This should be called only after a Terraform Core run has completed and 84 // recorded any results from running the checks in the given object. 85 func NewCheckResults(source *checks.State) *CheckResults { 86 ret := &CheckResults{ 87 ConfigResults: addrs.MakeMap[addrs.ConfigCheckable, *CheckResultAggregate](), 88 } 89 90 for _, configAddr := range source.AllConfigAddrs() { 91 aggr := &CheckResultAggregate{ 92 Status: source.AggregateCheckStatus(configAddr), 93 ObjectResults: addrs.MakeMap[addrs.Checkable, *CheckResultObject](), 94 } 95 96 for _, objectAddr := range source.ObjectAddrs(configAddr) { 97 obj := &CheckResultObject{ 98 Status: source.ObjectCheckStatus(objectAddr), 99 FailureMessages: source.ObjectFailureMessages(objectAddr), 100 } 101 aggr.ObjectResults.Put(objectAddr, obj) 102 } 103 104 ret.ConfigResults.Put(configAddr, aggr) 105 } 106 107 // If there aren't actually any configuration objects then we'll just 108 // leave the map as a whole nil, because having it be zero-value makes 109 // life easier for deep comparisons in unit tests elsewhere. 110 if ret.ConfigResults.Len() == 0 { 111 ret.ConfigResults.Elems = nil 112 } 113 114 return ret 115 } 116 117 // GetObjectResult looks up the result for a single object, or nil if there 118 // is no such object. 119 // 120 // In main code we shouldn't typically need to look up individual objects 121 // like this, since we'll usually be reporting check results in an aggregate 122 // form, but determining the result of a particular object is useful in our 123 // internal unit tests, and so this is here primarily for that purpose. 124 func (r *CheckResults) GetObjectResult(objectAddr addrs.Checkable) *CheckResultObject { 125 configAddr := objectAddr.ConfigCheckable() 126 127 aggr := r.ConfigResults.Get(configAddr) 128 if aggr == nil { 129 return nil 130 } 131 132 return aggr.ObjectResults.Get(objectAddr) 133 } 134 135 func (r *CheckResults) DeepCopy() *CheckResults { 136 if r == nil { 137 return nil 138 } 139 ret := &CheckResults{} 140 if r.ConfigResults.Elems == nil { 141 return ret 142 } 143 144 ret.ConfigResults = addrs.MakeMap[addrs.ConfigCheckable, *CheckResultAggregate]() 145 146 for _, configElem := range r.ConfigResults.Elems { 147 aggr := &CheckResultAggregate{ 148 Status: configElem.Value.Status, 149 } 150 151 if configElem.Value.ObjectResults.Elems != nil { 152 aggr.ObjectResults = addrs.MakeMap[addrs.Checkable, *CheckResultObject]() 153 154 for _, objectElem := range configElem.Value.ObjectResults.Elems { 155 result := &CheckResultObject{ 156 Status: objectElem.Value.Status, 157 158 // NOTE: We don't deep-copy this slice because it's 159 // immutable once constructed by convention. 160 FailureMessages: objectElem.Value.FailureMessages, 161 } 162 aggr.ObjectResults.Put(objectElem.Key, result) 163 } 164 } 165 166 ret.ConfigResults.Put(configElem.Key, aggr) 167 } 168 169 return ret 170 } 171 172 // ObjectAddrsKnown determines whether the set of objects recorded in this 173 // aggregate is accurate (true) or if it's incomplete as a result of the 174 // run being interrupted before instance expansion. 175 func (r *CheckResultAggregate) ObjectAddrsKnown() bool { 176 if r.ObjectResults.Len() != 0 { 177 // If there are any object results at all then we definitely know. 178 return true 179 } 180 181 // If we don't have any object addresses then we distinguish a known 182 // empty set of objects from an unknown set of objects by the aggregate 183 // status being unknown. 184 return r.Status != checks.StatusUnknown 185 }