mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
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.
209 lines
7.3 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|
|
}
|