configs: extend module.ProviderRequirements to include the addrs.Provider instead of just version constraints. (#23843)

Renamed file.ProviderRequirements to file.RequiredProviders to match the
name of the block in the configuration. file.RequiredProviders contains
the contents of the file(s); module.ProviderRequirements contains the
parsed and merged provider requirements.

Extended decodeRequiredProvidersBlock to parse the new provider source
syntax (version only, it will ignore any other attributes).

Added some tests; swapped deep.Equal with cmp.Equal in the
terraform/module_dependencies_test.go because deep was not catching
incorrect constraints.
This commit is contained in:
Kristin Laemmert 2020-01-13 11:31:47 -05:00 committed by GitHub
parent 0ca6f743a4
commit 272cb44d3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 148 additions and 29 deletions

View File

@ -30,7 +30,7 @@ type Module struct {
Backend *Backend Backend *Backend
ProviderConfigs map[string]*Provider ProviderConfigs map[string]*Provider
ProviderRequirements map[string][]VersionConstraint ProviderRequirements map[string]ProviderRequirements
Variables map[string]*Variable Variables map[string]*Variable
Locals map[string]*Local Locals map[string]*Local
@ -58,9 +58,9 @@ type File struct {
ActiveExperiments experiments.Set ActiveExperiments experiments.Set
Backends []*Backend Backends []*Backend
ProviderConfigs []*Provider ProviderConfigs []*Provider
ProviderRequirements []*ProviderRequirement RequiredProviders []*RequiredProvider
Variables []*Variable Variables []*Variable
Locals []*Local Locals []*Local
@ -84,7 +84,7 @@ func NewModule(primaryFiles, overrideFiles []*File) (*Module, hcl.Diagnostics) {
var diags hcl.Diagnostics var diags hcl.Diagnostics
mod := &Module{ mod := &Module{
ProviderConfigs: map[string]*Provider{}, ProviderConfigs: map[string]*Provider{},
ProviderRequirements: map[string][]VersionConstraint{}, ProviderRequirements: map[string]ProviderRequirements{},
Variables: map[string]*Variable{}, Variables: map[string]*Variable{},
Locals: map[string]*Local{}, Locals: map[string]*Local{},
Outputs: map[string]*Output{}, Outputs: map[string]*Output{},
@ -103,8 +103,7 @@ func NewModule(primaryFiles, overrideFiles []*File) (*Module, hcl.Diagnostics) {
diags = append(diags, fileDiags...) diags = append(diags, fileDiags...)
} }
moreDiags := checkModuleExperiments(mod) diags = append(diags, checkModuleExperiments(mod)...)
diags = append(diags, moreDiags...)
return mod, diags return mod, diags
} }
@ -170,8 +169,22 @@ func (m *Module) appendFile(file *File) hcl.Diagnostics {
m.ProviderConfigs[key] = pc m.ProviderConfigs[key] = pc
} }
for _, reqd := range file.ProviderRequirements { for _, reqd := range file.RequiredProviders {
m.ProviderRequirements[reqd.Name] = append(m.ProviderRequirements[reqd.Name], reqd.Requirement) // TODO: once the remaining provider source functionality is
// implemented, get addrs.Provider from source if set, or
// addrs.NewDefaultProvider(name) if not
if reqd.Source != "" {
panic("source is not yet supported")
}
fqn := addrs.NewLegacyProvider(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 _, v := range file.Variables { for _, v := range file.Variables {
@ -314,8 +327,8 @@ func (m *Module) mergeFile(file *File) hcl.Diagnostics {
} }
} }
if len(file.ProviderRequirements) != 0 { if len(file.RequiredProviders) != 0 {
mergeProviderVersionConstraints(m.ProviderRequirements, file.ProviderRequirements) mergeProviderVersionConstraints(m.ProviderRequirements, file.RequiredProviders)
} }
for _, v := range file.Variables { for _, v := range file.Variables {

View File

@ -35,7 +35,7 @@ func (p *Provider) merge(op *Provider) hcl.Diagnostics {
return diags return diags
} }
func mergeProviderVersionConstraints(recv map[string][]VersionConstraint, ovrd []*ProviderRequirement) { func mergeProviderVersionConstraints(recv map[string]ProviderRequirements, ovrd []*RequiredProvider) {
// Any provider name that's mentioned in the override gets nilled out in // 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 // our map so that we'll rebuild it below. Any provider not mentioned is
// left unchanged. // left unchanged.
@ -43,7 +43,8 @@ func mergeProviderVersionConstraints(recv map[string][]VersionConstraint, ovrd [
delete(recv, reqd.Name) delete(recv, reqd.Name)
} }
for _, reqd := range ovrd { for _, reqd := range ovrd {
recv[reqd.Name] = append(recv[reqd.Name], reqd.Requirement) fqn := addrs.NewLegacyProvider(reqd.Name)
recv[reqd.Name] = ProviderRequirements{Type: fqn, VersionConstraints: []VersionConstraint{reqd.Requirement}}
} }
} }

View File

@ -3,8 +3,10 @@ package configs
import ( import (
"testing" "testing"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl" "github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/terraform/addrs"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
) )
@ -199,3 +201,77 @@ func TestModuleOverrideDynamic(t *testing.T) {
} }
}) })
} }
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.NewLegacyProvider("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.NewLegacyProvider("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)
})
}
}

