github.com/opentofu/opentofu@v1.7.1/internal/checks/state_test.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package checks
     7  
     8  import (
     9  	"context"
    10  	"testing"
    11  
    12  	"github.com/google/go-cmp/cmp"
    13  
    14  	"github.com/opentofu/opentofu/internal/addrs"
    15  	"github.com/opentofu/opentofu/internal/configs/configload"
    16  	"github.com/opentofu/opentofu/internal/initwd"
    17  )
    18  
    19  func TestChecksHappyPath(t *testing.T) {
    20  	const fixtureDir = "testdata/happypath"
    21  	loader, close := configload.NewLoaderForTests(t)
    22  	defer close()
    23  	inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, nil)
    24  	_, instDiags := inst.InstallModules(context.Background(), fixtureDir, "tests", true, false, initwd.ModuleInstallHooksImpl{})
    25  	if instDiags.HasErrors() {
    26  		t.Fatal(instDiags.Err())
    27  	}
    28  	if err := loader.RefreshModules(); err != nil {
    29  		t.Fatalf("failed to refresh modules after installation: %s", err)
    30  	}
    31  
    32  	/////////////////////////////////////////////////////////////////////////
    33  
    34  	cfg, hclDiags := loader.LoadConfig(fixtureDir)
    35  	if hclDiags.HasErrors() {
    36  		t.Fatalf("invalid configuration: %s", hclDiags.Error())
    37  	}
    38  
    39  	resourceA := addrs.Resource{
    40  		Mode: addrs.ManagedResourceMode,
    41  		Type: "null_resource",
    42  		Name: "a",
    43  	}.InModule(addrs.RootModule)
    44  	resourceNoChecks := addrs.Resource{
    45  		Mode: addrs.ManagedResourceMode,
    46  		Type: "null_resource",
    47  		Name: "no_checks",
    48  	}.InModule(addrs.RootModule)
    49  	resourceNonExist := addrs.Resource{
    50  		Mode: addrs.ManagedResourceMode,
    51  		Type: "null_resource",
    52  		Name: "nonexist",
    53  	}.InModule(addrs.RootModule)
    54  	rootOutput := addrs.OutputValue{
    55  		Name: "a",
    56  	}.InModule(addrs.RootModule)
    57  	moduleChild := addrs.RootModule.Child("child")
    58  	resourceB := addrs.Resource{
    59  		Mode: addrs.ManagedResourceMode,
    60  		Type: "null_resource",
    61  		Name: "b",
    62  	}.InModule(moduleChild)
    63  	resourceC := addrs.Resource{
    64  		Mode: addrs.ManagedResourceMode,
    65  		Type: "null_resource",
    66  		Name: "c",
    67  	}.InModule(moduleChild)
    68  	childOutput := addrs.OutputValue{
    69  		Name: "b",
    70  	}.InModule(moduleChild)
    71  	checkBlock := addrs.Check{
    72  		Name: "check",
    73  	}.InModule(addrs.RootModule)
    74  
    75  	// First some consistency checks to make sure our configuration is the
    76  	// shape we are relying on it to be.
    77  	if addr := resourceA; cfg.Module.ResourceByAddr(addr.Resource) == nil {
    78  		t.Fatalf("configuration does not include %s", addr)
    79  	}
    80  	if addr := resourceB; cfg.Children["child"].Module.ResourceByAddr(addr.Resource) == nil {
    81  		t.Fatalf("configuration does not include %s", addr)
    82  	}
    83  	if addr := resourceNoChecks; cfg.Module.ResourceByAddr(addr.Resource) == nil {
    84  		t.Fatalf("configuration does not include %s", addr)
    85  	}
    86  	if addr := resourceNonExist; cfg.Module.ResourceByAddr(addr.Resource) != nil {
    87  		t.Fatalf("configuration includes %s, which is not supposed to exist", addr)
    88  	}
    89  	if addr := checkBlock; cfg.Module.Checks[addr.Check.Name] == nil {
    90  		t.Fatalf("configuration does not include %s", addr)
    91  	}
    92  
    93  	/////////////////////////////////////////////////////////////////////////
    94  
    95  	checks := NewState(cfg)
    96  
    97  	missing := 0
    98  	if addr := resourceA; !checks.ConfigHasChecks(addr) {
    99  		t.Errorf("checks not detected for %s", addr)
   100  		missing++
   101  	}
   102  	if addr := resourceB; !checks.ConfigHasChecks(addr) {
   103  		t.Errorf("checks not detected for %s", addr)
   104  		missing++
   105  	}
   106  	if addr := resourceC; !checks.ConfigHasChecks(addr) {
   107  		t.Errorf("checks not detected for %s", addr)
   108  		missing++
   109  	}
   110  	if addr := rootOutput; !checks.ConfigHasChecks(addr) {
   111  		t.Errorf("checks not detected for %s", addr)
   112  		missing++
   113  	}
   114  	if addr := childOutput; !checks.ConfigHasChecks(addr) {
   115  		t.Errorf("checks not detected for %s", addr)
   116  		missing++
   117  	}
   118  	if addr := resourceNoChecks; checks.ConfigHasChecks(addr) {
   119  		t.Errorf("checks detected for %s, even though it has none", addr)
   120  	}
   121  	if addr := resourceNonExist; checks.ConfigHasChecks(addr) {
   122  		t.Errorf("checks detected for %s, even though it doesn't exist", addr)
   123  	}
   124  	if addr := checkBlock; !checks.ConfigHasChecks(addr) {
   125  		t.Errorf("checks not detected for %s", addr)
   126  		missing++
   127  	}
   128  	if missing > 0 {
   129  		t.Fatalf("missing some configuration objects we'd need for subsequent testing")
   130  	}
   131  
   132  	/////////////////////////////////////////////////////////////////////////
   133  
   134  	// Everything should start with status unknown.
   135  
   136  	{
   137  		wantConfigAddrs := addrs.MakeSet[addrs.ConfigCheckable](
   138  			resourceA,
   139  			resourceB,
   140  			resourceC,
   141  			rootOutput,
   142  			childOutput,
   143  			checkBlock,
   144  		)
   145  		gotConfigAddrs := checks.AllConfigAddrs()
   146  		if diff := cmp.Diff(wantConfigAddrs, gotConfigAddrs); diff != "" {
   147  			t.Errorf("wrong detected config addresses\n%s", diff)
   148  		}
   149  
   150  		for _, configAddr := range gotConfigAddrs {
   151  			if got, want := checks.AggregateCheckStatus(configAddr), StatusUnknown; got != want {
   152  				t.Errorf("incorrect initial aggregate check status for %s: %s, but want %s", configAddr, got, want)
   153  			}
   154  		}
   155  	}
   156  
   157  	/////////////////////////////////////////////////////////////////////////
   158  
   159  	// The following are steps that would normally be done by OpenTofu Core
   160  	// as part of visiting checkable objects during the graph walk. We're
   161  	// simulating a likely sequence of calls here for testing purposes, but
   162  	// OpenTofu Core won't necessarily visit all of these in exactly the
   163  	// same order every time and so this is just one possible valid ordering
   164  	// of calls.
   165  
   166  	resourceInstA := resourceA.Resource.Absolute(addrs.RootModuleInstance).Instance(addrs.NoKey)
   167  	rootOutputInst := rootOutput.OutputValue.Absolute(addrs.RootModuleInstance)
   168  	moduleChildInst := addrs.RootModuleInstance.Child("child", addrs.NoKey)
   169  	resourceInstB := resourceB.Resource.Absolute(moduleChildInst).Instance(addrs.NoKey)
   170  	resourceInstC0 := resourceC.Resource.Absolute(moduleChildInst).Instance(addrs.IntKey(0))
   171  	resourceInstC1 := resourceC.Resource.Absolute(moduleChildInst).Instance(addrs.IntKey(1))
   172  	childOutputInst := childOutput.OutputValue.Absolute(moduleChildInst)
   173  	checkBlockInst := checkBlock.Check.Absolute(addrs.RootModuleInstance)
   174  
   175  	checks.ReportCheckableObjects(resourceA, addrs.MakeSet[addrs.Checkable](resourceInstA))
   176  	checks.ReportCheckResult(resourceInstA, addrs.ResourcePrecondition, 0, StatusPass)
   177  	checks.ReportCheckResult(resourceInstA, addrs.ResourcePrecondition, 1, StatusPass)
   178  	checks.ReportCheckResult(resourceInstA, addrs.ResourcePostcondition, 0, StatusPass)
   179  
   180  	checks.ReportCheckableObjects(resourceB, addrs.MakeSet[addrs.Checkable](resourceInstB))
   181  	checks.ReportCheckResult(resourceInstB, addrs.ResourcePrecondition, 0, StatusPass)
   182  
   183  	checks.ReportCheckableObjects(resourceC, addrs.MakeSet[addrs.Checkable](resourceInstC0, resourceInstC1))
   184  	checks.ReportCheckResult(resourceInstC0, addrs.ResourcePostcondition, 0, StatusPass)
   185  	checks.ReportCheckResult(resourceInstC1, addrs.ResourcePostcondition, 0, StatusPass)
   186  
   187  	checks.ReportCheckableObjects(childOutput, addrs.MakeSet[addrs.Checkable](childOutputInst))
   188  	checks.ReportCheckResult(childOutputInst, addrs.OutputPrecondition, 0, StatusPass)
   189  
   190  	checks.ReportCheckableObjects(rootOutput, addrs.MakeSet[addrs.Checkable](rootOutputInst))
   191  	checks.ReportCheckResult(rootOutputInst, addrs.OutputPrecondition, 0, StatusPass)
   192  
   193  	checks.ReportCheckableObjects(checkBlock, addrs.MakeSet[addrs.Checkable](checkBlockInst))
   194  	checks.ReportCheckResult(checkBlockInst, addrs.CheckAssertion, 0, StatusPass)
   195  
   196  	/////////////////////////////////////////////////////////////////////////
   197  
   198  	// This "section" is simulating what we might do to report the results
   199  	// of the checks after a run completes.
   200  
   201  	{
   202  		configCount := 0
   203  		for _, configAddr := range checks.AllConfigAddrs() {
   204  			configCount++
   205  			if got, want := checks.AggregateCheckStatus(configAddr), StatusPass; got != want {
   206  				t.Errorf("incorrect final aggregate check status for %s: %s, but want %s", configAddr, got, want)
   207  			}
   208  		}
   209  		if got, want := configCount, 6; got != want {
   210  			t.Errorf("incorrect number of known config addresses %d; want %d", got, want)
   211  		}
   212  	}
   213  
   214  	{
   215  		objAddrs := addrs.MakeSet[addrs.Checkable](
   216  			resourceInstA,
   217  			rootOutputInst,
   218  			resourceInstB,
   219  			resourceInstC0,
   220  			resourceInstC1,
   221  			childOutputInst,
   222  			checkBlockInst,
   223  		)
   224  		for _, addr := range objAddrs {
   225  			if got, want := checks.ObjectCheckStatus(addr), StatusPass; got != want {
   226  				t.Errorf("incorrect final check status for object %s: %s, but want %s", addr, got, want)
   227  			}
   228  		}
   229  	}
   230  }