opentofu/internal/states/checks.go
Martin Atkins 9e4861adbb states: Two-level representation of check results
A significant goal of the design changes around checks in earlier commits
(with the introduction of package "checks") was to allow us to
differentiate between a configuration object that we didn't expand at all
due to an upstream error, which has _unknown_ check status, and a
configuration object that expanded to zero dynamic objects, which
therefore has a _passing_ check status.

However, our initial lowering of checks.State into states.CheckResults
stayed with the older model of just recording each leaf check in isolation,
without any tracking of the containers.

This commit therefore lightly reworks our representation of check results
in the state and plan with two main goals:
- The results are grouped by the static configuration object they came
  from, and we capture an aggregate status for each of those so that
  we can differentiate an unknown aggregate result from a passing
  aggregate result which has zero dynamic associated objects.
- The granularity of results is whole checkable objects rather than
  individual checks, because checkable objects have durable addresses
  between runs, but individual checks for an object are more of a
  syntactic convenience to make it easier for module authors to declare
  many independent conditions that each have their own error messages.

Since v1.2 exposed some details of our checks model into the JSON plan
output there are some unanswered questions here about how we can shift to
reporting in the two-level heirarchy described above. For now I've
preserved structural compatibility but not semantic compatibility: any
parser that was written against that format should still function but will
now see fewer results. We'll revisit this in a later commit and consider
other structures and what to do about our compatibility constraint on the
v1.2 structure.

Otherwise though, this is an internal-only change which preserves all of
the existing main behaviors of conditions as before, and just gets us
ready to build user-facing features in terms of this new structure.
2022-08-26 15:47:29 -07:00

183 lines
6.6 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
}
// AllCheckedObjects returns a set of all of the objects that have at least
// one check in the set of results.
func (r *CheckResults) AllCheckedObjects() addrs.Set[addrs.Checkable] {
if r == nil || len(r.ConfigResults.Elems) == 0 {
return nil
}
ret := addrs.MakeSet[addrs.Checkable]()
for _, configElem := range r.ConfigResults.Elems {
for _, objElem := range configElem.Value.ObjectResults.Elems {
ret.Add(objElem.Key)
}
}
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
}