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.
This commit is contained in:
Kristin Laemmert 2020-06-25 10:49:48 -04:00 committed by GitHub
parent 98ff2065bc
commit 47e657c611
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 256 additions and 92 deletions

View File

@ -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)
}
}

View File

@ -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"
}
}
}

View File

@ -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,

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)
}

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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 == "" {

View File

@ -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

View File

@ -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))
}
}

View File

@ -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
}

View File

@ -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.

View File

@ -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]) {

View File

@ -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