configs: require normalized provider local names (#24945)

* addrs: replace NewLegacyProvider with NewDefaultProvider in ParseProviderSourceString

ParseProviderSourceString was still defaulting to NewLegacyProvider when
encountering single-part strings. This has been fixed.

This commit also adds a new function, IsProviderPartNormalized, which
returns a bool indicating if the string given is the same as a
normalized version (as normalized by ParseProviderPart) or an error.
This is intended for use by the configs package when decoding provider
configurations.

* terraform: fix provider local names in tests

* configs: validate that all provider names are normalized

The addrs package normalizes all source strings, but not the local
names. This caused very odd behavior if for e.g. a provider local name
was capitalized in one place and not another. We considered enabling
case-sensitivity for provider local names, but decided that since this
was not something that worked in previous versions of terraform (and we
have yet to encounter any use cases for this feature) we could generate
an error if the provider local name is not normalized. This error also
provides instructions on how to fix it.

* configs: refactor decodeProviderRequirements to consistently not set an FQN when there are errors
pull/24952/head
Kristin Laemmert 6 years ago committed by GitHub
parent c057487997
commit 041f4dd8ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -272,8 +272,7 @@ func ParseProviderSourceString(str string) (Provider, tfdiags.Diagnostics) {
ret.Hostname = DefaultRegistryHost
if len(parts) == 1 {
// FIXME: update this to NewDefaultProvider in the provider source release
return NewLegacyProvider(parts[0]), diags
return NewDefaultProvider(parts[0]), diags
}
if len(parts) >= 2 {
@ -406,3 +405,15 @@ func MustParseProviderPart(given string) string {
}
return result
}
// IsProviderPartNormalized compares a given string to the result of ParseProviderPart(string)
func IsProviderPartNormalized(str string) (bool, error) {
normalized, err := ParseProviderPart(str)
if err != nil {
return false, err
}
if str == normalized {
return true, nil
}
return false, nil
}

@ -282,20 +282,15 @@ func TestParseProviderSourceStr(t *testing.T) {
"aws": {
Provider{
Type: "aws",
Namespace: "-",
Namespace: "hashicorp",
Hostname: DefaultRegistryHost,
},
false,
},
"AWS": {
Provider{
// No case folding here because we're currently handling this
// as a legacy one. When this changes to be a _default_
// address in future (registry.terraform.io/hashicorp/aws)
// then we should start applying case folding to it, making
// Type appear as "aws" here instead.
Type: "AWS",
Namespace: "-",
Type: "aws",
Namespace: "hashicorp",
Hostname: DefaultRegistryHost,
},
false,

@ -40,8 +40,15 @@ func decodeProviderBlock(block *hcl.Block) (*Provider, hcl.Diagnostics) {
content, config, moreDiags := block.Body.PartialContent(providerBlockSchema)
diags = append(diags, moreDiags...)
// Provider names must be localized. Produce an error with a message
// indicating the action the user can take to fix this message if the local
// name is not localized.
name := block.Labels[0]
nameDiags := checkProviderNameNormalized(name, block.DefRange)
diags = append(diags, nameDiags...)
provider := &Provider{
Name: block.Labels[0],
Name: name,
NameRange: block.LabelRanges[0],
Config: config,
DeclRange: block.DefRange,
@ -207,3 +214,31 @@ var providerBlockSchema = &hcl.BodySchema{
{Type: "locals"},
},
}
// checkProviderNameNormalized verifies that the given string is already
// normalized and returns an error if not.
func checkProviderNameNormalized(name string, declrange hcl.Range) hcl.Diagnostics {
var diags hcl.Diagnostics
// verify that the provider local name is normalized
normalized, err := addrs.IsProviderPartNormalized(name)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid provider local name",
Detail: fmt.Sprintf("%s is an invalid provider local name: %s", name, err),
Subject: &declrange,
})
return diags
}
if !normalized {
// we would have returned this error already
normalizedProvider, _ := addrs.ParseProviderPart(name)
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid provider local name",
Detail: fmt.Sprintf("Provider names must be normalized. Replace %q with %q to fix this error.", name, normalizedProvider),
Subject: &declrange,
})
}
return diags
}

