Add a new warning when a provider cannot be downloaded and it was requested by an implicit usage (#2479)

Signed-off-by: yottta <andrei.ciobanu@opentofu.org>
Co-authored-by: AbstractionFactory <179820029+abstractionfactory@users.noreply.github.com>
This commit is contained in:
Andrei Ciobanu 2025-02-12 18:40:54 +02:00 committed by GitHub
parent 5ff40ae505
commit dfe2876931
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 313 additions and 23 deletions

View File

@ -18,6 +18,7 @@ ENHANCEMENTS:
* The `element` function now accepts negative indices, which extends the existing "wrapping" model into the negative direction. In particular, choosing element `-1` selects the final element in the sequence. ([#2371](https://github.com/opentofu/opentofu/pull/2371))
* `moved` now supports moving between different types ([#2370](https://github.com/opentofu/opentofu/pull/2370))
* `moved` block can now be used to migrate from the `null_resource` to the `terraform_data` resource. ([#2481](https://github.com/opentofu/opentofu/pull/2481))
* Warn on implicit references of providers without a `required_providers` entry. ([#2084](https://github.com/opentofu/opentofu/issues/2084))
BUG FIXES:

View File

@ -502,6 +502,68 @@ func TestInitProviderNotFound(t *testing.T) {
t.Errorf("wrong output:\n%s", cmp.Diff(stripAnsi(stderr), expectedErr))
}
})
t.Run("implicit provider resource and data not found", func(t *testing.T) {
implicitFixturePath := filepath.Join("testdata", "provider-implicit-ref-not-found/implicit-by-resource-and-data")
tf := e2e.NewBinary(t, tofuBin, implicitFixturePath)
stdout, _, err := tf.Run("init")
if err == nil {
t.Fatal("expected error, got success")
}
// Testing that the warn wrote to the user is containing the resource address from where the provider
// was registered to be downloaded
expectedContentInOutput := []string{
`(and one more similar warning elsewhere)`,
`
Warning: Automatically-inferred provider dependency
on main.tf line 2:
2: resource "nonexistingProv_res" "test1" {
Due to the prefix of the resource type name OpenTofu guessed that you
intended to associate nonexistingProv_res.test1 with a provider whose local
name is "nonexistingprov", but that name is not declared in this module's
required_providers block. OpenTofu therefore guessed that you intended to
use hashicorp/nonexistingprov, but that provider does not exist.
Make at least one of the following changes to tell OpenTofu which provider
to use:
- Add a declaration for local name "nonexistingprov" to this module's
required_providers block, specifying the full source address for the
provider you intended to use.
- Verify that "nonexistingProv_res" is the correct resource type name to
use. Did you omit a prefix which would imply the correct provider?
- Use a "provider" argument within this resource block to override
OpenTofu's automatic selection of the local name "nonexistingprov".
`}
for _, expectedOutput := range expectedContentInOutput {
if cleanOut := strings.TrimSpace(stripAnsi(stdout)); !strings.Contains(cleanOut, expectedOutput) {
t.Errorf("wrong output.\n\toutput:\n%s\n\n\tdoes not contain:\n%s", cleanOut, expectedOutput)
}
}
})
t.Run("resource pointing to a not configured provider does not warn on implicit reference", func(t *testing.T) {
implicitFixturePath := filepath.Join("testdata", "provider-implicit-ref-not-found/resource-with-provider-attribute")
tf := e2e.NewBinary(t, tofuBin, implicitFixturePath)
stdout, _, err := tf.Run("init")
if err == nil {
t.Fatal("expected error, got success")
}
// Ensure that the output does not contain the warning since the resource is pointing already to a specific
// provider (even though it is misspelled)
expectedOutput := `Initializing the backend...
Initializing provider plugins...
- Finding latest version of hashicorp/asw...`
if cleanOut := strings.TrimSpace(stripAnsi(stdout)); cleanOut != expectedOutput {
t.Errorf("wrong output:\n%s", cmp.Diff(cleanOut, expectedOutput))
}
})
}
// The following test is temporarily removed until the OpenTofu registry returns a deprecation warning

View File

@ -0,0 +1,10 @@
# This is for testing that the implicitly defined providers cannot be fetched and the user is getting an info of the root cause
resource "nonexistingProv_res" "test1" {
}
data "nonexistingProv2_data" "test2" {
}
module "testmod" {
source = "./mod"
}

View File

@ -0,0 +1,2 @@
resource "nonexistingProv_res" "test2" {
}

View File

@ -0,0 +1,6 @@
// when a resource is pointing to a provider that is missing required_providers definition, tofu does not show the warn
// about implicit reference of a provider
resource "aws_iam_role" "test" {
assume_role_policy = "test"
provider = asw.test
}

View File

@ -566,7 +566,7 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config,
// First we'll collect all the provider dependencies we can see in the
// configuration and the state.
reqs, hclDiags := config.ProviderRequirements()
reqs, qualifs, hclDiags := config.ProviderRequirements()
diags = diags.Append(hclDiags)
if hclDiags.HasErrors() {
return false, true, diags
@ -712,6 +712,9 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config,
suggestion += "\n\nIf you believe this provider is missing from the registry, please submit a issue on the OpenTofu Registry https://github.com/opentofu/registry/issues/new/choose"
}
warnDiags := warnOnFailedImplicitProvReference(provider, qualifs)
diags = diags.Append(warnDiags)
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to query available provider packages",
@ -1039,6 +1042,43 @@ version control system if they represent changes you intended to make.`))
return true, false, diags
}
// warnOnFailedImplicitProvReference returns a warn diagnostic when the downloader fails to fetch a provider that is implicitly referenced.
// In other words, if the failed to download provider is having no required_providers entry, this function is trying to give to the user
// more information on the source of the issue and gives also instructions on how to fix it.
func warnOnFailedImplicitProvReference(provider addrs.Provider, qualifs *getproviders.ProvidersQualification) tfdiags.Diagnostics {
if _, ok := qualifs.Explicit[provider]; ok {
return nil
}
refs, ok := qualifs.Implicit[provider]
if !ok || len(refs) == 0 {
// If there is no implicit reference for that provider, do not write the warn, let just the error to be returned.
return nil
}
// NOTE: if needed, in the future we can use the rest of the "refs" to print all the culprits or at least to give
// a hint on how many resources are causing this
ref := refs[0]
if ref.ProviderAttribute {
return nil
}
details := fmt.Sprintf(
implicitProviderReferenceBody,
ref.CfgRes.String(),
provider.Type,
provider.ForDisplay(),
provider.Type,
ref.CfgRes.Resource.Type,
provider.Type,
)
return tfdiags.Diagnostics{}.Append(
&hcl.Diagnostic{
Severity: hcl.DiagWarning,
Subject: ref.Ref.ToHCL().Ptr(),
Summary: implicitProviderReferenceHead,
Detail: details,
})
}
// backendConfigOverrideBody interprets the raw values of -backend-config
// arguments into a hcl Body that should override the backend settings given
// in the configuration.
@ -1379,3 +1419,14 @@ The current .terraform.lock.hcl file only includes checksums for %s, so OpenTofu
To calculate additional checksums for another platform, run:
tofu providers lock -platform=linux_amd64
(where linux_amd64 is the platform to generate)`
const implicitProviderReferenceHead = `Automatically-inferred provider dependency`
const implicitProviderReferenceBody = `Due to the prefix of the resource type name OpenTofu guessed that you intended to associate %s with a provider whose local name is "%s", but that name is not declared in this module's required_providers block. OpenTofu therefore guessed that you intended to use %s, but that provider does not exist.
Make at least one of the following changes to tell OpenTofu which provider to use:
- Add a declaration for local name "%s" to this module's required_providers block, specifying the full source address for the provider you intended to use.
- Verify that "%s" is the correct resource type name to use. Did you omit a prefix which would imply the correct provider?
- Use a "provider" argument within this resource block to override OpenTofu's automatic selection of the local name "%s".
`

View File

@ -124,7 +124,7 @@ func (c *ProvidersLockCommand) Run(args []string) int {
config, confDiags := c.loadConfig(".")
diags = diags.Append(confDiags)
reqs, hclDiags := config.ProviderRequirements()
reqs, _, hclDiags := config.ProviderRequirements()
diags = diags.Append(hclDiags)
// If we have explicit provider selections on the command line then

View File

@ -83,7 +83,7 @@ func (c *ProvidersMirrorCommand) Run(args []string) int {
config, confDiags := c.loadConfig(".")
diags = diags.Append(confDiags)
reqs, moreDiags := config.ProviderRequirements()
reqs, _, moreDiags := config.ProviderRequirements()
diags = diags.Append(moreDiags)
// Read lock file

View File

@ -12,10 +12,10 @@ import (
version "github.com/hashicorp/go-version"
"github.com/hashicorp/hcl/v2"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/depsfile"
"github.com/opentofu/opentofu/internal/getproviders"
"github.com/opentofu/opentofu/internal/tfdiags"
)
// A Config is a node in the tree of modules within a configuration.
@ -230,7 +230,7 @@ func (c *Config) EntersNewPackage() bool {
func (c *Config) VerifyDependencySelections(depLocks *depsfile.Locks) []error {
var errs []error
reqs, diags := c.ProviderRequirements()
reqs, _, diags := c.ProviderRequirements()
if diags.HasErrors() {
// It should be very unusual to get here, but unfortunately we can
// end up here in some edge cases where the config loader doesn't
@ -301,11 +301,12 @@ func (c *Config) VerifyDependencySelections(depLocks *depsfile.Locks) []error {
//
// If the returned diagnostics includes errors then the resulting Requirements
// may be incomplete.
func (c *Config) ProviderRequirements() (getproviders.Requirements, hcl.Diagnostics) {
func (c *Config) ProviderRequirements() (getproviders.Requirements, *getproviders.ProvidersQualification, hcl.Diagnostics) {
reqs := make(getproviders.Requirements)
diags := c.addProviderRequirements(reqs, true, true)
qualifs := new(getproviders.ProvidersQualification)
diags := c.addProviderRequirements(reqs, qualifs, true, true)
return reqs, diags
return reqs, qualifs, diags
}
// ProviderRequirementsShallow searches only the direct receiver for explicit
@ -315,7 +316,8 @@ func (c *Config) ProviderRequirements() (getproviders.Requirements, hcl.Diagnost
// may be incomplete.
func (c *Config) ProviderRequirementsShallow() (getproviders.Requirements, hcl.Diagnostics) {
reqs := make(getproviders.Requirements)
diags := c.addProviderRequirements(reqs, false, true)
qualifs := new(getproviders.ProvidersQualification)
diags := c.addProviderRequirements(reqs, qualifs, false, true)
return reqs, diags
}
@ -328,7 +330,8 @@ func (c *Config) ProviderRequirementsShallow() (getproviders.Requirements, hcl.D
// may be incomplete.
func (c *Config) ProviderRequirementsByModule() (*ModuleRequirements, hcl.Diagnostics) {
reqs := make(getproviders.Requirements)
diags := c.addProviderRequirements(reqs, false, false)
qualifs := new(getproviders.ProvidersQualification)
diags := c.addProviderRequirements(reqs, qualifs, false, false)
children := make(map[string]*ModuleRequirements)
for name, child := range c.Children {
@ -378,7 +381,7 @@ func (c *Config) ProviderRequirementsByModule() (*ModuleRequirements, hcl.Diagno
// implementation, gradually mutating a shared requirements object to
// eventually return. If the recurse argument is true, the requirements will
// include all descendant modules; otherwise, only the specified module.
func (c *Config) addProviderRequirements(reqs getproviders.Requirements, recurse, tests bool) hcl.Diagnostics {
func (c *Config) addProviderRequirements(reqs getproviders.Requirements, qualifs *getproviders.ProvidersQualification, recurse, tests bool) hcl.Diagnostics {
var diags hcl.Diagnostics
// First we'll deal with the requirements directly in _our_ module...
@ -409,6 +412,7 @@ func (c *Config) addProviderRequirements(reqs getproviders.Requirements, recurse
})
}
reqs[fqn] = append(reqs[fqn], constraints...)
qualifs.AddExplicitProvider(providerReqs.Type)
}
}
@ -418,17 +422,42 @@ func (c *Config) addProviderRequirements(reqs getproviders.Requirements, recurse
for _, rc := range c.Module.ManagedResources {
fqn := rc.Provider
if _, exists := reqs[fqn]; exists {
// If this is called for a child module, and the provider was added from another implicit reference and not
// from a top level required_provider, we need to collect the reference of this resource as well as implicit provider.
qualifs.AddImplicitProvider(fqn, getproviders.ResourceRef{
CfgRes: rc.Addr().InModule(c.Path),
Ref: tfdiags.SourceRangeFromHCL(rc.DeclRange),
ProviderAttribute: rc.ProviderConfigRef != nil,
})
// Explicit dependency already present
continue
}
qualifs.AddImplicitProvider(fqn, getproviders.ResourceRef{
CfgRes: rc.Addr().InModule(c.Path),
Ref: tfdiags.SourceRangeFromHCL(rc.DeclRange),
ProviderAttribute: rc.ProviderConfigRef != nil,
})
reqs[fqn] = nil
}
for _, rc := range c.Module.DataResources {
fqn := rc.Provider
if _, exists := reqs[fqn]; exists {
// If this is called for a child module, and the provider was added from another implicit reference and not
// from a top level required_provider, we need to collect the reference of this resource as well as implicit provider.
qualifs.AddImplicitProvider(fqn, getproviders.ResourceRef{
CfgRes: rc.Addr().InModule(c.Path),
Ref: tfdiags.SourceRangeFromHCL(rc.DeclRange),
ProviderAttribute: rc.ProviderConfigRef != nil,
})
// Explicit dependency already present
continue
}
qualifs.AddImplicitProvider(fqn, getproviders.ResourceRef{
CfgRes: rc.Addr().InModule(c.Path),
Ref: tfdiags.SourceRangeFromHCL(rc.DeclRange),
ProviderAttribute: rc.ProviderConfigRef != nil,
})
reqs[fqn] = nil
}
@ -445,6 +474,10 @@ func (c *Config) addProviderRequirements(reqs getproviders.Requirements, recurse
fqn := i.Provider
if _, exists := reqs[fqn]; !exists {
reqs[fqn] = nil
qualifs.AddImplicitProvider(i.Provider, getproviders.ResourceRef{
CfgRes: i.StaticTo,
Ref: tfdiags.SourceRangeFromHCL(i.DeclRange),
})
}
// TODO: This should probably be moved to provider_validation.go so that
@ -522,7 +555,7 @@ func (c *Config) addProviderRequirements(reqs getproviders.Requirements, recurse
// Then we'll also look for requirements in testing modules.
for _, run := range file.Runs {
if run.ConfigUnderTest != nil {
moreDiags := run.ConfigUnderTest.addProviderRequirements(reqs, true, false)
moreDiags := run.ConfigUnderTest.addProviderRequirements(reqs, qualifs, true, false)
diags = append(diags, moreDiags...)
}
}
@ -532,7 +565,7 @@ func (c *Config) addProviderRequirements(reqs getproviders.Requirements, recurse
if recurse {
for _, childConfig := range c.Children {
moreDiags := childConfig.addProviderRequirements(reqs, true, false)
moreDiags := childConfig.addProviderRequirements(reqs, qualifs, true, false)
diags = append(diags, moreDiags...)
}
}
@ -772,7 +805,7 @@ func (c *Config) resolveProviderTypesForTests(providers map[string]addrs.Provide
// versions for each provider.
func (c *Config) ProviderTypes() []addrs.Provider {
// Ignore diagnostics here because they relate to version constraints
reqs, _ := c.ProviderRequirements()
reqs, _, _ := c.ProviderRequirements()
ret := make([]addrs.Provider, 0, len(reqs))
for k := range reqs {

View File

@ -18,6 +18,7 @@ import (
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclparse"
"github.com/opentofu/opentofu/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
version "github.com/hashicorp/go-version"
@ -155,7 +156,7 @@ func TestConfigProviderRequirements(t *testing.T) {
configuredProvider := addrs.NewDefaultProvider("configured")
grandchildProvider := addrs.NewDefaultProvider("grandchild")
got, diags := cfg.ProviderRequirements()
got, qualifs, diags := cfg.ProviderRequirements()
assertNoDiagnostics(t, diags)
want := getproviders.Requirements{
// the nullProvider constraints from the two modules are merged
@ -170,9 +171,62 @@ func TestConfigProviderRequirements(t *testing.T) {
terraformProvider: nil,
grandchildProvider: nil,
}
wantQualifs := &getproviders.ProvidersQualification{
Implicit: map[addrs.Provider][]getproviders.ResourceRef{
grandchildProvider: {
{
CfgRes: addrs.ConfigResource{Module: []string{"kinder", "nested"}, Resource: addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "grandchild_foo", Name: "bar"}},
Ref: tfdiags.SourceRange{Filename: "testdata/provider-reqs/child/grandchild/provider-reqs-grandchild.tf", Start: tfdiags.SourcePos{Line: 3, Column: 1, Byte: 136}, End: tfdiags.SourcePos{Line: 3, Column: 32, Byte: 167}},
},
},
impliedProvider: {
{
CfgRes: addrs.ConfigResource{Resource: addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "implied_foo", Name: "bar"}},
Ref: tfdiags.SourceRange{Filename: "testdata/provider-reqs/provider-reqs-root.tf", Start: tfdiags.SourcePos{Line: 16, Column: 1, Byte: 317}, End: tfdiags.SourcePos{Line: 16, Column: 29, Byte: 345}},
},
},
importexplicitProvider: {
{
CfgRes: addrs.ConfigResource{Resource: addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "importimplied", Name: "targetB"}},
Ref: tfdiags.SourceRange{Filename: "testdata/provider-reqs/provider-reqs-root.tf", Start: tfdiags.SourcePos{Line: 42, Column: 1, Byte: 939}, End: tfdiags.SourcePos{Line: 42, Column: 7, Byte: 945}},
},
},
importimpliedProvider: {
{
CfgRes: addrs.ConfigResource{Resource: addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "importimplied", Name: "targetA"}},
Ref: tfdiags.SourceRange{Filename: "testdata/provider-reqs/provider-reqs-root.tf", Start: tfdiags.SourcePos{Line: 37, Column: 1, Byte: 886}, End: tfdiags.SourcePos{Line: 37, Column: 7, Byte: 892}},
},
},
terraformProvider: {
{
CfgRes: addrs.ConfigResource{Resource: addrs.Resource{Mode: addrs.DataResourceMode, Type: "terraform_remote_state", Name: "bar"}},
Ref: tfdiags.SourceRange{Filename: "testdata/provider-reqs/provider-reqs-root.tf", Start: tfdiags.SourcePos{Line: 27, Column: 1, Byte: 628}, End: tfdiags.SourcePos{Line: 27, Column: 36, Byte: 663}},
},
},
},
Explicit: map[addrs.Provider]struct{}{
happycloudProvider: {},
nullProvider: {},
randomProvider: {},
tlsProvider: {},
},
}
// These 2 assertions are strictly to ensure that later the "provider" blocks are not registered into the qualifications.
// Technically speaking, provider blocks are indeed implicit references, but the current warning message
// on implicitly referenced providers could be misleading for the "provider" blocks.
if _, okExpl := qualifs.Explicit[configuredProvider]; okExpl {
t.Errorf("provider blocks shouldn't be added into the explicit qualifications")
}
if _, okImpl := qualifs.Implicit[configuredProvider]; okImpl {
t.Errorf("provider blocks shouldn't be added into the implicit qualifications")
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("wrong result\n%s", diff)
t.Errorf("wrong reqs result\n%s", diff)
}
if diff := cmp.Diff(wantQualifs, qualifs); diff != "" {
t.Errorf("wrong qualifs result\n%s", diff)
}
}
@ -195,7 +249,7 @@ func TestConfigProviderRequirementsInclTests(t *testing.T) {
terraformProvider := addrs.NewBuiltInProvider("terraform")
configuredProvider := addrs.NewDefaultProvider("configured")
got, diags := cfg.ProviderRequirements()
got, qualifs, diags := cfg.ProviderRequirements()
assertNoDiagnostics(t, diags)
want := getproviders.Requirements{
// the nullProvider constraints from the two modules are merged
@ -207,9 +261,35 @@ func TestConfigProviderRequirementsInclTests(t *testing.T) {
terraformProvider: nil,
}
wantQualifs := &getproviders.ProvidersQualification{
Implicit: map[addrs.Provider][]getproviders.ResourceRef{
impliedProvider: {
{
CfgRes: addrs.ConfigResource{Resource: addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "implied_foo", Name: "bar"}},
Ref: tfdiags.SourceRange{Filename: "testdata/provider-reqs-with-tests/provider-reqs-root.tf", Start: tfdiags.SourcePos{Line: 12, Column: 1, Byte: 247}, End: tfdiags.SourcePos{Line: 12, Column: 29, Byte: 275}},
},
},
terraformProvider: {
{
CfgRes: addrs.ConfigResource{Resource: addrs.Resource{Mode: addrs.DataResourceMode, Type: "terraform_remote_state", Name: "bar"}},
Ref: tfdiags.SourceRange{Filename: "testdata/provider-reqs-with-tests/provider-reqs-root.tf", Start: tfdiags.SourcePos{Line: 19, Column: 1, Byte: 516}, End: tfdiags.SourcePos{Line: 19, Column: 36, Byte: 551}},
},
},
},
Explicit: map[addrs.Provider]struct{}{
nullProvider: {},
randomProvider: {},
tlsProvider: {},
},
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("wrong result\n%s", diff)
}
if diff := cmp.Diff(wantQualifs, qualifs); diff != "" {
t.Errorf("wrong qualifs result\n%s", diff)
}
}
func TestConfigProviderRequirementsDuplicate(t *testing.T) {
@ -584,7 +664,8 @@ func TestConfigAddProviderRequirements(t *testing.T) {
reqs := getproviders.Requirements{
addrs.NewDefaultProvider("null"): nil,
}
diags = cfg.addProviderRequirements(reqs, true, false)
qualifs := new(getproviders.ProvidersQualification)
diags = cfg.addProviderRequirements(reqs, qualifs, true, false)
assertNoDiagnostics(t, diags)
}
@ -609,8 +690,9 @@ Use the providers argument within the module block to configure providers for al
func TestConfigImportProviderClashesWithResources(t *testing.T) {
cfg, diags := testModuleConfigFromFile("testdata/invalid-import-files/import-and-resource-clash.tf")
assertNoDiagnostics(t, diags)
qualifs := new(getproviders.ProvidersQualification)
diags = cfg.addProviderRequirements(getproviders.Requirements{}, true, false)
diags = cfg.addProviderRequirements(getproviders.Requirements{}, qualifs, true, false)
assertExactDiagnostics(t, diags, []string{
`testdata/invalid-import-files/import-and-resource-clash.tf:9,3-19: Invalid import provider argument; The provider argument in the target resource block must match the import block.`,
})
@ -620,7 +702,8 @@ func TestConfigImportProviderWithNoResourceProvider(t *testing.T) {
cfg, diags := testModuleConfigFromFile("testdata/invalid-import-files/import-and-no-resource.tf")
assertNoDiagnostics(t, diags)
diags = cfg.addProviderRequirements(getproviders.Requirements{}, true, false)
qualifs := new(getproviders.ProvidersQualification)
diags = cfg.addProviderRequirements(getproviders.Requirements{}, qualifs, true, false)
assertExactDiagnostics(t, diags, []string{
`testdata/invalid-import-files/import-and-no-resource.tf:5,3-19: Invalid import provider argument; The provider argument in the target resource block must be specified and match the import block.`,
})

View File

@ -13,8 +13,8 @@ import (
"github.com/apparentlymart/go-versions/versions"
"github.com/apparentlymart/go-versions/versions/constraints"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/tfdiags"
)
// Version represents a particular single version of a provider.
@ -50,6 +50,48 @@ type Warnings = []string
// altogether, which means that it is not required at all.
type Requirements map[addrs.Provider]VersionConstraints
// ProvidersQualification is storing the implicit/explicit reference qualification of the providers.
// This is necessary to be able to warn the user when the resources are referencing a provider that
// is not specifically defined in a required_providers block. When the implicitly referenced
// provider is tried to be downloaded without a specific provider requirement, it will be tried
// from the default namespace (hashicorp), failing to download it when it does not exist in the default namespace.
// Therefore, we want to let the user know what resources are generating this situation.
type ProvidersQualification struct {
Implicit map[addrs.Provider][]ResourceRef
Explicit map[addrs.Provider]struct{}
}
type ResourceRef struct {
CfgRes addrs.ConfigResource
Ref tfdiags.SourceRange
ProviderAttribute bool
}
// AddImplicitProvider saves an addrs.Provider with the place in the configuration where this is generated from.
func (pq *ProvidersQualification) AddImplicitProvider(provider addrs.Provider, ref ResourceRef) {
if pq.Implicit == nil {
pq.Implicit = map[addrs.Provider][]ResourceRef{}
}
// This is avoiding adding the implicit reference of the provider if this is already explicitly configured.
// Done this way, because when collecting these qualifications, if there are at least 2 resources (A from root module and B from an imported module),
// root module could have no explicit definition but the module of B could have an explicit one. But in case none of the modules is having
// an explicit definition, we want to gather all the resources that are implicitly referencing a provider.
if _, ok := pq.Explicit[provider]; ok {
return
}
refs := pq.Implicit[provider]
refs = append(refs, ref)
pq.Implicit[provider] = refs
}
// AddExplicitProvider saves an addrs.Provider that is specifically configured in a required_providers block.
func (pq *ProvidersQualification) AddExplicitProvider(provider addrs.Provider) {
if pq.Explicit == nil {
pq.Explicit = map[addrs.Provider]struct{}{}
}
pq.Explicit[provider] = struct{}{}
}
// Merge takes the requirements in the receiver and the requirements in the
// other given value and produces a new set of requirements that combines
// all of the requirements of both.

View File

@ -358,7 +358,7 @@ func (c *Context) checkConfigDependencies(config *configs.Config) tfdiags.Diagno
// We only check that we have a factory for each required provider, and
// assume the caller already assured that any separately-installed
// plugins are of a suitable version, match expected checksums, etc.
providerReqs, hclDiags := config.ProviderRequirements()
providerReqs, _, hclDiags := config.ProviderRequirements()
diags = diags.Append(hclDiags)
if hclDiags.HasErrors() {
return diags

View File

@ -56,7 +56,7 @@ func MigrateStateProviderAddresses(config *configs.Config, state *states.State)
// config could be nil when we're e.g. showing a statefile without the configuration present
if config != nil {
var hclDiags hcl.Diagnostics
providers, hclDiags = config.ProviderRequirements()
providers, _, hclDiags = config.ProviderRequirements()
diags = diags.Append(hclDiags)
if hclDiags.HasErrors() {
return nil, diags