[testing framework] allow tests to define and override providers (#33466)

* [testing framework] prepare for beta phase of development

* [Testing Framework] Add module block to test run blocks

* [testing framework] allow tests to define and override providers
pull/33477/head^2
Liam Cervante 3 years ago committed by GitHub
parent 5acc95dda7
commit 4b34902fab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -232,7 +232,7 @@ func (c *InitCommand) Run(args []string) int {
// With all of the modules (hopefully) installed, we can now try to load the
// whole configuration tree.
config, confDiags := c.loadConfig(path)
config, confDiags := c.loadConfigWithTests(path, "tests")
// configDiags will be handled after the version constraint check, since an
// incorrect version of terraform may be producing errors for configuration
// constructs added in later versions.

@ -2741,6 +2741,49 @@ func TestInit_tests(t *testing.T) {
}
}
func TestInit_testsWithProvider(t *testing.T) {
// Create a temporary working directory that is empty
td := t.TempDir()
testCopyDir(t, testFixturePath("init-with-tests-with-provider"), td)
defer testChdir(t, td)()
provider := applyFixtureProvider() // We just want the types from this provider.
providerSource, close := newMockProviderSource(t, map[string][]string{
"hashicorp/test": {"1.0.0"},
})
defer close()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &InitCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(provider),
Ui: ui,
View: view,
ProviderSource: providerSource,
},
}
args := []string{}
if code := c.Run(args); code == 0 {
t.Fatalf("expected failure but got: \n%s", ui.OutputWriter.String())
}
got := ui.ErrorWriter.String()
want := `
Error: Failed to query available provider packages
Could not retrieve the list of available versions for provider
hashicorp/test: no available releases match the given constraints 1.0.1,
1.0.2
`
if diff := cmp.Diff(got, want); len(diff) > 0 {
t.Fatalf("wrong error message: \ngot:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff)
}
}
func TestInit_testsWithModule(t *testing.T) {
// Create a temporary working directory that is empty
td := t.TempDir()

@ -51,6 +51,23 @@ func (m *Meta) loadConfig(rootDir string) (*configs.Config, tfdiags.Diagnostics)
return config, diags
}
// loadConfigWithTests matches loadConfig, except it also loads any test files
// into the config alongside the main configuration.
func (m *Meta) loadConfigWithTests(rootDir, testDir string) (*configs.Config, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
rootDir = m.normalizePath(rootDir)
loader, err := m.initConfigLoader()
if err != nil {
diags = diags.Append(err)
return nil, diags
}
config, hclDiags := loader.LoadConfigWithTests(rootDir, testDir)
diags = diags.Append(hclDiags)
return config, diags
}
// loadSingleModule reads configuration from the given directory and returns
// a description of that module only, without attempting to assemble a module
// tree for referenced child modules.

@ -6,6 +6,7 @@ package command
import (
"fmt"
"path/filepath"
"strings"
"github.com/xlab/treeprint"
@ -69,7 +70,7 @@ func (c *ProvidersCommand) Run(args []string) int {
return 1
}
config, configDiags := c.loadConfig(configPath)
config, configDiags := c.loadConfigWithTests(configPath, "tests")
diags = diags.Append(configDiags)
if configDiags.HasErrors() {
c.showDiagnostics(diags)
@ -146,6 +147,24 @@ func (c *ProvidersCommand) populateTreeNode(tree treeprint.Tree, node *configs.M
}
tree.AddNode(fmt.Sprintf("provider[%s]%s", fqn.String(), versionsStr))
}
for name, testNode := range node.Tests {
name = strings.TrimSuffix(name, ".tftest")
name = strings.ReplaceAll(name, "/", ".")
branch := tree.AddBranch(fmt.Sprintf("test.%s", name))
for fqn, dep := range testNode.Requirements {
versionsStr := getproviders.VersionConstraintsString(dep)
if versionsStr != "" {
versionsStr = " " + versionsStr
}
branch.AddNode(fmt.Sprintf("provider[%s]%s", fqn.String(), versionsStr))
}
for _, run := range testNode.Runs {
branch := branch.AddBranch(fmt.Sprintf("run.%s", run.Name))
c.populateTreeNode(branch, run)
}
}
for name, childNode := range node.Children {
branch := tree.AddBranch(fmt.Sprintf("module.%s", name))
c.populateTreeNode(branch, childNode)

@ -166,3 +166,39 @@ func TestProviders_state(t *testing.T) {
}
}
}
func TestProviders_tests(t *testing.T) {
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("err: %s", err)
}
if err := os.Chdir(testFixturePath("providers/tests")); err != nil {
t.Fatalf("err: %s", err)
}
defer os.Chdir(cwd)
ui := new(cli.MockUi)
c := &ProvidersCommand{
Meta: Meta{
Ui: ui,
},
}
args := []string{}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
wantOutput := []string{
"provider[registry.terraform.io/hashicorp/foo]",
"test.main",
"provider[registry.terraform.io/hashicorp/bar]",
}
output := ui.OutputWriter.String()
for _, want := range wantOutput {
if !strings.Contains(output, want) {
t.Errorf("output missing %s:\n%s", want, output)
}
}
}

