-
Notifications
You must be signed in to change notification settings - Fork 10
/
state.go
290 lines (259 loc) · 9.68 KB
/
state.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
package checks
import (
"fmt"
"sort"
"sync"
"github.com/snyk/policy-engine/pkg/internal/terraform/addrs"
"github.com/snyk/policy-engine/pkg/internal/terraform/configs"
)
// State is a container for state tracking of all of the the checks declared in
// a particular Terraform configuration and their current statuses.
//
// A State object is mutable during plan and apply operations but should
// otherwise be treated as a read-only snapshot of the status of checks
// at a particular moment.
//
// The checks State tracks a few different concepts:
// - configuration objects: items in the configuration which statically
// declare some checks associated with zero or more checkable objects.
// - checkable objects: dynamically-determined objects that are each
// associated with one configuration object.
// - checks: a single check that is declared as part of a configuration
// object and then resolved once for each of its associated checkable
// objects.
// - check statuses: the current state of a particular check associated
// with a particular checkable object.
//
// This container type is concurrency-safe for both reads and writes through
// its various methods.
type State struct {
mu sync.Mutex
statuses addrs.Map[addrs.ConfigCheckable, *configCheckableState]
failureMsgs addrs.Map[addrs.Check, string]
}
// configCheckableState is an internal part of type State that represents
// the evaluation status for a particular addrs.ConfigCheckable address.
//
// Its initial state, at the beginning of a run, is that it doesn't even know
// how many checkable objects will be dynamically-declared yet. Terraform Core
// will notify the State object of the associated Checkables once
// it has decided the appropriate expansion of that configuration object,
// and then will gradually report the results of each check once the graph
// walk reaches it.
//
// This must be accessed only while holding the mutex inside the associated
// State object.
type configCheckableState struct {
// checkTypes captures the expected number of checks of each type
// associated with object declared by this configuration construct. Since
// checks are statically declared (even though the checkable objects
// aren't) we can compute this only from the configuration.
checkTypes map[addrs.CheckType]int
// objects represents the set of dynamic checkable objects associated
// with this configuration construct. This is initially nil to represent
// that we don't know the objects yet, and is replaced by a non-nil map
// once Terraform Core reports the expansion of this configuration
// construct.
//
// The leaf Status values will initially be StatusUnknown
// and then gradually updated by Terraform Core as it visits the
// individual checkable objects and reports their status.
objects addrs.Map[addrs.Checkable, map[addrs.CheckType][]Status]
}
// NOTE: For the "Report"-prefixed methods that we use to gradually update
// the structure with results during a plan or apply operation, see the
// state_report.go file also in this package.
// NewState returns a new State object representing the check statuses of
// objects declared in the given configuration.
//
// The configuration determines which configuration objects and associated
// checks we'll be expecting to see, so that we can seed their statuses as
// all unknown until we see affirmative reports sent by the Report-prefixed
// methods on Checks.
func NewState(config *configs.Config) *State {
return &State{
statuses: initialStatuses(config),
}
}
// ConfigHasChecks returns true if and only if the given address refers to
// a configuration object that this State object is expecting to recieve
// statuses for.
//
// Other methods of Checks will typically panic if given a config address
// that would not have returned true from ConfigHasChecked.
func (c *State) ConfigHasChecks(addr addrs.ConfigCheckable) bool {
c.mu.Lock()
defer c.mu.Unlock()
return c.statuses.Has(addr)
}
// AllConfigAddrs returns all of the addresses of all configuration objects
// that could potentially produce checkable objects at runtime.
//
// This is a good starting point for reporting on the outcome of all of the
// configured checks at the configuration level of granularity, e.g. for
// automated testing reports where we want to report the status of all
// configured checks even if the graph walk aborted before we reached any
// of their objects.
func (c *State) AllConfigAddrs() addrs.Set[addrs.ConfigCheckable] {
c.mu.Lock()
defer c.mu.Unlock()
return c.statuses.Keys()
}
// ObjectAddrs returns the addresses of individual checkable objects belonging
// to the configuration object with the given address.
//
// This will panic if the given address isn't a known configuration object
// that has checks.
func (c *State) ObjectAddrs(configAddr addrs.ConfigCheckable) addrs.Set[addrs.Checkable] {
c.mu.Lock()
defer c.mu.Unlock()
st, ok := c.statuses.GetOk(configAddr)
if !ok {
panic(fmt.Sprintf("unknown configuration object %s", configAddr))
}
ret := addrs.MakeSet[addrs.Checkable]()
for _, elem := range st.objects.Elems {
ret.Add(elem.Key)
}
return ret
}
// AggregateCheckStatus returns a summarization of all of the check results
// for a particular configuration object into a single status.
//
// The given address must refer to an object within the configuration that
// this Checks was instantiated from, or this method will panic.
func (c *State) AggregateCheckStatus(addr addrs.ConfigCheckable) Status {
c.mu.Lock()
defer c.mu.Unlock()
st, ok := c.statuses.GetOk(addr)
if !ok {
panic(fmt.Sprintf("request for status of unknown configuration object %s", addr))
}
if st.objects.Elems == nil {
// If we don't even know how many objects we have for this
// configuration construct then that summarizes as unknown.
// (Note: this is different than Elems being a non-nil empty map,
// which means that we know there are zero objects and therefore
// the aggregate result will be pass to pass below.)
return StatusUnknown
}
// Otherwise, our result depends on how many of our known objects are
// in each status.
errorCount := 0
failCount := 0
unknownCount := 0
for _, objects := range st.objects.Elems {
for _, checks := range objects.Value {
for _, status := range checks {
switch status {
case StatusPass:
// ok
case StatusFail:
failCount++
case StatusError:
errorCount++
default:
unknownCount++
}
}
}
}
return summarizeCheckStatuses(errorCount, failCount, unknownCount)
}
// ObjectCheckStatus returns a summarization of all of the check results
// for a particular checkable object into a single status.
//
// The given address must refer to a checkable object that Terraform Core
// previously reported while doing a graph walk, or this method will panic.
func (c *State) ObjectCheckStatus(addr addrs.Checkable) Status {
c.mu.Lock()
defer c.mu.Unlock()
configAddr := addr.ConfigCheckable()
st, ok := c.statuses.GetOk(configAddr)
if !ok {
panic(fmt.Sprintf("request for status of unknown object %s", addr))
}
if st.objects.Elems == nil {
panic(fmt.Sprintf("request for status of %s before establishing the checkable objects for %s", addr, configAddr))
}
checks, ok := st.objects.GetOk(addr)
if !ok {
panic(fmt.Sprintf("request for status of unknown object %s", addr))
}
errorCount := 0
failCount := 0
unknownCount := 0
for _, statuses := range checks {
for _, status := range statuses {
switch status {
case StatusPass:
// ok
case StatusFail:
failCount++
case StatusError:
errorCount++
default:
unknownCount++
}
}
}
return summarizeCheckStatuses(errorCount, failCount, unknownCount)
}
// ObjectFailureMessages returns the zero or more failure messages reported
// for the object with the given address.
//
// Failure messages are recorded only for checks whose status is StatusFail,
// but since this aggregates together the results of all of the checks
// on the given object it's possible for there to be a mixture of failures
// and errors at the same time, which would aggregate as StatusError in
// ObjectCheckStatus's result because errors are defined as "stronger"
// than failures.
func (c *State) ObjectFailureMessages(addr addrs.Checkable) []string {
var ret []string
configAddr := addr.ConfigCheckable()
st, ok := c.statuses.GetOk(configAddr)
if !ok {
panic(fmt.Sprintf("request for status of unknown object %s", addr))
}
if st.objects.Elems == nil {
panic(fmt.Sprintf("request for status of %s before establishing the checkable objects for %s", addr, configAddr))
}
checksByType, ok := st.objects.GetOk(addr)
if !ok {
panic(fmt.Sprintf("request for status of unknown object %s", addr))
}
for checkType, checks := range checksByType {
for i, status := range checks {
if status == StatusFail {
checkAddr := addrs.NewCheck(addr, checkType, i)
msg := c.failureMsgs.Get(checkAddr)
if msg != "" {
ret = append(ret, msg)
}
}
}
}
// We always return the messages in a lexical sort order just so that
// it'll be consistent between runs if we still have the same problems.
sort.Strings(ret)
return ret
}
func summarizeCheckStatuses(errorCount, failCount, unknownCount int) Status {
switch {
case errorCount > 0:
// If we saw any errors then we'll treat the whole thing as errored.
return StatusError
case failCount > 0:
// If anything failed then this whole configuration construct failed.
return StatusFail
case unknownCount > 0:
// If nothing failed but we still have unknowns then our outcome isn't
// known yet.
return StatusUnknown
default:
// If we have no failures and no unknowns then either we have all
// passes or no checkable objects at all, both of which summarize as
// a pass.
return StatusPass
}
}