mirror of https://github.com/hashicorp/terraform
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1204 lines
40 KiB
1204 lines
40 KiB
// Copyright IBM Corp. 2014, 2026
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package configs
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/gohcl"
|
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
|
|
|
"github.com/hashicorp/terraform/internal/addrs"
|
|
"github.com/hashicorp/terraform/internal/getmodules/moduleaddrs"
|
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
|
)
|
|
|
|
// TestCommand represents the Terraform a given run block will execute, plan
|
|
// or apply. Defaults to apply.
|
|
type TestCommand rune
|
|
|
|
// TestMode represents the plan mode that Terraform will use for a given run
|
|
// block, normal or refresh-only. Defaults to normal.
|
|
type TestMode rune
|
|
|
|
const (
|
|
// ApplyTestCommand causes the run block to execute a Terraform apply
|
|
// operation.
|
|
ApplyTestCommand TestCommand = 0
|
|
|
|
// PlanTestCommand causes the run block to execute a Terraform plan
|
|
// operation.
|
|
PlanTestCommand TestCommand = 'P'
|
|
|
|
// NormalTestMode causes the run block to execute in plans.NormalMode.
|
|
NormalTestMode TestMode = 0
|
|
|
|
// RefreshOnlyTestMode causes the run block to execute in
|
|
// plans.RefreshOnlyMode.
|
|
RefreshOnlyTestMode TestMode = 'R'
|
|
)
|
|
|
|
const (
|
|
// TestMainStateIdentifier is the default state identifier for the main
|
|
// state of the test file.
|
|
TestMainStateIdentifier = ""
|
|
)
|
|
|
|
// TestFile represents a single test file within a `terraform test` execution.
|
|
//
|
|
// A test file is made up of a sequential list of run blocks, each designating
|
|
// a command to execute and a series of validations to check after the command.
|
|
type TestFile struct {
|
|
|
|
// VariableDefinitions allows users to specify variables that should be
|
|
// provided externally (eg. from the command line or external files).
|
|
//
|
|
// This conflicts with the Variables block. Variables specified in the
|
|
// VariableDefinitions cannot also be specified within the Variables block.
|
|
VariableDefinitions map[string]*Variable
|
|
|
|
// Variables defines a set of global variable definitions that should be set
|
|
// for every run block within the test file.
|
|
Variables map[string]hcl.Expression
|
|
|
|
// Providers defines a set of providers that are available to run blocks
|
|
// within this test file.
|
|
//
|
|
// Some or all of these providers may be mocked providers.
|
|
//
|
|
// If empty, tests should use the default providers for the module under
|
|
// test.
|
|
Providers map[string]*Provider
|
|
|
|
// BackendConfigs is a map of state keys to structs that contain backend
|
|
// configuration. This should be used to set the state for a given state key
|
|
// at the start of a test command.
|
|
BackendConfigs map[string]RunBlockBackend
|
|
|
|
// Overrides contains any specific overrides that should be applied for this
|
|
// test outside any mock providers.
|
|
Overrides addrs.Map[addrs.Targetable, *Override]
|
|
|
|
// Runs defines the sequential list of run blocks that should be executed in
|
|
// order.
|
|
Runs []*TestRun
|
|
|
|
Config *TestFileConfig
|
|
|
|
VariablesDeclRange hcl.Range
|
|
}
|
|
|
|
// TestFileConfig represents the configuration block within a test file.
|
|
type TestFileConfig struct {
|
|
// Parallel: Indicates if test runs should be executed in parallel.
|
|
Parallel bool
|
|
|
|
// SkipCleanup: Indicates if the test runs should skip the cleanup phase.
|
|
SkipCleanup bool
|
|
|
|
DeclRange hcl.Range
|
|
}
|
|
|
|
// TestRun represents a single run block within a test file.
|
|
//
|
|
// Each run block represents a single Terraform command to be executed and a set
|
|
// of validations to run after the command.
|
|
type TestRun struct {
|
|
Name string
|
|
|
|
// Command is the Terraform command to execute.
|
|
//
|
|
// One of ['apply', 'plan'].
|
|
Command TestCommand
|
|
|
|
// Options contains the embedded plan options that will affect the given
|
|
// Command. These should map to the options documented here:
|
|
// - https://developer.hashicorp.com/terraform/cli/commands/plan#planning-options
|
|
//
|
|
// Note, that the Variables are a top level concept and not embedded within
|
|
// the options despite being listed as plan options in the documentation.
|
|
Options *TestRunOptions
|
|
|
|
// Variables defines a set of variable definitions for this command.
|
|
//
|
|
// Any variables specified locally that clash with the global variables will
|
|
// take precedence over the global definition.
|
|
Variables map[string]hcl.Expression
|
|
|
|
// Overrides contains any specific overrides that should be applied for this
|
|
// run block only outside any mock providers or overrides from the file.
|
|
Overrides addrs.Map[addrs.Targetable, *Override]
|
|
|
|
// Providers specifies the set of providers that should be loaded into the
|
|
// module for this run block.
|
|
//
|
|
// Providers specified here must be configured in one of the provider blocks
|
|
// for this file. If empty, the run block will load the default providers
|
|
// for the module under test.
|
|
Providers []PassedProviderConfig
|
|
|
|
// CheckRules defines the list of assertions/validations that should be
|
|
// checked by this run block.
|
|
CheckRules []*CheckRule
|
|
|
|
// Module defines an address of another module that should be loaded and
|
|
// executed as part of this run block instead of the module under test.
|
|
//
|
|
// In the initial version of the testing framework we will only support
|
|
// loading alternate modules from local directories or the registry.
|
|
Module *TestRunModuleCall
|
|
|
|
// ConfigUnderTest describes the configuration this run block should execute
|
|
// against.
|
|
//
|
|
// In typical cases, this will be null and the config under test is the
|
|
// configuration within the directory the terraform test command is
|
|
// executing within. However, when Module is set the config under test is
|
|
// whichever config is defined by Module. This field is then set during the
|
|
// configuration load process and should be used when the test is executed.
|
|
ConfigUnderTest *Config
|
|
|
|
// File is a reference to the parent TestFile that contains this run block.
|
|
File *TestFile
|
|
|
|
// ExpectFailures should be a list of checkable objects that are expected
|
|
// to report a failure from their custom conditions as part of this test
|
|
// run.
|
|
ExpectFailures []hcl.Traversal
|
|
|
|
// StateKey when given, will be used to identify the state file to use for
|
|
// this test run. If not provided, the state key is derived from the
|
|
// configuration under test.
|
|
StateKey string
|
|
|
|
// Parallel: Indicates if the test run should be executed in parallel.
|
|
// This, in combination with the state key, will determine if the test run
|
|
// will be executed in parallel with other test runs.
|
|
Parallel bool
|
|
|
|
Backend *Backend
|
|
|
|
// SkipCleanup: Indicates if the test run should skip the cleanup phase.
|
|
SkipCleanup bool
|
|
SkipCleanupRange *hcl.Range
|
|
|
|
NameDeclRange hcl.Range
|
|
VariablesDeclRange hcl.Range
|
|
DeclRange hcl.Range
|
|
}
|
|
|
|
// Validate does a very simple and cursory check across the test file to look
|
|
// for simple issues we can highlight early on.
|
|
//
|
|
// This function only returns warnings in the diagnostics. Callers of this
|
|
// function usually do not validate the returned diagnostics as a result. If
|
|
// you change this function, make sure to update the callers as well.
|
|
func (file *TestFile) Validate(config *Config) tfdiags.Diagnostics {
|
|
var diags tfdiags.Diagnostics
|
|
|
|
for _, provider := range file.Providers {
|
|
if !provider.Mock {
|
|
continue
|
|
}
|
|
|
|
for _, elem := range provider.MockData.Overrides.Elems {
|
|
if elem.Value.Source != MockProviderOverrideSource {
|
|
// Only check overrides that are defined directly within the
|
|
// mock provider block of this file. This is a safety check
|
|
// against any override blocks loaded from a dedicated data
|
|
// file, for these we won't raise warnings if they target
|
|
// resources that don't exist.
|
|
continue
|
|
}
|
|
|
|
if !file.canTarget(config, elem.Key) {
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagWarning,
|
|
Summary: "Invalid override target",
|
|
Detail: fmt.Sprintf("The override target %s does not exist within the configuration under test. This could indicate a typo in the target address or an unnecessary override.", elem.Key),
|
|
Subject: elem.Value.TargetRange.Ptr(),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, elem := range file.Overrides.Elems {
|
|
if !file.canTarget(config, elem.Key) {
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagWarning,
|
|
Summary: "Invalid override target",
|
|
Detail: fmt.Sprintf("The override target %s does not exist within the configuration under test. This could indicate a typo in the target address or an unnecessary override.", elem.Key),
|
|
Subject: elem.Value.TargetRange.Ptr(),
|
|
})
|
|
}
|
|
}
|
|
|
|
return diags
|
|
}
|
|
|
|
// canTarget is a helper function, that just checks all the available
|
|
// configurations to make sure at least one contains the specified target.
|
|
func (file *TestFile) canTarget(config *Config, target addrs.Targetable) bool {
|
|
// If the target is in the main configuration, then easy.
|
|
if config.TargetExists(target) {
|
|
return true
|
|
}
|
|
|
|
// But, we could be targeting something in configuration loaded by one of
|
|
// the run blocks.
|
|
for _, run := range file.Runs {
|
|
if run.Module != nil {
|
|
if run.ConfigUnderTest.TargetExists(target) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Validate does a very simple and cursory check across the run block to look
|
|
// for simple issues we can highlight early on.
|
|
func (run *TestRun) Validate(config *Config) tfdiags.Diagnostics {
|
|
var diags tfdiags.Diagnostics
|
|
|
|
// First, we want to make sure all the ExpectFailure references are the
|
|
// correct kind of reference.
|
|
for _, traversal := range run.ExpectFailures {
|
|
|
|
reference, refDiags := addrs.ParseRefFromTestingScope(traversal)
|
|
diags = diags.Append(refDiags)
|
|
if refDiags.HasErrors() {
|
|
continue
|
|
}
|
|
|
|
switch reference.Subject.(type) {
|
|
// You can only reference outputs, inputs, checks, and resources.
|
|
case addrs.OutputValue, addrs.InputVariable, addrs.Check, addrs.ResourceInstance, addrs.Resource:
|
|
// Do nothing, these are okay!
|
|
default:
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid `expect_failures` reference",
|
|
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()),
|
|
Subject: reference.SourceRange.ToHCL().Ptr(),
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
// All the overrides defined within a run block should target an existing
|
|
// configuration block.
|
|
for _, elem := range run.Overrides.Elems {
|
|
if !config.TargetExists(elem.Key) {
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagWarning,
|
|
Summary: "Invalid override target",
|
|
Detail: fmt.Sprintf("The override target %s does not exist within the configuration under test. This could indicate a typo in the target address or an unnecessary override.", elem.Key),
|
|
Subject: elem.Value.TargetRange.Ptr(),
|
|
})
|
|
}
|
|
}
|
|
|
|
// All the providers defined within a run block should target an existing
|
|
// provider block within the test file.
|
|
for _, ref := range run.Providers {
|
|
_, ok := run.File.Providers[ref.InParent.String()]
|
|
if !ok {
|
|
// Then this reference was invalid as we didn't have the
|
|
// specified provider in the parent. This should have been
|
|
// caught earlier in validation anyway so is unlikely to happen.
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: fmt.Sprintf("Missing provider definition for %s", ref.InParent.String()),
|
|
Detail: "This provider block references a provider definition that does not exist.",
|
|
Subject: ref.InParent.NameRange.Ptr(),
|
|
})
|
|
}
|
|
}
|
|
|
|
return diags
|
|
}
|
|
|
|
// TestRunModuleCall specifies which module should be executed by a given run
|
|
// block.
|
|
type TestRunModuleCall struct {
|
|
// Source is the source of the module to test.
|
|
Source addrs.ModuleSource
|
|
|
|
// Version is the version of the module to load from the registry.
|
|
Version VersionConstraint
|
|
|
|
DeclRange hcl.Range
|
|
SourceDeclRange hcl.Range
|
|
}
|
|
|
|
// TestRunOptions contains the plan options for a given run block.
|
|
type TestRunOptions struct {
|
|
// Mode is the planning mode to run in. One of ['normal', 'refresh-only'].
|
|
Mode TestMode
|
|
|
|
// Refresh is analogous to the -refresh=false Terraform plan option.
|
|
Refresh bool
|
|
|
|
// Replace is analogous to the -replace=ADDRESS Terraform plan option.
|
|
Replace []hcl.Traversal
|
|
|
|
// Target is analogous to the -target=ADDRESS Terraform plan option.
|
|
Target []hcl.Traversal
|
|
|
|
DeclRange hcl.Range
|
|
}
|
|
|
|
// RunBlockBackend records a backend block and which run block it was parsed
|
|
// from.
|
|
type RunBlockBackend struct {
|
|
Backend *Backend
|
|
|
|
// Run is the TestRun containing the backend block for this Backend.
|
|
// This is used in diagnostics to help avoid duplicate backends for a given
|
|
// internal state file or duplicated use of the same backend for multiple
|
|
// internal states.
|
|
Run *TestRun
|
|
}
|
|
|
|
func loadTestFile(body hcl.Body, experimentsAllowed bool) (*TestFile, hcl.Diagnostics) {
|
|
var diags hcl.Diagnostics
|
|
tf := &TestFile{
|
|
VariableDefinitions: make(map[string]*Variable),
|
|
Providers: make(map[string]*Provider),
|
|
BackendConfigs: make(map[string]RunBlockBackend),
|
|
Overrides: addrs.MakeMap[addrs.Targetable, *Override](),
|
|
}
|
|
|
|
// we need to retrieve the file config block first, because the run blocks
|
|
// may depend on some of its settings.
|
|
configContent, remain, contentDiags := body.PartialContent(&hcl.BodySchema{
|
|
Blocks: []hcl.BlockHeaderSchema{{Type: "test"}},
|
|
})
|
|
diags = append(diags, contentDiags...)
|
|
|
|
var cDiags hcl.Diagnostics
|
|
tf.Config, cDiags = decodeFileConfigBlock(configContent, experimentsAllowed)
|
|
diags = append(diags, cDiags...)
|
|
if diags.HasErrors() {
|
|
return nil, diags
|
|
}
|
|
|
|
content, contentDiags := remain.Content(testFileSchema)
|
|
diags = append(diags, contentDiags...)
|
|
|
|
runBlockNames := make(map[string]hcl.Range)
|
|
skipCleanups := make(map[string]string)
|
|
|
|
for _, block := range content.Blocks {
|
|
switch block.Type {
|
|
case "run":
|
|
nextRunIndex := len(tf.Runs)
|
|
|
|
run, runDiags := decodeTestRunBlock(block, tf, experimentsAllowed)
|
|
diags = append(diags, runDiags...)
|
|
if !runDiags.HasErrors() {
|
|
tf.Runs = append(tf.Runs, run)
|
|
}
|
|
|
|
if rng, exists := runBlockNames[run.Name]; exists {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Duplicate \"run\" block names",
|
|
Detail: fmt.Sprintf("This test file already has a run named %s block defined at %s.", run.Name, rng),
|
|
Subject: run.NameDeclRange.Ptr(),
|
|
})
|
|
} else {
|
|
runBlockNames[run.Name] = run.DeclRange
|
|
}
|
|
|
|
if run.SkipCleanup && run.SkipCleanupRange != nil {
|
|
if backend, found := tf.BackendConfigs[run.StateKey]; found {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagWarning,
|
|
Summary: "Duplicate \"skip_cleanup\" block",
|
|
Detail: fmt.Sprintf("The run %q has a skip_cleanup attribute set, but shares state with an earlier run %q that has a backend defined. The later run takes precedence, but the backend will still be used to manage this state.", run.Name, backend.Run.Name),
|
|
Subject: run.SkipCleanupRange,
|
|
})
|
|
} else {
|
|
if _, found := skipCleanups[run.StateKey]; found {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagWarning,
|
|
Summary: "Duplicate \"skip_cleanup\" block",
|
|
Detail: fmt.Sprintf("The run %q has a skip_cleanup attribute set, but shares state with an earlier run %q that also has skip_cleanup set. The later run takes precedence, and this attribute is ignored for the earlier run.", run.Name, skipCleanups[run.StateKey]),
|
|
Subject: run.SkipCleanupRange,
|
|
})
|
|
}
|
|
skipCleanups[run.StateKey] = run.Name
|
|
}
|
|
}
|
|
|
|
if run.Backend != nil {
|
|
if existing, exists := tf.BackendConfigs[run.StateKey]; exists {
|
|
// then we definitely have two run blocks with the same
|
|
// state key trying to load backends
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Duplicate backend blocks",
|
|
Detail: fmt.Sprintf("The run %q already uses an internal state file that's loaded by a backend in the run %q. Please ensure that a backend block is only in the first apply run block for a given internal state file.", run.Name, existing.Run.Name),
|
|
Subject: run.Backend.DeclRange.Ptr(),
|
|
})
|
|
continue
|
|
} else {
|
|
// Record the backend block in the test file, under the related state key
|
|
tf.BackendConfigs[run.StateKey] = RunBlockBackend{
|
|
Backend: run.Backend,
|
|
Run: run,
|
|
}
|
|
}
|
|
|
|
for ix := range nextRunIndex {
|
|
previousRun := tf.Runs[ix]
|
|
|
|
if previousRun.StateKey != run.StateKey {
|
|
continue
|
|
}
|
|
|
|
if previousRun.Command == ApplyTestCommand {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid backend block",
|
|
Detail: fmt.Sprintf("The run %q cannot load in state using a backend block, because internal state has already been created by an apply command in run %q. Backend blocks can only be present in the first apply command for a given internal state.", run.Name, previousRun.Name),
|
|
Subject: run.Backend.DeclRange.Ptr(),
|
|
})
|
|
break
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
case "variable":
|
|
variable, variableDiags := decodeVariableBlock(block, false)
|
|
diags = append(diags, variableDiags...)
|
|
if !variableDiags.HasErrors() {
|
|
if existing, exists := tf.VariableDefinitions[variable.Name]; exists {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Duplicate \"variable\" block names",
|
|
Detail: fmt.Sprintf("This test file already has a variable named %s defined at %s.", variable.Name, existing.DeclRange),
|
|
Subject: variable.DeclRange.Ptr(),
|
|
})
|
|
continue
|
|
}
|
|
tf.VariableDefinitions[variable.Name] = variable
|
|
|
|
if existing, exists := tf.Variables[variable.Name]; exists {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Duplicate \"variable\" block names",
|
|
Detail: fmt.Sprintf("This test file already has a variable named %s defined at %s.", variable.Name, existing.Range()),
|
|
Subject: variable.DeclRange.Ptr(),
|
|
})
|
|
}
|
|
}
|
|
|
|
case "variables":
|
|
if tf.Variables != nil {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Multiple \"variables\" blocks",
|
|
Detail: fmt.Sprintf("This test file already has a variables block defined at %s.", tf.VariablesDeclRange),
|
|
Subject: block.DefRange.Ptr(),
|
|
})
|
|
continue
|
|
}
|
|
|
|
tf.Variables = make(map[string]hcl.Expression)
|
|
tf.VariablesDeclRange = block.DefRange
|
|
|
|
vars, varsDiags := block.Body.JustAttributes()
|
|
diags = append(diags, varsDiags...)
|
|
for _, v := range vars {
|
|
tf.Variables[v.Name] = v.Expr
|
|
|
|
if existing, exists := tf.VariableDefinitions[v.Name]; exists {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Duplicate \"variable\" block names",
|
|
Detail: fmt.Sprintf("This test file already has a variable named %s defined at %s.", v.Name, v.Range),
|
|
Subject: existing.DeclRange.Ptr(),
|
|
})
|
|
}
|
|
}
|
|
case "provider":
|
|
provider, providerDiags := decodeProviderBlock(block, true)
|
|
diags = append(diags, providerDiags...)
|
|
if provider != nil {
|
|
key := provider.moduleUniqueKey()
|
|
if previous, exists := tf.Providers[key]; exists {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Duplicate provider block",
|
|
Detail: fmt.Sprintf("A provider for %s is already defined at %s.", key, previous.NameRange),
|
|
Subject: provider.DeclRange.Ptr(),
|
|
})
|
|
continue
|
|
}
|
|
tf.Providers[key] = provider
|
|
}
|
|
case "mock_provider":
|
|
provider, providerDiags := decodeMockProviderBlock(block)
|
|
diags = append(diags, providerDiags...)
|
|
if provider != nil {
|
|
key := provider.moduleUniqueKey()
|
|
if previous, exists := tf.Providers[key]; exists {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Duplicate provider block",
|
|
Detail: fmt.Sprintf("A provider for %s is already defined at %s.", key, previous.NameRange),
|
|
Subject: provider.DeclRange.Ptr(),
|
|
})
|
|
continue
|
|
}
|
|
tf.Providers[key] = provider
|
|
}
|
|
case "override_resource":
|
|
override, overrideDiags := decodeOverrideResourceBlock(block, false, TestFileOverrideSource)
|
|
diags = append(diags, overrideDiags...)
|
|
|
|
if override != nil && override.Target != nil {
|
|
subject := override.Target.Subject
|
|
if previous, ok := tf.Overrides.GetOk(subject); ok {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Duplicate override_resource block",
|
|
Detail: fmt.Sprintf("An override_resource block targeting %s has already been defined at %s.", subject, previous.Range),
|
|
Subject: override.Range.Ptr(),
|
|
})
|
|
continue
|
|
}
|
|
tf.Overrides.Put(subject, override)
|
|
}
|
|
case "override_data":
|
|
override, overrideDiags := decodeOverrideDataBlock(block, false, TestFileOverrideSource)
|
|
diags = append(diags, overrideDiags...)
|
|
|
|
if override != nil && override.Target != nil {
|
|
subject := override.Target.Subject
|
|
if previous, ok := tf.Overrides.GetOk(subject); ok {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Duplicate override_data block",
|
|
Detail: fmt.Sprintf("An override_data block targeting %s has already been defined at %s.", subject, previous.Range),
|
|
Subject: override.Range.Ptr(),
|
|
})
|
|
continue
|
|
}
|
|
tf.Overrides.Put(subject, override)
|
|
}
|
|
case "override_module":
|
|
override, overrideDiags := decodeOverrideModuleBlock(block, false, TestFileOverrideSource)
|
|
diags = append(diags, overrideDiags...)
|
|
|
|
if override != nil && override.Target != nil {
|
|
subject := override.Target.Subject
|
|
if previous, ok := tf.Overrides.GetOk(subject); ok {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Duplicate override_module block",
|
|
Detail: fmt.Sprintf("An override_module block targeting %s has already been defined at %s.", subject, previous.Range),
|
|
Subject: override.Range.Ptr(),
|
|
})
|
|
continue
|
|
}
|
|
tf.Overrides.Put(subject, override)
|
|
}
|
|
}
|
|
}
|
|
|
|
return tf, diags
|
|
}
|
|
|
|
func decodeFileConfigBlock(fileContent *hcl.BodyContent, experimentsAllowed bool) (*TestFileConfig, hcl.Diagnostics) {
|
|
var diags hcl.Diagnostics
|
|
|
|
// The "test" block is optional, so we just return a nil config if it doesn't exist.
|
|
if len(fileContent.Blocks) == 0 {
|
|
return nil, diags
|
|
}
|
|
|
|
block := fileContent.Blocks[0]
|
|
for _, other := range fileContent.Blocks[1:] {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Multiple \"test\" blocks",
|
|
Detail: fmt.Sprintf(`This test file already has a "test" block defined at %s.`, block.DefRange),
|
|
Subject: other.DefRange.Ptr(),
|
|
})
|
|
}
|
|
if diags.HasErrors() {
|
|
return nil, diags
|
|
}
|
|
|
|
ret := &TestFileConfig{DeclRange: block.DefRange}
|
|
|
|
content, contentDiags := block.Body.Content(testFileConfigBlockSchema)
|
|
diags = append(diags, contentDiags...)
|
|
if content == nil {
|
|
return ret, diags
|
|
}
|
|
|
|
if attr, exists := content.Attributes["parallel"]; exists {
|
|
rawDiags := gohcl.DecodeExpression(attr.Expr, nil, &ret.Parallel)
|
|
diags = append(diags, rawDiags...)
|
|
}
|
|
|
|
if attr, exists := content.Attributes["skip_cleanup"]; exists {
|
|
if !experimentsAllowed {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid attribute",
|
|
Detail: "The skip_cleanup attribute is only available in experimental builds of Terraform.",
|
|
Subject: attr.NameRange.Ptr(),
|
|
})
|
|
}
|
|
|
|
rawDiags := gohcl.DecodeExpression(attr.Expr, nil, &ret.SkipCleanup)
|
|
diags = append(diags, rawDiags...)
|
|
}
|
|
|
|
return ret, diags
|
|
}
|
|
|
|
func decodeTestRunBlock(block *hcl.Block, file *TestFile, experimentsAllowed bool) (*TestRun, hcl.Diagnostics) {
|
|
var diags hcl.Diagnostics
|
|
|
|
content, contentDiags := block.Body.Content(testRunBlockSchema)
|
|
diags = append(diags, contentDiags...)
|
|
|
|
r := TestRun{
|
|
Overrides: addrs.MakeMap[addrs.Targetable, *Override](),
|
|
File: file,
|
|
Name: block.Labels[0],
|
|
NameDeclRange: block.LabelRanges[0],
|
|
DeclRange: block.DefRange,
|
|
Parallel: file.Config != nil && file.Config.Parallel,
|
|
SkipCleanup: file.Config != nil && file.Config.SkipCleanup,
|
|
}
|
|
|
|
if !hclsyntax.ValidIdentifier(r.Name) {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid run block name",
|
|
Detail: badIdentifierDetail,
|
|
Subject: r.NameDeclRange.Ptr(),
|
|
})
|
|
}
|
|
|
|
var backendRange *hcl.Range // Stored for validation once all blocks/attrs processed
|
|
for _, block := range content.Blocks {
|
|
switch block.Type {
|
|
case "assert":
|
|
cr, crDiags := decodeCheckRuleBlock(block, false)
|
|
diags = append(diags, crDiags...)
|
|
if !crDiags.HasErrors() {
|
|
r.CheckRules = append(r.CheckRules, cr)
|
|
}
|
|
case "plan_options":
|
|
if r.Options != nil {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Multiple \"plan_options\" blocks",
|
|
Detail: fmt.Sprintf("This run block already has a plan_options block defined at %s.", r.Options.DeclRange),
|
|
Subject: block.DefRange.Ptr(),
|
|
})
|
|
continue
|
|
}
|
|
|
|
opts, optsDiags := decodeTestRunOptionsBlock(block)
|
|
diags = append(diags, optsDiags...)
|
|
if !optsDiags.HasErrors() {
|
|
r.Options = opts
|
|
}
|
|
case "variables":
|
|
if r.Variables != nil {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Multiple \"variables\" blocks",
|
|
Detail: fmt.Sprintf("This run block already has a variables block defined at %s.", r.VariablesDeclRange),
|
|
Subject: block.DefRange.Ptr(),
|
|
})
|
|
continue
|
|
}
|
|
|
|
r.Variables = make(map[string]hcl.Expression)
|
|
r.VariablesDeclRange = block.DefRange
|
|
|
|
vars, varsDiags := block.Body.JustAttributes()
|
|
diags = append(diags, varsDiags...)
|
|
for _, v := range vars {
|
|
r.Variables[v.Name] = v.Expr
|
|
}
|
|
case "module":
|
|
if r.Module != nil {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Multiple \"module\" blocks",
|
|
Detail: fmt.Sprintf("This run block already has a module block defined at %s.", r.Module.DeclRange),
|
|
Subject: block.DefRange.Ptr(),
|
|
})
|
|
}
|
|
|
|
module, moduleDiags := decodeTestRunModuleBlock(block)
|
|
diags = append(diags, moduleDiags...)
|
|
if !moduleDiags.HasErrors() {
|
|
r.Module = module
|
|
}
|
|
case "override_resource":
|
|
override, overrideDiags := decodeOverrideResourceBlock(block, false, RunBlockOverrideSource)
|
|
diags = append(diags, overrideDiags...)
|
|
|
|
if override != nil && override.Target != nil {
|
|
subject := override.Target.Subject
|
|
if previous, ok := r.Overrides.GetOk(subject); ok {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Duplicate override_resource block",
|
|
Detail: fmt.Sprintf("An override_resource block targeting %s has already been defined at %s.", subject, previous.Range),
|
|
Subject: override.Range.Ptr(),
|
|
})
|
|
continue
|
|
}
|
|
r.Overrides.Put(subject, override)
|
|
}
|
|
case "override_data":
|
|
override, overrideDiags := decodeOverrideDataBlock(block, false, RunBlockOverrideSource)
|
|
diags = append(diags, overrideDiags...)
|
|
|
|
if override != nil && override.Target != nil {
|
|
subject := override.Target.Subject
|
|
if previous, ok := r.Overrides.GetOk(subject); ok {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Duplicate override_data block",
|
|
Detail: fmt.Sprintf("An override_data block targeting %s has already been defined at %s.", subject, previous.Range),
|
|
Subject: override.Range.Ptr(),
|
|
})
|
|
continue
|
|
}
|
|
r.Overrides.Put(subject, override)
|
|
}
|
|
case "override_module":
|
|
override, overrideDiags := decodeOverrideModuleBlock(block, false, RunBlockOverrideSource)
|
|
diags = append(diags, overrideDiags...)
|
|
|
|
if override != nil && override.Target != nil {
|
|
subject := override.Target.Subject
|
|
if previous, ok := r.Overrides.GetOk(subject); ok {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Duplicate override_module block",
|
|
Detail: fmt.Sprintf("An override_module block targeting %s has already been defined at %s.", subject, previous.Range),
|
|
Subject: override.Range.Ptr(),
|
|
})
|
|
continue
|
|
}
|
|
r.Overrides.Put(subject, override)
|
|
}
|
|
case "backend":
|
|
if !experimentsAllowed {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid block",
|
|
Detail: "The backend block is only available within run blocks in experimental builds of Terraform.",
|
|
Subject: block.DefRange.Ptr(),
|
|
})
|
|
}
|
|
|
|
backend, backedDiags := decodeBackendBlock(block)
|
|
diags = append(diags, backedDiags...)
|
|
|
|
if backend.Type == "remote" {
|
|
// Enhanced backends are not in use
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid backend block",
|
|
Detail: fmt.Sprintf("The \"remote\" backend type cannot be used in the backend block in run %q at %s. Only state storage backends can be used in a test run.", r.Name, block.DefRange),
|
|
Subject: block.DefRange.Ptr(),
|
|
})
|
|
continue
|
|
}
|
|
|
|
if r.Backend != nil {
|
|
// We've already encountered a backend for this run block
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Duplicate backend blocks",
|
|
Detail: fmt.Sprintf("A backend block has already been defined inside the run %q at %s.", r.Name, backendRange),
|
|
Subject: block.DefRange.Ptr(),
|
|
})
|
|
continue
|
|
}
|
|
|
|
r.Backend = backend
|
|
backendRange = &block.DefRange
|
|
// Using a backend implies skipping cleanup for that run
|
|
r.SkipCleanup = true
|
|
}
|
|
}
|
|
|
|
if r.Variables == nil {
|
|
// There is no distinction between a nil map of variables or an empty
|
|
// map, but we can avoid any potential nil pointer exceptions by just
|
|
// creating an empty map.
|
|
r.Variables = make(map[string]hcl.Expression)
|
|
}
|
|
|
|
if r.Options == nil {
|
|
// Create an options with default values if the user didn't specify
|
|
// anything.
|
|
r.Options = &TestRunOptions{
|
|
Mode: NormalTestMode,
|
|
Refresh: true,
|
|
}
|
|
}
|
|
|
|
if attr, exists := content.Attributes["command"]; exists {
|
|
switch hcl.ExprAsKeyword(attr.Expr) {
|
|
case "apply":
|
|
r.Command = ApplyTestCommand
|
|
case "plan":
|
|
r.Command = PlanTestCommand
|
|
default:
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid \"command\" keyword",
|
|
Detail: "The \"command\" argument requires one of the following keywords without quotes: apply or plan.",
|
|
Subject: attr.Expr.Range().Ptr(),
|
|
})
|
|
}
|
|
} else {
|
|
r.Command = ApplyTestCommand // Default to apply
|
|
}
|
|
|
|
if attr, exists := content.Attributes["providers"]; exists {
|
|
providers, providerDiags := decodePassedProviderConfigs(attr)
|
|
diags = append(diags, providerDiags...)
|
|
r.Providers = append(r.Providers, providers...)
|
|
}
|
|
|
|
if attr, exists := content.Attributes["expect_failures"]; exists {
|
|
failures, failDiags := DecodeDependsOn(attr)
|
|
diags = append(diags, failDiags...)
|
|
r.ExpectFailures = failures
|
|
}
|
|
|
|
if attr, exists := content.Attributes["state_key"]; exists {
|
|
rawDiags := gohcl.DecodeExpression(attr.Expr, nil, &r.StateKey)
|
|
diags = append(diags, rawDiags...)
|
|
} else if r.Module != nil {
|
|
r.StateKey = r.Module.Source.String()
|
|
} else {
|
|
r.StateKey = TestMainStateIdentifier // redundant, but let's be explicit
|
|
}
|
|
|
|
if attr, exists := content.Attributes["parallel"]; exists {
|
|
rawDiags := gohcl.DecodeExpression(attr.Expr, nil, &r.Parallel)
|
|
diags = append(diags, rawDiags...)
|
|
}
|
|
|
|
if r.Command != ApplyTestCommand && r.Backend != nil {
|
|
// Backend blocks must be used in the first _apply_ run block for a given internal state file.
|
|
// So, they cannot be present in a plan run block
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid backend block",
|
|
Detail: "A backend block can only be used in the first apply run block for a given internal state file. It cannot be included in a block to run a plan command.",
|
|
Subject: backendRange.Ptr(),
|
|
})
|
|
}
|
|
|
|
if attr, exists := content.Attributes["skip_cleanup"]; exists {
|
|
if !experimentsAllowed {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid attribute",
|
|
Detail: "The skip_cleanup attribute is only available in experimental builds of Terraform.",
|
|
Subject: attr.NameRange.Ptr(),
|
|
})
|
|
}
|
|
|
|
rawDiags := gohcl.DecodeExpression(attr.Expr, nil, &r.SkipCleanup)
|
|
diags = append(diags, rawDiags...)
|
|
r.SkipCleanupRange = attr.NameRange.Ptr()
|
|
}
|
|
|
|
if r.SkipCleanupRange != nil && !r.SkipCleanup && r.Backend != nil {
|
|
// Stop user attempting to clean up long-lived resources
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Cannot use `skip_cleanup=false` in a run block that contains a backend block",
|
|
Detail: "Backend blocks are used in tests to allow reuse of long-lived resources. Due to this, cleanup behavior is implicitly skipped and backend blocks are incompatible with setting `skip_cleanup=false`",
|
|
Subject: r.SkipCleanupRange,
|
|
})
|
|
}
|
|
|
|
return &r, diags
|
|
}
|
|
|
|
func decodeTestRunModuleBlock(block *hcl.Block) (*TestRunModuleCall, hcl.Diagnostics) {
|
|
var diags hcl.Diagnostics
|
|
|
|
content, contentDiags := block.Body.Content(testRunModuleBlockSchema)
|
|
diags = append(diags, contentDiags...)
|
|
|
|
module := TestRunModuleCall{
|
|
DeclRange: block.DefRange,
|
|
}
|
|
|
|
haveVersionArg := false
|
|
if attr, exists := content.Attributes["version"]; exists {
|
|
var versionDiags hcl.Diagnostics
|
|
module.Version, versionDiags = decodeVersionConstraint(attr)
|
|
diags = append(diags, versionDiags...)
|
|
haveVersionArg = true
|
|
}
|
|
|
|
if attr, exists := content.Attributes["source"]; exists {
|
|
module.SourceDeclRange = attr.Range
|
|
|
|
var raw string
|
|
rawDiags := gohcl.DecodeExpression(attr.Expr, nil, &raw)
|
|
diags = append(diags, rawDiags...)
|
|
if !rawDiags.HasErrors() {
|
|
var err error
|
|
if haveVersionArg {
|
|
module.Source, err = moduleaddrs.ParseModuleSourceRegistry(raw)
|
|
} else {
|
|
module.Source, err = moduleaddrs.ParseModuleSource(raw)
|
|
}
|
|
if err != nil {
|
|
// NOTE: We leave mc.SourceAddr as nil for any situation where the
|
|
// source attribute is invalid, so any code which tries to carefully
|
|
// use the partial result of a failed config decode must be
|
|
// resilient to that.
|
|
module.Source = nil
|
|
|
|
// NOTE: In practice it's actually very unlikely to end up here,
|
|
// because our source address parser can turn just about any string
|
|
// into some sort of remote package address, and so for most errors
|
|
// we'll detect them only during module installation. There are
|
|
// still a _few_ purely-syntax errors we can catch at parsing time,
|
|
// though, mostly related to remote package sub-paths and local
|
|
// paths.
|
|
switch err := err.(type) {
|
|
case *moduleaddrs.MaybeRelativePathErr:
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid module source address",
|
|
Detail: fmt.Sprintf(
|
|
"Terraform 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.",
|
|
err.Addr, err.Addr,
|
|
),
|
|
Subject: module.SourceDeclRange.Ptr(),
|
|
})
|
|
default:
|
|
if haveVersionArg {
|
|
// In this case we'll include some extra context that
|
|
// we assumed a registry source address due to the
|
|
// version argument.
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid registry module source address",
|
|
Detail: fmt.Sprintf("Failed to parse module registry address: %s.\n\nTerraform assumed that you intended a module registry source address because you also set the argument \"version\", which applies only to registry modules.", err),
|
|
Subject: module.SourceDeclRange.Ptr(),
|
|
})
|
|
} else {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid module source address",
|
|
Detail: fmt.Sprintf("Failed to parse module source address: %s.", err),
|
|
Subject: module.SourceDeclRange.Ptr(),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
switch module.Source.(type) {
|
|
case addrs.ModuleSourceRemote:
|
|
// We only support local or registry modules when loading
|
|
// modules directly from alternate sources during a test
|
|
// execution.
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid module source address",
|
|
Detail: "Only local or registry module sources are currently supported from within test run blocks.",
|
|
Subject: module.SourceDeclRange.Ptr(),
|
|
})
|
|
}
|
|
}
|
|
} else {
|
|
// Must have a source attribute.
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Missing \"source\" attribute for module block",
|
|
Detail: "You must specify a source attribute when executing alternate modules during test executions.",
|
|
Subject: module.DeclRange.Ptr(),
|
|
})
|
|
}
|
|
|
|
return &module, diags
|
|
}
|
|
|
|
func decodeTestRunOptionsBlock(block *hcl.Block) (*TestRunOptions, hcl.Diagnostics) {
|
|
var diags hcl.Diagnostics
|
|
|
|
content, contentDiags := block.Body.Content(testRunOptionsBlockSchema)
|
|
diags = append(diags, contentDiags...)
|
|
|
|
opts := TestRunOptions{
|
|
DeclRange: block.DefRange,
|
|
}
|
|
|
|
if attr, exists := content.Attributes["mode"]; exists {
|
|
switch hcl.ExprAsKeyword(attr.Expr) {
|
|
case "refresh-only":
|
|
opts.Mode = RefreshOnlyTestMode
|
|
case "normal":
|
|
opts.Mode = NormalTestMode
|
|
default:
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid \"mode\" keyword",
|
|
Detail: "The \"mode\" argument requires one of the following keywords without quotes: normal or refresh-only",
|
|
Subject: attr.Expr.Range().Ptr(),
|
|
})
|
|
}
|
|
} else {
|
|
opts.Mode = NormalTestMode // Default to normal
|
|
}
|
|
|
|
if attr, exists := content.Attributes["refresh"]; exists {
|
|
diags = append(diags, gohcl.DecodeExpression(attr.Expr, nil, &opts.Refresh)...)
|
|
} else {
|
|
// Defaults to true.
|
|
opts.Refresh = true
|
|
}
|
|
|
|
if attr, exists := content.Attributes["replace"]; exists {
|
|
reps, repsDiags := DecodeDependsOn(attr)
|
|
diags = append(diags, repsDiags...)
|
|
opts.Replace = reps
|
|
}
|
|
|
|
if attr, exists := content.Attributes["target"]; exists {
|
|
tars, tarsDiags := DecodeDependsOn(attr)
|
|
diags = append(diags, tarsDiags...)
|
|
opts.Target = tars
|
|
}
|
|
|
|
if !opts.Refresh && opts.Mode == RefreshOnlyTestMode {
|
|
// These options are incompatible.
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Incompatible plan options",
|
|
Detail: "The \"refresh\" option cannot be set to false when running a test in \"refresh-only\" mode.",
|
|
Subject: content.Attributes["refresh"].Range.Ptr(),
|
|
})
|
|
}
|
|
|
|
return &opts, diags
|
|
}
|
|
|
|
var testFileSchema = &hcl.BodySchema{
|
|
Blocks: []hcl.BlockHeaderSchema{
|
|
{
|
|
Type: "run",
|
|
LabelNames: []string{"name"},
|
|
},
|
|
{
|
|
Type: "provider",
|
|
LabelNames: []string{"name"},
|
|
},
|
|
{
|
|
Type: "mock_provider",
|
|
LabelNames: []string{"name"},
|
|
},
|
|
{
|
|
Type: "variable",
|
|
LabelNames: []string{"name"},
|
|
},
|
|
{
|
|
Type: "variables",
|
|
},
|
|
{
|
|
Type: "override_resource",
|
|
},
|
|
{
|
|
Type: "override_data",
|
|
},
|
|
{
|
|
Type: "override_module",
|
|
},
|
|
},
|
|
}
|
|
|
|
var testFileConfigBlockSchema = &hcl.BodySchema{
|
|
Attributes: []hcl.AttributeSchema{
|
|
{Name: "parallel"},
|
|
{Name: "skip_cleanup"},
|
|
},
|
|
}
|
|
|
|
var testRunBlockSchema = &hcl.BodySchema{
|
|
Attributes: []hcl.AttributeSchema{
|
|
{Name: "command"},
|
|
{Name: "providers"},
|
|
{Name: "expect_failures"},
|
|
{Name: "state_key"},
|
|
{Name: "parallel"},
|
|
{Name: "skip_cleanup"},
|
|
},
|
|
Blocks: []hcl.BlockHeaderSchema{
|
|
{
|
|
Type: "plan_options",
|
|
},
|
|
{
|
|
Type: "assert",
|
|
},
|
|
{
|
|
Type: "variables",
|
|
},
|
|
{
|
|
Type: "module",
|
|
},
|
|
{
|
|
Type: "override_resource",
|
|
},
|
|
{
|
|
Type: "override_data",
|
|
},
|
|
{
|
|
Type: "override_module",
|
|
},
|
|
{
|
|
Type: "backend",
|
|
LabelNames: []string{"name"},
|
|
},
|
|
},
|
|
}
|
|
|
|
var testRunOptionsBlockSchema = &hcl.BodySchema{
|
|
Attributes: []hcl.AttributeSchema{
|
|
{Name: "mode"},
|
|
{Name: "refresh"},
|
|
{Name: "replace"},
|
|
{Name: "target"},
|
|
},
|
|
}
|
|
|
|
var testRunModuleBlockSchema = &hcl.BodySchema{
|
|
Attributes: []hcl.AttributeSchema{
|
|
{Name: "source"},
|
|
{Name: "version"},
|
|
},
|
|
}
|