View File

@ -75,7 +75,7 @@ func (p *Parser) loadConfigFile(path string, override bool) (*File, hcl.Diagnost
case "required_providers": case "required_providers":
reqs, reqsDiags := decodeRequiredProvidersBlock(innerBlock) reqs, reqsDiags := decodeRequiredProvidersBlock(innerBlock)
diags = append(diags, reqsDiags...) diags = append(diags, reqsDiags...)
file.ProviderRequirements = append(file.ProviderRequirements, reqs...) file.RequiredProviders = append(file.RequiredProviders, reqs...)
default: default:
// Should never happen because the above cases should be exhaustive // Should never happen because the above cases should be exhaustive

View File

@ -3,22 +3,31 @@ package configs
import ( import (
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"
) )
// ProviderRequirement represents a declaration of a dependency on a particular // RequiredProvider represents a declaration of a dependency on a particular
// provider version without actually configuring that provider. This is used in // provider version without actually configuring that provider. This is used in
// child modules that expect a provider to be passed in from their parent. // child modules that expect a provider to be passed in from their parent.
// //
// TODO: "Source" is a placeholder for an attribute that is not yet supported. // TODO: "Source" is a placeholder for an attribute that is not yet supported.
type ProviderRequirement struct { type RequiredProvider struct {
Name string Name string
Source string // TODO Source string // TODO
Requirement VersionConstraint Requirement VersionConstraint
} }
func decodeRequiredProvidersBlock(block *hcl.Block) ([]*ProviderRequirement, hcl.Diagnostics) { // ProviderRequirements represents merged provider version constraints.
// VersionConstraints come from terraform.require_providers blocks and provider
// blocks.
type ProviderRequirements struct {
Type addrs.Provider
VersionConstraints []VersionConstraint
}
func decodeRequiredProvidersBlock(block *hcl.Block) ([]*RequiredProvider, hcl.Diagnostics) {
attrs, diags := block.Body.JustAttributes() attrs, diags := block.Body.JustAttributes()
var reqs []*ProviderRequirement var reqs []*RequiredProvider
for name, attr := range attrs { for name, attr := range attrs {
expr, err := attr.Expr.Value(nil) expr, err := attr.Expr.Value(nil)
if err != nil { if err != nil {
@ -29,7 +38,7 @@ func decodeRequiredProvidersBlock(block *hcl.Block) ([]*ProviderRequirement, hcl
case expr.Type().IsPrimitiveType(): case expr.Type().IsPrimitiveType():
vc, reqDiags := decodeVersionConstraint(attr) vc, reqDiags := decodeVersionConstraint(attr)
diags = append(diags, reqDiags...) diags = append(diags, reqDiags...)
reqs = append(reqs, &ProviderRequirement{ reqs = append(reqs, &RequiredProvider{
Name: name, Name: name,
Requirement: vc, Requirement: vc,
}) })
@ -49,14 +58,14 @@ func decodeRequiredProvidersBlock(block *hcl.Block) ([]*ProviderRequirement, hcl
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: attr.Expr.Range().Ptr(),
}) })
reqs = append(reqs, &ProviderRequirement{Name: name}) reqs = append(reqs, &RequiredProvider{Name: name})
return reqs, diags return reqs, diags
} }
vc.Required = constraints vc.Required = constraints
reqs = append(reqs, &ProviderRequirement{Name: name, Requirement: vc}) reqs = append(reqs, &RequiredProvider{Name: name, Requirement: vc})
} }
// No version // No version
reqs = append(reqs, &ProviderRequirement{Name: name}) reqs = append(reqs, &RequiredProvider{Name: name})
default: default:
// should not happen // should not happen
diags = append(diags, &hcl.Diagnostic{ diags = append(diags, &hcl.Diagnostic{
@ -65,7 +74,7 @@ func decodeRequiredProvidersBlock(block *hcl.Block) ([]*ProviderRequirement, hcl
Detail: "provider_requirements entries must be strings or objects.", Detail: "provider_requirements entries must be strings or objects.",
Subject: attr.Expr.Range().Ptr(), Subject: attr.Expr.Range().Ptr(),
}) })
reqs = append(reqs, &ProviderRequirement{Name: name}) reqs = append(reqs, &RequiredProvider{Name: name})
return reqs, diags return reqs, diags
} }
} }

