add provider validation

Add validation which was removed from the configload package, along with
additional validation checks. The output is slightly different, as
instead of validating whether the modules are allowed to have provider
configurations, we validate the various combinations of provider
structures themselves.
This commit is contained in:
James Bardin 2021-02-10 11:09:35 -05:00
parent 7aaffac223
commit da252de1a0
28 changed files with 618 additions and 18 deletions

View File

@ -22,6 +22,9 @@ func BuildConfig(root *Module, walker ModuleWalker) (*Config, hcl.Diagnostics) {
}
cfg.Root = cfg // Root module is self-referential.
cfg.Children, diags = buildChildModules(cfg, walker)
diags = append(diags, validateProviderConfigs(nil, cfg, false)...)
return cfg, diags
}

View File

@ -2,6 +2,7 @@ package configs
import (
"fmt"
"io/ioutil"
"path/filepath"
"reflect"
"sort"
@ -154,3 +155,127 @@ func TestBuildConfigChildModuleBackend(t *testing.T) {
t.Fatalf("wrong result\ngot: %swant: %s", spew.Sdump(got), spew.Sdump(want))
}
}
func TestBuildConfigInvalidModules(t *testing.T) {
testDir := "testdata/config-diagnostics"
dirs, err := ioutil.ReadDir(testDir)
if err != nil {
t.Fatal(err)
}
for _, info := range dirs {
name := info.Name()
t.Run(name, func(t *testing.T) {
parser := NewParser(nil)
path := filepath.Join(testDir, name)
mod, diags := parser.LoadConfigDir(path)
if diags.HasErrors() {
// these tests should only trigger errors that are caught in
// the config loader.
t.Errorf("error loading config dir")
for _, diag := range diags {
t.Logf("- %s", diag)
}
}
readDiags := func(data []byte, _ error) []string {
var expected []string
for _, s := range strings.Split(string(data), "\n") {
msg := strings.TrimSpace(s)
msg = strings.ReplaceAll(msg, `\n`, "\n")
if msg != "" {
expected = append(expected, msg)
}
}
return expected
}
// Load expected errors and warnings.
// Each line in the file is matched as a substring against the
// diagnostic outputs.
// Capturing part of the path and source range in the message lets
// us also ensure the diagnostic is being attributed to the
// expected location in the source, but is not required.
// The literal characters `\n` are replaced with newlines, but
// otherwise the string is unchanged.
expectedErrs := readDiags(ioutil.ReadFile(filepath.Join(testDir, name, "errors")))
expectedWarnings := readDiags(ioutil.ReadFile(filepath.Join(testDir, name, "warnings")))
_, buildDiags := BuildConfig(mod, ModuleWalkerFunc(
func(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics) {
// for simplicity, these tests will treat all source
// addresses as relative to the root module
sourcePath := filepath.Join(path, req.SourceAddr)
mod, diags := parser.LoadConfigDir(sourcePath)
version, _ := version.NewVersion("1.0.0")
return mod, version, diags
},
))
// we can make this less repetitive later if we want
for _, msg := range expectedErrs {
found := false
for _, diag := range buildDiags {
if diag.Severity == hcl.DiagError && strings.Contains(diag.Error(), msg) {
found = true
break
}
}
if !found {
t.Errorf("Expected error diagnostic containing %q", msg)
}
}
for _, diag := range buildDiags {
if diag.Severity != hcl.DiagError {
continue
}
found := false
for _, msg := range expectedErrs {
if strings.Contains(diag.Error(), msg) {
found = true
break
}
}
if !found {
t.Errorf("Unexpected error: %q", diag)
}
}
for _, msg := range expectedWarnings {
found := false
for _, diag := range buildDiags {
if diag.Severity == hcl.DiagWarning && strings.Contains(diag.Error(), msg) {
found = true
break
}
}
if !found {
t.Errorf("Expected warning diagnostic containing %q", msg)
}
}
for _, diag := range buildDiags {
if diag.Severity != hcl.DiagWarning {
continue
}
found := false
for _, msg := range expectedWarnings {
if strings.Contains(diag.Error(), msg) {
found = true
break
}
}
if !found {
t.Errorf("Unexpected warning: %q", diag)
}
}
})
}
}

View File