@ -11,7 +11,6 @@ import (
"github.com/hashicorp/terraform/internal/command/arguments"
"github.com/hashicorp/terraform/internal/command/views"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/configload"
"github.com/hashicorp/terraform/internal/moduletest"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/states"
@ -21,8 +20,6 @@ import (
type TestCommand struct {
Meta
loader *configload.Loader
}
func (c *TestCommand) Help() string {
@ -60,15 +57,7 @@ func (c *TestCommand) Run(rawArgs []string) int {
view := views.NewTest(arguments.ViewHuman, c.View)
loader, err := c.initConfigLoader()
diags = diags.Append(err)
if err != nil {
view.Diagnostics(nil, nil, diags)
return 1
}
c.loader = loader
config, configDiags := loader.LoadConfigWithTests(".", "tests")
config, configDiags := c.loadConfigWithTests(".", "tests")
diags = diags.Append(configDiags)
if configDiags.HasErrors() {
view.Diagnostics(nil, nil, diags)
@ -161,13 +150,13 @@ func (c *TestCommand) ExecuteTestFile(ctx *terraform.Context, file *moduletest.F
if run.Config.ConfigUnderTest != nil {
// Then we want to execute a different module under a kind of
// sandbox.
state := c.ExecuteTestRun(ctx, run, states.NewState(), run.Config.ConfigUnderTest, file.Config.Variables)
state := c.ExecuteTestRun(ctx, run, file, states.NewState(), run.Config.ConfigUnderTest)
mgr.States = append(mgr.States, &TestModuleState{
State: state,
Run: run,
})
} else {
mgr.State = c.ExecuteTestRun(ctx, run, mgr.State, config, file.Config.Variables)
mgr.State = c.ExecuteTestRun(ctx, run, file, mgr.State, config)
}
file.Status = file.Status.Merge(run.Status)
}
@ -178,7 +167,23 @@ func (c *TestCommand) ExecuteTestFile(ctx *terraform.Context, file *moduletest.F
}
}
func (c *TestCommand) ExecuteTestRun(ctx *terraform.Context, run *moduletest.Run, state *states.State, config *configs.Config, globals map[string]hcl.Expression) *states.State {
func (c *TestCommand) ExecuteTestRun(ctx *terraform.Context, run *moduletest.Run, file *moduletest.File, state *states.State, config *configs.Config) *states.State {
// Since we don't want to modify the actual plan and apply operations for
// tests where possible, we insert provider blocks directly into the config
// under test for each test run.
//
// This function transforms the config under test by inserting relevant
// provider blocks. It returns a reset function which restores the config
// back to the original state.
cfgReset, cfgDiags := config.TransformForTest(run.Config, file.Config)
defer cfgReset()
run.Diagnostics = run.Diagnostics.Append(cfgDiags)
if cfgDiags.HasErrors() {
run.Status = moduletest.Error
return state
}
var targets []addrs.Targetable
for _, target := range run.Config.Options.Target {
addr, diags := addrs.ParseTarget(target)
@ -213,7 +218,7 @@ func (c *TestCommand) ExecuteTestRun(ctx *terraform.Context, run *moduletest.Run
replaces = append(replaces, addr)
}
variables, diags := c.GetInputValues(run.Config.Variables, globals, config)
variables, diags := c.GetInputValues(run.Config.Variables, file.Config.Variables, config)
run.Diagnostics = run.Diagnostics.Append(diags)
if diags.HasErrors() {
run.Status = moduletest.Error
@ -305,12 +310,34 @@ func (c *TestCommand) cleanupState(ctx *terraform.Context, view views.Test, run
return
}
var locals map[string]hcl.Expression
var locals, globals map[string]hcl.Expression
if run != nil {
locals = run.Config.Variables
}
if file != nil {
globals = file.Config.Variables
}
var cfgDiags tfdiags.Diagnostics
if run == nil {
cfgReset, diags := config.TransformForTest(nil, file.Config)
defer cfgReset()
cfgDiags = cfgDiags.Append(diags)
} else {
cfgReset, diags := config.TransformForTest(run.Config, file.Config)
defer cfgReset()
cfgDiags = cfgDiags.Append(diags)
}
if cfgDiags.HasErrors() {
// This shouldn't really trigger, as we will have applied this transform
// earlier and it will have worked so a problem now would be strange.
// To be safe, we'll handle it anyway.
view.DestroySummary(cfgDiags, run, file, state)
return
}
c.View.Diagnostics(cfgDiags)
variables, variableDiags := c.GetInputValues(locals, file.Config.Variables, config)
variables, variableDiags := c.GetInputValues(locals, globals, config)
if variableDiags.HasErrors() {
// This shouldn't really trigger, as we will have created something
// using these variables at an earlier stage so for them to have a

@ -138,10 +138,6 @@ func TestTest(t *testing.T) {
}
func TestTest_ProviderAlias(t *testing.T) {
// TODO(liamcervante): Enable this test once we have added support for
// provider aliasing and customisation into the testing framework.
t.Skip()
td := t.TempDir()
testCopyDir(t, testFixturePath(path.Join("test", "with_provider_alias")), td)
defer testChdir(t, td)()

@ -0,0 +1,12 @@
terraform {
required_providers {
test = {
source = "hashicorp/test"
version = "1.0.2"
}
}
}
resource "test_instance" "foo" {
ami = "bar"
}

@ -0,0 +1,12 @@
run "setup" {
module {
source = "./setup"
}
}
run "test" {
assert {
condition = test_instance.foo.ami == "bar"
error_message = "incorrect value"
}
}

@ -0,0 +1,12 @@
terraform {
required_providers {
test = {
source = "hashicorp/test"
version = "1.0.1"
}
}
}
resource "test_instance" "baz" {
ami = "baz"
}

@ -0,0 +1,3 @@
resource "bar_instance" "test" {
}

@ -10,6 +10,7 @@ import (
version "github.com/hashicorp/go-version"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/depsfile"
"github.com/hashicorp/terraform/internal/getproviders"
@ -93,6 +94,14 @@ type ModuleRequirements struct {
SourceDir string
Requirements getproviders.Requirements
Children map[string]*ModuleRequirements
Tests map[string]*TestFileModuleRequirements
}
// TestFileModuleRequirements maps the runs for a given test file to the module
// requirements for that run block.
type TestFileModuleRequirements struct {
Requirements getproviders.Requirements
Runs map[string]*ModuleRequirements
}
// NewEmptyConfig constructs a single-node configuration tree with an empty
@ -292,7 +301,7 @@ func (c *Config) VerifyDependencySelections(depLocks *depsfile.Locks) []error {
// may be incomplete.
func (c *Config) ProviderRequirements() (getproviders.Requirements, hcl.Diagnostics) {
reqs := make(getproviders.Requirements)
diags := c.addProviderRequirements(reqs, true)
diags := c.addProviderRequirements(reqs, true, true)
return reqs, diags
}
@ -304,7 +313,7 @@ func (c *Config) ProviderRequirements() (getproviders.Requirements, hcl.Diagnost
// may be incomplete.
func (c *Config) ProviderRequirementsShallow() (getproviders.Requirements, hcl.Diagnostics) {
reqs := make(getproviders.Requirements)
diags := c.addProviderRequirements(reqs, false)
diags := c.addProviderRequirements(reqs, false, true)
return reqs, diags
}
@ -317,7 +326,7 @@ func (c *Config) ProviderRequirementsShallow() (getproviders.Requirements, hcl.D
// may be incomplete.
func (c *Config) ProviderRequirementsByModule() (*ModuleRequirements, hcl.Diagnostics) {
reqs := make(getproviders.Requirements)
diags := c.addProviderRequirements(reqs, false)
diags := c.addProviderRequirements(reqs, false, false)
children := make(map[string]*ModuleRequirements)
for name, child := range c.Children {
@ -327,11 +336,37 @@ func (c *Config) ProviderRequirementsByModule() (*ModuleRequirements, hcl.Diagno
diags = append(diags, childDiags...)
}
tests := make(map[string]*TestFileModuleRequirements)
for name, test := range c.Module.Tests {
testReqs := &TestFileModuleRequirements{
Requirements: make(getproviders.Requirements),
Runs: make(map[string]*ModuleRequirements),
}
for _, provider := range test.Providers {
diags = append(diags, c.addProviderRequirementsFromProviderBlock(testReqs.Requirements, provider)...)
}
for _, run := range test.Runs {
if run.ConfigUnderTest == nil {
continue
}
runReqs, runDiags := run.ConfigUnderTest.ProviderRequirementsByModule()
runReqs.Name = run.Name
testReqs.Runs[run.Name] = runReqs
diags = append(diags, runDiags...)
}
tests[name] = testReqs
}
ret := &ModuleRequirements{
SourceAddr: c.SourceAddr,
SourceDir: c.Module.SourceDir,
Requirements: reqs,
Children: children,
Tests: tests,
}
return ret, diags
@ -341,7 +376,7 @@ func (c *Config) ProviderRequirementsByModule() (*ModuleRequirements, hcl.Diagno
// implementation, gradually mutating a shared requirements object to
// eventually return. If the recurse argument is true, the requirements will
// include all descendant modules; otherwise, only the specified module.
func (c *Config) addProviderRequirements(reqs getproviders.Requirements, recurse bool) hcl.Diagnostics {
func (c *Config) addProviderRequirements(reqs getproviders.Requirements, recurse, tests bool) hcl.Diagnostics {
var diags hcl.Diagnostics
// First we'll deal with the requirements directly in _our_ module...
@ -487,38 +522,34 @@ func (c *Config) addProviderRequirements(reqs getproviders.Requirements, recurse
// "provider" block can also contain version constraints
for _, provider := range c.Module.ProviderConfigs {
fqn := c.Module.ProviderForLocalConfig(addrs.LocalProviderConfig{LocalName: provider.Name})
if _, ok := reqs[fqn]; !ok {
// We'll at least have an unconstrained dependency then, but might
// add to this in the loop below.
reqs[fqn] = nil
}
if provider.Version.Required != nil {
// The model of version constraints in this package is still the
// old one using a different upstream module to represent versions,
// so we'll need to shim that out here for now. The two parsers
// don't exactly agree in practice 🙄 so this might produce new errors.
// TODO: Use the new parser throughout this package so we can get the
// better error messages it produces in more situations.
constraints, err := getproviders.ParseVersionConstraints(provider.Version.Required.String())
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid version constraint",
// The errors returned by ParseVersionConstraint already include
// the section of input that was incorrect, so we don't need to
// include that here.
Detail: fmt.Sprintf("Incorrect version constraint syntax: %s.", err.Error()),
Subject: provider.Version.DeclRange.Ptr(),
})
moreDiags := c.addProviderRequirementsFromProviderBlock(reqs, provider)
diags = append(diags, moreDiags...)
}
// We may have provider blocks and required_providers set in some testing
// files.
if tests {
for _, file := range c.Module.Tests {
for _, provider := range file.Providers {
moreDiags := c.addProviderRequirementsFromProviderBlock(reqs, provider)
diags = append(diags, moreDiags...)
}
if recurse {
// Then we'll also look for requirements in testing modules.
for _, run := range file.Runs {
if run.ConfigUnderTest != nil {
moreDiags := run.ConfigUnderTest.addProviderRequirements(reqs, true, false)
diags = append(diags, moreDiags...)
}
}
}
reqs[fqn] = append(reqs[fqn], constraints...)
}
}
if recurse {
for _, childConfig := range c.Children {
moreDiags := childConfig.addProviderRequirements(reqs, true)
moreDiags := childConfig.addProviderRequirements(reqs, true, false)
diags = append(diags, moreDiags...)
}
}
@ -526,6 +557,40 @@ func (c *Config) addProviderRequirements(reqs getproviders.Requirements, recurse
return diags
}
func (c *Config) addProviderRequirementsFromProviderBlock(reqs getproviders.Requirements, provider *Provider) hcl.Diagnostics {
var diags hcl.Diagnostics
fqn := c.Module.ProviderForLocalConfig(addrs.LocalProviderConfig{LocalName: provider.Name})
if _, ok := reqs[fqn]; !ok {
// We'll at least have an unconstrained dependency then, but might
// add to this in the loop below.
reqs[fqn] = nil
}
if provider.Version.Required != nil {
// The model of version constraints in this package is still the
// old one using a different upstream module to represent versions,
// so we'll need to shim that out here for now. The two parsers
// don't exactly agree in practice 🙄 so this might produce new errors.
// TODO: Use the new parser throughout this package so we can get the
// better error messages it produces in more situations.
constraints, err := getproviders.ParseVersionConstraints(provider.Version.Required.String())
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid version constraint",
// The errors returned by ParseVersionConstraint already include
// the section of input that was incorrect, so we don't need to
// include that here.
Detail: fmt.Sprintf("Incorrect version constraint syntax: %s.", err.Error()),
Subject: provider.Version.DeclRange.Ptr(),
})
}
reqs[fqn] = append(reqs[fqn], constraints...)
}
return diags
}
// resolveProviderTypes walks through the providers in the module and ensures
// the true types are assigned based on the provider requirements for the
// module.
@ -661,3 +726,94 @@ func (c *Config) CheckCoreVersionRequirements() hcl.Diagnostics {
return diags
}
// TransformForTest prepares the config to execute the given test.
//
// This function directly edits the config that is to be tested, and returns a
// function that will reset the config back to its original state.
//
// Tests will call this before they execute, and then call the deferred function
// to reset the config before the next test.
func (c *Config) TransformForTest(run *TestRun, file *TestFile) (func(), hcl.Diagnostics) {
var diags hcl.Diagnostics
// Currently, we only need to override the provider settings.
//
// We can have a set of providers defined within the config, we can also
// have a set of providers defined within the test file. Then the run can
// also specify a set of overrides that tell Terraform exactly which
// providers from the test file to apply into the config.
//
// The process here is as follows:
// 1. Take all the providers in the original config keyed by name.alias,
// we call this `previous`
// 2. Copy them all into a new map, we call this `next`.
// 3a. If the run has configuration specifying provider overrides, we copy
// only the specified providers from the test file into `next`. While
// doing this we ensure to preserve the name and alias from the
// original config.
// 3b. If the run has no override configuration, we copy all the providers
// from the test file into `next`, overriding all providers with name
// collisions from the original config.
// 4. We then modify the original configuration so that the providers it
// holds are the combination specified by the original config, the test
// file and the run file.
// 5. We then return a function that resets the original config back to
// its original state. This can be called by the surrounding test once
// completed so future run blocks can safely execute.
// First, initialise `previous` and `next`. `previous` contains a backup of
// the providers from the original config. `next` contains the set of
// providers that will be used by the test. `next` starts with the set of
// providers from the original config.
previous := c.Module.ProviderConfigs
next := make(map[string]*Provider)
for key, value := range previous {
next[key] = value
}
if run != nil && len(run.Providers) > 0 {
// Then we'll only copy over and overwrite the specific providers asked
// for by this run block.
for _, ref := range run.Providers {
testProvider, ok := 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 = append(diags, &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(),
})
continue
}
next[ref.InChild.String()] = &Provider{
Name: ref.InChild.Name,
NameRange: ref.InChild.NameRange,
Alias: ref.InChild.Alias,
AliasRange: ref.InChild.AliasRange,
Version: testProvider.Version,
Config: testProvider.Config,
DeclRange: testProvider.DeclRange,
}
}
} else {
// Otherwise, let's copy over and overwrite all providers specified by
// the test file itself.
for key, provider := range file.Providers {
next[key] = provider
}
}
c.Module.ProviderConfigs = next
return func() {
// Reset the original config within the returned function.
c.Module.ProviderConfigs = previous
}, diags
}

@ -4,17 +4,23 @@
package configs
import (
"bytes"
"fmt"
"os"
"strings"
"testing"
"github.com/go-test/deep"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclparse"
"github.com/zclconf/go-cty/cty"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/hcl/v2/hclsyntax"
svchost "github.com/hashicorp/terraform-svchost"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/depsfile"
"github.com/hashicorp/terraform/internal/getproviders"
@ -163,6 +169,42 @@ func TestConfigProviderRequirements(t *testing.T) {
}
}
func TestConfigProviderRequirementsInclTests(t *testing.T) {
cfg, diags := testNestedModuleConfigFromDirWithTests(t, "testdata/provider-reqs-with-tests")
// TODO: Version Constraint Deprecation.
// Once we've removed the version argument from provider configuration
// blocks, this can go back to expected 0 diagnostics.
// assertNoDiagnostics(t, diags)
assertDiagnosticCount(t, diags, 1)
assertDiagnosticSummary(t, diags, "Version constraints inside provider configuration blocks are deprecated")
tlsProvider := addrs.NewProvider(
addrs.DefaultProviderRegistryHost,
"hashicorp", "tls",
)
nullProvider := addrs.NewDefaultProvider("null")
randomProvider := addrs.NewDefaultProvider("random")
impliedProvider := addrs.NewDefaultProvider("implied")
terraformProvider := addrs.NewBuiltInProvider("terraform")
configuredProvider := addrs.NewDefaultProvider("configured")
got, diags := cfg.ProviderRequirements()
assertNoDiagnostics(t, diags)
want := getproviders.Requirements{
// the nullProvider constraints from the two modules are merged
nullProvider: getproviders.MustParseVersionConstraints("~> 2.0.0"),
randomProvider: getproviders.MustParseVersionConstraints("~> 1.2.0"),
tlsProvider: getproviders.MustParseVersionConstraints("~> 3.0"),
configuredProvider: getproviders.MustParseVersionConstraints("~> 1.4"),
impliedProvider: nil,
terraformProvider: nil,
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("wrong result\n%s", diff)
}
}
func TestConfigProviderRequirementsDuplicate(t *testing.T) {
_, diags := testNestedModuleConfigFromDir(t, "testdata/duplicate-local-name")
assertDiagnosticCount(t, diags, 3)
@ -205,6 +247,37 @@ func TestConfigProviderRequirementsShallow(t *testing.T) {
}
}
func TestConfigProviderRequirementsShallowInclTests(t *testing.T) {
cfg, diags := testNestedModuleConfigFromDirWithTests(t, "testdata/provider-reqs-with-tests")
// TODO: Version Constraint Deprecation.
// Once we've removed the version argument from provider configuration
// blocks, this can go back to expected 0 diagnostics.
// assertNoDiagnostics(t, diags)
assertDiagnosticCount(t, diags, 1)
assertDiagnosticSummary(t, diags, "Version constraints inside provider configuration blocks are deprecated")
tlsProvider := addrs.NewProvider(
addrs.DefaultProviderRegistryHost,
"hashicorp", "tls",
)
impliedProvider := addrs.NewDefaultProvider("implied")
terraformProvider := addrs.NewBuiltInProvider("terraform")
configuredProvider := addrs.NewDefaultProvider("configured")
got, diags := cfg.ProviderRequirementsShallow()
assertNoDiagnostics(t, diags)
want := getproviders.Requirements{
tlsProvider: getproviders.MustParseVersionConstraints("~> 3.0"),
configuredProvider: getproviders.MustParseVersionConstraints("~> 1.4"),
impliedProvider: nil,
terraformProvider: nil,
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("wrong result\n%s", diff)
}
}
func TestConfigProviderRequirementsByModule(t *testing.T) {
cfg, diags := testNestedModuleConfigFromDir(t, "testdata/provider-reqs")
// TODO: Version Constraint Deprecation.
@ -262,6 +335,69 @@ func TestConfigProviderRequirementsByModule(t *testing.T) {
grandchildProvider: nil,
},
Children: map[string]*ModuleRequirements{},
Tests: make(map[string]*TestFileModuleRequirements),
},
},
Tests: make(map[string]*TestFileModuleRequirements),
},
},
Tests: make(map[string]*TestFileModuleRequirements),
}
ignore := cmpopts.IgnoreUnexported(version.Constraint{}, cty.Value{}, hclsyntax.Body{})
if diff := cmp.Diff(want, got, ignore); diff != "" {
t.Errorf("wrong result\n%s", diff)
}
}
func TestConfigProviderRequirementsByModuleInclTests(t *testing.T) {
cfg, diags := testNestedModuleConfigFromDirWithTests(t, "testdata/provider-reqs-with-tests")
// TODO: Version Constraint Deprecation.
// Once we've removed the version argument from provider configuration
// blocks, this can go back to expected 0 diagnostics.
// assertNoDiagnostics(t, diags)
assertDiagnosticCount(t, diags, 1)
assertDiagnosticSummary(t, diags, "Version constraints inside provider configuration blocks are deprecated")
tlsProvider := addrs.NewProvider(
addrs.DefaultProviderRegistryHost,
"hashicorp", "tls",
)
nullProvider := addrs.NewDefaultProvider("null")
randomProvider := addrs.NewDefaultProvider("random")
impliedProvider := addrs.NewDefaultProvider("implied")
terraformProvider := addrs.NewBuiltInProvider("terraform")
configuredProvider := addrs.NewDefaultProvider("configured")
got, diags := cfg.ProviderRequirementsByModule()
assertNoDiagnostics(t, diags)
want := &ModuleRequirements{
Name: "",
SourceAddr: nil,
SourceDir: "testdata/provider-reqs-with-tests",
Requirements: getproviders.Requirements{
// Only the root module's version is present here
tlsProvider: getproviders.MustParseVersionConstraints("~> 3.0"),
impliedProvider: nil,
terraformProvider: nil,
},
Children: make(map[string]*ModuleRequirements),
Tests: map[string]*TestFileModuleRequirements{
"provider-reqs-root.tftest": {
Requirements: getproviders.Requirements{
configuredProvider: getproviders.MustParseVersionConstraints("~> 1.4"),
},
Runs: map[string]*ModuleRequirements{
"setup": {
Name: "setup",
SourceAddr: addrs.ModuleSourceLocal("./setup"),
SourceDir: "testdata/provider-reqs-with-tests/setup",
Requirements: getproviders.Requirements{
nullProvider: getproviders.MustParseVersionConstraints("~> 2.0.0"),
randomProvider: getproviders.MustParseVersionConstraints("~> 1.2.0"),
},
Children: make(map[string]*ModuleRequirements),
Tests: make(map[string]*TestFileModuleRequirements),
},
},
},
@ -421,7 +557,7 @@ func TestConfigAddProviderRequirements(t *testing.T) {
reqs := getproviders.Requirements{
addrs.NewDefaultProvider("null"): nil,
}
diags = cfg.addProviderRequirements(reqs, true)
diags = cfg.addProviderRequirements(reqs, true, false)
assertNoDiagnostics(t, diags)
}
@ -447,7 +583,7 @@ func TestConfigImportProviderClashesWithResources(t *testing.T) {
cfg, diags := testModuleConfigFromFile("testdata/invalid-import-files/import-and-resource-clash.tf")
assertNoDiagnostics(t, diags)
diags = cfg.addProviderRequirements(getproviders.Requirements{}, true)
diags = cfg.addProviderRequirements(getproviders.Requirements{}, true, false)
assertExactDiagnostics(t, diags, []string{
`testdata/invalid-import-files/import-and-resource-clash.tf:9,3-19: Invalid import provider argument; The provider argument can only be specified in import blocks that will generate configuration.
@ -459,10 +595,235 @@ func TestConfigImportProviderWithNoResourceProvider(t *testing.T) {
cfg, diags := testModuleConfigFromFile("testdata/invalid-import-files/import-and-no-resource.tf")
assertNoDiagnostics(t, diags)
diags = cfg.addProviderRequirements(getproviders.Requirements{}, true)
diags = cfg.addProviderRequirements(getproviders.Requirements{}, true, false)
assertExactDiagnostics(t, diags, []string{
`testdata/invalid-import-files/import-and-no-resource.tf:5,3-19: Invalid import provider argument; The provider argument can only be specified in import blocks that will generate configuration.
Use the provider argument in the target resource block to configure the provider for a resource with explicit provider configuration.`,
})
}
func TestTransformForTest(t *testing.T) {
str := func(providers map[string]string) string {
var buffer bytes.Buffer
for key, config := range providers {
buffer.WriteString(fmt.Sprintf("%s: %s\n", key, config))
}
return buffer.String()
}
convertToProviders := func(t *testing.T, contents map[string]string) map[string]*Provider {
t.Helper()
providers := make(map[string]*Provider)
for key, content := range contents {
parser := hclparse.NewParser()
file, diags := parser.ParseHCL([]byte(content), fmt.Sprintf("%s.hcl", key))
if diags.HasErrors() {
t.Fatal(diags.Error())
}
provider := &Provider{
Config: file.Body,
}
parts := strings.Split(key, ".")
provider.Name = parts[0]
if len(parts) > 1 {
provider.Alias = parts[1]
}
providers[key] = provider
}
return providers
}
validate := func(t *testing.T, msg string, expected map[string]string, actual map[string]*Provider) {
t.Helper()
converted := make(map[string]string)
for key, provider := range actual {
content, err := provider.Config.Content(&hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{Name: "source", Required: true},
},
})
if err != nil {
t.Fatal(err)
}
source, diags := content.Attributes["source"].Expr.Value(nil)
if diags.HasErrors() {
t.Fatal(diags.Error())
}
converted[key] = fmt.Sprintf("source = %q", source.AsString())
}
if diff := cmp.Diff(expected, converted); len(diff) > 0 {
t.Errorf("%s\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", msg, str(expected), str(converted), diff)
}
}
tcs := map[string]struct {
configProviders map[string]string
fileProviders map[string]string
runProviders []PassedProviderConfig
expectedProviders map[string]string
expectedErrors []string
}{
"empty": {
configProviders: make(map[string]string),
expectedProviders: make(map[string]string),
},
"only providers in config": {
configProviders: map[string]string{
"foo": "source = \"config\"",
"bar": "source = \"config\"",
},
expectedProviders: map[string]string{
"foo": "source = \"config\"",
"bar": "source = \"config\"",
},
},
"only providers in test file": {
configProviders: make(map[string]string),
fileProviders: map[string]string{
"foo": "source = \"testfile\"",
"bar": "source = \"testfile\"",
},
expectedProviders: map[string]string{
"foo": "source = \"testfile\"",
"bar": "source = \"testfile\"",
},
},
"only providers in run block": {
configProviders: make(map[string]string),
runProviders: []PassedProviderConfig{
{
InChild: &ProviderConfigRef{
Name: "foo",
},
InParent: &ProviderConfigRef{
Name: "bar",
},
},
},
expectedProviders: make(map[string]string),
expectedErrors: []string{
":0,0-0: Missing provider definition for bar; This provider block references a provider definition that does not exist.",
},
},
"subset of providers in test file": {
configProviders: make(map[string]string),
fileProviders: map[string]string{
"bar": "source = \"testfile\"",
},
runProviders: []PassedProviderConfig{
{
InChild: &ProviderConfigRef{
Name: "foo",
},
InParent: &ProviderConfigRef{
Name: "bar",
},
},
},
expectedProviders: map[string]string{
"foo": "source = \"testfile\"",
},
},
"overrides providers in config": {
configProviders: map[string]string{
"foo": "source = \"config\"",
"bar": "source = \"config\"",
},
fileProviders: map[string]string{
"bar": "source = \"testfile\"",
},
expectedProviders: map[string]string{
"foo": "source = \"config\"",
"bar": "source = \"testfile\"",
},
},
"overrides subset of providers in config": {
configProviders: map[string]string{
"foo": "source = \"config\"",
"bar": "source = \"config\"",
},
fileProviders: map[string]string{
"foo": "source = \"testfile\"",
"bar": "source = \"testfile\"",
},
runProviders: []PassedProviderConfig{
{
InChild: &ProviderConfigRef{
Name: "bar",
},
InParent: &ProviderConfigRef{
Name: "bar",
},
},
},
expectedProviders: map[string]string{
"foo": "source = \"config\"",
"bar": "source = \"testfile\"",
},
},
"handles aliases": {
configProviders: map[string]string{
"foo.primary": "source = \"config\"",
"foo.secondary": "source = \"config\"",
},
fileProviders: map[string]string{
"foo": "source = \"testfile\"",
},
runProviders: []PassedProviderConfig{
{
InChild: &ProviderConfigRef{
Name: "foo.secondary",
},
InParent: &ProviderConfigRef{
Name: "foo",
},
},
},
expectedProviders: map[string]string{
"foo.primary": "source = \"config\"",
"foo.secondary": "source = \"testfile\"",
},
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
config := &Config{
Module: &Module{
ProviderConfigs: convertToProviders(t, tc.configProviders),
},
}
file := &TestFile{
Providers: convertToProviders(t, tc.fileProviders),
}
run := &TestRun{
Providers: tc.runProviders,
}
reset, diags := config.TransformForTest(run, file)
var actualErrs []string
for _, err := range diags.Errs() {
actualErrs = append(actualErrs, err.Error())
}
if diff := cmp.Diff(actualErrs, tc.expectedErrors, cmpopts.IgnoreUnexported()); len(diff) > 0 {
t.Errorf("unmatched errors\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", strings.Join(tc.expectedErrors, "\n"), strings.Join(actualErrs, "\n"), diff)
}
validate(t, "after transform mismatch", tc.expectedProviders, config.Module.ProviderConfigs)
reset()
validate(t, "after reset mismatch", tc.configProviders, config.Module.ProviderConfigs)
})
}
}

@ -9,6 +9,7 @@ import (
"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"
)
@ -157,36 +158,9 @@ func decodeModuleBlock(block *hcl.Block, override bool) (*ModuleCall, hcl.Diagno
}
if attr, exists := content.Attributes["providers"]; exists {
seen := make(map[string]hcl.Range)
pairs, pDiags := hcl.ExprMap(attr.Expr)
diags = append(diags, pDiags...)
for _, pair := range pairs {
key, keyDiags := decodeProviderConfigRef(pair.Key, "providers")
diags = append(diags, keyDiags...)
value, valueDiags := decodeProviderConfigRef(pair.Value, "providers")
diags = append(diags, valueDiags...)
if keyDiags.HasErrors() || valueDiags.HasErrors() {
continue
}
matchKey := key.String()
if prev, exists := seen[matchKey]; exists {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Duplicate provider address",
Detail: fmt.Sprintf("A provider configuration was already passed to %s at %s. Each child provider configuration can be assigned only once.", matchKey, prev),
Subject: pair.Value.Range().Ptr(),
})
continue
}
rng := hcl.RangeBetween(pair.Key.Range(), pair.Value.Range())
seen[matchKey] = rng
mc.Providers = append(mc.Providers, PassedProviderConfig{
InChild: key,
InParent: value,
})
}
providers, providerDiags := decodePassedProviderConfigs(attr)
diags = append(diags, providerDiags...)
mc.Providers = append(mc.Providers, providers...)
}
var seenEscapeBlock *hcl.Block
@ -246,6 +220,43 @@ type PassedProviderConfig struct {
InParent *ProviderConfigRef
}
func decodePassedProviderConfigs(attr *hcl.Attribute) ([]PassedProviderConfig, hcl.Diagnostics) {
var diags hcl.Diagnostics
var providers []PassedProviderConfig
seen := make(map[string]hcl.Range)
pairs, pDiags := hcl.ExprMap(attr.Expr)
diags = append(diags, pDiags...)
for _, pair := range pairs {
key, keyDiags := decodeProviderConfigRef(pair.Key, "providers")
diags = append(diags, keyDiags...)
value, valueDiags := decodeProviderConfigRef(pair.Value, "providers")
diags = append(diags, valueDiags...)
if keyDiags.HasErrors() || valueDiags.HasErrors() {
continue
}
matchKey := key.String()
if prev, exists := seen[matchKey]; exists {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Duplicate provider address",
Detail: fmt.Sprintf("A provider configuration was already passed to %s at %s. Each child provider configuration can be assigned only once.", matchKey, prev),
Subject: pair.Value.Range().Ptr(),
})
continue
}
rng := hcl.RangeBetween(pair.Key.Range(), pair.Value.Range())
seen[matchKey] = rng
providers = append(providers, PassedProviderConfig{
InChild: key,
InParent: value,
})
}
return providers, diags
}
var moduleBlockSchema = &hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{

@ -68,6 +68,23 @@ func testModuleConfigFromDir(path string) (*Config, hcl.Diagnostics) {
return cfg, append(diags, moreDiags...)
}
// testNestedModuleConfigFromDirWithTests matches testNestedModuleConfigFromDir
// except it also loads any test files within the directory.
func testNestedModuleConfigFromDirWithTests(t *testing.T, path string) (*Config, hcl.Diagnostics) {
t.Helper()
parser := NewParser(nil)
mod, diags := parser.LoadConfigDirWithTests(path, "tests")
if mod == nil {
t.Fatal("got nil root module; want non-nil")
}
cfg, nestedDiags := buildNestedModuleConfig(mod, path, parser)
diags = append(diags, nestedDiags...)
return cfg, diags
}
// testNestedModuleConfigFromDir reads configuration from the given directory path as
// a module with (optional) submodules and returns its configuration. This is a
// helper for use in unit tests.
@ -80,8 +97,15 @@ func testNestedModuleConfigFromDir(t *testing.T, path string) (*Config, hcl.Diag
t.Fatal("got nil root module; want non-nil")
}
cfg, nestedDiags := buildNestedModuleConfig(mod, path, parser)
diags = append(diags, nestedDiags...)
return cfg, diags
}
func buildNestedModuleConfig(mod *Module, path string, parser *Parser) (*Config, hcl.Diagnostics) {
versionI := 0
cfg, nestedDiags := BuildConfig(mod, ModuleWalkerFunc(
return BuildConfig(mod, ModuleWalkerFunc(
func(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics) {
// For the sake of this test we're going to just treat our
// SourceAddr as a path relative to the calling module.
@ -103,9 +127,6 @@ func testNestedModuleConfigFromDir(t *testing.T, path string) (*Config, hcl.Diag
return mod, version, diags
},
))
diags = append(diags, nestedDiags...)
return cfg, diags
}
func assertNoDiagnostics(t *testing.T, diags hcl.Diagnostics) bool {

@ -44,6 +44,13 @@ type TestFile struct {
// 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.
//
// If empty, tests should use the default providers for the module under
// test.
Providers map[string]*Provider
// Runs defines the sequential list of run blocks that should be executed in
// order.
Runs []*TestRun
@ -77,6 +84,14 @@ type TestRun struct {
// take precedence over the global definition.
Variables map[string]hcl.Expression
// 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
@ -144,7 +159,9 @@ func loadTestFile(body hcl.Body) (*TestFile, hcl.Diagnostics) {
content, contentDiags := body.Content(testFileSchema)
diags = append(diags, contentDiags...)
tf := TestFile{}
tf := TestFile{
Providers: make(map[string]*Provider),
}
for _, block := range content.Blocks {
switch block.Type {
@ -173,6 +190,12 @@ func loadTestFile(body hcl.Body) (*TestFile, hcl.Diagnostics) {
for _, v := range vars {
tf.Variables[v.Name] = v.Expr
}
case "provider":
provider, providerDiags := decodeProviderBlock(block)
diags = append(diags, providerDiags...)
if provider != nil {
tf.Providers[provider.moduleUniqueKey()] = provider
}
}
}
@ -285,6 +308,12 @@ func decodeTestRunBlock(block *hcl.Block) (*TestRun, hcl.Diagnostics) {
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...)
@ -464,6 +493,10 @@ var testFileSchema = &hcl.BodySchema{
Type: "run",
LabelNames: []string{"name"},
},
{
Type: "provider",
LabelNames: []string{"name"},
},
{
Type: "variables",
},
@ -473,6 +506,7 @@ var testFileSchema = &hcl.BodySchema{
var testRunBlockSchema = &hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{Name: "command"},
{Name: "providers"},
{Name: "expect_failures"},
},
Blocks: []hcl.BlockHeaderSchema{

@ -0,0 +1,20 @@
terraform {
required_providers {
tls = {
source = "hashicorp/tls"
version = "~> 3.0"
}
}
}
# There is no provider in required_providers called "implied", so this
# implicitly declares a dependency on "hashicorp/implied".
resource "implied_foo" "bar" {
}
# There is no provider in required_providers called "terraform", but for
# this name in particular we imply terraform.io/builtin/terraform instead,
# to avoid selecting the now-unmaintained
# registry.terraform.io/hashicorp/terraform.
data "terraform_remote_state" "bar" {
}

@ -0,0 +1,11 @@
# There is no provider in required_providers called "configured", so the version
# constraint should come from this configuration block.
provider "configured" {
version = "~> 1.4"
}
run "setup" {
module {
source = "./setup"
}
}

@ -0,0 +1,8 @@
terraform {
required_providers {
null = "~> 2.0.0"
random = {
version = "~> 1.2.0"
}
}
}
Loading…
Cancel
Save