Merge pull request #24763 from hashicorp/alisdair/required-providers-refactor
configs: Simplify required_providers blocks
This commit is contained in:
commit
c1137430c4
|
@ -185,24 +185,22 @@ func (c *Config) addProviderRequirements(reqs getproviders.Requirements) hcl.Dia
|
|||
var diags hcl.Diagnostics
|
||||
|
||||
// First we'll deal with the requirements directly in _our_ module...
|
||||
for _, providerReqs := range c.Module.ProviderRequirements {
|
||||
for _, providerReqs := range c.Module.ProviderRequirements.RequiredProviders {
|
||||
fqn := providerReqs.Type
|
||||
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
|
||||
}
|
||||
for _, constraintsSrc := range providerReqs.VersionConstraints {
|
||||
// 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. We assume this
|
||||
// will always succeed because these constraints already succeeded
|
||||
// parsing with the other constraint parser, which uses the same
|
||||
// syntax.
|
||||
constraints := getproviders.MustParseVersionConstraints(constraintsSrc.Required.String())
|
||||
constraints := getproviders.MustParseVersionConstraints(providerReqs.Requirement.Required.String())
|
||||
reqs[fqn] = append(reqs[fqn], constraints...)
|
||||
}
|
||||
}
|
||||
// Each resource in the configuration creates an *implicit* provider
|
||||
// dependency, though we'll only record it if there isn't already
|
||||
// an explicit dependency on the same provider.
|
||||
|
@ -321,7 +319,7 @@ func (c *Config) ResolveAbsProviderAddr(addr addrs.ProviderConfig, inModule addr
|
|||
}
|
||||
|
||||
var provider addrs.Provider
|
||||
if providerReq, exists := c.Module.ProviderRequirements[addr.LocalName]; exists {
|
||||
if providerReq, exists := c.Module.ProviderRequirements.RequiredProviders[addr.LocalName]; exists {
|
||||
provider = providerReq.Type
|
||||
} else {
|
||||
provider = addrs.ImpliedProviderForUnqualifiedType(addr.LocalName)
|
||||
|
@ -343,7 +341,7 @@ func (c *Config) ResolveAbsProviderAddr(addr addrs.ProviderConfig, inModule addr
|
|||
// by checking for the provider in module.ProviderRequirements and falling
|
||||
// back to addrs.NewDefaultProvider if it is not found.
|
||||
func (c *Config) ProviderForConfigAddr(addr addrs.LocalProviderConfig) addrs.Provider {
|
||||
if provider, exists := c.Module.ProviderRequirements[addr.LocalName]; exists {
|
||||
if provider, exists := c.Module.ProviderRequirements.RequiredProviders[addr.LocalName]; exists {
|
||||
return provider.Type
|
||||
}
|
||||
return c.ResolveAbsProviderAddr(addr, addrs.RootModule).Provider
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
|
||||
"github.com/hashicorp/terraform/addrs"
|
||||
"github.com/hashicorp/terraform/experiments"
|
||||
"github.com/hashicorp/terraform/tfdiags"
|
||||
)
|
||||
|
||||
// Module is a container for a set of configuration constructs that are
|
||||
|
@ -31,7 +30,7 @@ type Module struct {
|
|||
|
||||
Backend *Backend
|
||||
ProviderConfigs map[string]*Provider
|
||||
ProviderRequirements map[string]ProviderRequirements
|
||||
ProviderRequirements *RequiredProviders
|
||||
ProviderLocalNames map[addrs.Provider]string
|
||||
ProviderMetas map[addrs.Provider]*ProviderMeta
|
||||
|
||||
|
@ -64,7 +63,7 @@ type File struct {
|
|||
Backends []*Backend
|
||||
ProviderConfigs []*Provider
|
||||
ProviderMetas []*ProviderMeta
|
||||
RequiredProviders []*RequiredProvider
|
||||
RequiredProviders []*RequiredProviders
|
||||
|
||||
Variables []*Variable
|
||||
Locals []*Local
|
||||
|
@ -88,7 +87,6 @@ func NewModule(primaryFiles, overrideFiles []*File) (*Module, hcl.Diagnostics) {
|
|||
var diags hcl.Diagnostics
|
||||
mod := &Module{
|
||||
ProviderConfigs: map[string]*Provider{},
|
||||
ProviderRequirements: map[string]ProviderRequirements{},
|
||||
ProviderLocalNames: map[addrs.Provider]string{},
|
||||
Variables: map[string]*Variable{},
|
||||
Locals: map[string]*Local{},
|
||||
|
@ -99,6 +97,41 @@ func NewModule(primaryFiles, overrideFiles []*File) (*Module, hcl.Diagnostics) {
|
|||
ProviderMetas: map[addrs.Provider]*ProviderMeta{},
|
||||
}
|
||||
|
||||
// Process the required_providers blocks first, to ensure that all
|
||||
// resources have access to the correct provider FQNs
|
||||
for _, file := range primaryFiles {
|
||||
for _, r := range file.RequiredProviders {
|
||||
if mod.ProviderRequirements != nil {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Duplicate required providers configuration",
|
||||
Detail: fmt.Sprintf("A module may have only one required providers configuration. The required providers were previously configured at %s.", mod.ProviderRequirements.DeclRange),
|
||||
Subject: &r.DeclRange,
|
||||
})
|
||||
continue
|
||||
}
|
||||
mod.ProviderRequirements = r
|
||||
}
|
||||
}
|
||||
|
||||
// If no required_providers block is configured, create a useful empty
|
||||
// state to reduce nil checks elsewhere
|
||||
if mod.ProviderRequirements == nil {
|
||||
mod.ProviderRequirements = &RequiredProviders{
|
||||
RequiredProviders: make(map[string]*RequiredProvider),
|
||||
}
|
||||
}
|
||||
|
||||
// Any required_providers blocks in override files replace the entire
|
||||
// block for each provider
|
||||
for _, file := range overrideFiles {
|
||||
for _, override := range file.RequiredProviders {
|
||||
for name, rp := range override.RequiredProviders {
|
||||
mod.ProviderRequirements.RequiredProviders[name] = rp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, file := range primaryFiles {
|
||||
fileDiags := mod.appendFile(file)
|
||||
diags = append(diags, fileDiags...)
|
||||
|
@ -178,35 +211,6 @@ func (m *Module) appendFile(file *File) hcl.Diagnostics {
|
|||
m.ProviderConfigs[key] = pc
|
||||
}
|
||||
|
||||
for _, reqd := range file.RequiredProviders {
|
||||
var fqn addrs.Provider
|
||||
if reqd.Source.SourceStr != "" {
|
||||
var sourceDiags tfdiags.Diagnostics
|
||||
fqn, sourceDiags = addrs.ParseProviderSourceString(reqd.Source.SourceStr)
|
||||
hclDiags := sourceDiags.ToHCL()
|
||||
// The diagnostics from ParseProviderSourceString don't contain
|
||||
// source location information because it has no context to compute
|
||||
// them from, and so we'll add those in quickly here before we
|
||||
// return.
|
||||
for _, diag := range hclDiags {
|
||||
if diag.Subject == nil {
|
||||
diag.Subject = reqd.Source.DeclRange.Ptr()
|
||||
}
|
||||
}
|
||||
diags = append(diags, hclDiags...)
|
||||
} else {
|
||||
fqn = addrs.ImpliedProviderForUnqualifiedType(reqd.Name)
|
||||
}
|
||||
if existing, exists := m.ProviderRequirements[reqd.Name]; exists {
|
||||
if existing.Type != fqn {
|
||||
panic("provider fqn mismatch")
|
||||
}
|
||||
existing.VersionConstraints = append(existing.VersionConstraints, reqd.Requirement)
|
||||
} else {
|
||||
m.ProviderRequirements[reqd.Name] = ProviderRequirements{Type: fqn, VersionConstraints: []VersionConstraint{reqd.Requirement}}
|
||||
}
|
||||
}
|
||||
|
||||
for _, pm := range file.ProviderMetas {
|
||||
provider := m.ProviderForLocalConfig(addrs.LocalProviderConfig{LocalName: pm.Provider})
|
||||
if existing, exists := m.ProviderMetas[provider]; exists {
|
||||
|
@ -283,7 +287,7 @@ func (m *Module) appendFile(file *File) hcl.Diagnostics {
|
|||
|
||||
// set the provider FQN for the resource
|
||||
if r.ProviderConfigRef != nil {
|
||||
if existing, exists := m.ProviderRequirements[r.ProviderConfigAddr().LocalName]; exists {
|
||||
if existing, exists := m.ProviderRequirements.RequiredProviders[r.ProviderConfigAddr().LocalName]; exists {
|
||||
r.Provider = existing.Type
|
||||
} else {
|
||||
r.Provider = addrs.ImpliedProviderForUnqualifiedType(r.ProviderConfigAddr().LocalName)
|
||||
|
@ -308,7 +312,7 @@ func (m *Module) appendFile(file *File) hcl.Diagnostics {
|
|||
|
||||
// set the provider FQN for the resource
|
||||
if r.ProviderConfigRef != nil {
|
||||
if existing, exists := m.ProviderRequirements[r.ProviderConfigAddr().LocalName]; exists {
|
||||
if existing, exists := m.ProviderRequirements.RequiredProviders[r.ProviderConfigAddr().LocalName]; exists {
|
||||
r.Provider = existing.Type
|
||||
} else {
|
||||
r.Provider = addrs.ImpliedProviderForUnqualifiedType(r.ProviderConfigAddr().LocalName)
|
||||
|
@ -382,10 +386,6 @@ func (m *Module) mergeFile(file *File) hcl.Diagnostics {
|
|||
}
|
||||
}
|
||||
|
||||
if len(file.RequiredProviders) != 0 {
|
||||
mergeProviderVersionConstraints(m.ProviderRequirements, file.RequiredProviders)
|
||||
}
|
||||
|
||||
for _, v := range file.Variables {
|
||||
existing, exists := m.Variables[v.Name]
|
||||
if !exists {
|
||||
|
@ -458,7 +458,7 @@ func (m *Module) mergeFile(file *File) hcl.Diagnostics {
|
|||
})
|
||||
continue
|
||||
}
|
||||
mergeDiags := existing.merge(r, m.ProviderRequirements)
|
||||
mergeDiags := existing.merge(r, m.ProviderRequirements.RequiredProviders)
|
||||
diags = append(diags, mergeDiags...)
|
||||
}
|
||||
|
||||
|
@ -474,7 +474,7 @@ func (m *Module) mergeFile(file *File) hcl.Diagnostics {
|
|||
})
|
||||
continue
|
||||
}
|
||||
mergeDiags := existing.merge(r, m.ProviderRequirements)
|
||||
mergeDiags := existing.merge(r, m.ProviderRequirements.RequiredProviders)
|
||||
diags = append(diags, mergeDiags...)
|
||||
}
|
||||
|
||||
|
@ -487,7 +487,7 @@ func (m *Module) mergeFile(file *File) hcl.Diagnostics {
|
|||
// only be populated after the module has been parsed.
|
||||
func (m *Module) gatherProviderLocalNames() {
|
||||
providers := make(map[addrs.Provider]string)
|
||||
for k, v := range m.ProviderRequirements {
|
||||
for k, v := range m.ProviderRequirements.RequiredProviders {
|
||||
providers[v.Type] = k
|
||||
}
|
||||
m.ProviderLocalNames = providers
|
||||
|
@ -507,7 +507,7 @@ func (m *Module) LocalNameForProvider(p addrs.Provider) string {
|
|||
|
||||
// ProviderForLocalConfig returns the provider FQN for a given LocalProviderConfig
|
||||
func (m *Module) ProviderForLocalConfig(pc addrs.LocalProviderConfig) addrs.Provider {
|
||||
if provider, exists := m.ProviderRequirements[pc.LocalName]; exists {
|
||||
if provider, exists := m.ProviderRequirements.RequiredProviders[pc.LocalName]; exists {
|
||||
return provider.Type
|
||||
}
|
||||
return addrs.ImpliedProviderForUnqualifiedType(pc.LocalName)
|
||||
|
|
|
@ -35,25 +35,6 @@ func (p *Provider) merge(op *Provider) hcl.Diagnostics {
|
|||
return diags
|
||||
}
|
||||
|
||||
func mergeProviderVersionConstraints(recv map[string]ProviderRequirements, ovrd []*RequiredProvider) {
|
||||
// Any provider name that's mentioned in the override gets nilled out in
|
||||
// our map so that we'll rebuild it below. Any provider not mentioned is
|
||||
// left unchanged.
|
||||
for _, reqd := range ovrd {
|
||||
delete(recv, reqd.Name)
|
||||
}
|
||||
for _, reqd := range ovrd {
|
||||
var fqn addrs.Provider
|
||||
if reqd.Source.SourceStr != "" {
|
||||
// any errors parsing the source string will have already been captured.
|
||||
fqn, _ = addrs.ParseProviderSourceString(reqd.Source.SourceStr)
|
||||
} else {
|
||||
fqn = addrs.ImpliedProviderForUnqualifiedType(reqd.Name)
|
||||
}
|
||||
recv[reqd.Name] = ProviderRequirements{Type: fqn, VersionConstraints: []VersionConstraint{reqd.Requirement}}
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Variable) merge(ov *Variable) hcl.Diagnostics {
|
||||
var diags hcl.Diagnostics
|
||||
|
||||
|
@ -197,7 +178,7 @@ func (mc *ModuleCall) merge(omc *ModuleCall) hcl.Diagnostics {
|
|||
return diags
|
||||
}
|
||||
|
||||
func (r *Resource) merge(or *Resource, prs map[string]ProviderRequirements) hcl.Diagnostics {
|
||||
func (r *Resource) merge(or *Resource, rps map[string]*RequiredProvider) hcl.Diagnostics {
|
||||
var diags hcl.Diagnostics
|
||||
|
||||
if r.Mode != or.Mode {
|
||||
|
@ -215,7 +196,7 @@ func (r *Resource) merge(or *Resource, prs map[string]ProviderRequirements) hcl.
|
|||
|
||||
if or.ProviderConfigRef != nil {
|
||||
r.ProviderConfigRef = or.ProviderConfigRef
|
||||
if existing, exists := prs[or.ProviderConfigRef.Name]; exists {
|
||||
if existing, exists := rps[or.ProviderConfigRef.Name]; exists {
|
||||
r.Provider = existing.Type
|
||||
} else {
|
||||
r.Provider = addrs.ImpliedProviderForUnqualifiedType(r.ProviderConfigRef.Name)
|
||||
|
|
|
@ -3,7 +3,6 @@ package configs
|
|||
import (
|
||||
"testing"
|
||||
|
||||
version "github.com/hashicorp/go-version"
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/gohcl"
|
||||
"github.com/hashicorp/terraform/addrs"
|
||||
|
@ -232,98 +231,3 @@ func TestModuleOverrideResourceFQNs(t *testing.T) {
|
|||
t.Fatalf("wrong result: found provider config ref %s, expected nil", got.ProviderConfigRef)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeProviderVersionConstraints(t *testing.T) {
|
||||
v1, _ := version.NewConstraint("1.0.0")
|
||||
vc1 := VersionConstraint{
|
||||
Required: v1,
|
||||
}
|
||||
v2, _ := version.NewConstraint("2.0.0")
|
||||
vc2 := VersionConstraint{
|
||||
Required: v2,
|
||||
}
|
||||
|
||||
tests := map[string]struct {
|
||||
Input map[string]ProviderRequirements
|
||||
Override []*RequiredProvider
|
||||
Want map[string]ProviderRequirements
|
||||
}{
|
||||
"basic merge": {
|
||||
map[string]ProviderRequirements{
|
||||
"random": ProviderRequirements{
|
||||
Type: addrs.Provider{Type: "random"},
|
||||
VersionConstraints: []VersionConstraint{},
|
||||
},
|
||||
},
|
||||
[]*RequiredProvider{
|
||||
&RequiredProvider{
|
||||
Name: "null",
|
||||
Requirement: VersionConstraint{},
|
||||
},
|
||||
},
|
||||
map[string]ProviderRequirements{
|
||||
"random": ProviderRequirements{
|
||||
Type: addrs.Provider{Type: "random"},
|
||||
VersionConstraints: []VersionConstraint{},
|
||||
},
|
||||
"null": ProviderRequirements{
|
||||
Type: addrs.NewDefaultProvider("null"),
|
||||
VersionConstraints: []VersionConstraint{
|
||||
VersionConstraint{
|
||||
Required: version.Constraints(nil),
|
||||
DeclRange: hcl.Range{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"override version constraint": {
|
||||
map[string]ProviderRequirements{
|
||||
"random": ProviderRequirements{
|
||||
Type: addrs.Provider{Type: "random"},
|
||||
VersionConstraints: []VersionConstraint{vc1},
|
||||
},
|
||||
},
|
||||
[]*RequiredProvider{
|
||||
&RequiredProvider{
|
||||
Name: "random",
|
||||
Requirement: vc2,
|
||||
},
|
||||
},
|
||||
map[string]ProviderRequirements{
|
||||
"random": ProviderRequirements{
|
||||
Type: addrs.NewDefaultProvider("random"),
|
||||
VersionConstraints: []VersionConstraint{vc2},
|
||||
},
|
||||
},
|
||||
},
|
||||
"merge with source constraint": {
|
||||
map[string]ProviderRequirements{
|
||||
"random": ProviderRequirements{
|
||||
Type: addrs.Provider{Type: "random"},
|
||||
VersionConstraints: []VersionConstraint{vc1},
|
||||
},
|
||||
},
|
||||
[]*RequiredProvider{
|
||||
&RequiredProvider{
|
||||
Name: "random",
|
||||
Source: Source{SourceStr: "hashicorp/random"},
|
||||
Requirement: vc2,
|
||||
},
|
||||
},
|
||||
map[string]ProviderRequirements{
|
||||
"random": ProviderRequirements{
|
||||
Type: addrs.NewDefaultProvider("random"),
|
||||
VersionConstraints: []VersionConstraint{vc2},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
mergeProviderVersionConstraints(test.Input, test.Override)
|
||||
assertResultDeepEqual(t, test.Input, test.Want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package configs
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/addrs"
|
||||
|
@ -111,3 +112,94 @@ func TestProviderForLocalConfig(t *testing.T) {
|
|||
t.Fatalf("wrong result! got %#v, want %#v\n", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// At most one required_providers block per module is permitted.
|
||||
func TestModule_required_providers_multiple(t *testing.T) {
|
||||
_, diags := testModuleFromDir("testdata/invalid-modules/multiple-required-providers")
|
||||
if !diags.HasErrors() {
|
||||
t.Fatal("module should have error diags, but does not")
|
||||
}
|
||||
|
||||
want := `Duplicate required providers configuration`
|
||||
if got := diags.Error(); !strings.Contains(got, want) {
|
||||
t.Fatalf("expected error to contain %q\nerror was:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
// A module may have required_providers configured in files loaded later than
|
||||
// resources. These provider settings should still be reflected in the
|
||||
// resources' configuration.
|
||||
func TestModule_required_providers_after_resource(t *testing.T) {
|
||||
mod, diags := testModuleFromDir("testdata/valid-modules/required-providers-after-resource")
|
||||
if diags.HasErrors() {
|
||||
t.Fatal(diags.Error())
|
||||
}
|
||||
|
||||
want := addrs.NewProvider(addrs.DefaultRegistryHost, "foo", "test")
|
||||
|
||||
req, exists := mod.ProviderRequirements.RequiredProviders["test"]
|
||||
if !exists {
|
||||
t.Fatal("no provider requirements found for \"test\"")
|
||||
}
|
||||
if req.Type != want {
|
||||
t.Errorf("wrong provider addr for \"test\"\ngot: %s\nwant: %s",
|
||||
req.Type, want,
|
||||
)
|
||||
}
|
||||
|
||||
if got := mod.ManagedResources["test_instance.my-instance"].Provider; !got.Equals(want) {
|
||||
t.Errorf("wrong provider addr for \"test_instance.my-instance\"\ngot: %s\nwant: %s",
|
||||
got, want,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// We support overrides for required_providers blocks, which should replace the
|
||||
// entire block for each provider localname, leaving other blocks unaffected.
|
||||
// This should also be reflected in any resources in the module using this
|
||||
// provider.
|
||||
func TestModule_required_provider_overrides(t *testing.T) {
|
||||
mod, diags := testModuleFromDir("testdata/valid-modules/required-providers-overrides")
|
||||
if diags.HasErrors() {
|
||||
t.Fatal(diags.Error())
|
||||
}
|
||||
|
||||
// The foo provider and resource should be unaffected
|
||||
want := addrs.NewProvider(addrs.DefaultRegistryHost, "acme", "foo")
|
||||
req, exists := mod.ProviderRequirements.RequiredProviders["foo"]
|
||||
if !exists {
|
||||
t.Fatal("no provider requirements found for \"foo\"")
|
||||
}
|
||||
if req.Type != want {
|
||||
t.Errorf("wrong provider addr for \"foo\"\ngot: %s\nwant: %s",
|
||||
req.Type, want,
|
||||
)
|
||||
}
|
||||
if got := mod.ManagedResources["foo_thing.ft"].Provider; !got.Equals(want) {
|
||||
t.Errorf("wrong provider addr for \"foo_thing.ft\"\ngot: %s\nwant: %s",
|
||||
got, want,
|
||||
)
|
||||
}
|
||||
|
||||
// The bar provider and resource should be using the override config
|
||||
want = addrs.NewProvider(addrs.DefaultRegistryHost, "blorp", "bar")
|
||||
req, exists = mod.ProviderRequirements.RequiredProviders["bar"]
|
||||
if !exists {
|
||||
t.Fatal("no provider requirements found for \"bar\"")
|
||||
}
|
||||
if req.Type != want {
|
||||
t.Errorf("wrong provider addr for \"bar\"\ngot: %s\nwant: %s",
|
||||
req.Type, want,
|
||||
)
|
||||
}
|
||||
if gotVer, wantVer := req.Requirement.Required.String(), "~>2.0.0"; gotVer != wantVer {
|
||||
t.Errorf("wrong provider version constraint for \"bar\"\ngot: %s\nwant: %s",
|
||||
gotVer, wantVer,
|
||||
)
|
||||
}
|
||||
if got := mod.ManagedResources["bar_thing.bt"].Provider; !got.Equals(want) {
|
||||
t.Errorf("wrong provider addr for \"bar_thing.bt\"\ngot: %s\nwant: %s",
|
||||
got, want,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,7 +75,7 @@ func (p *Parser) loadConfigFile(path string, override bool) (*File, hcl.Diagnost
|
|||
case "required_providers":
|
||||
reqs, reqsDiags := decodeRequiredProvidersBlock(innerBlock)
|
||||
diags = append(diags, reqsDiags...)
|
||||
file.RequiredProviders = append(file.RequiredProviders, reqs...)
|
||||
file.RequiredProviders = append(file.RequiredProviders, reqs)
|
||||
|
||||
case "provider_meta":
|
||||
providerCfg, cfgDiags := decodeProviderMetaBlock(innerBlock)
|
||||
|
|
|
@ -12,41 +12,40 @@ import (
|
|||
// parent.
|
||||
type RequiredProvider struct {
|
||||
Name string
|
||||
Source Source
|
||||
Type addrs.Provider
|
||||
Requirement VersionConstraint
|
||||
}
|
||||
|
||||
type Source struct {
|
||||
SourceStr string
|
||||
DeclRange hcl.Range
|
||||
}
|
||||
|
||||
// ProviderRequirements represents provider version constraints from
|
||||
// required_providers blocks.
|
||||
type ProviderRequirements struct {
|
||||
Type addrs.Provider
|
||||
VersionConstraints []VersionConstraint
|
||||
type RequiredProviders struct {
|
||||
RequiredProviders map[string]*RequiredProvider
|
||||
DeclRange hcl.Range
|
||||
}
|
||||
|
||||
func decodeRequiredProvidersBlock(block *hcl.Block) ([]*RequiredProvider, hcl.Diagnostics) {
|
||||
func decodeRequiredProvidersBlock(block *hcl.Block) (*RequiredProviders, hcl.Diagnostics) {
|
||||
attrs, diags := block.Body.JustAttributes()
|
||||
var reqs []*RequiredProvider
|
||||
ret := &RequiredProviders{
|
||||
RequiredProviders: make(map[string]*RequiredProvider),
|
||||
DeclRange: block.DefRange,
|
||||
}
|
||||
for name, attr := range attrs {
|
||||
expr, err := attr.Expr.Value(nil)
|
||||
if err != nil {
|
||||
diags = append(diags, err...)
|
||||
}
|
||||
|
||||
rp := &RequiredProvider{
|
||||
Name: name,
|
||||
DeclRange: attr.Expr.Range(),
|
||||
}
|
||||
|
||||
switch {
|
||||
case expr.Type().IsPrimitiveType():
|
||||
vc, reqDiags := decodeVersionConstraint(attr)
|
||||
diags = append(diags, reqDiags...)
|
||||
reqs = append(reqs, &RequiredProvider{
|
||||
Name: name,
|
||||
Requirement: vc,
|
||||
})
|
||||
rp.Requirement = vc
|
||||
|
||||
case expr.Type().IsObjectType():
|
||||
ret := &RequiredProvider{Name: name}
|
||||
if expr.Type().HasAttribute("version") {
|
||||
vc := VersionConstraint{
|
||||
DeclRange: attr.Range,
|
||||
|
@ -64,25 +63,55 @@ func decodeRequiredProvidersBlock(block *hcl.Block) ([]*RequiredProvider, hcl.Di
|
|||
})
|
||||
} else {
|
||||
vc.Required = constraints
|
||||
ret.Requirement = vc
|
||||
rp.Requirement = vc
|
||||
}
|
||||
}
|
||||
if expr.Type().HasAttribute("source") {
|
||||
ret.Source.SourceStr = expr.GetAttr("source").AsString()
|
||||
ret.Source.DeclRange = attr.Range
|
||||
fqn, sourceDiags := addrs.ParseProviderSourceString(expr.GetAttr("source").AsString())
|
||||
|
||||
if sourceDiags.HasErrors() {
|
||||
hclDiags := sourceDiags.ToHCL()
|
||||
// The diagnostics from ParseProviderSourceString don't contain
|
||||
// source location information because it has no context to compute
|
||||
// them from, and so we'll add those in quickly here before we
|
||||
// return.
|
||||
for _, diag := range hclDiags {
|
||||
if diag.Subject == nil {
|
||||
diag.Subject = attr.Expr.Range().Ptr()
|
||||
}
|
||||
reqs = append(reqs, ret)
|
||||
}
|
||||
diags = append(diags, hclDiags...)
|
||||
} else {
|
||||
rp.Type = fqn
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
// should not happen
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid provider_requirements syntax",
|
||||
Detail: "provider_requirements entries must be strings or objects.",
|
||||
Summary: "Invalid required_providers syntax",
|
||||
Detail: "required_providers entries must be strings or objects.",
|
||||
Subject: attr.Expr.Range().Ptr(),
|
||||
})
|
||||
reqs = append(reqs, &RequiredProvider{Name: name})
|
||||
return reqs, diags
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
return reqs, diags
|
||||
|
||||
ret.RequiredProviders[rp.Name] = rp
|
||||
}
|
||||
|
||||
return ret, diags
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package configs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
@ -10,6 +8,7 @@ import (
|
|||
version "github.com/hashicorp/go-version"
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hcltest"
|
||||
"github.com/hashicorp/terraform/addrs"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
|
@ -19,18 +18,35 @@ var (
|
|||
if x.Name != y.Name {
|
||||
return false
|
||||
}
|
||||
if x.Source != y.Source {
|
||||
if x.Type != y.Type {
|
||||
return false
|
||||
}
|
||||
if x.Requirement.Required.String() != y.Requirement.Required.String() {
|
||||
return false
|
||||
}
|
||||
if x.DeclRange != y.DeclRange {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
blockRange = hcl.Range{
|
||||
Filename: "mock.tf",
|
||||
Start: hcl.Pos{Line: 3, Column: 12, Byte: 27},
|
||||
End: hcl.Pos{Line: 3, Column: 19, Byte: 34},
|
||||
}
|
||||
mockRange = hcl.Range{
|
||||
Filename: "MockExprLiteral",
|
||||
}
|
||||
)
|
||||
|
||||
func TestDecodeRequiredProvidersBlock_legacy(t *testing.T) {
|
||||
block := &hcl.Block{
|
||||
func TestDecodeRequiredProvidersBlock(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
Block *hcl.Block
|
||||
Want *RequiredProviders
|
||||
Error string
|
||||
}{
|
||||
"legacy": {
|
||||
Block: &hcl.Block{
|
||||
Type: "required_providers",
|
||||
Body: hcltest.MockBody(&hcl.BodyContent{
|
||||
Attributes: hcl.Attributes{
|
||||
|
@ -40,33 +56,22 @@ func TestDecodeRequiredProvidersBlock_legacy(t *testing.T) {
|
|||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
want := &RequiredProvider{
|
||||
DefRange: blockRange,
|
||||
},
|
||||
Want: &RequiredProviders{
|
||||
RequiredProviders: map[string]*RequiredProvider{
|
||||
"default": {
|
||||
Name: "default",
|
||||
Type: addrs.NewDefaultProvider("default"),
|
||||
Requirement: testVC("1.0.0"),
|
||||
}
|
||||
|
||||
got, diags := decodeRequiredProvidersBlock(block)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("unexpected error")
|
||||
}
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("wrong number of results, got %d, wanted 1", len(got))
|
||||
}
|
||||
if !cmp.Equal(got[0], want, ignoreUnexported, comparer) {
|
||||
t.Fatalf("wrong result:\n %s", cmp.Diff(got[0], want, ignoreUnexported, comparer))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeRequiredProvidersBlock_provider_source(t *testing.T) {
|
||||
mockRange := hcl.Range{
|
||||
Filename: "mock.tf",
|
||||
Start: hcl.Pos{Line: 3, Column: 12, Byte: 27},
|
||||
End: hcl.Pos{Line: 3, Column: 19, Byte: 34},
|
||||
}
|
||||
|
||||
block := &hcl.Block{
|
||||
DeclRange: mockRange,
|
||||
},
|
||||
},
|
||||
DeclRange: blockRange,
|
||||
},
|
||||
},
|
||||
"provider source": {
|
||||
Block: &hcl.Block{
|
||||
Type: "required_providers",
|
||||
Body: hcltest.MockBody(&hcl.BodyContent{
|
||||
Attributes: hcl.Attributes{
|
||||
|
@ -76,31 +81,25 @@ func TestDecodeRequiredProvidersBlock_provider_source(t *testing.T) {
|
|||
"source": cty.StringVal("mycloud/test"),
|
||||
"version": cty.StringVal("2.0.0"),
|
||||
})),
|
||||
Range: mockRange,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
want := &RequiredProvider{
|
||||
DefRange: blockRange,
|
||||
},
|
||||
Want: &RequiredProviders{
|
||||
RequiredProviders: map[string]*RequiredProvider{
|
||||
"my_test": {
|
||||
Name: "my_test",
|
||||
Source: Source{SourceStr: "mycloud/test", DeclRange: mockRange},
|
||||
Type: addrs.NewProvider(addrs.DefaultRegistryHost, "mycloud", "test"),
|
||||
Requirement: testVC("2.0.0"),
|
||||
}
|
||||
got, diags := decodeRequiredProvidersBlock(block)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("unexpected error")
|
||||
}
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("wrong number of results, got %d, wanted 1", len(got))
|
||||
}
|
||||
if !cmp.Equal(got[0], want, ignoreUnexported, comparer) {
|
||||
t.Fatalf("wrong result:\n %s", cmp.Diff(got[0], want, ignoreUnexported, comparer))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeRequiredProvidersBlock_mixed(t *testing.T) {
|
||||
block := &hcl.Block{
|
||||
DeclRange: mockRange,
|
||||
},
|
||||
},
|
||||
DeclRange: blockRange,
|
||||
},
|
||||
},
|
||||
"mixed": {
|
||||
Block: &hcl.Block{
|
||||
Type: "required_providers",
|
||||
Body: hcltest.MockBody(&hcl.BodyContent{
|
||||
Attributes: hcl.Attributes{
|
||||
|
@ -117,41 +116,112 @@ func TestDecodeRequiredProvidersBlock_mixed(t *testing.T) {
|
|||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
want := []*RequiredProvider{
|
||||
{
|
||||
DefRange: blockRange,
|
||||
},
|
||||
Want: &RequiredProviders{
|
||||
RequiredProviders: map[string]*RequiredProvider{
|
||||
"legacy": {
|
||||
Name: "legacy",
|
||||
Type: addrs.NewDefaultProvider("legacy"),
|
||||
Requirement: testVC("1.0.0"),
|
||||
DeclRange: mockRange,
|
||||
},
|
||||
{
|
||||
"my_test": {
|
||||
Name: "my_test",
|
||||
Source: Source{SourceStr: "mycloud/test", DeclRange: hcl.Range{}},
|
||||
Type: addrs.NewProvider(addrs.DefaultRegistryHost, "mycloud", "test"),
|
||||
Requirement: testVC("2.0.0"),
|
||||
DeclRange: mockRange,
|
||||
},
|
||||
}
|
||||
|
||||
got, diags := decodeRequiredProvidersBlock(block)
|
||||
|
||||
sort.SliceStable(got, func(i, j int) bool {
|
||||
return got[i].Name < got[j].Name
|
||||
})
|
||||
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("unexpected error")
|
||||
}
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("wrong number of results, got %d, wanted 2", len(got))
|
||||
}
|
||||
for i, rp := range want {
|
||||
if !cmp.Equal(got[i], rp, ignoreUnexported, comparer) {
|
||||
t.Fatalf("wrong result:\n %s", cmp.Diff(got[0], rp, ignoreUnexported, comparer))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeRequiredProvidersBlock_version_error(t *testing.T) {
|
||||
block := &hcl.Block{
|
||||
},
|
||||
DeclRange: blockRange,
|
||||
},
|
||||
},
|
||||
"version-only block": {
|
||||
Block: &hcl.Block{
|
||||
Type: "required_providers",
|
||||
Body: hcltest.MockBody(&hcl.BodyContent{
|
||||
Attributes: hcl.Attributes{
|
||||
"test": {
|
||||
Name: "test",
|
||||
Expr: hcltest.MockExprLiteral(cty.ObjectVal(map[string]cty.Value{
|
||||
"version": cty.StringVal("~>2.0.0"),
|
||||
})),
|
||||
},
|
||||
},
|
||||
}),
|
||||
DefRange: blockRange,
|
||||
},
|
||||
Want: &RequiredProviders{
|
||||
RequiredProviders: map[string]*RequiredProvider{
|
||||
"test": {
|
||||
Name: "test",
|
||||
Type: addrs.NewDefaultProvider("test"),
|
||||
Requirement: testVC("~>2.0.0"),
|
||||
DeclRange: mockRange,
|
||||
},
|
||||
},
|
||||
DeclRange: blockRange,
|
||||
},
|
||||
},
|
||||
"invalid source": {
|
||||
Block: &hcl.Block{
|
||||
Type: "required_providers",
|
||||
Body: hcltest.MockBody(&hcl.BodyContent{
|
||||
Attributes: hcl.Attributes{
|
||||
"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"),
|
||||
})),
|
||||
},
|
||||
},
|
||||
}),
|
||||
DefRange: blockRange,
|
||||
},
|
||||
Want: &RequiredProviders{
|
||||
RequiredProviders: map[string]*RequiredProvider{
|
||||
"my_test": {
|
||||
Name: "my_test",
|
||||
Type: addrs.Provider{},
|
||||
Requirement: testVC("~>2.0.0"),
|
||||
DeclRange: mockRange,
|
||||
},
|
||||
},
|
||||
DeclRange: blockRange,
|
||||
},
|
||||
Error: "Invalid provider source string",
|
||||
},
|
||||
"localname is invalid provider name": {
|
||||
Block: &hcl.Block{
|
||||
Type: "required_providers",
|
||||
Body: hcltest.MockBody(&hcl.BodyContent{
|
||||
Attributes: hcl.Attributes{
|
||||
"my_test": {
|
||||
Name: "my_test",
|
||||
Expr: hcltest.MockExprLiteral(cty.ObjectVal(map[string]cty.Value{
|
||||
"version": cty.StringVal("~>2.0.0"),
|
||||
})),
|
||||
},
|
||||
},
|
||||
}),
|
||||
DefRange: blockRange,
|
||||
},
|
||||
Want: &RequiredProviders{
|
||||
RequiredProviders: map[string]*RequiredProvider{
|
||||
"my_test": {
|
||||
Name: "my_test",
|
||||
Type: addrs.Provider{},
|
||||
Requirement: testVC("~>2.0.0"),
|
||||
DeclRange: mockRange,
|
||||
},
|
||||
},
|
||||
DeclRange: blockRange,
|
||||
},
|
||||
Error: "Invalid provider name",
|
||||
},
|
||||
"version constraint error": {
|
||||
Block: &hcl.Block{
|
||||
Type: "required_providers",
|
||||
Body: hcltest.MockBody(&hcl.BodyContent{
|
||||
Attributes: hcl.Attributes{
|
||||
|
@ -164,28 +234,65 @@ func TestDecodeRequiredProvidersBlock_version_error(t *testing.T) {
|
|||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
want := []*RequiredProvider{
|
||||
{
|
||||
DefRange: blockRange,
|
||||
},
|
||||
Want: &RequiredProviders{
|
||||
RequiredProviders: map[string]*RequiredProvider{
|
||||
"my_test": {
|
||||
Name: "my_test",
|
||||
Source: Source{SourceStr: "mycloud/test", DeclRange: hcl.Range{}},
|
||||
Type: addrs.NewProvider(addrs.DefaultRegistryHost, "mycloud", "test"),
|
||||
DeclRange: mockRange,
|
||||
},
|
||||
},
|
||||
DeclRange: blockRange,
|
||||
},
|
||||
Error: "Invalid version constraint",
|
||||
},
|
||||
"invalid required_providers attribute value": {
|
||||
Block: &hcl.Block{
|
||||
Type: "required_providers",
|
||||
Body: hcltest.MockBody(&hcl.BodyContent{
|
||||
Attributes: hcl.Attributes{
|
||||
"test": {
|
||||
Name: "test",
|
||||
Expr: hcltest.MockExprLiteral(cty.ListVal([]cty.Value{cty.StringVal("2.0.0")})),
|
||||
},
|
||||
},
|
||||
}),
|
||||
DefRange: blockRange,
|
||||
},
|
||||
Want: &RequiredProviders{
|
||||
RequiredProviders: map[string]*RequiredProvider{
|
||||
"test": {
|
||||
Name: "test",
|
||||
Type: addrs.NewDefaultProvider("test"),
|
||||
DeclRange: mockRange,
|
||||
},
|
||||
},
|
||||
DeclRange: blockRange,
|
||||
},
|
||||
Error: "Invalid required_providers syntax",
|
||||
},
|
||||
}
|
||||
|
||||
got, diags := decodeRequiredProvidersBlock(block)
|
||||
if !diags.HasErrors() {
|
||||
t.Fatalf("expected error, got success")
|
||||
} else {
|
||||
fmt.Printf(diags[0].Summary)
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, diags := decodeRequiredProvidersBlock(test.Block)
|
||||
if diags.HasErrors() {
|
||||
if test.Error == "" {
|
||||
t.Fatalf("unexpected error")
|
||||
}
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("wrong number of results, got %d, wanted 1", len(got))
|
||||
if gotErr := diags[0].Summary; gotErr != test.Error {
|
||||
t.Errorf("wrong error, got %q, want %q", gotErr, test.Error)
|
||||
}
|
||||
for i, rp := range want {
|
||||
if !cmp.Equal(got[i], rp, ignoreUnexported, comparer) {
|
||||
t.Fatalf("wrong result:\n %s", cmp.Diff(got[0], rp, ignoreUnexported, comparer))
|
||||
} else if test.Error != "" {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
|
||||
if !cmp.Equal(got, test.Want, ignoreUnexported, comparer) {
|
||||
t.Fatalf("wrong result:\n %s", cmp.Diff(got, test.Want, ignoreUnexported, comparer))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
terraform {
|
||||
required_providers {
|
||||
bar = {
|
||||
version = "~>1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
terraform {
|
||||
required_providers {
|
||||
foo = {
|
||||
version = "~>2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
resource test_instance "my-instance" {
|
||||
provider = test
|
||||
}
|
8
configs/testdata/valid-modules/required-providers-after-resource/providers.tf
vendored
Normal file
8
configs/testdata/valid-modules/required-providers-after-resource/providers.tf
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
terraform {
|
||||
required_providers {
|
||||
test = {
|
||||
source = "foo/test"
|
||||
version = "~>1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
9
configs/testdata/valid-modules/required-providers-overrides/bar_provider_override.tf
vendored
Normal file
9
configs/testdata/valid-modules/required-providers-overrides/bar_provider_override.tf
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
terraform {
|
||||
required_providers {
|
||||
bar = {
|
||||
source = "blorp/bar"
|
||||
version = "~>2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
resource bar_thing "bt" {
|
||||
provider = bar
|
||||
}
|
||||
|
||||
resource foo_thing "ft" {
|
||||
provider = foo
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
terraform {
|
||||
required_providers {
|
||||
bar = {
|
||||
source = "acme/bar"
|
||||
}
|
||||
|
||||
foo = {
|
||||
source = "acme/foo"
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue