From 47e657c611d1adf6972f6842d87b5fef9bf6fe93 Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Thu, 25 Jun 2020 10:49:48 -0400 Subject: [PATCH] internal/getproviders: decode and return any registry warnings (#25337) * internal/getproviders: decode and return any registry warnings The public registry may include a list of warnings in the "versions" response for any given provider. This PR adds support for warnings from the registry and an installer event to return those warnings to the user. --- command/e2etest/init_test.go | 22 +++++ .../testdata/provider-warnings/main.tf | 12 +++ command/init.go | 15 +++ command/init_test.go | 2 +- command/providers_mirror.go | 2 +- .../getproviders/filesystem_mirror_source.go | 6 +- .../filesystem_mirror_source_test.go | 2 +- internal/getproviders/http_mirror_source.go | 4 +- internal/getproviders/memoize_source.go | 10 +- internal/getproviders/memoize_source_test.go | 42 +++++++-- internal/getproviders/mock_source.go | 16 +++- internal/getproviders/multi_source.go | 18 ++-- internal/getproviders/multi_source_test.go | 91 ++++++++++++++++--- internal/getproviders/registry_client.go | 25 ++--- internal/getproviders/registry_client_test.go | 4 +- internal/getproviders/registry_source.go | 35 ++++--- internal/getproviders/registry_source_test.go | 27 ++++-- internal/getproviders/source.go | 2 +- internal/getproviders/types.go | 3 + internal/providercache/installer.go | 7 +- internal/providercache/installer_events.go | 3 + 21 files changed, 256 insertions(+), 92 deletions(-) create mode 100644 command/e2etest/testdata/provider-warnings/main.tf diff --git a/command/e2etest/init_test.go b/command/e2etest/init_test.go index 3411b403a..ed5e112d5 100644 --- a/command/e2etest/init_test.go +++ b/command/e2etest/init_test.go @@ -362,3 +362,25 @@ func TestInitProviderNotFound(t *testing.T) { } }) } + +func TestInitProviderWarnings(t *testing.T) { + t.Parallel() + + // This test will reach out to registry.terraform.io as one of the possible + // installation locations for hashicorp/nonexist, which should not exist. + skipIfCannotAccessNetwork(t) + + fixturePath := filepath.Join("testdata", "provider-warnings") + tf := e2e.NewBinary(terraformBin, fixturePath) + defer tf.Close() + + stdout, _, err := tf.Run("init") + if err == nil { + t.Fatal("expected error, got success") + } + + if !strings.Contains(stdout, "This provider is archived and no longer needed. The terraform_remote_state\ndata source is built into the latest Terraform release.") { + t.Errorf("expected warning message is missing from output:\n%s", stdout) + } + +} diff --git a/command/e2etest/testdata/provider-warnings/main.tf b/command/e2etest/testdata/provider-warnings/main.tf new file mode 100644 index 000000000..4300f04f8 --- /dev/null +++ b/command/e2etest/testdata/provider-warnings/main.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + terraform = { + // hashicorp/terraform is published in the registry, but it is + // archived (since it is internal) and returns a warning: + // + // "This provider is archived and no longer needed. The terraform_remote_state + // data source is built into the latest Terraform release." + source = "hashicorp/terraform" + } + } +} diff --git a/command/init.go b/command/init.go index 547e6f4e5..11ccca5b2 100644 --- a/command/init.go +++ b/command/init.go @@ -527,6 +527,21 @@ func (c *InitCommand) getProviders(config *configs.Config, state *states.State, } }, + QueryPackagesWarning: func(provider addrs.Provider, warnings []string) { + displayWarnings := make([]string, len(warnings)) + for i, warning := range warnings { + displayWarnings[i] = fmt.Sprintf("- %s", warning) + } + + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Warning, + "Additional provider information from registry", + fmt.Sprintf("The remote registry returned warnings for %s:\n%s", + provider.String(), + strings.Join(displayWarnings, "\n"), + ), + )) + }, LinkFromCacheFailure: func(provider addrs.Provider, version getproviders.Version, err error) { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, diff --git a/command/init_test.go b/command/init_test.go index 4028fd7d2..0163efb88 100644 --- a/command/init_test.go +++ b/command/init_test.go @@ -1718,7 +1718,7 @@ func newMockProviderSource(t *testing.T, availableProviderVersions map[string][] } } - return getproviders.NewMockSource(packages), close + return getproviders.NewMockSource(packages, nil), close } // installFakeProviderPackages installs a fake package for the given provider diff --git a/command/providers_mirror.go b/command/providers_mirror.go index ee6e2bc35..e9d904b13 100644 --- a/command/providers_mirror.go +++ b/command/providers_mirror.go @@ -123,7 +123,7 @@ func (c *ProvidersMirrorCommand) Run(args []string) int { // First we'll look for the latest version that matches the given // constraint, which we'll then try to mirror for each target platform. acceptable := versions.MeetingConstraints(constraints) - avail, err := source.AvailableVersions(provider) + avail, _, err := source.AvailableVersions(provider) candidates := avail.Filter(acceptable) if err == nil && len(candidates) == 0 { err = fmt.Errorf("no releases match the given constraints %s", constraintsStr) diff --git a/internal/getproviders/filesystem_mirror_source.go b/internal/getproviders/filesystem_mirror_source.go index 801116e69..2119411e5 100644 --- a/internal/getproviders/filesystem_mirror_source.go +++ b/internal/getproviders/filesystem_mirror_source.go @@ -28,11 +28,11 @@ func NewFilesystemMirrorSource(baseDir string) *FilesystemMirrorSource { // AvailableVersions scans the directory structure under the source's base // directory for locally-mirrored packages for the given provider, returning // a list of version numbers for the providers it found. -func (s *FilesystemMirrorSource) AvailableVersions(provider addrs.Provider) (VersionList, error) { +func (s *FilesystemMirrorSource) AvailableVersions(provider addrs.Provider) (VersionList, Warnings, error) { // s.allPackages is populated if scanAllVersions succeeds err := s.scanAllVersions() if err != nil { - return nil, err + return nil, nil, err } // There might be multiple packages for a given version in the filesystem, @@ -47,7 +47,7 @@ func (s *FilesystemMirrorSource) AvailableVersions(provider addrs.Provider) (Ver ret = append(ret, v) } ret.Sort() - return ret, nil + return ret, nil, nil } // PackageMeta checks to see if the source's base directory contains a diff --git a/internal/getproviders/filesystem_mirror_source_test.go b/internal/getproviders/filesystem_mirror_source_test.go index 24ecd595f..b526c8bc4 100644 --- a/internal/getproviders/filesystem_mirror_source_test.go +++ b/internal/getproviders/filesystem_mirror_source_test.go @@ -104,7 +104,7 @@ func TestFilesystemMirrorSourceAllAvailablePackages_invalid(t *testing.T) { func TestFilesystemMirrorSourceAvailableVersions(t *testing.T) { source := NewFilesystemMirrorSource("testdata/filesystem-mirror") - got, err := source.AvailableVersions(nullProvider) + got, _, err := source.AvailableVersions(nullProvider) if err != nil { t.Fatal(err) } diff --git a/internal/getproviders/http_mirror_source.go b/internal/getproviders/http_mirror_source.go index d00323c32..e56aab0eb 100644 --- a/internal/getproviders/http_mirror_source.go +++ b/internal/getproviders/http_mirror_source.go @@ -26,8 +26,8 @@ func NewHTTPMirrorSource(baseURL *url.URL) *HTTPMirrorSource { // AvailableVersions retrieves the available versions for the given provider // from the object's underlying HTTP mirror service. -func (s *HTTPMirrorSource) AvailableVersions(provider addrs.Provider) (VersionList, error) { - return nil, fmt.Errorf("Network-based provider mirrors are not supported in this version of Terraform") +func (s *HTTPMirrorSource) AvailableVersions(provider addrs.Provider) (VersionList, Warnings, error) { + return nil, nil, fmt.Errorf("Network-based provider mirrors are not supported in this version of Terraform") } // PackageMeta retrieves metadata for the requested provider package diff --git a/internal/getproviders/memoize_source.go b/internal/getproviders/memoize_source.go index 4513ea4a7..5149e7ade 100644 --- a/internal/getproviders/memoize_source.go +++ b/internal/getproviders/memoize_source.go @@ -26,6 +26,7 @@ type MemoizeSource struct { type memoizeAvailableVersionsRet struct { VersionList VersionList + Warnings Warnings Err error } @@ -55,20 +56,21 @@ func NewMemoizeSource(underlying Source) *MemoizeSource { // AvailableVersions requests the available versions from the underlying source // and caches them before returning them, or on subsequent calls returns the // result directly from the cache. -func (s *MemoizeSource) AvailableVersions(provider addrs.Provider) (VersionList, error) { +func (s *MemoizeSource) AvailableVersions(provider addrs.Provider) (VersionList, Warnings, error) { s.mu.Lock() defer s.mu.Unlock() if existing, exists := s.availableVersions[provider]; exists { - return existing.VersionList, existing.Err + return existing.VersionList, nil, existing.Err } - ret, err := s.underlying.AvailableVersions(provider) + ret, warnings, err := s.underlying.AvailableVersions(provider) s.availableVersions[provider] = memoizeAvailableVersionsRet{ VersionList: ret, Err: err, + Warnings: warnings, } - return ret, err + return ret, warnings, err } // PackageMeta requests package metadata from the underlying source and caches diff --git a/internal/getproviders/memoize_source_test.go b/internal/getproviders/memoize_source_test.go index 12c8b9dc5..f8ced5012 100644 --- a/internal/getproviders/memoize_source_test.go +++ b/internal/getproviders/memoize_source_test.go @@ -17,10 +17,10 @@ func TestMemoizeSource(t *testing.T) { nonexistPlatform := Platform{OS: "gamegear", Arch: "z80"} t.Run("AvailableVersions for existing provider", func(t *testing.T) { - mock := NewMockSource([]PackageMeta{meta}) + mock := NewMockSource([]PackageMeta{meta}, nil) source := NewMemoizeSource(mock) - got, err := source.AvailableVersions(provider) + got, _, err := source.AvailableVersions(provider) want := VersionList{version} if err != nil { t.Fatalf("unexpected error: %s", err) @@ -29,7 +29,7 @@ func TestMemoizeSource(t *testing.T) { t.Fatalf("wrong result from first call to AvailableVersions\n%s", diff) } - got, err = source.AvailableVersions(provider) + got, _, err = source.AvailableVersions(provider) want = VersionList{version} if err != nil { t.Fatalf("unexpected error: %s", err) @@ -38,12 +38,12 @@ func TestMemoizeSource(t *testing.T) { t.Fatalf("wrong result from second call to AvailableVersions\n%s", diff) } - _, err = source.AvailableVersions(nonexistProvider) + _, _, err = source.AvailableVersions(nonexistProvider) if want, ok := err.(ErrRegistryProviderNotKnown); !ok { t.Fatalf("wrong error type from nonexist call:\ngot: %T\nwant: %T", err, want) } - got, err = source.AvailableVersions(provider) + got, _, err = source.AvailableVersions(provider) want = VersionList{version} if err != nil { t.Fatalf("unexpected error: %s", err) @@ -64,8 +64,30 @@ func TestMemoizeSource(t *testing.T) { t.Fatalf("unexpected call log\n%s", diff) } }) + t.Run("AvailableVersions with warnings", func(t *testing.T) { + warnProvider := addrs.NewDefaultProvider("warning") + meta := FakePackageMeta(warnProvider, version, protocols, platform) + mock := NewMockSource([]PackageMeta{meta}, map[addrs.Provider]Warnings{warnProvider: {"WARNING!"}}) + source := NewMemoizeSource(mock) + + got, warns, err := source.AvailableVersions(warnProvider) + want := VersionList{version} + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + if diff := cmp.Diff(want, got); diff != "" { + t.Fatalf("wrong result from first call to AvailableVersions\n%s", diff) + } + if len(warns) != 1 { + t.Fatalf("wrong number of warnings. Got %d, expected 1", len(warns)) + } + if warns[0] != "WARNING!" { + t.Fatalf("wrong result! Got %s, expected \"WARNING!\"", warns[0]) + } + + }) t.Run("PackageMeta for existing provider", func(t *testing.T) { - mock := NewMockSource([]PackageMeta{meta}) + mock := NewMockSource([]PackageMeta{meta}, nil) source := NewMemoizeSource(mock) got, err := source.PackageMeta(provider, version, platform) @@ -118,14 +140,14 @@ func TestMemoizeSource(t *testing.T) { } }) t.Run("AvailableVersions for non-existing provider", func(t *testing.T) { - mock := NewMockSource([]PackageMeta{meta}) + mock := NewMockSource([]PackageMeta{meta}, nil) source := NewMemoizeSource(mock) - _, err := source.AvailableVersions(nonexistProvider) + _, _, err := source.AvailableVersions(nonexistProvider) if want, ok := err.(ErrRegistryProviderNotKnown); !ok { 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.(ErrRegistryProviderNotKnown); !ok { t.Fatalf("wrong error type from second call:\ngot: %T\nwant: %T", err, want) } @@ -140,7 +162,7 @@ func TestMemoizeSource(t *testing.T) { } }) t.Run("PackageMeta for non-existing provider", func(t *testing.T) { - mock := NewMockSource([]PackageMeta{meta}) + mock := NewMockSource([]PackageMeta{meta}, nil) source := NewMemoizeSource(mock) _, err := source.PackageMeta(nonexistProvider, version, platform) diff --git a/internal/getproviders/mock_source.go b/internal/getproviders/mock_source.go index 1672cc896..f85abfd4a 100644 --- a/internal/getproviders/mock_source.go +++ b/internal/getproviders/mock_source.go @@ -20,6 +20,7 @@ import ( // This should not be used outside of unit test code. type MockSource struct { packages []PackageMeta + warnings map[addrs.Provider]Warnings calls [][]interface{} } @@ -31,16 +32,17 @@ var _ Source = (*MockSource)(nil) // exist on disk or over the network, unless the calling test is planning to // use (directly or indirectly) the results for further provider installation // actions. -func NewMockSource(packages []PackageMeta) *MockSource { +func NewMockSource(packages []PackageMeta, warns map[addrs.Provider]Warnings) *MockSource { return &MockSource{ packages: packages, + warnings: warns, } } // AvailableVersions returns all of the versions of the given provider that // are available in the fixed set of packages that were passed to // NewMockSource when creating the receiving source. -func (s *MockSource) AvailableVersions(provider addrs.Provider) (VersionList, error) { +func (s *MockSource) AvailableVersions(provider addrs.Provider) (VersionList, Warnings, error) { s.calls = append(s.calls, []interface{}{"AvailableVersions", provider}) var ret VersionList for _, pkg := range s.packages { @@ -48,13 +50,19 @@ func (s *MockSource) AvailableVersions(provider addrs.Provider) (VersionList, er ret = append(ret, pkg.Version) } } + var warns []string + if s.warnings != nil { + if warnings, ok := s.warnings[provider]; ok { + warns = warnings + } + } 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, ErrRegistryProviderNotKnown{provider} + return nil, warns, ErrRegistryProviderNotKnown{provider} } ret.Sort() - return ret, nil + return ret, warns, nil } // PackageMeta returns the first package from the list given to NewMockSource diff --git a/internal/getproviders/multi_source.go b/internal/getproviders/multi_source.go index 1d25938cf..5286bb029 100644 --- a/internal/getproviders/multi_source.go +++ b/internal/getproviders/multi_source.go @@ -28,20 +28,21 @@ var _ Source = MultiSource(nil) // AvailableVersions retrieves all of the versions of the given provider // that are available across all of the underlying selectors, while respecting // each selector's matching patterns. -func (s MultiSource) AvailableVersions(provider addrs.Provider) (VersionList, error) { +func (s MultiSource) AvailableVersions(provider addrs.Provider) (VersionList, Warnings, error) { if len(s) == 0 { // Easy case: there can be no available versions - return nil, nil + return nil, nil, nil } // 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 + var warnings []string for _, selector := range s { if !selector.CanHandleProvider(provider) { continue // doesn't match the given patterns } - thisSourceVersions, err := selector.Source.AvailableVersions(provider) + thisSourceVersions, warningsResp, err := selector.Source.AvailableVersions(provider) switch err.(type) { case nil: // okay @@ -51,18 +52,21 @@ func (s MultiSource) AvailableVersions(provider addrs.Provider) (VersionList, er case ErrProviderNotFound: continue // ignore, then default: - return nil, err + return nil, nil, err } for _, v := range thisSourceVersions { vs[v] = struct{}{} } + if len(warningsResp) > 0 { + warnings = append(warnings, warningsResp...) + } } if len(vs) == 0 { if registryError { - return nil, ErrRegistryProviderNotKnown{provider} + return nil, nil, ErrRegistryProviderNotKnown{provider} } else { - return nil, ErrProviderNotFound{provider, s.sourcesForProvider(provider)} + return nil, nil, ErrProviderNotFound{provider, s.sourcesForProvider(provider)} } } ret := make(VersionList, 0, len(vs)) @@ -71,7 +75,7 @@ func (s MultiSource) AvailableVersions(provider addrs.Provider) (VersionList, er } ret.Sort() - return ret, nil + return ret, warnings, nil } // PackageMeta retrieves the package metadata for the requested provider package diff --git a/internal/getproviders/multi_source_test.go b/internal/getproviders/multi_source_test.go index db81355fd..f8a2178fe 100644 --- a/internal/getproviders/multi_source_test.go +++ b/internal/getproviders/multi_source_test.go @@ -31,7 +31,9 @@ func TestMultiSourceAvailableVersions(t *testing.T) { VersionList{MustParseVersion("5.0")}, platform2, ), - }) + }, + nil, + ) s2 := NewMockSource([]PackageMeta{ FakePackageMeta( addrs.NewDefaultProvider("foo"), @@ -51,7 +53,9 @@ func TestMultiSourceAvailableVersions(t *testing.T) { VersionList{MustParseVersion("5.0")}, platform1, ), - }) + }, + nil, + ) multi := MultiSource{ {Source: s1}, {Source: s2}, @@ -59,7 +63,7 @@ func TestMultiSourceAvailableVersions(t *testing.T) { // AvailableVersions produces the union of all versions available // across all of the sources. - got, err := multi.AvailableVersions(addrs.NewDefaultProvider("foo")) + got, _, err := multi.AvailableVersions(addrs.NewDefaultProvider("foo")) if err != nil { t.Fatalf("unexpected error: %s", err) } @@ -72,7 +76,7 @@ func TestMultiSourceAvailableVersions(t *testing.T) { t.Errorf("wrong result\n%s", diff) } - _, err = multi.AvailableVersions(addrs.NewDefaultProvider("baz")) + _, _, err = multi.AvailableVersions(addrs.NewDefaultProvider("baz")) if want, ok := err.(ErrRegistryProviderNotKnown); !ok { t.Fatalf("wrong error type:\ngot: %T\nwant: %T", err, want) } @@ -96,7 +100,9 @@ func TestMultiSourceAvailableVersions(t *testing.T) { VersionList{MustParseVersion("5.0")}, platform1, ), - }) + }, + nil, + ) s2 := NewMockSource([]PackageMeta{ FakePackageMeta( addrs.NewDefaultProvider("foo"), @@ -110,7 +116,9 @@ func TestMultiSourceAvailableVersions(t *testing.T) { VersionList{MustParseVersion("5.0")}, platform1, ), - }) + }, + nil, + ) multi := MultiSource{ { Source: s1, @@ -122,7 +130,7 @@ func TestMultiSourceAvailableVersions(t *testing.T) { }, } - got, err := multi.AvailableVersions(addrs.NewDefaultProvider("foo")) + got, _, err := multi.AvailableVersions(addrs.NewDefaultProvider("foo")) if err != nil { t.Fatalf("unexpected error: %s", err) } @@ -134,7 +142,7 @@ func TestMultiSourceAvailableVersions(t *testing.T) { t.Errorf("wrong result\n%s", diff) } - got, err = multi.AvailableVersions(addrs.NewDefaultProvider("bar")) + got, _, err = multi.AvailableVersions(addrs.NewDefaultProvider("bar")) if err != nil { t.Fatalf("unexpected error: %s", err) } @@ -146,21 +154,21 @@ func TestMultiSourceAvailableVersions(t *testing.T) { t.Errorf("wrong result\n%s", diff) } - _, err = multi.AvailableVersions(addrs.NewDefaultProvider("baz")) + _, _, err = multi.AvailableVersions(addrs.NewDefaultProvider("baz")) if want, ok := err.(ErrRegistryProviderNotKnown); !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) + s1 := NewMockSource(nil, nil) + s2 := NewMockSource(nil, nil) multi := MultiSource{ {Source: s1}, {Source: s2}, } - _, err := multi.AvailableVersions(addrs.NewDefaultProvider("foo")) + _, _, err := multi.AvailableVersions(addrs.NewDefaultProvider("foo")) if err == nil { t.Fatal("expected error, got success") } @@ -172,6 +180,57 @@ func TestMultiSourceAvailableVersions(t *testing.T) { } }) + + t.Run("merging with warnings", func(t *testing.T) { + platform1 := Platform{OS: "amigaos", Arch: "m68k"} + platform2 := Platform{OS: "aros", Arch: "arm"} + s1 := NewMockSource([]PackageMeta{ + FakePackageMeta( + addrs.NewDefaultProvider("bar"), + MustParseVersion("1.0.0"), + VersionList{MustParseVersion("5.0")}, + platform2, + ), + }, + map[addrs.Provider]Warnings{ + addrs.NewDefaultProvider("bar"): {"WARNING!"}, + }, + ) + s2 := NewMockSource([]PackageMeta{ + FakePackageMeta( + addrs.NewDefaultProvider("bar"), + MustParseVersion("1.0.0"), + VersionList{MustParseVersion("5.0")}, + platform1, + ), + }, + nil, + ) + multi := MultiSource{ + {Source: s1}, + {Source: s2}, + } + + // AvailableVersions produces the union of all versions available + // across all of the sources. + got, warns, err := multi.AvailableVersions(addrs.NewDefaultProvider("bar")) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + want := VersionList{ + MustParseVersion("1.0.0"), + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + + if len(warns) != 1 { + t.Fatalf("wrong number of warnings. Got %d, wanted 1", len(warns)) + } + if warns[0] != "WARNING!" { + t.Fatalf("wrong warnings. Got %s, wanted \"WARNING!\"", warns[0]) + } + }) } func TestMultiSourcePackageMeta(t *testing.T) { @@ -214,7 +273,9 @@ func TestMultiSourcePackageMeta(t *testing.T) { VersionList{MustParseVersion("5.0")}, platform2, )), - }) + }, + nil, + ) s2 := NewMockSource([]PackageMeta{ inBothS2, onlyInS2, @@ -224,7 +285,7 @@ func TestMultiSourcePackageMeta(t *testing.T) { VersionList{MustParseVersion("5.0")}, platform1, )), - }) + }, nil) multi := MultiSource{ {Source: s1}, {Source: s2}, @@ -297,7 +358,7 @@ func TestMultiSourcePackageMeta(t *testing.T) { } func TestMultiSourceSelector(t *testing.T) { - emptySource := NewMockSource(nil) + emptySource := NewMockSource(nil, nil) tests := map[string]struct { Selector MultiSourceSelector diff --git a/internal/getproviders/registry_client.go b/internal/getproviders/registry_client.go index 14bb016e0..b3f6a27c5 100644 --- a/internal/getproviders/registry_client.go +++ b/internal/getproviders/registry_client.go @@ -97,24 +97,23 @@ func newRegistryClient(baseURL *url.URL, creds svcauth.HostCredentials) *registr // 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. -func (c *registryClient) ProviderVersions(addr addrs.Provider) (map[string][]string, error) { +func (c *registryClient) ProviderVersions(addr addrs.Provider) (map[string][]string, []string, error) { endpointPath, err := url.Parse(path.Join(addr.Namespace, addr.Type, "versions")) if err != nil { // Should never happen because we're constructing this from // already-validated components. - return nil, err + return nil, nil, err } endpointURL := c.baseURL.ResolveReference(endpointPath) - req, err := retryablehttp.NewRequest("GET", endpointURL.String(), nil) if err != nil { - return nil, err + return nil, nil, err } c.addHeadersToRequest(req.Request) resp, err := c.httpClient.Do(req) if err != nil { - return nil, c.errQueryFailed(addr, err) + return nil, nil, c.errQueryFailed(addr, err) } defer resp.Body.Close() @@ -122,13 +121,13 @@ func (c *registryClient) ProviderVersions(addr addrs.Provider) (map[string][]str case http.StatusOK: // Great! case http.StatusNotFound: - return nil, ErrRegistryProviderNotKnown{ + return nil, nil, ErrRegistryProviderNotKnown{ Provider: addr, } case http.StatusUnauthorized, http.StatusForbidden: - return nil, c.errUnauthorized(addr.Hostname) + return nil, nil, c.errUnauthorized(addr.Hostname) default: - return nil, c.errQueryFailed(addr, errors.New(resp.Status)) + return nil, nil, c.errQueryFailed(addr, errors.New(resp.Status)) } // We ignore the platforms portion of the response body, because the @@ -139,23 +138,25 @@ func (c *registryClient) ProviderVersions(addr addrs.Provider) (map[string][]str Version string `json:"version"` Protocols []string `json:"protocols"` } `json:"versions"` + Warnings []string `json:"warnings"` } var body ResponseBody dec := json.NewDecoder(resp.Body) if err := dec.Decode(&body); err != nil { - return nil, c.errQueryFailed(addr, err) + return nil, nil, c.errQueryFailed(addr, err) } if len(body.Versions) == 0 { - return nil, nil + return nil, body.Warnings, nil } ret := make(map[string][]string, len(body.Versions)) for _, v := range body.Versions { ret[v.Version] = v.Protocols } - return ret, nil + + return ret, body.Warnings, nil } // PackageMeta returns metadata about a distribution package for a provider. @@ -360,7 +361,7 @@ func (c *registryClient) PackageMeta(provider addrs.Provider, version Version, t // 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 - available, err := c.ProviderVersions(provider) + available, _, err := c.ProviderVersions(provider) if err != nil { return UnspecifiedVersion, err } diff --git a/internal/getproviders/registry_client_test.go b/internal/getproviders/registry_client_test.go index 8e1bf271d..f556583a3 100644 --- a/internal/getproviders/registry_client_test.go +++ b/internal/getproviders/registry_client_test.go @@ -220,7 +220,7 @@ func fakeRegistryHandler(resp http.ResponseWriter, req *http.Request) { case "weaksauce/no-versions": resp.Header().Set("Content-Type", "application/json") resp.WriteHeader(200) - resp.Write([]byte(`{"versions":[]}`)) + resp.Write([]byte(`{"versions":[],"warnings":["this provider is weaksauce"]}`)) case "-/legacy": resp.Header().Set("Content-Type", "application/json") resp.WriteHeader(200) @@ -335,7 +335,7 @@ func TestProviderVersions(t *testing.T) { t.Fatal(err) } - gotVersions, err := client.ProviderVersions(test.provider) + gotVersions, _, err := client.ProviderVersions(test.provider) if err != nil { if test.wantErr == "" { diff --git a/internal/getproviders/registry_source.go b/internal/getproviders/registry_source.go index 042a028f2..57773699f 100644 --- a/internal/getproviders/registry_source.go +++ b/internal/getproviders/registry_source.go @@ -32,40 +32,39 @@ func NewRegistrySource(services *disco.Disco) *RegistrySource { // ErrHostNoProviders, ErrHostUnreachable, ErrUnauthenticated, // ErrProviderNotKnown, or ErrQueryFailed. Callers must be defensive and // expect errors of other types too, to allow for future expansion. -func (s *RegistrySource) AvailableVersions(provider addrs.Provider) (VersionList, error) { +func (s *RegistrySource) AvailableVersions(provider addrs.Provider) (VersionList, Warnings, error) { client, err := s.registryClient(provider.Hostname) if err != nil { - return nil, err + return nil, nil, err } - versionProtosMap, err := client.ProviderVersions(provider) + versionsResponse, warnings, err := client.ProviderVersions(provider) if err != nil { - return nil, err + return nil, nil, err } - if len(versionProtosMap) == 0 { - return nil, nil + if len(versionsResponse) == 0 { + return nil, warnings, nil } - // We ignore everything except the version numbers here because our goal - // is to find out which versions are available _at all_. Which ones are - // compatible with the current Terraform becomes relevant only once we've - // selected one, at which point we'll return an error if the selected one - // is incompatible. + // We ignore protocols here because our goal is to find out which versions + // are available _at all_. Which ones are compatible with the current + // Terraform becomes relevant only once we've selected one, at which point + // we'll return an error if the selected one is incompatible. // // We intentionally produce an error on incompatibility, rather than // silently ignoring an incompatible version, in order to give the user // explicit feedback about why their selection wasn't valid and allow them // to decide whether to fix that by changing the selection or by some other // action such as upgrading Terraform, using a different OS to run - // Terraform, etc. Changes that affect compatibility are considered - // breaking changes from a provider API standpoint, so provider teams - // should change compatibility only in new major versions. - ret := make(VersionList, 0, len(versionProtosMap)) - for str := range versionProtosMap { + // Terraform, etc. Changes that affect compatibility are considered breaking + // changes from a provider API standpoint, so provider teams should change + // compatibility only in new major versions. + ret := make(VersionList, 0, len(versionsResponse)) + for str := range versionsResponse { v, err := ParseVersion(str) if err != nil { - return nil, ErrQueryFailed{ + return nil, nil, ErrQueryFailed{ Provider: provider, Wrapped: fmt.Errorf("registry response includes invalid version string %q: %s", str, err), } @@ -73,7 +72,7 @@ func (s *RegistrySource) AvailableVersions(provider addrs.Provider) (VersionList ret = append(ret, v) } ret.Sort() // lowest precedence first, preserving order when equal precedence - return ret, nil + return ret, warnings, nil } // PackageMeta returns metadata about the location and capabilities of diff --git a/internal/getproviders/registry_source_test.go b/internal/getproviders/registry_source_test.go index 19848f9bb..bbdc22387 100644 --- a/internal/getproviders/registry_source_test.go +++ b/internal/getproviders/registry_source_test.go @@ -58,16 +58,8 @@ func TestSourceAvailableVersions(t *testing.T) { for _, test := range tests { t.Run(test.provider, func(t *testing.T) { - // TEMP: We don't yet have a function for parsing provider - // source addresses so we'll just fake it in here for now. - parts := strings.Split(test.provider, "/") - providerAddr := addrs.Provider{ - Hostname: svchost.Hostname(parts[0]), - Namespace: parts[1], - Type: parts[2], - } - - gotVersions, err := source.AvailableVersions(providerAddr) + provider := addrs.MustParseProviderSourceString(test.provider) + gotVersions, _, err := source.AvailableVersions(provider) if err != nil { if test.wantErr == "" { @@ -96,6 +88,21 @@ func TestSourceAvailableVersions(t *testing.T) { } }) } +} + +func TestSourceAvailableVersions_warnings(t *testing.T) { + source, _, close := testRegistrySource(t) + defer close() + + provider := addrs.MustParseProviderSourceString("example.com/weaksauce/no-versions") + _, warnings, err := source.AvailableVersions(provider) + if err != nil { + t.Fatalf("unexpected error: %s", err.Error()) + } + + if len(warnings) != 1 { + t.Fatalf("wrong number of warnings. Expected 1, got %d", len(warnings)) + } } diff --git a/internal/getproviders/source.go b/internal/getproviders/source.go index 7c1e30d5f..f465d8c05 100644 --- a/internal/getproviders/source.go +++ b/internal/getproviders/source.go @@ -7,7 +7,7 @@ import ( // A Source can query a particular source for information about providers // that are available to install. type Source interface { - AvailableVersions(provider addrs.Provider) (VersionList, error) + AvailableVersions(provider addrs.Provider) (VersionList, Warnings, error) PackageMeta(provider addrs.Provider, version Version, target Platform) (PackageMeta, error) ForDisplay(provider addrs.Provider) string } diff --git a/internal/getproviders/types.go b/internal/getproviders/types.go index cc416a42c..f82af8cff 100644 --- a/internal/getproviders/types.go +++ b/internal/getproviders/types.go @@ -32,6 +32,9 @@ type VersionSet = versions.Set // define the membership of a VersionSet by exclusion. type VersionConstraints = constraints.IntersectionSpec +// Warnings represents a list of warnings returned by a Registry source. +type Warnings = []string + // Requirements gathers together requirements for many different providers // into a single data structure, as a convenient way to represent the full // set of requirements for a particular configuration or state or both. diff --git a/internal/providercache/installer.go b/internal/providercache/installer.go index a210afd6b..c0fe08406 100644 --- a/internal/providercache/installer.go +++ b/internal/providercache/installer.go @@ -237,7 +237,7 @@ NeedProvider: if cb := evts.QueryPackagesBegin; cb != nil { cb(provider, reqs[provider]) } - available, err := i.source.AvailableVersions(provider) + available, warnings, err := i.source.AvailableVersions(provider) if err != nil { // TODO: Consider retrying a few times for certain types of // source errors that seem likely to be transient. @@ -248,6 +248,11 @@ NeedProvider: // We will take no further actions for this provider. continue } + if len(warnings) > 0 { + if cb := evts.QueryPackagesWarning; cb != nil { + cb(provider, warnings) + } + } available.Sort() // put the versions in increasing order of precedence for i := len(available) - 1; i >= 0; i-- { // walk backwards to consider newer versions first if acceptableVersions.Has(available[i]) { diff --git a/internal/providercache/installer_events.go b/internal/providercache/installer_events.go index d3a433853..b93eb9546 100644 --- a/internal/providercache/installer_events.go +++ b/internal/providercache/installer_events.go @@ -64,9 +64,12 @@ type InstallerEvents struct { // // The Begin, Success, and Failure events will each occur only once per // distinct provider. + // + // The Warning event is unique to the registry source QueryPackagesBegin func(provider addrs.Provider, versionConstraints getproviders.VersionConstraints) QueryPackagesSuccess func(provider addrs.Provider, selectedVersion getproviders.Version) QueryPackagesFailure func(provider addrs.Provider, err error) + QueryPackagesWarning func(provider addrs.Provider, warn []string) // The LinkFromCache... family of events delimit the operation of linking // a selected provider package from the system-wide shared cache into the