blob: bde8341677f6aeee548335d6e71a7e698683ecb2 [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package checks
import (
"fmt"
"sort"
"sync"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/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.CheckRule, 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.CheckRuleType]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.CheckRuleType][]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.NewCheckRule(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
}
}