View File

@ -58,9 +58,8 @@ func configTreeConfigDependencies(root *configs.Config, inheritProviders map[str
// The main way to declare a provider dependency is explicitly inside // The main way to declare a provider dependency is explicitly inside
// the "terraform" block, which allows declaring a requirement without // the "terraform" block, which allows declaring a requirement without
// also creating a configuration. // also creating a configuration.
for fullName, constraints := range module.ProviderRequirements { for fullName, req := range module.ProviderRequirements {
inst := moduledeps.ProviderInstance(fullName) inst := moduledeps.ProviderInstance(fullName)
// The handling here is a bit fiddly because the moduledeps package // The handling here is a bit fiddly because the moduledeps package
// was designed around the legacy (pre-0.12) configuration model // was designed around the legacy (pre-0.12) configuration model
// and hasn't yet been revised to handle the new model. As a result, // and hasn't yet been revised to handle the new model. As a result,
@ -69,7 +68,7 @@ func configTreeConfigDependencies(root *configs.Config, inheritProviders map[str
// can also retain the source location of each constraint, for // can also retain the source location of each constraint, for
// more informative output from the "terraform providers" command. // more informative output from the "terraform providers" command.
var rawConstraints version.Constraints var rawConstraints version.Constraints
for _, constraint := range constraints { for _, constraint := range req.VersionConstraints {
rawConstraints = append(rawConstraints, constraint.Required...) rawConstraints = append(rawConstraints, constraint.Required...)
} }
discoConstraints := discovery.NewConstraints(rawConstraints) discoConstraints := discovery.NewConstraints(rawConstraints)

View File

@ -3,7 +3,7 @@ package terraform
import ( import (
"testing" "testing"
"github.com/go-test/deep" "github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/moduledeps" "github.com/hashicorp/terraform/moduledeps"
@ -52,6 +52,20 @@ func TestModuleTreeDependencies(t *testing.T) {
Children: nil, Children: nil,
}, },
}, },
"required_providers block": {
"module-deps-required-providers",
nil,
&moduledeps.Module{
Name: "root",
Providers: moduledeps.Providers{
"foo": moduledeps.ProviderDependency{
Constraints: discovery.ConstraintStr(">=1.0.0").MustParse(),
Reason: moduledeps.ProviderDependencyExplicit,
},
},
Children: nil,
},
},
"explicit provider unconstrained": { "explicit provider unconstrained": {
"module-deps-explicit-provider-unconstrained", "module-deps-explicit-provider-unconstrained",
nil, nil,
@ -251,8 +265,8 @@ func TestModuleTreeDependencies(t *testing.T) {
} }
got := ConfigTreeDependencies(root, MustShimLegacyState(test.State)) got := ConfigTreeDependencies(root, MustShimLegacyState(test.State))
for _, problem := range deep.Equal(got, test.Want) { if !cmp.Equal(got, test.Want) {
t.Error(problem) t.Error(cmp.Diff(got, test.Want))
} }
}) })
} }

View File

@ -0,0 +1,7 @@
terraform {
required_providers {
foo = {
version = ">=1.0.0"
}
}
}