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

209 lines
7.3 KiB
Go

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.
{
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 we might do to report the results
// of the checks after a run completes.
{
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)
}
}
{
objAddrs := addrs.MakeSet[addrs.Checkable](
resourceInstA,
rootOutputInst,
resourceInstB,
resourceInstC0,
resourceInstC1,
childOutputInst,
)
for _, addr := range objAddrs {
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)
}
}
}
}