opentofu/internal/checks/state_report.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

116 lines
4.6 KiB
Go

package checks
import (
"fmt"
"github.com/hashicorp/terraform/internal/addrs"
)
// These are the "Report"-prefixed methods of Checks used by Terraform Core
// to gradually signal the results of checks during a plan or apply operation.
// ReportCheckableObjects is the interface by which Terraform Core should
// tell the State object which specific checkable objects were declared
// by the given configuration object.
//
// This method will panic if the given configuration address isn't one known
// by this Checks to have pending checks, and if any of the given object
// addresses don't belong to the given configuration address.
func (c *State) ReportCheckableObjects(configAddr addrs.ConfigCheckable, objectAddrs addrs.Set[addrs.Checkable]) {
c.mu.Lock()
defer c.mu.Unlock()
st, ok := c.statuses.GetOk(configAddr)
if !ok {
panic(fmt.Sprintf("checkable objects report for unknown configuration object %s", configAddr))
}
if st.objects.Elems != nil {
// Can only report checkable objects once per configuration object
panic(fmt.Sprintf("duplicate checkable objects report for %s ", configAddr))
}
// At this point we pre-populate all of the check results as StatusUnknown,
// so that even if we never hear from Terraform Core again we'll still
// remember that these results were all pending.
st.objects = addrs.MakeMap[addrs.Checkable, map[addrs.CheckType][]Status]()
for _, objectAddr := range objectAddrs {
if gotConfigAddr := objectAddr.ConfigCheckable(); !addrs.Equivalent(configAddr, gotConfigAddr) {
// All of the given object addresses must belong to the specified configuration address
panic(fmt.Sprintf("%s belongs to %s, not %s", objectAddr, gotConfigAddr, configAddr))
}
checks := make(map[addrs.CheckType][]Status, len(st.checkTypes))
for checkType, count := range st.checkTypes {
// NOTE: This is intentionally a slice of count of the zero value
// of Status, which is StatusUnknown to represent that we don't
// yet have a report for that particular check.
checks[checkType] = make([]Status, count)
}
st.objects.Put(objectAddr, checks)
}
}
// ReportCheckResult is the interface by which Terraform Core should tell the
// State object the result of a specific check for an object that was
// previously registered with ReportCheckableObjects.
//
// If the given object address doesn't match a previously-reported object,
// or if the check index is out of bounds for the number of checks expected
// of the given type, this method will panic to indicate a bug in the caller.
//
// This method will also panic if the specified check already had a known
// status; each check should have its result reported only once.
func (c *State) ReportCheckResult(objectAddr addrs.Checkable, checkType addrs.CheckType, index int, status Status) {
c.mu.Lock()
defer c.mu.Unlock()
c.reportCheckResult(objectAddr, checkType, index, status)
}
// ReportCheckFailure is a more specialized version of ReportCheckResult which
// captures a failure outcome in particular, giving the opportunity to capture
// an author-specified error message string along with the failure.
//
// This always records the given check as having StatusFail. Don't use this for
// situations where the check condition was itself invalid, because that
// should be represented by StatusError instead, and the error signalled via
// diagnostics as normal.
func (c *State) ReportCheckFailure(objectAddr addrs.Checkable, checkType addrs.CheckType, index int, errorMessage string) {
c.mu.Lock()
defer c.mu.Unlock()
c.reportCheckResult(objectAddr, checkType, index, StatusFail)
if c.failureMsgs.Elems == nil {
c.failureMsgs = addrs.MakeMap[addrs.Check, string]()
}
checkAddr := addrs.NewCheck(objectAddr, checkType, index)
c.failureMsgs.Put(checkAddr, errorMessage)
}
// reportCheckResult is shared between both ReportCheckResult and
// ReportCheckFailure, and assumes its caller already holds the mutex.
func (c *State) reportCheckResult(objectAddr addrs.Checkable, checkType addrs.CheckType, index int, status Status) {
configAddr := objectAddr.ConfigCheckable()
st, ok := c.statuses.GetOk(configAddr)
if !ok {
panic(fmt.Sprintf("checkable object status report for unknown configuration object %s", configAddr))
}
checks, ok := st.objects.GetOk(objectAddr)
if !ok {
panic(fmt.Sprintf("checkable object status report for unexpected checkable object %s", objectAddr))
}
if index >= len(checks[checkType]) {
panic(fmt.Sprintf("%s index %d out of range for %s", checkType, index, objectAddr))
}
if checks[checkType][index] != StatusUnknown {
panic(fmt.Sprintf("duplicate status report for %s %s %d", objectAddr, checkType, index))
}
checks[checkType][index] = status
}