mirror of https://github.com/hashicorp/terraform
The checks.Checks type aims to encapsulate keeping track of check results during a run and then reporting on them afterwards even if the run was aborted early for some reason. The intended model here is that each new run starts with an entirely fresh checks.Checks, with all of the statuses therefore initially unknown, and gradually populates the check results as we walk the graph in Terraform Core. This means that even if we don't complete the run due to an error or due to targeting options we'll still report anything we didn't visit yet as unknown. This commit only includes the modeling of checks in the checks package. For now this is just dead code, and we'll wire it in to Terraform Core in subsequent commits.pull/31706/head
parent
696b403bf3
commit
9dea19807f
@ -0,0 +1,5 @@
|
||||
// Package checks contains the models for representing various kinds of
|
||||
// declarative condition checks that can be defined in a Terraform module
|
||||
// and then evaluated and reported by Terraform Core during plan and apply
|
||||
// operations.
|
||||
package checks
|
||||
@ -0,0 +1,295 @@
|
||||
package checks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/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.
|
||||
//
|
||||
// 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]
|
||||
}
|
||||
|
||||
// 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.CheckType]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.CheckType][]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()
|
||||
}
|
||||
|
||||
// AllObjectAddrs returns all of the addresses of individual checkable objects
|
||||
// that were registered as part of the Terraform Core operation that this
|
||||
// object is describing.
|
||||
//
|
||||
// If the corresponding Terraform Core operation returned an error or was
|
||||
// using targeting to exclude some objects from the graph then this may not
|
||||
// include declared objects for all of the expected configuration objects.
|
||||
func (c *State) AllObjectAddrs() addrs.Set[addrs.Checkable] {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
ret := addrs.MakeSet[addrs.Checkable]()
|
||||
for _, st := range c.statuses.Elems {
|
||||
for _, elem := range st.Value.objects.Elems {
|
||||
ret.Add(elem.Key)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// OverallCheckStatus returns a summarization of all of the check results
|
||||
// across the entire configuration as a single unified status.
|
||||
//
|
||||
// This is a reasonable approximation for deciding an overall result for an
|
||||
// automated testing scenario.
|
||||
func (c *State) OverallCheckStatus() Status {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
errorCount := 0
|
||||
failCount := 0
|
||||
unknownCount := 0
|
||||
|
||||
for _, elem := range c.statuses.Elems {
|
||||
aggrStatus := c.aggregateCheckStatus(elem.Key)
|
||||
switch aggrStatus {
|
||||
case StatusPass:
|
||||
// ok
|
||||
case StatusFail:
|
||||
failCount++
|
||||
case StatusError:
|
||||
errorCount++
|
||||
default:
|
||||
unknownCount++
|
||||
}
|
||||
}
|
||||
|
||||
return summarizeCheckStatuses(errorCount, failCount, unknownCount)
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
return c.aggregateCheckStatus(addr)
|
||||
}
|
||||
|
||||
// aggregateCheckStatus is the main implementation of the public
|
||||
// AggregateCheckStatus, which assumes that the caller has already acquired
|
||||
// the mutex before calling.
|
||||
func (c *State) aggregateCheckStatus(addr addrs.ConfigCheckable) Status {
|
||||
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.
|
||||
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)
|
||||
}
|
||||
|
||||
// AllCheckStatuses returns a flat map of all of the statuses of all of the
|
||||
// checks associated with all of the objects know to the reciever.
|
||||
//
|
||||
// If the corresponding Terraform Core operation returned an error or was
|
||||
// using targeting to exclude some objects from the graph then this may not
|
||||
// include declared objects for all of the expected configuration objects.
|
||||
func (c *State) AllCheckStatuses() addrs.Map[addrs.Check, Status] {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
ret := addrs.MakeMap[addrs.Check, Status]()
|
||||
|
||||
for _, configElem := range c.statuses.Elems {
|
||||
for _, objectElem := range configElem.Value.objects.Elems {
|
||||
objectAddr := objectElem.Key
|
||||
for checkType, checks := range objectElem.Value {
|
||||
for idx, status := range checks {
|
||||
checkAddr := addrs.Check{
|
||||
Container: objectAddr,
|
||||
Type: checkType,
|
||||
Index: idx,
|
||||
}
|
||||
ret.Put(checkAddr, status)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
package checks
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/configs"
|
||||
)
|
||||
|
||||
func initialStatuses(cfg *configs.Config) addrs.Map[addrs.ConfigCheckable, *configCheckableState] {
|
||||
ret := addrs.MakeMap[addrs.ConfigCheckable, *configCheckableState]()
|
||||
|
||||
collectInitialStatuses(ret, cfg)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func collectInitialStatuses(into addrs.Map[addrs.ConfigCheckable, *configCheckableState], cfg *configs.Config) {
|
||||
moduleAddr := cfg.Path
|
||||
|
||||
for _, rc := range cfg.Module.ManagedResources {
|
||||
addr := rc.Addr().InModule(moduleAddr)
|
||||
collectInitialStatusForResource(into, addr, rc)
|
||||
}
|
||||
for _, rc := range cfg.Module.DataResources {
|
||||
addr := rc.Addr().InModule(moduleAddr)
|
||||
collectInitialStatusForResource(into, addr, rc)
|
||||
}
|
||||
|
||||
for _, oc := range cfg.Module.Outputs {
|
||||
addr := oc.Addr().InModule(moduleAddr)
|
||||
|
||||
ct := len(oc.Preconditions)
|
||||
if ct == 0 {
|
||||
// We just ignore output values that don't declare any checks.
|
||||
continue
|
||||
}
|
||||
|
||||
st := &configCheckableState{}
|
||||
|
||||
st.checkTypes = map[addrs.CheckType]int{
|
||||
addrs.OutputPrecondition: ct,
|
||||
}
|
||||
|
||||
into.Put(addr, st)
|
||||
}
|
||||
|
||||
// Must also visit child modules to collect everything
|
||||
for _, child := range cfg.Children {
|
||||
collectInitialStatuses(into, child)
|
||||
}
|
||||
}
|
||||
|
||||
func collectInitialStatusForResource(into addrs.Map[addrs.ConfigCheckable, *configCheckableState], addr addrs.ConfigResource, rc *configs.Resource) {
|
||||
if (len(rc.Preconditions) + len(rc.Postconditions)) == 0 {
|
||||
// Don't bother with any resource that doesn't have at least
|
||||
// one condition.
|
||||
return
|
||||
}
|
||||
|
||||
st := &configCheckableState{
|
||||
checkTypes: make(map[addrs.CheckType]int),
|
||||
}
|
||||
|
||||
if ct := len(rc.Preconditions); ct > 0 {
|
||||
st.checkTypes[addrs.ResourcePrecondition] = ct
|
||||
}
|
||||
if ct := len(rc.Postconditions); ct > 0 {
|
||||
st.checkTypes[addrs.ResourcePostcondition] = ct
|
||||
}
|
||||
|
||||
into.Put(addr, st)
|
||||
}
|
||||
@ -0,0 +1,88 @@
|
||||
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()
|
||||
|
||||
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
|
||||
}
|
||||
@ -0,0 +1,241 @@
|
||||
package checks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/configs/configload"
|
||||
"github.com/hashicorp/terraform/internal/initwd"
|
||||
)
|
||||
|
||||
func TestChecksHappyPath(t *testing.T) {
|
||||
const fixtureDir = "testdata/happypath"
|
||||
loader, close := configload.NewLoaderForTests(t)
|
||||
defer close()
|
||||
inst := initwd.NewModuleInstaller(loader.ModulesDir(), nil)
|
||||
_, instDiags := inst.InstallModules(context.Background(), fixtureDir, true, initwd.ModuleInstallHooksImpl{})
|
||||
if instDiags.HasErrors() {
|
||||
t.Fatal(instDiags.Err())
|
||||
}
|
||||
if err := loader.RefreshModules(); err != nil {
|
||||
t.Fatalf("failed to refresh modules after installation: %s", err)
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
|
||||
cfg, hclDiags := loader.LoadConfig(fixtureDir)
|
||||
if hclDiags.HasErrors() {
|
||||
t.Fatalf("invalid configuration: %s", hclDiags.Error())
|
||||
}
|
||||
|
||||
resourceA := addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "null_resource",
|
||||
Name: "a",
|
||||
}.InModule(addrs.RootModule)
|
||||
resourceNoChecks := addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "null_resource",
|
||||
Name: "no_checks",
|
||||
}.InModule(addrs.RootModule)
|
||||
resourceNonExist := addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "null_resource",
|
||||
Name: "nonexist",
|
||||
}.InModule(addrs.RootModule)
|
||||
rootOutput := addrs.OutputValue{
|
||||
Name: "a",
|
||||
}.InModule(addrs.RootModule)
|
||||
moduleChild := addrs.RootModule.Child("child")
|
||||
resourceB := addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "null_resource",
|
||||
Name: "b",
|
||||
}.InModule(moduleChild)
|
||||
resourceC := addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "null_resource",
|
||||
Name: "c",
|
||||
}.InModule(moduleChild)
|
||||
childOutput := addrs.OutputValue{
|
||||
Name: "b",
|
||||
}.InModule(moduleChild)
|
||||
|
||||
// First some consistency checks to make sure our configuration is the
|
||||
// shape we are relying on it to be.
|
||||
if addr := resourceA; cfg.Module.ResourceByAddr(addr.Resource) == nil {
|
||||
t.Fatalf("configuration does not include %s", addr)
|
||||
}
|
||||
if addr := resourceB; cfg.Children["child"].Module.ResourceByAddr(addr.Resource) == nil {
|
||||
t.Fatalf("configuration does not include %s", addr)
|
||||
}
|
||||
if addr := resourceNoChecks; cfg.Module.ResourceByAddr(addr.Resource) == nil {
|
||||
t.Fatalf("configuration does not include %s", addr)
|
||||
}
|
||||
if addr := resourceNonExist; cfg.Module.ResourceByAddr(addr.Resource) != nil {
|
||||
t.Fatalf("configuration includes %s, which is not supposed to exist", addr)
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
|
||||
checks := NewState(cfg)
|
||||
|
||||
missing := 0
|
||||
if addr := resourceA; !checks.ConfigHasChecks(addr) {
|
||||
t.Errorf("checks not detected for %s", addr)
|
||||
missing++
|
||||
}
|
||||
if addr := resourceB; !checks.ConfigHasChecks(addr) {
|
||||
t.Errorf("checks not detected for %s", addr)
|
||||
missing++
|
||||
}
|
||||
if addr := resourceC; !checks.ConfigHasChecks(addr) {
|
||||
t.Errorf("checks not detected for %s", addr)
|
||||
missing++
|
||||
}
|
||||
if addr := rootOutput; !checks.ConfigHasChecks(addr) {
|
||||
t.Errorf("checks not detected for %s", addr)
|
||||
missing++
|
||||
}
|
||||
if addr := childOutput; !checks.ConfigHasChecks(addr) {
|
||||
t.Errorf("checks not detected for %s", addr)
|
||||
missing++
|
||||
}
|
||||
if addr := resourceNoChecks; checks.ConfigHasChecks(addr) {
|
||||
t.Errorf("checks detected for %s, even though it has none", addr)
|
||||
}
|
||||
if addr := resourceNonExist; checks.ConfigHasChecks(addr) {
|
||||
t.Errorf("checks detected for %s, even though it doesn't exist", addr)
|
||||
}
|
||||
if missing > 0 {
|
||||
t.Fatalf("missing some configuration objects we'd need for subsequent testing")
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Everything should start with status unknown.
|
||||
|
||||
if got, want := checks.OverallCheckStatus(), StatusUnknown; got != want {
|
||||
t.Errorf("incorrect initial overall check status %s; want %s", got, want)
|
||||
}
|
||||
|
||||
{
|
||||
wantConfigAddrs := addrs.MakeSet[addrs.ConfigCheckable](
|
||||
resourceA,
|
||||
resourceB,
|
||||
resourceC,
|
||||
rootOutput,
|
||||
childOutput,
|
||||
)
|
||||
gotConfigAddrs := checks.AllConfigAddrs()
|
||||
if diff := cmp.Diff(wantConfigAddrs, gotConfigAddrs); diff != "" {
|
||||
t.Errorf("wrong detected config addresses\n%s", diff)
|
||||
}
|
||||
|
||||
for _, configAddr := range gotConfigAddrs {
|
||||
if got, want := checks.AggregateCheckStatus(configAddr), StatusUnknown; got != want {
|
||||
t.Errorf("incorrect initial aggregate check status for %s: %s, but want %s", configAddr, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// The following are steps that would normally be done by Terraform Core
|
||||
// as part of visiting checkable objects during the graph walk. We're
|
||||
// simulating a likely sequence of calls here for testing purposes, but
|
||||
// Terraform Core won't necessarily visit all of these in exactly the
|
||||
// same order every time and so this is just one possible valid ordering
|
||||
// of calls.
|
||||
|
||||
resourceInstA := resourceA.Resource.Absolute(addrs.RootModuleInstance).Instance(addrs.NoKey)
|
||||
rootOutputInst := rootOutput.OutputValue.Absolute(addrs.RootModuleInstance)
|
||||
moduleChildInst := addrs.RootModuleInstance.Child("child", addrs.NoKey)
|
||||
resourceInstB := resourceB.Resource.Absolute(moduleChildInst).Instance(addrs.NoKey)
|
||||
resourceInstC0 := resourceC.Resource.Absolute(moduleChildInst).Instance(addrs.IntKey(0))
|
||||
resourceInstC1 := resourceC.Resource.Absolute(moduleChildInst).Instance(addrs.IntKey(1))
|
||||
childOutputInst := childOutput.OutputValue.Absolute(moduleChildInst)
|
||||
|
||||
checks.ReportCheckableObjects(resourceA, addrs.MakeSet[addrs.Checkable](resourceInstA))
|
||||
checks.ReportCheckResult(resourceInstA, addrs.ResourcePrecondition, 0, StatusPass)
|
||||
checks.ReportCheckResult(resourceInstA, addrs.ResourcePrecondition, 1, StatusPass)
|
||||
checks.ReportCheckResult(resourceInstA, addrs.ResourcePostcondition, 0, StatusPass)
|
||||
|
||||
checks.ReportCheckableObjects(resourceB, addrs.MakeSet[addrs.Checkable](resourceInstB))
|
||||
checks.ReportCheckResult(resourceInstB, addrs.ResourcePrecondition, 0, StatusPass)
|
||||
|
||||
checks.ReportCheckableObjects(resourceC, addrs.MakeSet[addrs.Checkable](resourceInstC0, resourceInstC1))
|
||||
checks.ReportCheckResult(resourceInstC0, addrs.ResourcePostcondition, 0, StatusPass)
|
||||
checks.ReportCheckResult(resourceInstC1, addrs.ResourcePostcondition, 0, StatusPass)
|
||||
|
||||
checks.ReportCheckableObjects(childOutput, addrs.MakeSet[addrs.Checkable](childOutputInst))
|
||||
checks.ReportCheckResult(childOutputInst, addrs.OutputPrecondition, 0, StatusPass)
|
||||
|
||||
checks.ReportCheckableObjects(rootOutput, addrs.MakeSet[addrs.Checkable](rootOutputInst))
|
||||
checks.ReportCheckResult(rootOutputInst, addrs.OutputPrecondition, 0, StatusPass)
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// This "section" is simulating what a check reporting UI might do in
|
||||
// order to analyze the checks after the Terraform operation completes.
|
||||
// Since we reported that everything passed above, we should be able to
|
||||
// see that when we query the object in various ways.
|
||||
if got, want := checks.OverallCheckStatus(), StatusPass; got != want {
|
||||
t.Errorf("incorrect final overall check status %s; want %s", got, want)
|
||||
}
|
||||
|
||||
{
|
||||
configCount := 0
|
||||
for _, configAddr := range checks.AllConfigAddrs() {
|
||||
configCount++
|
||||
if got, want := checks.AggregateCheckStatus(configAddr), StatusPass; got != want {
|
||||
t.Errorf("incorrect final aggregate check status for %s: %s, but want %s", configAddr, got, want)
|
||||
}
|
||||
}
|
||||
if got, want := configCount, 5; got != want {
|
||||
t.Errorf("incorrect number of known config addresses %d; want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
wantObjAddrs := addrs.MakeSet[addrs.Checkable](
|
||||
resourceInstA,
|
||||
rootOutputInst,
|
||||
resourceInstB,
|
||||
resourceInstC0,
|
||||
resourceInstC1,
|
||||
childOutputInst,
|
||||
)
|
||||
gotObjAddrs := checks.AllObjectAddrs()
|
||||
if diff := cmp.Diff(wantObjAddrs, gotObjAddrs); diff != "" {
|
||||
t.Errorf("wrong object addresses\n%s", diff)
|
||||
}
|
||||
for _, addr := range gotObjAddrs {
|
||||
if got, want := checks.ObjectCheckStatus(addr), StatusPass; got != want {
|
||||
t.Errorf("incorrect final check status for object %s: %s, but want %s", addr, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
wantStatuses := addrs.MakeMap(
|
||||
addrs.MakeMapElem(addrs.NewCheck(resourceInstA, addrs.ResourcePrecondition, 0), StatusPass),
|
||||
addrs.MakeMapElem(addrs.NewCheck(resourceInstA, addrs.ResourcePrecondition, 1), StatusPass),
|
||||
addrs.MakeMapElem(addrs.NewCheck(resourceInstA, addrs.ResourcePostcondition, 0), StatusPass),
|
||||
|
||||
addrs.MakeMapElem(addrs.NewCheck(resourceInstB, addrs.ResourcePrecondition, 0), StatusPass),
|
||||
|
||||
addrs.MakeMapElem(addrs.NewCheck(resourceInstC0, addrs.ResourcePostcondition, 0), StatusPass),
|
||||
addrs.MakeMapElem(addrs.NewCheck(resourceInstC1, addrs.ResourcePostcondition, 0), StatusPass),
|
||||
|
||||
addrs.MakeMapElem(addrs.NewCheck(rootOutputInst, addrs.OutputPrecondition, 0), StatusPass),
|
||||
addrs.MakeMapElem(addrs.NewCheck(childOutputInst, addrs.OutputPrecondition, 0), StatusPass),
|
||||
)
|
||||
gotStatuses := checks.AllCheckStatuses()
|
||||
if diff := cmp.Diff(wantStatuses, gotStatuses); diff != "" {
|
||||
t.Errorf("wrong check statuses\n%s", diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
package checks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// Status represents the status of an individual check associated with a
|
||||
// checkable object.
|
||||
type Status rune
|
||||
|
||||
//go:generate go run golang.org/x/tools/cmd/stringer -type=Status
|
||||
|
||||
const (
|
||||
// StatusUnknown represents that there is not yet a conclusive result
|
||||
// for the check, either because we haven't yet visited its associated
|
||||
// object or because the check condition itself depends on a value not
|
||||
// yet known during planning.
|
||||
StatusUnknown Status = 0
|
||||
// NOTE: Our implementation relies on StatusUnknown being the zero value
|
||||
// of Status.
|
||||
|
||||
// StatusPass represents that Terraform Core has evaluated the check's
|
||||
// condition and it returned true, indicating success.
|
||||
StatusPass Status = 'P'
|
||||
|
||||
// StatusFail represents that Terraform Core has evaluated the check's
|
||||
// condition and it returned false, indicating failure.
|
||||
StatusFail Status = 'F'
|
||||
|
||||
// StatusError represents that Terraform Core tried to evaluate the check's
|
||||
// condition but encountered an error while evaluating the check expression.
|
||||
//
|
||||
// This is different than StatusFail because StatusFail indiciates that
|
||||
// the condition was valid and returned false, whereas StatusError
|
||||
// indicates that the condition was not valid at all.
|
||||
StatusError Status = 'E'
|
||||
)
|
||||
|
||||
// StatusForCtyValue returns the Status value corresponding to the given
|
||||
// cty Value, which must be one of either cty.True, cty.False, or
|
||||
// cty.UnknownVal(cty.Bool) or else this function will panic.
|
||||
//
|
||||
// The current behavior of this function is:
|
||||
//
|
||||
// cty.True StatusPass
|
||||
// cty.False StatusFail
|
||||
// cty.UnknownVal(cty.Bool) StatusUnknown
|
||||
//
|
||||
// Any other input will panic. Note that there's no value that can produce
|
||||
// StatusError, because in case of a condition error there will not typically
|
||||
// be a result value at all.
|
||||
func StatusForCtyValue(v cty.Value) Status {
|
||||
if !v.Type().Equals(cty.Bool) {
|
||||
panic(fmt.Sprintf("cannot use %s as check status", v.Type().FriendlyName()))
|
||||
}
|
||||
if v.IsNull() {
|
||||
panic("cannot use null as check status")
|
||||
}
|
||||
|
||||
switch {
|
||||
case v == cty.True:
|
||||
return StatusPass
|
||||
case v == cty.False:
|
||||
return StatusFail
|
||||
case !v.IsKnown():
|
||||
return StatusUnknown
|
||||
default:
|
||||
// Should be impossible to get here unless something particularly
|
||||
// weird is going on, like a marked condition result.
|
||||
panic(fmt.Sprintf("cannot use %#v as check status", v))
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
// Code generated by "stringer -type=Status"; DO NOT EDIT.
|
||||
|
||||
package checks
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[StatusUnknown-0]
|
||||
_ = x[StatusPass-80]
|
||||
_ = x[StatusFail-70]
|
||||
_ = x[StatusError-69]
|
||||
}
|
||||
|
||||
const (
|
||||
_Status_name_0 = "StatusUnknown"
|
||||
_Status_name_1 = "StatusErrorStatusFail"
|
||||
_Status_name_2 = "StatusPass"
|
||||
)
|
||||
|
||||
var (
|
||||
_Status_index_1 = [...]uint8{0, 11, 21}
|
||||
)
|
||||
|
||||
func (i Status) String() string {
|
||||
switch {
|
||||
case i == 0:
|
||||
return _Status_name_0
|
||||
case 69 <= i && i <= 70:
|
||||
i -= 69
|
||||
return _Status_name_1[_Status_index_1[i]:_Status_index_1[i+1]]
|
||||
case i == 80:
|
||||
return _Status_name_2
|
||||
default:
|
||||
return "Status(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
resource "null_resource" "a" {
|
||||
lifecycle {
|
||||
precondition {
|
||||
condition = null_resource.no_checks == ""
|
||||
error_message = "Impossible."
|
||||
}
|
||||
precondition {
|
||||
condition = null_resource.no_checks == ""
|
||||
error_message = "Also impossible."
|
||||
}
|
||||
postcondition {
|
||||
condition = null_resource.no_checks == ""
|
||||
error_message = "Definitely not possible."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "null_resource" "no_checks" {
|
||||
}
|
||||
|
||||
module "child" {
|
||||
source = "./child"
|
||||
}
|
||||
|
||||
output "a" {
|
||||
value = null_resource.a.id
|
||||
|
||||
precondition {
|
||||
condition = null_resource.a.id != ""
|
||||
error_message = "A has no id."
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
resource "null_resource" "b" {
|
||||
lifecycle {
|
||||
precondition {
|
||||
condition = self.id == ""
|
||||
error_message = "Impossible."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "null_resource" "c" {
|
||||
count = 2
|
||||
|
||||
lifecycle {
|
||||
postcondition {
|
||||
condition = self.id == ""
|
||||
error_message = "Impossible."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output "b" {
|
||||
value = null_resource.b.id
|
||||
|
||||
precondition {
|
||||
condition = null_resource.b.id != ""
|
||||
error_message = "B has no id."
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in new issue