diff --git a/configs/module.go b/configs/module.go index a5ba28581..010dc9aee 100644 --- a/configs/module.go +++ b/configs/module.go @@ -30,7 +30,7 @@ type Module struct { Backend *Backend ProviderConfigs map[string]*Provider - ProviderRequirements map[string][]VersionConstraint + ProviderRequirements map[string]ProviderRequirements Variables map[string]*Variable Locals map[string]*Local @@ -58,9 +58,9 @@ type File struct { ActiveExperiments experiments.Set - Backends []*Backend - ProviderConfigs []*Provider - ProviderRequirements []*ProviderRequirement + Backends []*Backend + ProviderConfigs []*Provider + RequiredProviders []*RequiredProvider Variables []*Variable Locals []*Local @@ -84,7 +84,7 @@ func NewModule(primaryFiles, overrideFiles []*File) (*Module, hcl.Diagnostics) { var diags hcl.Diagnostics mod := &Module{ ProviderConfigs: map[string]*Provider{}, - ProviderRequirements: map[string][]VersionConstraint{}, + ProviderRequirements: map[string]ProviderRequirements{}, Variables: map[string]*Variable{}, Locals: map[string]*Local{}, Outputs: map[string]*Output{}, @@ -103,8 +103,7 @@ func NewModule(primaryFiles, overrideFiles []*File) (*Module, hcl.Diagnostics) { diags = append(diags, fileDiags...) } - moreDiags := checkModuleExperiments(mod) - diags = append(diags, moreDiags...) + diags = append(diags, checkModuleExperiments(mod)...) return mod, diags } @@ -170,8 +169,22 @@ func (m *Module) appendFile(file *File) hcl.Diagnostics { m.ProviderConfigs[key] = pc } - for _, reqd := range file.ProviderRequirements { - m.ProviderRequirements[reqd.Name] = append(m.ProviderRequirements[reqd.Name], reqd.Requirement) + for _, reqd := range file.RequiredProviders { + // 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 { @@ -314,8 +327,8 @@ func (m *Module) mergeFile(file *File) hcl.Diagnostics { } } - if len(file.ProviderRequirements) != 0 { - mergeProviderVersionConstraints(m.ProviderRequirements, file.ProviderRequirements) + if len(file.RequiredProviders) != 0 { + mergeProviderVersionConstraints(m.ProviderRequirements, file.RequiredProviders) } for _, v := range file.Variables { diff --git a/configs/module_merge.go b/configs/module_merge.go index 401b1c0a8..6ab19c451 100644 --- a/configs/module_merge.go +++ b/configs/module_merge.go @@ -35,7 +35,7 @@ func (p *Provider) merge(op *Provider) hcl.Diagnostics { 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 // our map so that we'll rebuild it below. Any provider not mentioned is // left unchanged. @@ -43,7 +43,8 @@ func mergeProviderVersionConstraints(recv map[string][]VersionConstraint, ovrd [ delete(recv, reqd.Name) } 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}} } } diff --git a/configs/module_merge_test.go b/configs/module_merge_test.go index 6d3b4a2c4..4575339d3 100644 --- a/configs/module_merge_test.go +++ b/configs/module_merge_test.go @@ -3,8 +3,10 @@ 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" "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) + }) + } +} diff --git a/configs/parser_config.go b/configs/parser_config.go index c592dfa45..e8f94721d 100644 --- a/configs/parser_config.go +++ b/configs/parser_config.go @@ -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.ProviderRequirements = append(file.ProviderRequirements, reqs...) + file.RequiredProviders = append(file.RequiredProviders, reqs...) default: // Should never happen because the above cases should be exhaustive diff --git a/configs/provider_requirements.go b/configs/provider_requirements.go index 0347b1117..f2a78e501 100644 --- a/configs/provider_requirements.go +++ b/configs/provider_requirements.go @@ -3,22 +3,31 @@ package configs import ( version "github.com/hashicorp/go-version" "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 // 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. -type ProviderRequirement struct { +type RequiredProvider struct { Name string Source string // TODO 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() - var reqs []*ProviderRequirement + var reqs []*RequiredProvider for name, attr := range attrs { expr, err := attr.Expr.Value(nil) if err != nil { @@ -29,7 +38,7 @@ func decodeRequiredProvidersBlock(block *hcl.Block) ([]*ProviderRequirement, hcl case expr.Type().IsPrimitiveType(): vc, reqDiags := decodeVersionConstraint(attr) diags = append(diags, reqDiags...) - reqs = append(reqs, &ProviderRequirement{ + reqs = append(reqs, &RequiredProvider{ Name: name, Requirement: vc, }) @@ -49,14 +58,14 @@ func decodeRequiredProvidersBlock(block *hcl.Block) ([]*ProviderRequirement, hcl Detail: "This string does not use correct version constraint syntax.", Subject: attr.Expr.Range().Ptr(), }) - reqs = append(reqs, &ProviderRequirement{Name: name}) + reqs = append(reqs, &RequiredProvider{Name: name}) return reqs, diags } vc.Required = constraints - reqs = append(reqs, &ProviderRequirement{Name: name, Requirement: vc}) + reqs = append(reqs, &RequiredProvider{Name: name, Requirement: vc}) } // No version - reqs = append(reqs, &ProviderRequirement{Name: name}) + reqs = append(reqs, &RequiredProvider{Name: name}) default: // should not happen 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.", Subject: attr.Expr.Range().Ptr(), }) - reqs = append(reqs, &ProviderRequirement{Name: name}) + reqs = append(reqs, &RequiredProvider{Name: name}) return reqs, diags } } diff --git a/terraform/module_dependencies.go b/terraform/module_dependencies.go index 66a68c7de..0eecf9f42 100644 --- a/terraform/module_dependencies.go +++ b/terraform/module_dependencies.go @@ -58,9 +58,8 @@ func configTreeConfigDependencies(root *configs.Config, inheritProviders map[str // The main way to declare a provider dependency is explicitly inside // the "terraform" block, which allows declaring a requirement without // also creating a configuration. - for fullName, constraints := range module.ProviderRequirements { + for fullName, req := range module.ProviderRequirements { inst := moduledeps.ProviderInstance(fullName) - // The handling here is a bit fiddly because the moduledeps package // was designed around the legacy (pre-0.12) configuration model // 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 // more informative output from the "terraform providers" command. var rawConstraints version.Constraints - for _, constraint := range constraints { + for _, constraint := range req.VersionConstraints { rawConstraints = append(rawConstraints, constraint.Required...) } discoConstraints := discovery.NewConstraints(rawConstraints) diff --git a/terraform/module_dependencies_test.go b/terraform/module_dependencies_test.go index 64a8edbbb..917ffc0bc 100644 --- a/terraform/module_dependencies_test.go +++ b/terraform/module_dependencies_test.go @@ -3,7 +3,7 @@ package terraform import ( "testing" - "github.com/go-test/deep" + "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/moduledeps" @@ -52,6 +52,20 @@ func TestModuleTreeDependencies(t *testing.T) { 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": { "module-deps-explicit-provider-unconstrained", nil, @@ -251,8 +265,8 @@ func TestModuleTreeDependencies(t *testing.T) { } got := ConfigTreeDependencies(root, MustShimLegacyState(test.State)) - for _, problem := range deep.Equal(got, test.Want) { - t.Error(problem) + if !cmp.Equal(got, test.Want) { + t.Error(cmp.Diff(got, test.Want)) } }) } diff --git a/terraform/testdata/module-deps-required-providers/main.tf b/terraform/testdata/module-deps-required-providers/main.tf new file mode 100644 index 000000000..e39cc897b --- /dev/null +++ b/terraform/testdata/module-deps-required-providers/main.tf @@ -0,0 +1,7 @@ +terraform { + required_providers { + foo = { + version = ">=1.0.0" + } + } +}