initial support for parsing configuration_aliases

Add support for parsing configuration_aliases in required_providers
entries. The decoder needed to be re-written here in order to support
the bare reference style usage of provider names so that they match the
usage in other location within configuration. The only change to
existing handling of the required_providers block is more precise error
locations in a couple cases.
This commit is contained in:
James Bardin 2021-02-09 08:38:30 -05:00
parent a033598224
commit ac585be079
4 changed files with 226 additions and 168 deletions

View File

@ -1,6 +1,8 @@
package configs package configs
import ( import (
"fmt"
version "github.com/hashicorp/go-version" version "github.com/hashicorp/go-version"
"github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/addrs"
@ -17,6 +19,7 @@ type RequiredProvider struct {
Type addrs.Provider Type addrs.Provider
Requirement VersionConstraint Requirement VersionConstraint
DeclRange hcl.Range DeclRange hcl.Range
Aliases []addrs.LocalProviderConfig
} }
type RequiredProviders struct { type RequiredProviders struct {
@ -26,45 +29,97 @@ type RequiredProviders struct {
func decodeRequiredProvidersBlock(block *hcl.Block) (*RequiredProviders, hcl.Diagnostics) { func decodeRequiredProvidersBlock(block *hcl.Block) (*RequiredProviders, hcl.Diagnostics) {
attrs, diags := block.Body.JustAttributes() attrs, diags := block.Body.JustAttributes()
if diags.HasErrors() {
return nil, diags
}
ret := &RequiredProviders{ ret := &RequiredProviders{
RequiredProviders: make(map[string]*RequiredProvider), RequiredProviders: make(map[string]*RequiredProvider),
DeclRange: block.DefRange, DeclRange: block.DefRange,
} }
for name, attr := range attrs { for name, attr := range attrs {
expr, err := attr.Expr.Value(nil)
if err != nil {
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{ rp := &RequiredProvider{
Name: name, Name: name,
DeclRange: attr.Expr.Range(), DeclRange: attr.Expr.Range(),
} }
switch { // Look for a single static string, in case we have the legacy version-only
case expr.Type().IsPrimitiveType(): // format in the configuration.
if expr, err := attr.Expr.Value(nil); err == nil && expr.Type().IsPrimitiveType() {
vc, reqDiags := decodeVersionConstraint(attr) vc, reqDiags := decodeVersionConstraint(attr)
diags = append(diags, reqDiags...) diags = append(diags, reqDiags...)
rp.Requirement = vc
case expr.Type().IsObjectType(): pType, err := addrs.ParseProviderPart(rp.Name)
if expr.Type().HasAttribute("version") { if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid provider name",
Detail: err.Error(),
Subject: attr.Expr.Range().Ptr(),
})
continue
}
rp.Requirement = vc
rp.Type = addrs.ImpliedProviderForUnqualifiedType(pType)
ret.RequiredProviders[name] = rp
continue
}
// verify that the local name is already localized or produce an error.
nameDiags := checkProviderNameNormalized(name, attr.Expr.Range())
if nameDiags.HasErrors() {
diags = append(diags, nameDiags...)
continue
}
kvs, mapDiags := hcl.ExprMap(attr.Expr)
if mapDiags.HasErrors() {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid required_providers object",
Detail: "required_providers entries must be strings or objects.",
Subject: attr.Expr.Range().Ptr(),
})
continue
}
for _, kv := range kvs {
key, keyDiags := kv.Key.Value(nil)
if keyDiags.HasErrors() {
diags = append(diags, keyDiags...)
continue
}
if key.Type() != cty.String {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid Attribute",
Detail: fmt.Sprintf("Invalid attribute value for provider requirement: %#v", key),
Subject: kv.Key.Range().Ptr(),
})
continue
}
switch key.AsString() {
case "version":
vc := VersionConstraint{ vc := VersionConstraint{
DeclRange: attr.Range, DeclRange: attr.Range,
} }
constraint := expr.GetAttr("version")
if !constraint.Type().Equals(cty.String) || constraint.IsNull() { constraint, valDiags := kv.Value.Value(nil)
if valDiags.HasErrors() || !constraint.Type().Equals(cty.String) {
diags = append(diags, &hcl.Diagnostic{ diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError, Severity: hcl.DiagError,
Summary: "Invalid version constraint", Summary: "Invalid version constraint",
Detail: "Version must be specified as a string.", Detail: "Version must be specified as a string.",
Subject: attr.Expr.Range().Ptr(), Subject: kv.Value.Range().Ptr(),
}) })
} else { continue
}
constraintStr := constraint.AsString() constraintStr := constraint.AsString()
constraints, err := version.NewConstraint(constraintStr) constraints, err := version.NewConstraint(constraintStr)
if err != nil { if err != nil {
@ -74,28 +129,27 @@ func decodeRequiredProvidersBlock(block *hcl.Block) (*RequiredProviders, hcl.Dia
Severity: hcl.DiagError, Severity: hcl.DiagError,
Summary: "Invalid version constraint", Summary: "Invalid version constraint",
Detail: "This string does not use correct version constraint syntax.", Detail: "This string does not use correct version constraint syntax.",
Subject: attr.Expr.Range().Ptr(), Subject: kv.Value.Range().Ptr(),
}) })
} else { continue
}
vc.Required = constraints vc.Required = constraints
rp.Requirement = vc rp.Requirement = vc
}
} case "source":
} source, err := kv.Value.Value(nil)
if expr.Type().HasAttribute("source") { if err != nil || !source.Type().Equals(cty.String) {
source := expr.GetAttr("source")
if !source.Type().Equals(cty.String) || source.IsNull() {
diags = append(diags, &hcl.Diagnostic{ diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError, Severity: hcl.DiagError,
Summary: "Invalid source", Summary: "Invalid source",
Detail: "Source must be specified as a string.", Detail: "Source must be specified as a string.",
Subject: attr.Expr.Range().Ptr(), Subject: kv.Value.Range().Ptr(),
}) })
} else { continue
rp.Source = source.AsString() }
fqn, sourceDiags := addrs.ParseProviderSourceString(rp.Source)
fqn, sourceDiags := addrs.ParseProviderSourceString(source.AsString())
if sourceDiags.HasErrors() { if sourceDiags.HasErrors() {
hclDiags := sourceDiags.ToHCL() hclDiags := sourceDiags.ToHCL()
// The diagnostics from ParseProviderSourceString don't contain // The diagnostics from ParseProviderSourceString don't contain
@ -104,40 +158,70 @@ func decodeRequiredProvidersBlock(block *hcl.Block) (*RequiredProviders, hcl.Dia
// return. // return.
for _, diag := range hclDiags { for _, diag := range hclDiags {
if diag.Subject == nil { if diag.Subject == nil {
diag.Subject = attr.Expr.Range().Ptr() diag.Subject = kv.Value.Range().Ptr()
} }
} }
diags = append(diags, hclDiags...) diags = append(diags, hclDiags...)
} else {
rp.Type = fqn
}
}
}
attrTypes := expr.Type().AttributeTypes()
for name := range attrTypes {
if name == "version" || name == "source" {
continue continue
} }
rp.Source = source.AsString()
rp.Type = fqn
case "configuration_aliases":
exprs, listDiags := hcl.ExprList(kv.Value)
if listDiags.HasErrors() {
diags = append(diags, listDiags...)
continue
}
for _, expr := range exprs {
traversal, travDiags := hcl.AbsTraversalForExpr(expr)
if travDiags.HasErrors() {
diags = append(diags, travDiags...)
continue
}
addr, cfgDiags := ParseProviderConfigCompact(traversal)
if cfgDiags.HasErrors() {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid configuration_aliases value",
Detail: `Configuration aliases can only contain references to local provider configuration names in the format of provider.alias`,
Subject: kv.Value.Range().Ptr(),
})
continue
}
if addr.LocalName != name {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid configuration_aliases value",
Detail: fmt.Sprintf(`Configuration aliases must be prefixed with the provider name. Expected %q, but found %q.`, name, addr.LocalName),
Subject: kv.Value.Range().Ptr(),
})
continue
}
rp.Aliases = append(rp.Aliases, addr)
}
default:
diags = append(diags, &hcl.Diagnostic{ diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError, Severity: hcl.DiagError,
Summary: "Invalid required_providers object", Summary: "Invalid required_providers object",
Detail: `required_providers objects can only contain "version" and "source" attributes. To configure a provider, use a "provider" block.`, Detail: `required_providers objects can only contain "version", "source" and "configuration_aliases" attributes. To configure a provider, use a "provider" block.`,
Subject: attr.Expr.Range().Ptr(), Subject: kv.Key.Range().Ptr(),
}) })
break break
} }
default:
// should not happen
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid required_providers syntax",
Detail: "required_providers entries must be strings or objects.",
Subject: attr.Expr.Range().Ptr(),
})
} }
if rp.Type.IsZero() && !diags.HasErrors() { // Don't try to generate an FQN if we've encountered errors // 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) pType, err := addrs.ParseProviderPart(rp.Name)
if err != nil { if err != nil {
diags = append(diags, &hcl.Diagnostic{ diags = append(diags, &hcl.Diagnostic{
@ -153,6 +237,7 @@ func decodeRequiredProvidersBlock(block *hcl.Block) (*RequiredProviders, hcl.Dia
ret.RequiredProviders[rp.Name] = rp ret.RequiredProviders[rp.Name] = rp
} }
}
return ret, diags return ret, diags
} }

View File

@ -185,14 +185,7 @@ func TestDecodeRequiredProvidersBlock(t *testing.T) {
DefRange: blockRange, DefRange: blockRange,
}, },
Want: &RequiredProviders{ Want: &RequiredProviders{
RequiredProviders: map[string]*RequiredProvider{ RequiredProviders: map[string]*RequiredProvider{},
"my-test": {
Name: "my-test",
Source: "some/invalid/provider/source/test",
Requirement: testVC("~>2.0.0"),
DeclRange: mockRange,
},
},
DeclRange: blockRange, DeclRange: blockRange,
}, },
Error: "Invalid provider source string", Error: "Invalid provider source string",
@ -213,14 +206,7 @@ func TestDecodeRequiredProvidersBlock(t *testing.T) {
DefRange: blockRange, DefRange: blockRange,
}, },
Want: &RequiredProviders{ Want: &RequiredProviders{
RequiredProviders: map[string]*RequiredProvider{ RequiredProviders: map[string]*RequiredProvider{},
"my_test": {
Name: "my_test",
Type: addrs.Provider{},
Requirement: testVC("~>2.0.0"),
DeclRange: mockRange,
},
},
DeclRange: blockRange, DeclRange: blockRange,
}, },
Error: "Invalid provider local name", Error: "Invalid provider local name",
@ -241,14 +227,7 @@ func TestDecodeRequiredProvidersBlock(t *testing.T) {
DefRange: blockRange, DefRange: blockRange,
}, },
Want: &RequiredProviders{ Want: &RequiredProviders{
RequiredProviders: map[string]*RequiredProvider{ RequiredProviders: map[string]*RequiredProvider{},
"MYTEST": {
Name: "MYTEST",
Type: addrs.Provider{},
Requirement: testVC("~>2.0.0"),
DeclRange: mockRange,
},
},
DeclRange: blockRange, DeclRange: blockRange,
}, },
Error: "Invalid provider local name", Error: "Invalid provider local name",
@ -270,14 +249,7 @@ func TestDecodeRequiredProvidersBlock(t *testing.T) {
DefRange: blockRange, DefRange: blockRange,
}, },
Want: &RequiredProviders{ Want: &RequiredProviders{
RequiredProviders: map[string]*RequiredProvider{ RequiredProviders: map[string]*RequiredProvider{},
"my-test": {
Name: "my-test",
Source: "mycloud/test",
Type: addrs.NewProvider(addrs.DefaultRegistryHost, "mycloud", "test"),
DeclRange: mockRange,
},
},
DeclRange: blockRange, DeclRange: blockRange,
}, },
Error: "Invalid version constraint", Error: "Invalid version constraint",
@ -296,15 +268,10 @@ func TestDecodeRequiredProvidersBlock(t *testing.T) {
DefRange: blockRange, DefRange: blockRange,
}, },
Want: &RequiredProviders{ Want: &RequiredProviders{
RequiredProviders: map[string]*RequiredProvider{ RequiredProviders: map[string]*RequiredProvider{},
"test": {
Name: "test",
DeclRange: mockRange,
},
},
DeclRange: blockRange, DeclRange: blockRange,
}, },
Error: "Invalid required_providers syntax", Error: "Invalid required_providers object",
}, },
"invalid source attribute type": { "invalid source attribute type": {
Block: &hcl.Block{ Block: &hcl.Block{
@ -322,12 +289,7 @@ func TestDecodeRequiredProvidersBlock(t *testing.T) {
DefRange: blockRange, DefRange: blockRange,
}, },
Want: &RequiredProviders{ Want: &RequiredProviders{
RequiredProviders: map[string]*RequiredProvider{ RequiredProviders: map[string]*RequiredProvider{},
"my-test": {
Name: "my-test",
DeclRange: mockRange,
},
},
DeclRange: blockRange, DeclRange: blockRange,
}, },
Error: "Invalid source", Error: "Invalid source",
@ -350,15 +312,7 @@ func TestDecodeRequiredProvidersBlock(t *testing.T) {
DefRange: blockRange, DefRange: blockRange,
}, },
Want: &RequiredProviders{ Want: &RequiredProviders{
RequiredProviders: map[string]*RequiredProvider{ RequiredProviders: map[string]*RequiredProvider{},
"my-test": {
Name: "my-test",
Source: "mycloud/test",
Type: addrs.NewProvider(addrs.DefaultRegistryHost, "mycloud", "test"),
Requirement: testVC("2.0.0"),
DeclRange: mockRange,
},
},
DeclRange: blockRange, DeclRange: blockRange,
}, },
Error: "Invalid required_providers object", Error: "Invalid required_providers object",
@ -370,7 +324,7 @@ func TestDecodeRequiredProvidersBlock(t *testing.T) {
got, diags := decodeRequiredProvidersBlock(test.Block) got, diags := decodeRequiredProvidersBlock(test.Block)
if diags.HasErrors() { if diags.HasErrors() {
if test.Error == "" { if test.Error == "" {
t.Fatalf("unexpected error") t.Fatalf("unexpected error: %v", diags)
} }
if gotErr := diags[0].Summary; gotErr != test.Error { if gotErr := diags[0].Summary; gotErr != test.Error {
t.Errorf("wrong error, got %q, want %q", gotErr, test.Error) t.Errorf("wrong error, got %q, want %q", gotErr, test.Error)

View File

@ -1,10 +1,10 @@
terraform { terraform {
required_providers { required_providers {
usererror = { # ERROR: Invalid provider type usererror = {
source = "foo/terraform-provider-foo" source = "foo/terraform-provider-foo" # ERROR: Invalid provider type
} }
badname = { # ERROR: Invalid provider type badname = {
source = "foo/terraform-foo" source = "foo/terraform-foo" # ERROR: Invalid provider type
} }
} }
} }

View File

@ -0,0 +1,19 @@
terraform {
required_providers {
foo-test = {
source = "foo/test"
// TODO: these are strings until the parsing code is refactored to allow
// raw references
configuration_aliases = [foo-test.a, foo-test.b]
}
}
}
resource "test_instance" "explicit" {
provider = foo-test.a
}
data "test_resource" "explicit" {
provider = foo-test.b
}