// Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: BUSL-1.1 package states import ( "slices" "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 } func (r *CheckResults) Equal(other *CheckResults) bool { if r == other { return true } if r == nil || other == nil { return false } if r.ConfigResults.Len() != other.ConfigResults.Len() { return false } for key, elem := range r.ConfigResults.Iter() { otherElem := other.ConfigResults.Get(key) if otherElem == nil { return false } if elem.Status != otherElem.Status { return false } if elem.ObjectResults.Len() != otherElem.ObjectResults.Len() { return false } for key, res := range elem.ObjectResults.Iter() { otherRes := otherElem.ObjectResults.Get(key) if otherRes == nil { return false } if res.Status != otherRes.Status { return false } if !slices.Equal(res.FailureMessages, otherRes.FailureMessages) { return false } } } return true } // 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 }