getproviders: add a registry-specific error and modify output when a

provider is not found.

Previously a user would see the following error even if terraform was
only searching the local filesystem:

"provider registry registry.terraform.io does not have a provider named
...."

This PR adds a registry-specific error type and modifies the MultiSource
installer to check for registry errors. It will return the
registry-specific error message if there is one, but if not the error
message will list all locations searched.
This commit is contained in:
Kristin Laemmert 2020-05-14 14:04:13 -04:00
parent a33a613703
commit 8d28d73de3
13 changed files with 135 additions and 33 deletions

View File

@ -576,13 +576,12 @@ func (c *ZeroThirteenUpgradeCommand) detectProviderSources(requiredProviders map
if err == nil { if err == nil {
rp.Type = p rp.Type = p
} else { } else {
if _, ok := err.(getproviders.ErrProviderNotKnown); ok {
// Setting the provider address to a zero value struct // Setting the provider address to a zero value struct
// indicates that there is no known FQN for this provider, // indicates that there is no known FQN for this provider,
// which will cause us to write an explanatory comment in the // which will cause us to write an explanatory comment in the
// HCL output advising the user what to do about this. // HCL output advising the user what to do about this.
rp.Type = addrs.Provider{} rp.Type = addrs.Provider{}
}
diags = diags.Append(tfdiags.Sourceless( diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning, tfdiags.Warning,
"Could not detect provider source", "Could not detect provider source",

View File

@ -453,7 +453,7 @@ func (c *InitCommand) getProviders(earlyConfig *earlyconfig.Config, state *state
// The default (or configured) search paths are logged earlier, in provider_source.go // The default (or configured) search paths are logged earlier, in provider_source.go
// Log that those are being overridden by the `-plugin-dir` command line options // Log that those are being overridden by the `-plugin-dir` command line options
log.Printf("[DEBUG] init: overriding provider plugin search paths") log.Println("[DEBUG] init: overriding provider plugin search paths")
log.Printf("[DEBUG] will search for provider plugins in %s", pluginDirs) log.Printf("[DEBUG] will search for provider plugins in %s", pluginDirs)
} }
@ -495,11 +495,30 @@ func (c *InitCommand) getProviders(earlyConfig *earlyconfig.Config, state *state
c.Ui.Info(fmt.Sprintf("- Installing %s v%s...", provider.ForDisplay(), version)) c.Ui.Info(fmt.Sprintf("- Installing %s v%s...", provider.ForDisplay(), version))
}, },
QueryPackagesFailure: func(provider addrs.Provider, err error) { QueryPackagesFailure: func(provider addrs.Provider, err error) {
switch errorTy := err.(type) {
case getproviders.ErrProviderNotFound:
sources := errorTy.Sources
displaySources := make([]string, len(sources))
for i, source := range sources {
displaySources[i] = fmt.Sprintf("- %s", source)
}
diags = diags.Append(tfdiags.Sourceless( diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error, tfdiags.Error,
"Failed to query available provider packages", "Failed to query available provider packages",
fmt.Sprintf("Could not retrieve the list of available versions for provider %s: %s.", provider.ForDisplay(), err), fmt.Sprintf("Could not retrieve the list of available versions for provider %s: %s\n\n%s ",
provider.ForDisplay(), err, strings.Join(displaySources, "\n"),
),
)) ))
default:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to query available provider packages",
fmt.Sprintf("Could not retrieve the list of available versions for provider %s: %s ",
provider.ForDisplay(), err,
),
))
}
}, },
LinkFromCacheFailure: func(provider addrs.Provider, version getproviders.Version, err error) { LinkFromCacheFailure: func(provider addrs.Provider, version getproviders.Version, err error) {
diags = diags.Append(tfdiags.Sourceless( diags = diags.Append(tfdiags.Sourceless(

View File

@ -73,7 +73,22 @@ func (err ErrUnauthorized) Error() string {
} }
} }
// ErrProviderNotKnown is an error type used to indicate that the hostname // ErrProviderNotFound is an error type used to indicate that requested provider
// was not found in the source(s) included in the Description field. This can be
// used to produce user-friendly error messages.
type ErrProviderNotFound struct {
Provider addrs.Provider
Sources []string
}
func (err ErrProviderNotFound) Error() string {
return fmt.Sprintf(
"provider %s was not found in any of the search locations",
err.Provider,
)
}
// ErrRegistryProviderNotKnown is an error type used to indicate that the hostname
// given in a provider address does appear to be a provider registry but that // given in a provider address does appear to be a provider registry but that
// registry does not know about the given provider namespace or type. // registry does not know about the given provider namespace or type.
// //
@ -85,11 +100,11 @@ func (err ErrUnauthorized) Error() string {
// because we expect that the caller will have better context to decide what // because we expect that the caller will have better context to decide what
// hints are appropriate, e.g. by looking at the configuration given by the // hints are appropriate, e.g. by looking at the configuration given by the
// user. // user.
type ErrProviderNotKnown struct { type ErrRegistryProviderNotKnown struct {
Provider addrs.Provider Provider addrs.Provider
} }
func (err ErrProviderNotKnown) Error() string { func (err ErrRegistryProviderNotKnown) Error() string {
return fmt.Sprintf( return fmt.Sprintf(
"provider registry %s does not have a provider named %s", "provider registry %s does not have a provider named %s",
err.Provider.Hostname.ForDisplay(), err.Provider.Hostname.ForDisplay(),
@ -181,7 +196,7 @@ func (err ErrQueryFailed) Unwrap() error {
// grow in future. // grow in future.
func ErrIsNotExist(err error) bool { func ErrIsNotExist(err error) bool {
switch err.(type) { switch err.(type) {
case ErrProviderNotKnown, ErrPlatformNotSupported: case ErrProviderNotFound, ErrPlatformNotSupported:
return true return true
default: default:
return false return false

View File

@ -120,3 +120,8 @@ func (s *FilesystemMirrorSource) scanAllVersions() error {
s.allPackages = ret s.allPackages = ret
return nil return nil
} }
func (s *FilesystemMirrorSource) ForDisplay(provider addrs.Provider) string {
// TODO: Since we have the provider, this could show the entire search path
return s.baseDir
}

View File

@ -35,3 +35,8 @@ func (s *HTTPMirrorSource) AvailableVersions(provider addrs.Provider) (VersionLi
func (s *HTTPMirrorSource) PackageMeta(provider addrs.Provider, version Version, target Platform) (PackageMeta, error) { func (s *HTTPMirrorSource) PackageMeta(provider addrs.Provider, version Version, target Platform) (PackageMeta, error) {
return PackageMeta{}, fmt.Errorf("Network-based provider mirrors are not supported in this version of Terraform") return PackageMeta{}, fmt.Errorf("Network-based provider mirrors are not supported in this version of Terraform")
} }
// ForDisplay returns a string description of the source for user-facing output.
func (s *HTTPMirrorSource) ForDisplay(provider addrs.Provider) string {
return "Network-based provider mirrors are not supported in this version of Terraform"
}

View File

@ -94,3 +94,7 @@ func (s *MemoizeSource) PackageMeta(provider addrs.Provider, version Version, ta
} }
return ret, err return ret, err
} }
func (s *MemoizeSource) ForDisplay(provider addrs.Provider) string {
return s.underlying.ForDisplay(provider)
}

View File

@ -39,7 +39,7 @@ func TestMemoizeSource(t *testing.T) {
} }
_, err = source.AvailableVersions(nonexistProvider) _, err = source.AvailableVersions(nonexistProvider)
if want, ok := err.(ErrProviderNotKnown); !ok { if want, ok := err.(ErrProviderNotFound); !ok {
t.Fatalf("wrong error type from nonexist call:\ngot: %T\nwant: %T", err, want) t.Fatalf("wrong error type from nonexist call:\ngot: %T\nwant: %T", err, want)
} }
@ -122,11 +122,11 @@ func TestMemoizeSource(t *testing.T) {
source := NewMemoizeSource(mock) source := NewMemoizeSource(mock)
_, err := source.AvailableVersions(nonexistProvider) _, err := source.AvailableVersions(nonexistProvider)
if want, ok := err.(ErrProviderNotKnown); !ok { if want, ok := err.(ErrProviderNotFound); !ok {
t.Fatalf("wrong error type from first call:\ngot: %T\nwant: %T", err, want) t.Fatalf("wrong error type from first call:\ngot: %T\nwant: %T", err, want)
} }
_, err = source.AvailableVersions(nonexistProvider) _, err = source.AvailableVersions(nonexistProvider)
if want, ok := err.(ErrProviderNotKnown); !ok { if want, ok := err.(ErrProviderNotFound); !ok {
t.Fatalf("wrong error type from second call:\ngot: %T\nwant: %T", err, want) t.Fatalf("wrong error type from second call:\ngot: %T\nwant: %T", err, want)
} }

View File

@ -51,7 +51,7 @@ func (s *MockSource) AvailableVersions(provider addrs.Provider) (VersionList, er
if len(ret) == 0 { if len(ret) == 0 {
// In this case, we'll behave like a registry that doesn't know about // In this case, we'll behave like a registry that doesn't know about
// this provider at all, rather than just returning an empty result. // this provider at all, rather than just returning an empty result.
return nil, ErrProviderNotKnown{provider} return nil, ErrProviderNotFound{provider, []string{"mock source"}}
} }
ret.Sort() ret.Sort()
return ret, nil return ret, nil
@ -198,3 +198,7 @@ func FakeInstallablePackageMeta(provider addrs.Provider, version Version, protoc
} }
return meta, close, nil return meta, close, nil
} }
func (s *MockSource) ForDisplay(provider addrs.Provider) string {
return "mock source"
}

View File

@ -36,6 +36,7 @@ func (s MultiSource) AvailableVersions(provider addrs.Provider) (VersionList, er
// We will return the union of all versions reported by the nested // We will return the union of all versions reported by the nested
// sources that have matching patterns that accept the given provider. // sources that have matching patterns that accept the given provider.
vs := make(map[Version]struct{}) vs := make(map[Version]struct{})
var registryError bool
for _, selector := range s { for _, selector := range s {
if !selector.CanHandleProvider(provider) { if !selector.CanHandleProvider(provider) {
continue // doesn't match the given patterns continue // doesn't match the given patterns
@ -44,7 +45,10 @@ func (s MultiSource) AvailableVersions(provider addrs.Provider) (VersionList, er
switch err.(type) { switch err.(type) {
case nil: case nil:
// okay // okay
case ErrProviderNotKnown: case ErrRegistryProviderNotKnown:
registryError = true
continue // ignore, then
case ErrProviderNotFound:
continue // ignore, then continue // ignore, then
default: default:
return nil, err return nil, err
@ -55,7 +59,11 @@ func (s MultiSource) AvailableVersions(provider addrs.Provider) (VersionList, er
} }
if len(vs) == 0 { if len(vs) == 0 {
return nil, ErrProviderNotKnown{provider} if registryError {
return nil, ErrRegistryProviderNotKnown{provider}
} else {
return nil, ErrProviderNotFound{provider, s.sourcesForProvider(provider)}
}
} }
ret := make(VersionList, 0, len(vs)) ret := make(VersionList, 0, len(vs))
for v := range vs { for v := range vs {
@ -70,7 +78,7 @@ func (s MultiSource) AvailableVersions(provider addrs.Provider) (VersionList, er
// from the first selector that indicates availability of it. // from the first selector that indicates availability of it.
func (s MultiSource) PackageMeta(provider addrs.Provider, version Version, target Platform) (PackageMeta, error) { func (s MultiSource) PackageMeta(provider addrs.Provider, version Version, target Platform) (PackageMeta, error) {
if len(s) == 0 { // Easy case: no providers exist at all if len(s) == 0 { // Easy case: no providers exist at all
return PackageMeta{}, ErrProviderNotKnown{provider} return PackageMeta{}, ErrProviderNotFound{provider, s.sourcesForProvider(provider)}
} }
for _, selector := range s { for _, selector := range s {
@ -81,7 +89,7 @@ func (s MultiSource) PackageMeta(provider addrs.Provider, version Version, targe
switch err.(type) { switch err.(type) {
case nil: case nil:
return meta, nil return meta, nil
case ErrProviderNotKnown, ErrPlatformNotSupported: case ErrProviderNotFound, ErrRegistryProviderNotKnown, ErrPlatformNotSupported:
continue // ignore, then continue // ignore, then
default: default:
return PackageMeta{}, err return PackageMeta{}, err
@ -224,3 +232,20 @@ func normalizeProviderNameOrWildcard(s string) (string, error) {
} }
return addrs.ParseProviderPart(s) return addrs.ParseProviderPart(s)
} }
func (s MultiSource) ForDisplay(provider addrs.Provider) string {
return strings.Join(s.sourcesForProvider(provider), "\n")
}
// sourcesForProvider returns a list of source display strings configured for a
// given provider, taking into account any `Exclude` statements.
func (s MultiSource) sourcesForProvider(provider addrs.Provider) []string {
ret := make([]string, 0)
for _, selector := range s {
if !selector.CanHandleProvider(provider) {
continue // doesn't match the given patterns
}
ret = append(ret, selector.Source.ForDisplay(provider))
}
return ret
}

View File

@ -73,7 +73,7 @@ func TestMultiSourceAvailableVersions(t *testing.T) {
} }
_, err = multi.AvailableVersions(addrs.NewDefaultProvider("baz")) _, err = multi.AvailableVersions(addrs.NewDefaultProvider("baz"))
if want, ok := err.(ErrProviderNotKnown); !ok { if want, ok := err.(ErrProviderNotFound); !ok {
t.Fatalf("wrong error type:\ngot: %T\nwant: %T", err, want) t.Fatalf("wrong error type:\ngot: %T\nwant: %T", err, want)
} }
}) })
@ -147,10 +147,32 @@ func TestMultiSourceAvailableVersions(t *testing.T) {
} }
_, err = multi.AvailableVersions(addrs.NewDefaultProvider("baz")) _, err = multi.AvailableVersions(addrs.NewDefaultProvider("baz"))
if want, ok := err.(ErrProviderNotKnown); !ok { if want, ok := err.(ErrProviderNotFound); !ok {
t.Fatalf("wrong error type:\ngot: %T\nwant: %T", err, want) t.Fatalf("wrong error type:\ngot: %T\nwant: %T", err, want)
} }
}) })
t.Run("provider not found", func(t *testing.T) {
s1 := NewMockSource(nil)
s2 := NewMockSource(nil)
multi := MultiSource{
{Source: s1},
{Source: s2},
}
_, err := multi.AvailableVersions(addrs.NewDefaultProvider("foo"))
if err == nil {
t.Fatal("expected error, got success")
}
wantErr := `provider registry.terraform.io/hashicorp/foo was not found in any of the search locations`
_ = []string{"mock source", "mock source"}
if err.Error() != wantErr {
t.Fatalf("wrong error.\ngot: %s\nwant: %s\n", err, wantErr)
}
})
} }
func TestMultiSourcePackageMeta(t *testing.T) { func TestMultiSourcePackageMeta(t *testing.T) {

View File

@ -93,7 +93,7 @@ func newRegistryClient(baseURL *url.URL, creds svcauth.HostCredentials) *registr
// ProviderVersions returns the raw version and protocol strings produced by the // ProviderVersions returns the raw version and protocol strings produced by the
// registry for the given provider. // registry for the given provider.
// //
// The returned error will be ErrProviderNotKnown if the registry responds with // The returned error will be ErrRegistryProviderNotKnown if the registry responds with
// 404 Not Found to indicate that the namespace or provider type are not known, // 404 Not Found to indicate that the namespace or provider type are not known,
// ErrUnauthorized if the registry responds with 401 or 403 status codes, or // ErrUnauthorized if the registry responds with 401 or 403 status codes, or
// ErrQueryFailed for any other protocol or operational problem. // ErrQueryFailed for any other protocol or operational problem.
@ -122,7 +122,7 @@ func (c *registryClient) ProviderVersions(addr addrs.Provider) (map[string][]str
case http.StatusOK: case http.StatusOK:
// Great! // Great!
case http.StatusNotFound: case http.StatusNotFound:
return nil, ErrProviderNotKnown{ return nil, ErrRegistryProviderNotKnown{
Provider: addr, Provider: addr,
} }
case http.StatusUnauthorized, http.StatusForbidden: case http.StatusUnauthorized, http.StatusForbidden:
@ -357,7 +357,6 @@ func (c *registryClient) PackageMeta(provider addrs.Provider, version Version, t
return ret, nil return ret, nil
} }
// LegacyProviderDefaultNamespace returns the raw address strings produced by
// findClosestProtocolCompatibleVersion searches for the provider version with the closest protocol match. // findClosestProtocolCompatibleVersion searches for the provider version with the closest protocol match.
func (c *registryClient) findClosestProtocolCompatibleVersion(provider addrs.Provider, version Version) (Version, error) { func (c *registryClient) findClosestProtocolCompatibleVersion(provider addrs.Provider, version Version) (Version, error) {
var match Version var match Version
@ -401,7 +400,7 @@ FindMatch:
return match, nil return match, nil
} }
// LegacyProviderCanonicalAddress returns the raw address strings produced by // LegacyProviderDefaultNamespace returns the raw address strings produced by
// the registry when asked about the given unqualified provider type name. // the registry when asked about the given unqualified provider type name.
// The returned namespace string is taken verbatim from the registry's response. // The returned namespace string is taken verbatim from the registry's response.
// //
@ -437,7 +436,7 @@ func (c *registryClient) LegacyProviderDefaultNamespace(typeName string) (string
case http.StatusOK: case http.StatusOK:
// Great! // Great!
case http.StatusNotFound: case http.StatusNotFound:
return "", ErrProviderNotKnown{ return "", ErrProviderNotFound{
Provider: placeholderProviderAddr, Provider: placeholderProviderAddr,
} }
case http.StatusUnauthorized, http.StatusForbidden: case http.StatusUnauthorized, http.StatusForbidden:

View File

@ -167,3 +167,7 @@ func (s *RegistrySource) registryClient(hostname svchost.Hostname) (*registryCli
return newRegistryClient(url, creds), nil return newRegistryClient(url, creds), nil
} }
func (s *RegistrySource) ForDisplay(provider addrs.Provider) string {
return fmt.Sprintf("registry %s", provider.Hostname.ForDisplay())
}

View File

@ -9,4 +9,5 @@ import (
type Source interface { type Source interface {
AvailableVersions(provider addrs.Provider) (VersionList, error) AvailableVersions(provider addrs.Provider) (VersionList, error)
PackageMeta(provider addrs.Provider, version Version, target Platform) (PackageMeta, error) PackageMeta(provider addrs.Provider, version Version, target Platform) (PackageMeta, error)
ForDisplay(provider addrs.Provider) string
} }