github.com/opentofu/opentofu@v1.7.1/internal/configs/test_file.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 configs
     7  
     8  import (
     9  	"fmt"
    10  
    11  	"github.com/hashicorp/hcl/v2"
    12  	"github.com/hashicorp/hcl/v2/gohcl"
    13  	"github.com/hashicorp/hcl/v2/hclsyntax"
    14  
    15  	"github.com/opentofu/opentofu/internal/addrs"
    16  	"github.com/opentofu/opentofu/internal/getmodules"
    17  	"github.com/opentofu/opentofu/internal/tfdiags"
    18  )
    19  
    20  // TestCommand represents the OpenTofu a given run block will execute, plan
    21  // or apply. Defaults to apply.
    22  type TestCommand rune
    23  
    24  // TestMode represents the plan mode that OpenTofu will use for a given run
    25  // block, normal or refresh-only. Defaults to normal.
    26  type TestMode rune
    27  
    28  const (
    29  	// ApplyTestCommand causes the run block to execute a OpenTofu apply
    30  	// operation.
    31  	ApplyTestCommand TestCommand = 0
    32  
    33  	// PlanTestCommand causes the run block to execute a OpenTofu plan
    34  	// operation.
    35  	PlanTestCommand TestCommand = 'P'
    36  
    37  	// NormalTestMode causes the run block to execute in plans.NormalMode.
    38  	NormalTestMode TestMode = 0
    39  
    40  	// RefreshOnlyTestMode causes the run block to execute in
    41  	// plans.RefreshOnlyMode.
    42  	RefreshOnlyTestMode TestMode = 'R'
    43  )
    44  
    45  // TestFile represents a single test file within a `tofu test` execution.
    46  //
    47  // A test file is made up of a sequential list of run blocks, each designating
    48  // a command to execute and a series of validations to check after the command.
    49  type TestFile struct {
    50  	// Variables defines a set of global variable definitions that should be set
    51  	// for every run block within the test file.
    52  	Variables map[string]hcl.Expression
    53  
    54  	// Providers defines a set of providers that are available to run blocks
    55  	// within this test file.
    56  	//
    57  	// If empty, tests should use the default providers for the module under
    58  	// test.
    59  	Providers map[string]*Provider
    60  
    61  	// Runs defines the sequential list of run blocks that should be executed in
    62  	// order.
    63  	Runs []*TestRun
    64  
    65  	VariablesDeclRange hcl.Range
    66  }
    67  
    68  // TestRun represents a single run block within a test file.
    69  //
    70  // Each run block represents a single OpenTofu command to be executed and a set
    71  // of validations to run after the command.
    72  type TestRun struct {
    73  	Name string
    74  
    75  	// Command is the OpenTofu command to execute.
    76  	//
    77  	// One of ['apply', 'plan'].
    78  	Command TestCommand
    79  
    80  	// Options contains the embedded plan options that will affect the given
    81  	// Command. These should map to the options documented here:
    82  	//   - https://opentofu.org/docs/cli/commands/plan/#planning-options
    83  	//
    84  	// Note, that the Variables are a top level concept and not embedded within
    85  	// the options despite being listed as plan options in the documentation.
    86  	Options *TestRunOptions
    87  
    88  	// Variables defines a set of variable definitions for this command.
    89  	//
    90  	// Any variables specified locally that clash with the global variables will
    91  	// take precedence over the global definition.
    92  	Variables map[string]hcl.Expression
    93  
    94  	// Providers specifies the set of providers that should be loaded into the
    95  	// module for this run block.
    96  	//
    97  	// Providers specified here must be configured in one of the provider blocks
    98  	// for this file. If empty, the run block will load the default providers
    99  	// for the module under test.
   100  	Providers []PassedProviderConfig
   101  
   102  	// CheckRules defines the list of assertions/validations that should be
   103  	// checked by this run block.
   104  	CheckRules []*CheckRule
   105  
   106  	// Module defines an address of another module that should be loaded and
   107  	// executed as part of this run block instead of the module under test.
   108  	//
   109  	// In the initial version of the testing framework we will only support
   110  	// loading alternate modules from local directories or the registry.
   111  	Module *TestRunModuleCall
   112  
   113  	// ConfigUnderTest describes the configuration this run block should execute
   114  	// against.
   115  	//
   116  	// In typical cases, this will be null and the config under test is the
   117  	// configuration within the directory the tofu test command is
   118  	// executing within. However, when Module is set the config under test is
   119  	// whichever config is defined by Module. This field is then set during the
   120  	// configuration load process and should be used when the test is executed.
   121  	ConfigUnderTest *Config
   122  
   123  	// ExpectFailures should be a list of checkable objects that are expected
   124  	// to report a failure from their custom conditions as part of this test
   125  	// run.
   126  	ExpectFailures []hcl.Traversal
   127  
   128  	NameDeclRange      hcl.Range
   129  	VariablesDeclRange hcl.Range
   130  	DeclRange          hcl.Range
   131  }
   132  
   133  // Validate does a very simple and cursory check across the run block to look
   134  // for simple issues we can highlight early on.
   135  func (run *TestRun) Validate() tfdiags.Diagnostics {
   136  	var diags tfdiags.Diagnostics
   137  
   138  	// For now, we only want to make sure all the ExpectFailure references are
   139  	// the correct kind of reference.
   140  	for _, traversal := range run.ExpectFailures {
   141  
   142  		reference, refDiags := addrs.ParseRefFromTestingScope(traversal)
   143  		diags = diags.Append(refDiags)
   144  		if refDiags.HasErrors() {
   145  			continue
   146  		}
   147  
   148  		switch reference.Subject.(type) {
   149  		// You can only reference outputs, inputs, checks, and resources.
   150  		case addrs.OutputValue, addrs.InputVariable, addrs.Check, addrs.ResourceInstance, addrs.Resource:
   151  			// Do nothing, these are okay!
   152  		default:
   153  			diags = diags.Append(&hcl.Diagnostic{
   154  				Severity: hcl.DiagError,
   155  				Summary:  "Invalid `expect_failures` reference",
   156  				Detail:   fmt.Sprintf("You cannot expect failures from %s. You can only expect failures from checkable objects such as input variables, output values, check blocks, managed resources and data sources.", reference.Subject.String()),
   157  				Subject:  reference.SourceRange.ToHCL().Ptr(),
   158  			})
   159  		}
   160  
   161  	}
   162  
   163  	return diags
   164  }
   165  
   166  // TestRunModuleCall specifies which module should be executed by a given run
   167  // block.
   168  type TestRunModuleCall struct {
   169  	// Source is the source of the module to test.
   170  	Source addrs.ModuleSource
   171  
   172  	// Version is the version of the module to load from the registry.
   173  	Version VersionConstraint
   174  
   175  	DeclRange       hcl.Range
   176  	SourceDeclRange hcl.Range
   177  }
   178  
   179  // TestRunOptions contains the plan options for a given run block.
   180  type TestRunOptions struct {
   181  	// Mode is the planning mode to run in. One of ['normal', 'refresh-only'].
   182  	Mode TestMode
   183  
   184  	// Refresh is analogous to the -refresh=false OpenTofu plan option.
   185  	Refresh bool
   186  
   187  	// Replace is analogous to the -refresh=ADDRESS OpenTofu plan option.
   188  	Replace []hcl.Traversal
   189  
   190  	// Target is analogous to the -target=ADDRESS OpenTofu plan option.
   191  	Target []hcl.Traversal
   192  
   193  	DeclRange hcl.Range
   194  }
   195  
   196  func loadTestFile(body hcl.Body) (*TestFile, hcl.Diagnostics) {
   197  	var diags hcl.Diagnostics
   198  
   199  	content, contentDiags := body.Content(testFileSchema)
   200  	diags = append(diags, contentDiags...)
   201  
   202  	tf := TestFile{
   203  		Providers: make(map[string]*Provider),
   204  	}
   205  
   206  	for _, block := range content.Blocks {
   207  		switch block.Type {
   208  		case "run":
   209  			run, runDiags := decodeTestRunBlock(block)
   210  			diags = append(diags, runDiags...)
   211  			if !runDiags.HasErrors() {
   212  				tf.Runs = append(tf.Runs, run)
   213  			}
   214  		case "variables":
   215  			if tf.Variables != nil {
   216  				diags = append(diags, &hcl.Diagnostic{
   217  					Severity: hcl.DiagError,
   218  					Summary:  "Multiple \"variables\" blocks",
   219  					Detail:   fmt.Sprintf("This test file already has a variables block defined at %s.", tf.VariablesDeclRange),
   220  					Subject:  block.DefRange.Ptr(),
   221  				})
   222  				continue
   223  			}
   224  
   225  			tf.Variables = make(map[string]hcl.Expression)
   226  			tf.VariablesDeclRange = block.DefRange
   227  
   228  			vars, varsDiags := block.Body.JustAttributes()
   229  			diags = append(diags, varsDiags...)
   230  			for _, v := range vars {
   231  				tf.Variables[v.Name] = v.Expr
   232  			}
   233  		case "provider":
   234  			provider, providerDiags := decodeProviderBlock(block)
   235  			diags = append(diags, providerDiags...)
   236  			if provider != nil {
   237  				tf.Providers[provider.moduleUniqueKey()] = provider
   238  			}
   239  		}
   240  	}
   241  
   242  	return &tf, diags
   243  }
   244  
   245  func decodeTestRunBlock(block *hcl.Block) (*TestRun, hcl.Diagnostics) {
   246  	var diags hcl.Diagnostics
   247  
   248  	content, contentDiags := block.Body.Content(testRunBlockSchema)
   249  	diags = append(diags, contentDiags...)
   250  
   251  	r := TestRun{
   252  		Name:          block.Labels[0],
   253  		NameDeclRange: block.LabelRanges[0],
   254  		DeclRange:     block.DefRange,
   255  	}
   256  
   257  	if !hclsyntax.ValidIdentifier(r.Name) {
   258  		diags = append(diags, &hcl.Diagnostic{
   259  			Severity: hcl.DiagError,
   260  			Summary:  "Invalid run block name",
   261  			Detail:   badIdentifierDetail,
   262  			Subject:  &block.LabelRanges[0],
   263  		})
   264  	}
   265  
   266  	for _, block := range content.Blocks {
   267  		switch block.Type {
   268  		case "assert":
   269  			cr, crDiags := decodeCheckRuleBlock(block, false)
   270  			diags = append(diags, crDiags...)
   271  			if !crDiags.HasErrors() {
   272  				r.CheckRules = append(r.CheckRules, cr)
   273  			}
   274  		case "plan_options":
   275  			if r.Options != nil {
   276  				diags = append(diags, &hcl.Diagnostic{
   277  					Severity: hcl.DiagError,
   278  					Summary:  "Multiple \"plan_options\" blocks",
   279  					Detail:   fmt.Sprintf("This run block already has a plan_options block defined at %s.", r.Options.DeclRange),
   280  					Subject:  block.DefRange.Ptr(),
   281  				})
   282  				continue
   283  			}
   284  
   285  			opts, optsDiags := decodeTestRunOptionsBlock(block)
   286  			diags = append(diags, optsDiags...)
   287  			if !optsDiags.HasErrors() {
   288  				r.Options = opts
   289  			}
   290  		case "variables":
   291  			if r.Variables != nil {
   292  				diags = append(diags, &hcl.Diagnostic{
   293  					Severity: hcl.DiagError,
   294  					Summary:  "Multiple \"variables\" blocks",
   295  					Detail:   fmt.Sprintf("This run block already has a variables block defined at %s.", r.VariablesDeclRange),
   296  					Subject:  block.DefRange.Ptr(),
   297  				})
   298  				continue
   299  			}
   300  
   301  			r.Variables = make(map[string]hcl.Expression)
   302  			r.VariablesDeclRange = block.DefRange
   303  
   304  			vars, varsDiags := block.Body.JustAttributes()
   305  			diags = append(diags, varsDiags...)
   306  			for _, v := range vars {
   307  				r.Variables[v.Name] = v.Expr
   308  			}
   309  		case "module":
   310  			if r.Module != nil {
   311  				diags = append(diags, &hcl.Diagnostic{
   312  					Severity: hcl.DiagError,
   313  					Summary:  "Multiple \"module\" blocks",
   314  					Detail:   fmt.Sprintf("This run block already has a module block defined at %s.", r.Module.DeclRange),
   315  					Subject:  block.DefRange.Ptr(),
   316  				})
   317  			}
   318  
   319  			module, moduleDiags := decodeTestRunModuleBlock(block)
   320  			diags = append(diags, moduleDiags...)
   321  			if !moduleDiags.HasErrors() {
   322  				r.Module = module
   323  			}
   324  		}
   325  	}
   326  
   327  	if r.Variables == nil {
   328  		// There is no distinction between a nil map of variables or an empty
   329  		// map, but we can avoid any potential nil pointer exceptions by just
   330  		// creating an empty map.
   331  		r.Variables = make(map[string]hcl.Expression)
   332  	}
   333  
   334  	if r.Options == nil {
   335  		// Create an options with default values if the user didn't specify
   336  		// anything.
   337  		r.Options = &TestRunOptions{
   338  			Mode:    NormalTestMode,
   339  			Refresh: true,
   340  		}
   341  	}
   342  
   343  	if attr, exists := content.Attributes["command"]; exists {
   344  		switch hcl.ExprAsKeyword(attr.Expr) {
   345  		case "apply":
   346  			r.Command = ApplyTestCommand
   347  		case "plan":
   348  			r.Command = PlanTestCommand
   349  		default:
   350  			diags = append(diags, &hcl.Diagnostic{
   351  				Severity: hcl.DiagError,
   352  				Summary:  "Invalid \"command\" keyword",
   353  				Detail:   "The \"command\" argument requires one of the following keywords without quotes: apply or plan.",
   354  				Subject:  attr.Expr.Range().Ptr(),
   355  			})
   356  		}
   357  	} else {
   358  		r.Command = ApplyTestCommand // Default to apply
   359  	}
   360  
   361  	if attr, exists := content.Attributes["providers"]; exists {
   362  		providers, providerDiags := decodePassedProviderConfigs(attr)
   363  		diags = append(diags, providerDiags...)
   364  		r.Providers = append(r.Providers, providers...)
   365  	}
   366  
   367  	if attr, exists := content.Attributes["expect_failures"]; exists {
   368  		failures, failDiags := decodeDependsOn(attr)
   369  		diags = append(diags, failDiags...)
   370  		r.ExpectFailures = failures
   371  	}
   372  
   373  	return &r, diags
   374  }
   375  
   376  func decodeTestRunModuleBlock(block *hcl.Block) (*TestRunModuleCall, hcl.Diagnostics) {
   377  	var diags hcl.Diagnostics
   378  
   379  	content, contentDiags := block.Body.Content(testRunModuleBlockSchema)
   380  	diags = append(diags, contentDiags...)
   381  
   382  	module := TestRunModuleCall{
   383  		DeclRange: block.DefRange,
   384  	}
   385  
   386  	haveVersionArg := false
   387  	if attr, exists := content.Attributes["version"]; exists {
   388  		var versionDiags hcl.Diagnostics
   389  		module.Version, versionDiags = decodeVersionConstraint(attr)
   390  		diags = append(diags, versionDiags...)
   391  		haveVersionArg = true
   392  	}
   393  
   394  	if attr, exists := content.Attributes["source"]; exists {
   395  		module.SourceDeclRange = attr.Range
   396  
   397  		var raw string
   398  		rawDiags := gohcl.DecodeExpression(attr.Expr, nil, &raw)
   399  		diags = append(diags, rawDiags...)
   400  		if !rawDiags.HasErrors() {
   401  			var err error
   402  			if haveVersionArg {
   403  				module.Source, err = addrs.ParseModuleSourceRegistry(raw)
   404  			} else {
   405  				module.Source, err = addrs.ParseModuleSource(raw)
   406  			}
   407  			if err != nil {
   408  				// NOTE: We leave mc.SourceAddr as nil for any situation where the
   409  				// source attribute is invalid, so any code which tries to carefully
   410  				// use the partial result of a failed config decode must be
   411  				// resilient to that.
   412  				module.Source = nil
   413  
   414  				// NOTE: In practice it's actually very unlikely to end up here,
   415  				// because our source address parser can turn just about any string
   416  				// into some sort of remote package address, and so for most errors
   417  				// we'll detect them only during module installation. There are
   418  				// still a _few_ purely-syntax errors we can catch at parsing time,
   419  				// though, mostly related to remote package sub-paths and local
   420  				// paths.
   421  				switch err := err.(type) {
   422  				case *getmodules.MaybeRelativePathErr:
   423  					diags = append(diags, &hcl.Diagnostic{
   424  						Severity: hcl.DiagError,
   425  						Summary:  "Invalid module source address",
   426  						Detail: fmt.Sprintf(
   427  							"OpenTofu failed to determine your intended installation method for remote module package %q.\n\nIf you intended this as a path relative to the current module, use \"./%s\" instead. The \"./\" prefix indicates that the address is a relative filesystem path.",
   428  							err.Addr, err.Addr,
   429  						),
   430  						Subject: module.SourceDeclRange.Ptr(),
   431  					})
   432  				default:
   433  					if haveVersionArg {
   434  						// In this case we'll include some extra context that
   435  						// we assumed a registry source address due to the
   436  						// version argument.
   437  						diags = append(diags, &hcl.Diagnostic{
   438  							Severity: hcl.DiagError,
   439  							Summary:  "Invalid registry module source address",
   440  							Detail:   fmt.Sprintf("Failed to parse module registry address: %s.\n\nOpenTofu assumed that you intended a module registry source address because you also set the argument \"version\", which applies only to registry modules.", err),
   441  							Subject:  module.SourceDeclRange.Ptr(),
   442  						})
   443  					} else {
   444  						diags = append(diags, &hcl.Diagnostic{
   445  							Severity: hcl.DiagError,
   446  							Summary:  "Invalid module source address",
   447  							Detail:   fmt.Sprintf("Failed to parse module source address: %s.", err),
   448  							Subject:  module.SourceDeclRange.Ptr(),
   449  						})
   450  					}
   451  				}
   452  			}
   453  
   454  			switch module.Source.(type) {
   455  			case addrs.ModuleSourceRemote:
   456  				// We only support local or registry modules when loading
   457  				// modules directly from alternate sources during a test
   458  				// execution.
   459  				diags = append(diags, &hcl.Diagnostic{
   460  					Severity: hcl.DiagError,
   461  					Summary:  "Invalid module source address",
   462  					Detail:   "Only local or registry module sources are currently supported from within test run blocks.",
   463  					Subject:  module.SourceDeclRange.Ptr(),
   464  				})
   465  			}
   466  		}
   467  	} else {
   468  		// Must have a source attribute.
   469  		diags = append(diags, &hcl.Diagnostic{
   470  			Severity: hcl.DiagError,
   471  			Summary:  "Missing \"source\" attribute for module block",
   472  			Detail:   "You must specify a source attribute when executing alternate modules during test executions.",
   473  			Subject:  module.DeclRange.Ptr(),
   474  		})
   475  	}
   476  
   477  	return &module, diags
   478  }
   479  
   480  func decodeTestRunOptionsBlock(block *hcl.Block) (*TestRunOptions, hcl.Diagnostics) {
   481  	var diags hcl.Diagnostics
   482  
   483  	content, contentDiags := block.Body.Content(testRunOptionsBlockSchema)
   484  	diags = append(diags, contentDiags...)
   485  
   486  	opts := TestRunOptions{
   487  		DeclRange: block.DefRange,
   488  	}
   489  
   490  	if attr, exists := content.Attributes["mode"]; exists {
   491  		switch hcl.ExprAsKeyword(attr.Expr) {
   492  		case "refresh-only":
   493  			opts.Mode = RefreshOnlyTestMode
   494  		case "normal":
   495  			opts.Mode = NormalTestMode
   496  		default:
   497  			diags = append(diags, &hcl.Diagnostic{
   498  				Severity: hcl.DiagError,
   499  				Summary:  "Invalid \"mode\" keyword",
   500  				Detail:   "The \"mode\" argument requires one of the following keywords without quotes: normal or refresh-only",
   501  				Subject:  attr.Expr.Range().Ptr(),
   502  			})
   503  		}
   504  	} else {
   505  		opts.Mode = NormalTestMode // Default to normal
   506  	}
   507  
   508  	if attr, exists := content.Attributes["refresh"]; exists {
   509  		diags = append(diags, gohcl.DecodeExpression(attr.Expr, nil, &opts.Refresh)...)
   510  	} else {
   511  		// Defaults to true.
   512  		opts.Refresh = true
   513  	}
   514  
   515  	if attr, exists := content.Attributes["replace"]; exists {
   516  		reps, repsDiags := decodeDependsOn(attr)
   517  		diags = append(diags, repsDiags...)
   518  		opts.Replace = reps
   519  	}
   520  
   521  	if attr, exists := content.Attributes["target"]; exists {
   522  		tars, tarsDiags := decodeDependsOn(attr)
   523  		diags = append(diags, tarsDiags...)
   524  		opts.Target = tars
   525  	}
   526  
   527  	if !opts.Refresh && opts.Mode == RefreshOnlyTestMode {
   528  		// These options are incompatible.
   529  		diags = append(diags, &hcl.Diagnostic{
   530  			Severity: hcl.DiagError,
   531  			Summary:  "Incompatible plan options",
   532  			Detail:   "The \"refresh\" option cannot be set to false when running a test in \"refresh-only\" mode.",
   533  			Subject:  content.Attributes["refresh"].Range.Ptr(),
   534  		})
   535  	}
   536  
   537  	return &opts, diags
   538  }
   539  
   540  var testFileSchema = &hcl.BodySchema{
   541  	Blocks: []hcl.BlockHeaderSchema{
   542  		{
   543  			Type:       "run",
   544  			LabelNames: []string{"name"},
   545  		},
   546  		{
   547  			Type:       "provider",
   548  			LabelNames: []string{"name"},
   549  		},
   550  		{
   551  			Type: "variables",
   552  		},
   553  	},
   554  }
   555  
   556  var testRunBlockSchema = &hcl.BodySchema{
   557  	Attributes: []hcl.AttributeSchema{
   558  		{Name: "command"},
   559  		{Name: "providers"},
   560  		{Name: "expect_failures"},
   561  	},
   562  	Blocks: []hcl.BlockHeaderSchema{
   563  		{
   564  			Type: "plan_options",
   565  		},
   566  		{
   567  			Type: "assert",
   568  		},
   569  		{
   570  			Type: "variables",
   571  		},
   572  		{
   573  			Type: "module",
   574  		},
   575  	},
   576  }
   577  
   578  var testRunOptionsBlockSchema = &hcl.BodySchema{
   579  	Attributes: []hcl.AttributeSchema{
   580  		{Name: "mode"},
   581  		{Name: "refresh"},
   582  		{Name: "replace"},
   583  		{Name: "target"},
   584  	},
   585  }
   586  
   587  var testRunModuleBlockSchema = &hcl.BodySchema{
   588  	Attributes: []hcl.AttributeSchema{
   589  		{Name: "source"},
   590  		{Name: "version"},
   591  	},
   592  }