Skip to content

Commit

Permalink
Give suggestions & remind users to use required_providers when provid…
Browse files Browse the repository at this point in the history
…er not in registry (#28014)

* Add helper suggestion when failed registry err

When someone has a failed registry error on init, remind them that
they should have required_providers in every module

* Give suggestion for a provider based on reqs

Suggest another provider on a registry error, from the list of
requirements we have on init. This skips the legacy lookup
process if there is a similar provider existing in requirements.
  • Loading branch information
Pam Selle authored Mar 11, 2021
1 parent c542895 commit 242f319
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 10 deletions.
10 changes: 10 additions & 0 deletions command/e2etest/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,10 @@ func TestInitProviderNotFound(t *testing.T) {
if !strings.Contains(oneLineStderr, "provider registry registry.terraform.io does not have a provider named registry.terraform.io/hashicorp/nonexist") {
t.Errorf("expected error message is missing from output:\n%s", stderr)
}

if !strings.Contains(oneLineStderr, "All modules should specify their required_providers") {
t.Errorf("expected error message is missing from output:\n%s", stderr)
}
})

t.Run("local provider not found", func(t *testing.T) {
Expand Down Expand Up @@ -375,6 +379,12 @@ func TestInitProviderNotFound(t *testing.T) {
│ Could not retrieve the list of available versions for provider
│ hashicorp/nonexist: provider registry registry.terraform.io does not have a
│ provider named registry.terraform.io/hashicorp/nonexist
│ All modules should specify their required_providers so that external
│ consumers will get the correct providers when using a module. To see which
│ modules are currently depending on hashicorp/nonexist, run the following
│ command:
│ terraform providers
`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
terraform {
required_providers {
nonexist = {
source = "teamterraform/nonexist"
}
}
}
4 changes: 2 additions & 2 deletions command/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -513,8 +513,8 @@ func (c *InitCommand) getProviders(config *configs.Config, state *states.State,
case getproviders.ErrRegistryProviderNotKnown:
// We might be able to suggest an alternative provider to use
// instead of this one.
var suggestion string
alternative := getproviders.MissingProviderSuggestion(ctx, provider, inst.ProviderSource())
suggestion := fmt.Sprintf("\n\nAll modules should specify their required_providers so that external consumers will get the correct providers when using a module. To see which modules are currently depending on %s, run the following command:\n terraform providers", provider.ForDisplay())
alternative := getproviders.MissingProviderSuggestion(ctx, provider, inst.ProviderSource(), reqs)
if alternative != provider {
suggestion = fmt.Sprintf(
"\n\nDid you intend to use %s? If so, you must specify that source address in each module which requires that provider. To see which modules are currently depending on %s, run the following command:\n terraform providers",
Expand Down
11 changes: 10 additions & 1 deletion internal/getproviders/didyoumean.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,20 @@ import (
// If the given context is cancelled then this function might not return a
// renaming suggestion even if one would've been available for a completed
// request.
func MissingProviderSuggestion(ctx context.Context, addr addrs.Provider, source Source) addrs.Provider {
func MissingProviderSuggestion(ctx context.Context, addr addrs.Provider, source Source, reqs Requirements) addrs.Provider {
if !addr.IsDefault() {
return addr
}

// Before possibly looking up legacy naming, see if the user has another provider
// named in their requirements that is of the same type, and offer that
// as a suggestion
for req := range reqs {
if req != addr && req.Type == addr.Type {
return req
}
}

// Our strategy here, for a default provider, is to use the default
// registry's special API for looking up "legacy" providers and try looking
// for a legacy provider whose type name matches the type of the given
Expand Down
81 changes: 74 additions & 7 deletions internal/getproviders/didyoumean_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@ func TestMissingProviderSuggestion(t *testing.T) {

// testRegistrySource handles -/legacy as a valid legacy provider
// lookup mapping to legacycorp/legacy.
legacyAddr := addrs.NewDefaultProvider("legacy")
got := MissingProviderSuggestion(
ctx,
addrs.NewDefaultProvider("legacy"),
source,
Requirements{
legacyAddr: MustParseVersionConstraints(">= 1.0.0"),
},
)

want := addrs.Provider{
Expand All @@ -48,19 +52,48 @@ func TestMissingProviderSuggestion(t *testing.T) {
// copy in some other namespace for v0.13 or later to use. Our naming
// suggestions ignore the v0.12-compatible one and suggest the
// other one.
moved := addrs.NewDefaultProvider("moved")
want := addrs.Provider{
Hostname: defaultRegistryHost,
Namespace: "acme",
Type: "moved",
}

got := MissingProviderSuggestion(
ctx,
addrs.NewDefaultProvider("moved"),
moved,
source,
Requirements{
moved: MustParseVersionConstraints(">= 1.0.0"),
},
)

want := addrs.Provider{
if got != want {
t.Errorf("wrong result\ngot: %s\nwant: %s", got, want)
}

// If a provider has moved, but there's provider requirements
// for something of the same type, we'll return that one
// and skip the legacy lookup process. In practice,
// hopefully this is also "acme" but it's "zcme" here to
// exercise the codepath
want2 := addrs.Provider{
Hostname: defaultRegistryHost,
Namespace: "acme",
Namespace: "zcme",
Type: "moved",
}
if got != want {
t.Errorf("wrong result\ngot: %s\nwant: %s", got, want)
got2 := MissingProviderSuggestion(
ctx,
moved,
source,
Requirements{
moved: MustParseVersionConstraints(">= 1.0.0"),
want2: MustParseVersionConstraints(">= 1.0.0"),
},
)

if got2 != want2 {
t.Errorf("wrong result\ngot: %s\nwant: %s", got2, want2)
}
})
t.Run("invalid response", func(t *testing.T) {
Expand All @@ -76,6 +109,9 @@ func TestMissingProviderSuggestion(t *testing.T) {
ctx,
want,
source,
Requirements{
want: MustParseVersionConstraints(">= 1.0.0"),
},
)
if got != want {
t.Errorf("wrong result\ngot: %s\nwant: %s", got, want)
Expand All @@ -98,6 +134,9 @@ func TestMissingProviderSuggestion(t *testing.T) {
ctx,
want,
source,
Requirements{
want: MustParseVersionConstraints(">= 1.0.0"),
},
)
if got != want {
t.Errorf("wrong result\ngot: %s\nwant: %s", got, want)
Expand All @@ -109,8 +148,8 @@ func TestMissingProviderSuggestion(t *testing.T) {
defer close()

// Because this provider address isn't in
// registry.terraform.io/hashicorp/..., MissingProviderSuggestion won't
// even attempt to make a suggestion for it.
// registry.terraform.io/hashicorp/..., MissingProviderSuggestion
// will provide the same addr since there's no alternative in Requirements
want := addrs.Provider{
Hostname: defaultRegistryHost,
Namespace: "whatever",
Expand All @@ -120,9 +159,37 @@ func TestMissingProviderSuggestion(t *testing.T) {
ctx,
want,
source,
Requirements{
want: MustParseVersionConstraints(">= 1.0.0"),
},
)
if got != want {
t.Errorf("wrong result\ngot: %s\nwant: %s", got, want)
}

// If there is a provider required that has the same type,
// but different namespace, we can suggest that
foo := addrs.Provider{
Hostname: defaultRegistryHost,
Namespace: "hashicorp",
Type: "foo",
}
realFoo := addrs.Provider{
Hostname: defaultRegistryHost,
Namespace: "acme",
Type: "foo",
}
got2 := MissingProviderSuggestion(
ctx,
foo,
source,
Requirements{
foo: MustParseVersionConstraints(">= 1.0.0"),
realFoo: MustParseVersionConstraints(">= 1.0.0"),
},
)
if got2 != realFoo {
t.Errorf("wrong result\ngot: %s\nwant: %s", got2, realFoo)
}
})
}

0 comments on commit 242f319

Please sign in to comment.