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 {
rp.Type = p
} else {
if _, ok := err.(getproviders.ErrProviderNotKnown); ok {
// Setting the provider address to a zero value struct
// indicates that there is no known FQN for this provider,
// which will cause us to write an explanatory comment in the
// HCL output advising the user what to do about this.
rp.Type = addrs.Provider{}
}
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"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
// 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)
}
@ -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))
},
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(
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),
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) {
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
// 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
// hints are appropriate, e.g. by looking at the configuration given by the
// user.
type ErrProviderNotKnown struct {
type ErrRegistryProviderNotKnown struct {
Provider addrs.Provider
}
func (err ErrProviderNotKnown) Error() string {
func (err ErrRegistryProviderNotKnown) Error() string {
return fmt.Sprintf(
"provider registry %s does not have a provider named %s",
err.Provider.Hostname.ForDisplay(),
@ -181,7 +196,7 @@ func (err ErrQueryFailed) Unwrap() error {
// grow in future.
func ErrIsNotExist(err error) bool {
switch err.(type) {
case ErrProviderNotKnown, ErrPlatformNotSupported:
case ErrProviderNotFound, ErrPlatformNotSupported:
return true
default:
return false

View File

@ -120,3 +120,8 @@ func (s *FilesystemMirrorSource) scanAllVersions() error {
s.allPackages = ret
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) {
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
}
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)
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)
}
@ -122,11 +122,11 @@ func TestMemoizeSource(t *testing.T) {
source := NewMemoizeSource(mock)
_, 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)
}
_, 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)
}

View File

@ -51,7 +51,7 @@ func (s *MockSource) AvailableVersions(provider addrs.Provider) (VersionList, er
if len(ret) == 0 {
// 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.
return nil, ErrProviderNotKnown{provider}
return nil, ErrProviderNotFound{provider, []string{"mock source"}}
}
ret.Sort()
return ret, nil
@ -198,3 +198,7 @@ func FakeInstallablePackageMeta(provider addrs.Provider, version Version, protoc
}
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
// sources that have matching patterns that accept the given provider.
vs := make(map[Version]struct{})
var registryError bool
for _, selector := range s {
if !selector.CanHandleProvider(provider) {
continue // doesn't match the given patterns
@ -44,7 +45,10 @@ func (s MultiSource) AvailableVersions(provider addrs.Provider) (VersionList, er
switch err.(type) {
case nil:
// okay
case ErrProviderNotKnown:
case ErrRegistryProviderNotKnown:
registryError = true
continue // ignore, then
case ErrProviderNotFound:
continue // ignore, then
default:
return nil, err
@ -55,7 +59,11 @@ func (s MultiSource) AvailableVersions(provider addrs.Provider) (VersionList, er
}
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))
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.
func (s MultiSource) PackageMeta(provider addrs.Provider, version Version, target Platform) (PackageMeta, error) {
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 {
@ -81,7 +89,7 @@ func (s MultiSource) PackageMeta(provider addrs.Provider, version Version, targe
switch err.(type) {
case nil:
return meta, nil
case ErrProviderNotKnown, ErrPlatformNotSupported:
case ErrProviderNotFound, ErrRegistryProviderNotKnown, ErrPlatformNotSupported:
continue // ignore, then
default:
return PackageMeta{}, err
@ -224,3 +232,20 @@ func normalizeProviderNameOrWildcard(s string) (string, error) {
}
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"))
if want, ok := err.(ErrProviderNotKnown); !ok {
if want, ok := err.(ErrProviderNotFound); !ok {
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"))
if want, ok := err.(ErrProviderNotKnown); !ok {
if want, ok := err.(ErrProviderNotFound); !ok {
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) {

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
// 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,
// ErrUnauthorized if the registry responds with 401 or 403 status codes, or
// 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:
// Great!
case http.StatusNotFound:
return nil, ErrProviderNotKnown{
return nil, ErrRegistryProviderNotKnown{
Provider: addr,
}
case http.StatusUnauthorized, http.StatusForbidden:
@ -357,7 +357,6 @@ func (c *registryClient) PackageMeta(provider addrs.Provider, version Version, t
return ret, nil
}
// LegacyProviderDefaultNamespace returns the raw address strings produced by
// findClosestProtocolCompatibleVersion searches for the provider version with the closest protocol match.
func (c *registryClient) findClosestProtocolCompatibleVersion(provider addrs.Provider, version Version) (Version, error) {
var match Version
@ -401,7 +400,7 @@ FindMatch:
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 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:
// Great!
case http.StatusNotFound:
return "", ErrProviderNotKnown{
return "", ErrProviderNotFound{
Provider: placeholderProviderAddr,
}
case http.StatusUnauthorized, http.StatusForbidden:

View File

@ -167,3 +167,7 @@ func (s *RegistrySource) registryClient(hostname svchost.Hostname) (*registryCli
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 {
AvailableVersions(provider addrs.Provider) (VersionList, error)
PackageMeta(provider addrs.Provider, version Version, target Platform) (PackageMeta, error)
ForDisplay(provider addrs.Provider) string
}