@ -218,25 +218,27 @@ func decodeRequiredProvidersBlock(block *hcl.Block) (*RequiredProviders, hcl.Dia
}
// finally add the required provider as long as there were no errors
if !diags.HasErrors() {
// if a source was not given, create an implied type
if rp.Type.IsZero() {
pType, err := addrs.ParseProviderPart(rp.Name)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid provider name",
Detail: err.Error(),
Subject: attr.Expr.Range().Ptr(),
})
} else {
rp.Type = addrs.ImpliedProviderForUnqualifiedType(pType)
}
}
ret.RequiredProviders[rp.Name] = rp
if diags.HasErrors() {
continue
}
// We can add the required provider when there are no errors.
// If a source was not given, create an implied type.
if rp.Type.IsZero() {
pType, err := addrs.ParseProviderPart(rp.Name)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid provider name",
Detail: err.Error(),
Subject: attr.Expr.Range().Ptr(),
})
} else {
rp.Type = addrs.ImpliedProviderForUnqualifiedType(pType)
}
}
ret.RequiredProviders[rp.Name] = rp
}
return ret, diags

View File

@ -0,0 +1,243 @@
package configs
import (
"fmt"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/addrs"
)
// validateProviderConfigs walks the full configuration tree from the root
// module outward, static validation rules to the various combinations of
// provider configuration, required_providers values, and module call providers
// mappings.
//
// To retain compatibility with previous terraform versions, empty "proxy
// provider blocks" are still allowed within modules, though they will
// generate warnings when the configuration is loaded. The new validation
// however will generate an error if a suitable provider configuration is not
// passed in through the module call.
//
// The call argument is the ModuleCall for the provided Config cfg. The
// noProviderConfig argument is passed down the call stack, indicating that the
// module call, or a parent module call, has used a feature that precludes
// providers from being configured at all within the module.
func validateProviderConfigs(call *ModuleCall, cfg *Config, noProviderConfig bool) (diags hcl.Diagnostics) {
for name, child := range cfg.Children {
mc := cfg.Module.ModuleCalls[name]
// if the module call has any of count, for_each or depends_on,
// providers are prohibited from being configured in this module, or
// any module beneath this module.
nope := noProviderConfig || mc.Count != nil || mc.ForEach != nil || mc.DependsOn != nil
diags = append(diags, validateProviderConfigs(mc, child, nope)...)
}
// nothing else to do in the root module
if call == nil {
return diags
}
// the set of provider configuration names passed into the module, with the
// source range of the provider assignment in the module call.
passedIn := map[string]PassedProviderConfig{}
// the set of empty configurations that could be proxy configurations, with
// the source range of the empty configuration block.
emptyConfigs := map[string]*hcl.Range{}
// the set of provider with a defined configuration, with the source range
// of the configuration block declaration.
configured := map[string]*hcl.Range{}
// the set of configuration_aliases defined in the required_providers
// block, with the fully qualified provider type.
configAliases := map[string]addrs.AbsProviderConfig{}
// the set of provider names defined in the required_providers block, and
// their provider types.
localNames := map[string]addrs.AbsProviderConfig{}
for _, passed := range call.Providers {
name := providerName(passed.InChild.Name, passed.InChild.Alias)
passedIn[name] = passed
}
mod := cfg.Module
for _, pc := range mod.ProviderConfigs {
name := providerName(pc.Name, pc.Alias)
// Validate the config against an empty schema to see if it's empty.
_, pcConfigDiags := pc.Config.Content(&hcl.BodySchema{})
if pcConfigDiags.HasErrors() || pc.Version.Required != nil {
configured[name] = &pc.DeclRange
} else {
emptyConfigs[name] = &pc.DeclRange
}
}
if mod.ProviderRequirements != nil {
for _, req := range mod.ProviderRequirements.RequiredProviders {
addr := addrs.AbsProviderConfig{
Module: cfg.Path,
Provider: req.Type,
}
localNames[req.Name] = addr
for _, alias := range req.Aliases {
addr := addrs.AbsProviderConfig{
Module: cfg.Path,
Provider: req.Type,
Alias: alias.Alias,
}
configAliases[providerName(alias.LocalName, alias.Alias)] = addr
}
}
}
// there cannot be any configurations if no provider config is allowed
if len(configured) > 0 && noProviderConfig {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Module %s contains provider configuration", cfg.Path),
Detail: "Providers cannot be configured within modules using count, for_each or depends_on.",
})
}
// now check that the user is not attempting to override a config
for name := range configured {
if passed, ok := passedIn[name]; ok {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Cannot override provider configuration",
Detail: fmt.Sprintf("Provider %s is configured within the module %s and cannot be overridden.", name, cfg.Path),
Subject: &passed.InChild.NameRange,
})
}
}
// A declared alias requires either a matching configuration within the
// module, or one must be passed in.
for name, providerAddr := range configAliases {
_, confOk := configured[name]
_, passedOk := passedIn[name]
if confOk || passedOk {
continue
}
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("No configuration for provider %s", name),
Detail: fmt.Sprintf("Configuration required for %s.", providerAddr),
Subject: &call.DeclRange,
})
}
// You cannot pass in a provider that cannot be used
for name, passed := range passedIn {
providerAddr := addrs.AbsProviderConfig{
Module: cfg.Path,
Provider: addrs.NewDefaultProvider(passed.InChild.Name),
Alias: passed.InChild.Alias,
}
localAddr, localName := localNames[name]
if localName {
providerAddr = localAddr
}
aliasAddr, configAlias := configAliases[name]
if configAlias {
providerAddr = aliasAddr
}
_, emptyConfig := emptyConfigs[name]
if !(localName || configAlias || emptyConfig) {
severity := hcl.DiagError
// we still allow default configs, so switch to a warning if the incoming provider is a default
if providerAddr.Provider.IsDefault() {
severity = hcl.DiagWarning
}
diags = append(diags, &hcl.Diagnostic{
Severity: severity,
Summary: fmt.Sprintf("Provider %s is undefined", name),
Detail: fmt.Sprintf("Module %s does not declare a provider named %s.\n", cfg.Path, name) +
fmt.Sprintf("If you wish to specify a provider configuration for the module, add an entry for %s in the required_providers block within the module.", name),
Subject: &passed.InChild.NameRange,
})
}
// The provider being passed in must also be of the correct type.
// While we would like to ensure required_providers exists here,
// implied default configuration is still allowed.
pTy := addrs.NewDefaultProvider(passed.InParent.Name)
// use the full address for a nice diagnostic output
parentAddr := addrs.AbsProviderConfig{
Module: cfg.Parent.Path,
Provider: pTy,
Alias: passed.InParent.Alias,
}
if cfg.Parent.Module.ProviderRequirements != nil {
req, defined := cfg.Parent.Module.ProviderRequirements.RequiredProviders[name]
if defined {
parentAddr.Provider = req.Type
}
}
if !providerAddr.Provider.Equals(parentAddr.Provider) {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Invalid type for provider %s", providerAddr),
Detail: fmt.Sprintf("Cannot use configuration from %s for %s. ", parentAddr, providerAddr) +
"The given provider configuration is for a different provider type.",
Subject: &passed.InChild.NameRange,
})
}
}
// Empty configurations are no longer needed
for name, src := range emptyConfigs {
detail := fmt.Sprintf("Remove the %s provider block from %s.", name, cfg.Path)
isAlias := strings.Contains(name, ".")
_, isConfigAlias := configAliases[name]
_, isLocalName := localNames[name]
if isAlias && !isConfigAlias {
localName := strings.Split(name, ".")[0]
detail = fmt.Sprintf("Remove the %s provider block from %s. Add %s to the list of configuration_aliases for %s in required_providers to define the provider configuration name.", name, cfg.Path, name, localName)
}
if !isAlias && !isLocalName {
// if there is no local name, add a note to include it in the
// required_provider block
detail += fmt.Sprintf("\nTo ensure the correct provider configuration is used, add %s to the required_providers configuration", name)
}
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: "Empty provider configuration blocks are not required",
Detail: detail,
Subject: src,
})
}
if diags.HasErrors() {
return diags
}
return diags
}
func providerName(name, alias string) string {
if alias != "" {
name = name + "." + alias
}
return name
}

