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