opentofu/internal/states/checks.go
Martin Atkins d63871f70d core: Propagate check results accurately from plan to apply
In an earlier commit we changed the states.CheckResults model to
explicitly model the config object vs. dynamic checkable object hierarchy,
but neglected to update the logic in Terraform Core to take that into
account when propagating the object expansion decisions from the plan
phase to the apply phase. That meant that we were incorrectly classifying
zero-instance resources always as having an unknown number of instances,
rather than possibly being known to have zero instances.

This now follows the two-level heirarchy of the data structure, which has
the nice side-effect that we can remove some of the special-case methods
from checks.State that we were using to bulk-load data: the data is now
shaped in the appropriate way to reload the data using the same method
the plan phase would've used to record the results in the first place.
2022-08-26 15:47:29 -07:00

183 lines
6.8 KiB
Go

package states
import (
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/checks"
)
// CheckResults represents a summary snapshot of the status of a set of checks
// declared in configuration, updated after each Terraform Core run that
// changes the state or remote system in a way that might impact the check
// results.
//
// Unlike a checks.State, this type only tracks the overall results for
// each checkable object and doesn't aim to preserve the identity of individual
// checks in the configuration. For our UI reporting purposes, it is entire
// objects that pass or fail based on their declared checks; the individual
// checks have no durable identity between runs, and so are only a language
// design convenience to help authors describe various independent conditions
// with different failure messages each.
//
// CheckResults should typically be considered immutable once constructed:
// instead of updating it in-place,instead construct an entirely new
// CheckResults object based on a fresh checks.State.
type CheckResults struct {
// ConfigResults has all of the individual check results grouped by the
// configuration object they relate to.
//
// The top-level map here will always have a key for every configuration
// object that includes checks at the time of evaluating the results,
// even if there turned out to be no instances of that object and
// therefore no individual check results.
ConfigResults addrs.Map[addrs.ConfigCheckable, *CheckResultAggregate]
}
// CheckResultAggregate represents both the overall result for a particular
// configured object that has checks and the individual checkable objects
// it declared, if any.
type CheckResultAggregate struct {
// Status is the aggregate status across all objects.
//
// Sometimes an error or check failure during planning will prevent
// Terraform Core from even determining the individual checkable objects
// associated with a downstream configuration object, and that situation is
// described here by this Status being checks.StatusUnknown and there being
// no elements in the ObjectResults field.
//
// That's different than Terraform Core explicitly reporting that there are
// no instances of the config object (e.g. a resource with count = 0),
// which leads to the aggregate status being checks.StatusPass while
// ObjectResults is still empty.
Status checks.Status
ObjectResults addrs.Map[addrs.Checkable, *CheckResultObject]
}
// CheckResultObject is the check status for a single checkable object.
//
// This aggregates together all of the checks associated with a particular
// object into a single pass/fail/error/unknown result, because checkable
// objects have durable addresses that can survive between runs, but their
// individual checks do not. (Module authors are free to reorder their checks
// for a particular object in the configuration with no change in meaning.)
type CheckResultObject struct {
// Status is the check status of the checkable object, derived from the
// results of all of its individual checks.
Status checks.Status
// FailureMessages is an optional set of module-author-defined messages
// describing the problems that the checks detected, for objects whose
// status is checks.StatusFail.
//
// (checks.StatusError problems get reported as normal diagnostics during
// evaluation instead, and so will not appear here.)
FailureMessages []string
}
// NewCheckResults constructs a new states.CheckResults object that is a
// snapshot of the check statuses recorded in the given checks.State object.
//
// This should be called only after a Terraform Core run has completed and
// recorded any results from running the checks in the given object.
func NewCheckResults(source *checks.State) *CheckResults {
ret := &CheckResults{
ConfigResults: addrs.MakeMap[addrs.ConfigCheckable, *CheckResultAggregate](),
}
for _, configAddr := range source.AllConfigAddrs() {
aggr := &CheckResultAggregate{
Status: source.AggregateCheckStatus(configAddr),
ObjectResults: addrs.MakeMap[addrs.Checkable, *CheckResultObject](),
}
for _, objectAddr := range source.ObjectAddrs(configAddr) {
obj := &CheckResultObject{
Status: source.ObjectCheckStatus(objectAddr),
FailureMessages: source.ObjectFailureMessages(objectAddr),
}
aggr.ObjectResults.Put(objectAddr, obj)
}
ret.ConfigResults.Put(configAddr, aggr)
}
// If there aren't actually any configuration objects then we'll just
// leave the map as a whole nil, because having it be zero-value makes
// life easier for deep comparisons in unit tests elsewhere.
if ret.ConfigResults.Len() == 0 {
ret.ConfigResults.Elems = nil
}
return ret
}
// GetObjectResult looks up the result for a single object, or nil if there
// is no such object.
//
// In main code we shouldn't typically need to look up individual objects
// like this, since we'll usually be reporting check results in an aggregate
// form, but determining the result of a particular object is useful in our
// internal unit tests, and so this is here primarily for that purpose.
func (r *CheckResults) GetObjectResult(objectAddr addrs.Checkable) *CheckResultObject {
configAddr := objectAddr.ConfigCheckable()
aggr := r.ConfigResults.Get(configAddr)
if aggr == nil {
return nil
}
return aggr.ObjectResults.Get(objectAddr)
}
func (r *CheckResults) DeepCopy() *CheckResults {
if r == nil {
return nil
}
ret := &CheckResults{}
if r.ConfigResults.Elems == nil {
return ret
}
ret.ConfigResults = addrs.MakeMap[addrs.ConfigCheckable, *CheckResultAggregate]()
for _, configElem := range r.ConfigResults.Elems {
aggr := &CheckResultAggregate{
Status: configElem.Value.Status,
}
if configElem.Value.ObjectResults.Elems != nil {
aggr.ObjectResults = addrs.MakeMap[addrs.Checkable, *CheckResultObject]()
for _, objectElem := range configElem.Value.ObjectResults.Elems {
result := &CheckResultObject{
Status: objectElem.Value.Status,
// NOTE: We don't deep-copy this slice because it's
// immutable once constructed by convention.
FailureMessages: objectElem.Value.FailureMessages,
}
aggr.ObjectResults.Put(objectElem.Key, result)
}
}
ret.ConfigResults.Put(configElem.Key, aggr)
}
return ret
}
// ObjectAddrsKnown determines whether the set of objects recorded in this
// aggregate is accurate (true) or if it's incomplete as a result of the
// run being interrupted before instance expansion.
func (r *CheckResultAggregate) ObjectAddrsKnown() bool {
if r.ObjectResults.Len() != 0 {
// If there are any object results at all then we definitely know.
return true
}
// If we don't have any object addresses then we distinguish a known
// empty set of objects from an unknown set of objects by the aggregate
// status being unknown.
return r.Status != checks.StatusUnknown
}