View File

@ -0,0 +1,20 @@
terraform {
required_providers {
foo = {
source = "hashicorp/foo"
}
baz = {
source = "hashicorp/baz"
}
}
}
module "mod" {
source = "./mod"
providers = {
foo = foo
foo.bar = foo
baz = baz
baz.bing = baz
}
}

View File

@ -0,0 +1,22 @@
terraform {
required_providers {
foo = {
source = "hashicorp/foo"
configuration_aliases = [ foo.bar ]
}
}
}
provider "foo" {
}
provider "foo" {
alias = "bar"
}
provider "baz" {
}
provider "baz" {
alias = "bing"
}

View File

@ -0,0 +1,4 @@
empty-configs/mod/main.tf:10,1-15: Empty provider configuration blocks are not required; Remove the foo provider block from module.mod
empty-configs/mod/main.tf:13,1-15: Empty provider configuration blocks are not required; Remove the foo.bar provider block from module.mod
empty-configs/mod/main.tf:17,1-15: Empty provider configuration blocks are not required; Remove the baz provider block from module.mod.\nTo ensure the correct provider configuration is used, add baz to the required_providers configuration
empty-configs/mod/main.tf:20,1-15: Empty provider configuration blocks are not required; Remove the baz.bing provider block from module.mod. Add baz.bing to the list of configuration_aliases for baz in required_providers to define the provider configuration name

