From 8d28d73de3f9441333fc44de7abfbae44e2cc671 Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Thu, 14 May 2020 14:04:13 -0400 Subject: [PATCH] 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. --- command/013_config_upgrade.go | 13 ++++--- command/init.go | 31 ++++++++++++---- internal/getproviders/errors.go | 23 +++++++++--- .../getproviders/filesystem_mirror_source.go | 5 +++ internal/getproviders/http_mirror_source.go | 5 +++ internal/getproviders/memoize_source.go | 4 +++ internal/getproviders/memoize_source_test.go | 6 ++-- internal/getproviders/mock_source.go | 6 +++- internal/getproviders/multi_source.go | 35 ++++++++++++++++--- internal/getproviders/multi_source_test.go | 26 ++++++++++++-- internal/getproviders/registry_client.go | 9 +++-- internal/getproviders/registry_source.go | 4 +++ internal/getproviders/source.go | 1 + 13 files changed, 135 insertions(+), 33 deletions(-) diff --git a/command/013_config_upgrade.go b/command/013_config_upgrade.go index 890752384..62260bb50 100644 --- a/command/013_config_upgrade.go +++ b/command/013_config_upgrade.go @@ -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{} - } + // 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", diff --git a/command/init.go b/command/init.go index a2add07d7..f5fd8cd17 100644 --- a/command/init.go +++ b/command/init.go @@ -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) { - 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), - )) + 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\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( diff --git a/internal/getproviders/errors.go b/internal/getproviders/errors.go index 96ff8d129..ca8cb046d 100644 --- a/internal/getproviders/errors.go +++ b/internal/getproviders/errors.go @@ -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 diff --git a/internal/getproviders/filesystem_mirror_source.go b/internal/getproviders/filesystem_mirror_source.go index 12b3c2437..18cfba6cb 100644 --- a/internal/getproviders/filesystem_mirror_source.go +++ b/internal/getproviders/filesystem_mirror_source.go @@ -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 +} diff --git a/internal/getproviders/http_mirror_source.go b/internal/getproviders/http_mirror_source.go index c2dedf9db..d00323c32 100644 --- a/internal/getproviders/http_mirror_source.go +++ b/internal/getproviders/http_mirror_source.go @@ -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" +} diff --git a/internal/getproviders/memoize_source.go b/internal/getproviders/memoize_source.go index 19e08d58c..4513ea4a7 100644 --- a/internal/getproviders/memoize_source.go +++ b/internal/getproviders/memoize_source.go @@ -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) +} diff --git a/internal/getproviders/memoize_source_test.go b/internal/getproviders/memoize_source_test.go index d085bfcdd..a2765d877 100644 --- a/internal/getproviders/memoize_source_test.go +++ b/internal/getproviders/memoize_source_test.go @@ -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) } diff --git a/internal/getproviders/mock_source.go b/internal/getproviders/mock_source.go index 6b67103e3..3e90a07e8 100644 --- a/internal/getproviders/mock_source.go +++ b/internal/getproviders/mock_source.go @@ -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" +} diff --git a/internal/getproviders/multi_source.go b/internal/getproviders/multi_source.go index 7b0334dfc..1d25938cf 100644 --- a/internal/getproviders/multi_source.go +++ b/internal/getproviders/multi_source.go @@ -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 @@ -43,8 +44,11 @@ func (s MultiSource) AvailableVersions(provider addrs.Provider) (VersionList, er thisSourceVersions, err := selector.Source.AvailableVersions(provider) switch err.(type) { case nil: - // okay - case ErrProviderNotKnown: + // okay + 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 +} diff --git a/internal/getproviders/multi_source_test.go b/internal/getproviders/multi_source_test.go index 01dcba93b..448354833 100644 --- a/internal/getproviders/multi_source_test.go +++ b/internal/getproviders/multi_source_test.go @@ -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) { diff --git a/internal/getproviders/registry_client.go b/internal/getproviders/registry_client.go index e302db812..14bb016e0 100644 --- a/internal/getproviders/registry_client.go +++ b/internal/getproviders/registry_client.go @@ -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: diff --git a/internal/getproviders/registry_source.go b/internal/getproviders/registry_source.go index 52037b4f9..042a028f2 100644 --- a/internal/getproviders/registry_source.go +++ b/internal/getproviders/registry_source.go @@ -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()) +} diff --git a/internal/getproviders/source.go b/internal/getproviders/source.go index 2921e2d76..7c1e30d5f 100644 --- a/internal/getproviders/source.go +++ b/internal/getproviders/source.go @@ -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 }