@ -13,10 +13,13 @@ type ProviderMeta struct {
}
func decodeProviderMetaBlock(block *hcl.Block) (*ProviderMeta, hcl.Diagnostics) {
// verify that the local name is already localized or produce an error.
diags := checkProviderNameNormalized(block.Labels[0], block.DefRange)
return &ProviderMeta{
Provider: block.Labels[0],
ProviderRange: block.LabelRanges[0],
Config: block.Body,
DeclRange: block.DefRange,
}, nil
}, diags
}

@ -35,6 +35,10 @@ func decodeRequiredProvidersBlock(block *hcl.Block) (*RequiredProviders, hcl.Dia
diags = append(diags, err...)
}
// verify that the local name is already localized or produce an error.
nameDiags := checkProviderNameNormalized(name, attr.Expr.Range())
diags = append(diags, nameDiags...)
rp := &RequiredProvider{
Name: name,
DeclRange: attr.Expr.Range(),
@ -69,6 +73,7 @@ func decodeRequiredProvidersBlock(block *hcl.Block) (*RequiredProviders, hcl.Dia
}
if expr.Type().HasAttribute("source") {
rp.Source = expr.GetAttr("source").AsString()
fqn, sourceDiags := addrs.ParseProviderSourceString(rp.Source)
if sourceDiags.HasErrors() {
@ -98,7 +103,7 @@ func decodeRequiredProvidersBlock(block *hcl.Block) (*RequiredProviders, hcl.Dia
})
}
if rp.Type.IsZero() {
if rp.Type.IsZero() && !diags.HasErrors() { // Don't try to generate an FQN if we've encountered errors
pType, err := addrs.ParseProviderPart(rp.Name)
if err != nil {
diags = append(diags, &hcl.Diagnostic{

@ -78,8 +78,8 @@ func TestDecodeRequiredProvidersBlock(t *testing.T) {
Type: "required_providers",
Body: hcltest.MockBody(&hcl.BodyContent{
Attributes: hcl.Attributes{
"my_test": {
Name: "my_test",
"my-test": {
Name: "my-test",
Expr: hcltest.MockExprLiteral(cty.ObjectVal(map[string]cty.Value{
"source": cty.StringVal("mycloud/test"),
"version": cty.StringVal("2.0.0"),
@ -91,8 +91,8 @@ func TestDecodeRequiredProvidersBlock(t *testing.T) {
},
Want: &RequiredProviders{
RequiredProviders: map[string]*RequiredProvider{
"my_test": {
Name: "my_test",
"my-test": {
Name: "my-test",
Source: "mycloud/test",
Type: addrs.NewProvider(addrs.DefaultRegistryHost, "mycloud", "test"),
Requirement: testVC("2.0.0"),
@ -111,8 +111,8 @@ func TestDecodeRequiredProvidersBlock(t *testing.T) {
Name: "legacy",
Expr: hcltest.MockExprLiteral(cty.StringVal("1.0.0")),
},
"my_test": {
Name: "my_test",
"my-test": {
Name: "my-test",
Expr: hcltest.MockExprLiteral(cty.ObjectVal(map[string]cty.Value{
"source": cty.StringVal("mycloud/test"),
"version": cty.StringVal("2.0.0"),
@ -130,8 +130,8 @@ func TestDecodeRequiredProvidersBlock(t *testing.T) {
Requirement: testVC("1.0.0"),
DeclRange: mockRange,
},
"my_test": {
Name: "my_test",
"my-test": {
Name: "my-test",
Source: "mycloud/test",
Type: addrs.NewProvider(addrs.DefaultRegistryHost, "mycloud", "test"),
Requirement: testVC("2.0.0"),
@ -173,8 +173,8 @@ func TestDecodeRequiredProvidersBlock(t *testing.T) {
Type: "required_providers",
Body: hcltest.MockBody(&hcl.BodyContent{
Attributes: hcl.Attributes{
"my_test": {
Name: "my_test",
"my-test": {
Name: "my-test",
Expr: hcltest.MockExprLiteral(cty.ObjectVal(map[string]cty.Value{
"source": cty.StringVal("some/invalid/provider/source/test"),
"version": cty.StringVal("~>2.0.0"),
@ -186,10 +186,9 @@ func TestDecodeRequiredProvidersBlock(t *testing.T) {
},
Want: &RequiredProviders{
RequiredProviders: map[string]*RequiredProvider{
"my_test": {
Name: "my_test",
"my-test": {
Name: "my-test",
Source: "some/invalid/provider/source/test",
Type: addrs.Provider{},
Requirement: testVC("~>2.0.0"),
DeclRange: mockRange,
},
@ -198,7 +197,7 @@ func TestDecodeRequiredProvidersBlock(t *testing.T) {
},
Error: "Invalid provider source string",
},
"localname is invalid provider name": {
"invalid localname": {
Block: &hcl.Block{
Type: "required_providers",
Body: hcltest.MockBody(&hcl.BodyContent{
@ -224,15 +223,43 @@ func TestDecodeRequiredProvidersBlock(t *testing.T) {
},
DeclRange: blockRange,
},
Error: "Invalid provider name",
Error: "Invalid provider local name",
},
"invalid localname caps": {
Block: &hcl.Block{
Type: "required_providers",
Body: hcltest.MockBody(&hcl.BodyContent{
Attributes: hcl.Attributes{
"MYTEST": {
Name: "MYTEST",
Expr: hcltest.MockExprLiteral(cty.ObjectVal(map[string]cty.Value{
"version": cty.StringVal("~>2.0.0"),
})),
},
},
}),
DefRange: blockRange,
},
Want: &RequiredProviders{
RequiredProviders: map[string]*RequiredProvider{
"MYTEST": {
Name: "MYTEST",
Type: addrs.Provider{},
Requirement: testVC("~>2.0.0"),
DeclRange: mockRange,
},
},
DeclRange: blockRange,
},
Error: "Invalid provider local name",
},
"version constraint error": {
Block: &hcl.Block{
Type: "required_providers",
Body: hcltest.MockBody(&hcl.BodyContent{
Attributes: hcl.Attributes{
"my_test": {
Name: "my_test",
"my-test": {
Name: "my-test",
Expr: hcltest.MockExprLiteral(cty.ObjectVal(map[string]cty.Value{
"source": cty.StringVal("mycloud/test"),
"version": cty.StringVal("invalid"),
@ -244,8 +271,8 @@ func TestDecodeRequiredProvidersBlock(t *testing.T) {
},
Want: &RequiredProviders{
RequiredProviders: map[string]*RequiredProvider{
"my_test": {
Name: "my_test",
"my-test": {
Name: "my-test",
Source: "mycloud/test",
Type: addrs.NewProvider(addrs.DefaultRegistryHost, "mycloud", "test"),
DeclRange: mockRange,
@ -272,7 +299,6 @@ func TestDecodeRequiredProvidersBlock(t *testing.T) {
RequiredProviders: map[string]*RequiredProvider{
"test": {
Name: "test",
Type: addrs.NewDefaultProvider("test"),
DeclRange: mockRange,
},
},

@ -418,8 +418,13 @@ func decodeProviderConfigRef(expr hcl.Expression, argName string) (*ProviderConf
return nil, diags
}
// verify that the provider local name is normalized
name := traversal.RootName()
nameDiags := checkProviderNameNormalized(name, traversal[0].SourceRange())
diags = append(diags, nameDiags...)
ret := &ProviderConfigRef{
Name: traversal.RootName(),
Name: name,
NameRange: traversal[0].SourceRange(),
}

@ -0,0 +1,20 @@
terraform {
required_providers {
test = {
source = "mycorp/test"
}
}
}
provider "TEST" {
}
resource test_resource "test" {
// this resource is (implicitly) provided by "mycorp/test"
}
resource test_resource "TEST" {
// this resource is (explicitly) provided by "hashicorp/test"
provider = TEST
}

@ -1,11 +1,11 @@
terraform {
required_providers {
your_aws = {
your-aws = {
source = "hashicorp/aws"
}
}
}
resource "aws_instance" "web" {
provider = "your_aws"
provider = "your-aws"
}

@ -1,11 +1,11 @@
terraform {
required_providers {
my_aws = {
my-aws = {
source = "hashicorp/aws"
}
}
}
resource "aws_instance" "web" {
provider = "my_aws"
provider = "my-aws"
}

@ -1,11 +1,11 @@
terraform {
required_providers {
my_aws = {
my-aws = {
source = "hashicorp/aws"
}
}
}
resource "aws_instance" "web" {
provider = "my_aws"
provider = "my-aws"
}

Loading…
Cancel
Save