// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package checks import ( "fmt" "sort" "sync" "github.com/placeholderplaceholderplaceholder/opentf/internal/addrs" "github.com/placeholderplaceholderplaceholder/opentf/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 } }