github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/moduletest/run.go (about)

     1  package moduletest
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/hashicorp/hcl/v2"
     7  
     8  	"github.com/terramate-io/tf/addrs"
     9  	"github.com/terramate-io/tf/configs"
    10  	"github.com/terramate-io/tf/configs/configschema"
    11  	"github.com/terramate-io/tf/plans"
    12  	"github.com/terramate-io/tf/providers"
    13  	"github.com/terramate-io/tf/states"
    14  	"github.com/terramate-io/tf/tfdiags"
    15  )
    16  
    17  type Run struct {
    18  	Config *configs.TestRun
    19  
    20  	Verbose *Verbose
    21  
    22  	Name   string
    23  	Index  int
    24  	Status Status
    25  
    26  	Diagnostics tfdiags.Diagnostics
    27  }
    28  
    29  // Verbose is a utility struct that holds all the information required for a run
    30  // to render the results verbosely.
    31  //
    32  // At the moment, this basically means printing out the plan. To do that we need
    33  // all the information within this struct.
    34  type Verbose struct {
    35  	Plan         *plans.Plan
    36  	State        *states.State
    37  	Config       *configs.Config
    38  	Providers    map[addrs.Provider]providers.ProviderSchema
    39  	Provisioners map[string]*configschema.Block
    40  }
    41  
    42  func (run *Run) GetTargets() ([]addrs.Targetable, tfdiags.Diagnostics) {
    43  	var diagnostics tfdiags.Diagnostics
    44  	var targets []addrs.Targetable
    45  
    46  	for _, target := range run.Config.Options.Target {
    47  		addr, diags := addrs.ParseTarget(target)
    48  		diagnostics = diagnostics.Append(diags)
    49  		if addr != nil {
    50  			targets = append(targets, addr.Subject)
    51  		}
    52  	}
    53  
    54  	return targets, diagnostics
    55  }
    56  
    57  func (run *Run) GetReplaces() ([]addrs.AbsResourceInstance, tfdiags.Diagnostics) {
    58  	var diagnostics tfdiags.Diagnostics
    59  	var replaces []addrs.AbsResourceInstance
    60  
    61  	for _, replace := range run.Config.Options.Replace {
    62  		addr, diags := addrs.ParseAbsResourceInstance(replace)
    63  		diagnostics = diagnostics.Append(diags)
    64  		if diags.HasErrors() {
    65  			continue
    66  		}
    67  
    68  		if addr.Resource.Resource.Mode != addrs.ManagedResourceMode {
    69  			diagnostics = diagnostics.Append(&hcl.Diagnostic{
    70  				Severity: hcl.DiagError,
    71  				Summary:  "Can only target managed resources for forced replacements.",
    72  				Detail:   addr.String(),
    73  				Subject:  replace.SourceRange().Ptr(),
    74  			})
    75  			continue
    76  		}
    77  
    78  		replaces = append(replaces, addr)
    79  	}
    80  
    81  	return replaces, diagnostics
    82  }
    83  
    84  func (run *Run) GetReferences() ([]*addrs.Reference, tfdiags.Diagnostics) {
    85  	var diagnostics tfdiags.Diagnostics
    86  	var references []*addrs.Reference
    87  
    88  	for _, rule := range run.Config.CheckRules {
    89  		for _, variable := range rule.Condition.Variables() {
    90  			reference, diags := addrs.ParseRef(variable)
    91  			diagnostics = diagnostics.Append(diags)
    92  			if reference != nil {
    93  				references = append(references, reference)
    94  			}
    95  		}
    96  		for _, variable := range rule.ErrorMessage.Variables() {
    97  			reference, diags := addrs.ParseRef(variable)
    98  			diagnostics = diagnostics.Append(diags)
    99  			if reference != nil {
   100  				references = append(references, reference)
   101  			}
   102  		}
   103  	}
   104  
   105  	return references, diagnostics
   106  }
   107  
   108  // ValidateExpectedFailures steps through the provided diagnostics (which should
   109  // be the result of a plan or an apply operation), and does 3 things:
   110  //  1. Removes diagnostics that match the expected failures from the config.
   111  //  2. Upgrades warnings from check blocks into errors where appropriate so the
   112  //     test will fail later.
   113  //  3. Adds diagnostics for any expected failures that were not satisfied.
   114  //
   115  // Point 2 is a bit complicated so worth expanding on. In normal Terraform
   116  // execution, any error that originates within a check block (either from an
   117  // assertion or a scoped data source) is wrapped up as a Warning to be
   118  // identified to the user but not to fail the actual Terraform operation. During
   119  // test execution, we want to upgrade (or rollback) these warnings into errors
   120  // again so the test will fail. We do that as part of this function as we are
   121  // already processing the diagnostics from check blocks in here anyway.
   122  //
   123  // The way the function works out which diagnostics are relevant to expected
   124  // failures is by using the tfdiags Extra functionality to detect which
   125  // diagnostics were generated by custom conditions. Terraform adds the
   126  // addrs.CheckRule that generated each diagnostic to the diagnostic itself so we
   127  // can tell which diagnostics can be expected.
   128  func (run *Run) ValidateExpectedFailures(originals tfdiags.Diagnostics) tfdiags.Diagnostics {
   129  
   130  	// We're going to capture all the checkable objects that are referenced
   131  	// from the expected failures.
   132  	expectedFailures := addrs.MakeMap[addrs.Referenceable, bool]()
   133  	sourceRanges := addrs.MakeMap[addrs.Referenceable, tfdiags.SourceRange]()
   134  
   135  	for _, traversal := range run.Config.ExpectFailures {
   136  		// Ignore the diagnostics returned from the reference parsing, these
   137  		// references will have been checked earlier in the process by the
   138  		// validate stage so we don't need to do that again here.
   139  		reference, _ := addrs.ParseRefFromTestingScope(traversal)
   140  		expectedFailures.Put(reference.Subject, false)
   141  		sourceRanges.Put(reference.Subject, reference.SourceRange)
   142  	}
   143  
   144  	var diags tfdiags.Diagnostics
   145  	for _, diag := range originals {
   146  
   147  		if rule, ok := addrs.DiagnosticOriginatesFromCheckRule(diag); ok {
   148  			switch rule.Container.CheckableKind() {
   149  			case addrs.CheckableOutputValue:
   150  				addr := rule.Container.(addrs.AbsOutputValue)
   151  				if !addr.Module.IsRoot() {
   152  					// failures can only be expected against checkable objects
   153  					// in the root module. This diagnostic will be added into
   154  					// returned set below.
   155  					break
   156  				}
   157  
   158  				if diag.Severity() == tfdiags.Warning {
   159  					// Warnings don't count as errors. This diagnostic will be
   160  					// added into the returned set below.
   161  					break
   162  				}
   163  
   164  				if expectedFailures.Has(addr.OutputValue) {
   165  					// Then this failure is expected! Mark the original map as
   166  					// having found a failure and swallow this error by
   167  					// continuing and not adding it into the returned set of
   168  					// diagnostics.
   169  					expectedFailures.Put(addr.OutputValue, true)
   170  					continue
   171  				}
   172  
   173  				// Otherwise, this isn't an expected failure so just fall out
   174  				// and add it into the returned set of diagnostics below.
   175  
   176  			case addrs.CheckableInputVariable:
   177  				addr := rule.Container.(addrs.AbsInputVariableInstance)
   178  				if !addr.Module.IsRoot() {
   179  					// failures can only be expected against checkable objects
   180  					// in the root module. This diagnostic will be added into
   181  					// returned set below.
   182  					break
   183  				}
   184  
   185  				if diag.Severity() == tfdiags.Warning {
   186  					// Warnings don't count as errors. This diagnostic will be
   187  					// added into the returned set below.
   188  					break
   189  				}
   190  				if expectedFailures.Has(addr.Variable) {
   191  					// Then this failure is expected! Mark the original map as
   192  					// having found a failure and swallow this error by
   193  					// continuing and not adding it into the returned set of
   194  					// diagnostics.
   195  					expectedFailures.Put(addr.Variable, true)
   196  					continue
   197  				}
   198  
   199  				// Otherwise, this isn't an expected failure so just fall out
   200  				// and add it into the returned set of diagnostics below.
   201  
   202  			case addrs.CheckableResource:
   203  				addr := rule.Container.(addrs.AbsResourceInstance)
   204  				if !addr.Module.IsRoot() {
   205  					// failures can only be expected against checkable objects
   206  					// in the root module. This diagnostic will be added into
   207  					// returned set below.
   208  					break
   209  				}
   210  
   211  				if diag.Severity() == tfdiags.Warning {
   212  					// Warnings don't count as errors. This diagnostic will be
   213  					// added into the returned set below.
   214  					break
   215  				}
   216  
   217  				if expectedFailures.Has(addr.Resource) {
   218  					// Then this failure is expected! Mark the original map as
   219  					// having found a failure and swallow this error by
   220  					// continuing and not adding it into the returned set of
   221  					// diagnostics.
   222  					expectedFailures.Put(addr.Resource, true)
   223  					continue
   224  				}
   225  
   226  				if expectedFailures.Has(addr.Resource.Resource) {
   227  					// We can also blanket expect failures in all instances for
   228  					// a resource so we check for that here as well.
   229  					expectedFailures.Put(addr.Resource.Resource, true)
   230  					continue
   231  				}
   232  
   233  				// Otherwise, this isn't an expected failure so just fall out
   234  				// and add it into the returned set of diagnostics below.
   235  
   236  			case addrs.CheckableCheck:
   237  				addr := rule.Container.(addrs.AbsCheck)
   238  
   239  				// Check blocks are a bit more difficult than the others. Check
   240  				// block diagnostics could be from a nested data block, or
   241  				// from a failed assertion, and have all been marked as just
   242  				// warning severity.
   243  				//
   244  				// For diagnostics from failed assertions, we want to check if
   245  				// it was expected and skip it if it was. But if it wasn't
   246  				// expected we want to upgrade the diagnostic from a warning
   247  				// into an error so the test case will fail overall.
   248  				//
   249  				// For diagnostics from nested data blocks, we have two
   250  				// categories of diagnostics. First, diagnostics that were
   251  				// originally errors and we mapped into warnings. Second,
   252  				// diagnostics that were originally warnings and stayed that
   253  				// way. For the first case, we want to turn these back to errors
   254  				// and use them as part of the expected failures functionality.
   255  				// The second case should remain as warnings and be ignored by
   256  				// the expected failures functionality.
   257  				//
   258  				// Note, as well that we still want to upgrade failed checks
   259  				// from child modules into errors, so in the other branches we
   260  				// just do a simple blanket skip off all diagnostics not
   261  				// from the root module. We're more selective here, only
   262  				// diagnostics from the root module are considered for the
   263  				// expect failures functionality but we do also upgrade
   264  				// diagnostics from child modules back into errors.
   265  
   266  				if rule.Type == addrs.CheckAssertion {
   267  					// Then this diagnostic is from a check block assertion, it
   268  					// is something we want to treat as an error even though it
   269  					// is actually claiming to be a warning.
   270  
   271  					if addr.Module.IsRoot() && expectedFailures.Has(addr.Check) {
   272  						// Then this failure is expected! Mark the original map as
   273  						// having found a failure and continue.
   274  						expectedFailures.Put(addr.Check, true)
   275  						continue
   276  					}
   277  
   278  					// Otherwise, let's package this up as an error and move on.
   279  					diags = diags.Append(tfdiags.Override(diag, tfdiags.Error, nil))
   280  					continue
   281  				} else if rule.Type == addrs.CheckDataResource {
   282  					// Then the diagnostic we have was actually overridden so
   283  					// let's get back to the original.
   284  					original := tfdiags.UndoOverride(diag)
   285  
   286  					// This diagnostic originated from a scoped data source.
   287  					if addr.Module.IsRoot() && original.Severity() == tfdiags.Error {
   288  						// Okay, we have a genuine error from the root module,
   289  						// so we can now check if we want to ignore it or not.
   290  						if expectedFailures.Has(addr.Check) {
   291  							// Then this failure is expected! Mark the original map as
   292  							// having found a failure and continue.
   293  							expectedFailures.Put(addr.Check, true)
   294  							continue
   295  						}
   296  					}
   297  
   298  					// In all other cases, we want to add the original error
   299  					// into the set we return to the testing framework and move
   300  					// onto the next one.
   301  					diags = diags.Append(original)
   302  					continue
   303  				} else {
   304  					panic("invalid CheckType: " + rule.Type.String())
   305  				}
   306  			default:
   307  				panic("unrecognized CheckableKind: " + rule.Container.CheckableKind().String())
   308  			}
   309  		}
   310  
   311  		// If we get here, then we're not modifying the original diagnostic at
   312  		// all. We just want the testing framework to treat it as normal.
   313  		diags = diags.Append(diag)
   314  	}
   315  
   316  	// Okay, we've checked all our diagnostics to see if any were expected.
   317  	// Now, let's make sure that all the checkable objects we expected to fail
   318  	// actually did!
   319  
   320  	for _, elem := range expectedFailures.Elems {
   321  		addr := elem.Key
   322  		failed := elem.Value
   323  
   324  		if !failed {
   325  			// Then we expected a failure, and it did not occur. Add it to the
   326  			// diagnostics.
   327  			diags = diags.Append(&hcl.Diagnostic{
   328  				Severity: hcl.DiagError,
   329  				Summary:  "Missing expected failure",
   330  				Detail:   fmt.Sprintf("The checkable object, %s, was expected to report an error but did not.", addr.String()),
   331  				Subject:  sourceRanges.Get(addr).ToHCL().Ptr(),
   332  			})
   333  		}
   334  	}
   335  
   336  	return diags
   337  }