View File

@ -0,0 +1 @@
incorrect-type/main.tf:15,5-8: Invalid type for provider module.mod.provider["example.com/vendor/foo"]; Cannot use configuration from provider["registry.terraform.io/hashicorp/foo"] for module.mod.provider["example.com/vendor/foo"]

View File

@ -0,0 +1,18 @@
terraform {
required_providers {
foo = {
source = "hashicorp/foo"
}
baz = {
source = "hashicorp/baz"
}
}
}
module "mod" {
source = "./mod"
providers = {
foo = foo
baz = baz
}
}

View File

@ -0,0 +1,14 @@
terraform {
required_providers {
foo = {
source = "example.com/vendor/foo"
}
}
}
resource "foo_resource" "a" {
}
// implied default provider baz
resource "baz_resource" "a" {
}

View File

@ -0,0 +1 @@
incorrect-type/main.tf:16,5-8: Provider baz is undefined; Module module.mod does not declare a provider named baz.\nIf you wish to specify a provider configuration for the module

View File

@ -0,0 +1,7 @@
provider "aws" {
value = "foo"
}
output "my_output" {
value = "my output"
}

View File

@ -0,0 +1,4 @@
module "child2" {
// the test fixture treats these sources as relative to the root
source = "./child/child2"
}

View File

@ -0,0 +1,3 @@
Module module.child.module.child2 contains provider configuration; Providers cannot be configured within modules using count, for_each or depends_on

View File

@ -0,0 +1,4 @@
module "child" {
count = 1
source = "./child"
}

View File

@ -0,0 +1 @@
override-provider/main.tf:17,5-8: Cannot override provider configuration; Provider bar is configured within the module module.mod and cannot be overridden.

View File

@ -0,0 +1,19 @@
terraform {
required_providers {
bar = {
version = "~>1.0.0"
}
}
}
provider "bar" {
value = "not ok"
}
// this module configures its own provider, which cannot be overridden
module "mod" {
source = "./mod"
providers = {
bar = bar
}
}

View File

@ -0,0 +1,12 @@
terraform {
required_providers {
bar = {
version = "~>1.0.0"
}
}
}
// this configuration cannot be overridden from an outside module
provider "bar" {
value = "ok"
}

View File

@ -0,0 +1 @@
required-alias/main.tf:1,1-13: No configuration for provider foo.bar; Configuration required for module.mod.provider["registry.terraform.io/hashicorp/foo"].bar

View File

@ -0,0 +1,4 @@
module "mod" {
source = "./mod"
// missing providers with foo.bar provider config
}

View File

@ -0,0 +1,13 @@
terraform {
required_providers {
foo = {
source = "hashicorp/foo"
version = "1.0.0"
configuration_aliases = [ foo.bar ]
}
}
}
resource "foo_resource" "a" {
provider = foo.bar
}

View File

@ -0,0 +1,15 @@
terraform {
required_providers {
foo = {
source = "hashicorp/foo"
version = "1.0.0"
}
}
}
module "mod" {
source = "./mod"
providers = {
foo = foo
}
}

View File

@ -0,0 +1,2 @@
resource "foo_resource" "a" {
}

View File

@ -0,0 +1,2 @@
unexpected-provider/main.tf:13,5-8: Provider foo is undefined; Module module.mod does not declare a provider named foo.

View File

@ -0,0 +1,14 @@
terraform {
required_providers {
foo = {
source = "hashicorp/foo"
}
}
}
module "mod2" {
source = "./mod1"
providers = {
foo = foo
}
}

View File

@ -0,0 +1,19 @@
terraform {
required_providers {
foo = {
source = "hashicorp/foo"
}
}
}
resource "foo_resource" "a" {
}
module "mod2" {
depends_on = [foo_resource.a]
// test fixture source is from root
source = "./mod1/mod2"
providers = {
foo = foo
}
}

View File

@ -0,0 +1,15 @@
terraform {
required_providers {
foo = {
source = "hashicorp/foo"
}
}
}
module "mod3" {
// test fixture source is from root
source = "./mod1/mod2/mod3"
providers = {
foo.bar = foo
}
}

View File

@ -0,0 +1,12 @@
terraform {
required_providers {
foo = {
source = "hashicorp/foo"
configuration_aliases = [ foo.bar ]
}
}
}
resource "foo_resource" "a" {
providers = foo.bar
}