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) { LinkFromCacheFailure: func(provider addrs.Provider, version getproviders.Version, err error) {
diags = diags.Append(tfdiags.Sourceless( diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error, 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 // 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 // First we'll look for the latest version that matches the given
// constraint, which we'll then try to mirror for each target platform. // constraint, which we'll then try to mirror for each target platform.
acceptable := versions.MeetingConstraints(constraints) acceptable := versions.MeetingConstraints(constraints)
avail, err := source.AvailableVersions(provider) avail, _, err := source.AvailableVersions(provider)
candidates := avail.Filter(acceptable) candidates := avail.Filter(acceptable)
if err == nil && len(candidates) == 0 { if err == nil && len(candidates) == 0 {
err = fmt.Errorf("no releases match the given constraints %s", constraintsStr) 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 // AvailableVersions scans the directory structure under the source's base
// directory for locally-mirrored packages for the given provider, returning // directory for locally-mirrored packages for the given provider, returning
// a list of version numbers for the providers it found. // 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 // s.allPackages is populated if scanAllVersions succeeds
err := s.scanAllVersions() err := s.scanAllVersions()
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
// There might be multiple packages for a given version in the filesystem, // 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 = append(ret, v)
} }
ret.Sort() ret.Sort()
return ret, nil return ret, nil, nil
} }
// PackageMeta checks to see if the source's base directory contains a // 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) { func TestFilesystemMirrorSourceAvailableVersions(t *testing.T) {
source := NewFilesystemMirrorSource("testdata/filesystem-mirror") source := NewFilesystemMirrorSource("testdata/filesystem-mirror")
got, err := source.AvailableVersions(nullProvider) got, _, err := source.AvailableVersions(nullProvider)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -26,8 +26,8 @@ func NewHTTPMirrorSource(baseURL *url.URL) *HTTPMirrorSource {
// AvailableVersions retrieves the available versions for the given provider // AvailableVersions retrieves the available versions for the given provider
// from the object's underlying HTTP mirror service. // from the object's underlying HTTP mirror service.
func (s *HTTPMirrorSource) AvailableVersions(provider addrs.Provider) (VersionList, error) { func (s *HTTPMirrorSource) AvailableVersions(provider addrs.Provider) (VersionList, Warnings, error) {
return nil, fmt.Errorf("Network-based provider mirrors are not supported in this version of Terraform") 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 // PackageMeta retrieves metadata for the requested provider package

View File

@ -26,6 +26,7 @@ type MemoizeSource struct {
type memoizeAvailableVersionsRet struct { type memoizeAvailableVersionsRet struct {
VersionList VersionList VersionList VersionList
Warnings Warnings
Err error Err error
} }
@ -55,20 +56,21 @@ func NewMemoizeSource(underlying Source) *MemoizeSource {
// AvailableVersions requests the available versions from the underlying source // AvailableVersions requests the available versions from the underlying source
// and caches them before returning them, or on subsequent calls returns the // and caches them before returning them, or on subsequent calls returns the
// result directly from the cache. // 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() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
if existing, exists := s.availableVersions[provider]; exists { 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{ s.availableVersions[provider] = memoizeAvailableVersionsRet{
VersionList: ret, VersionList: ret,
Err: err, Err: err,
Warnings: warnings,
} }
return ret, err return ret, warnings, err
} }
// PackageMeta requests package metadata from the underlying source and caches // 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"} nonexistPlatform := Platform{OS: "gamegear", Arch: "z80"}
t.Run("AvailableVersions for existing provider", func(t *testing.T) { t.Run("AvailableVersions for existing provider", func(t *testing.T) {
mock := NewMockSource([]PackageMeta{meta}) mock := NewMockSource([]PackageMeta{meta}, nil)
source := NewMemoizeSource(mock) source := NewMemoizeSource(mock)
got, err := source.AvailableVersions(provider) got, _, err := source.AvailableVersions(provider)
want := VersionList{version} want := VersionList{version}
if err != nil { if err != nil {
t.Fatalf("unexpected error: %s", err) 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) 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} want = VersionList{version}
if err != nil { if err != nil {
t.Fatalf("unexpected error: %s", err) 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) 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 { if want, ok := err.(ErrRegistryProviderNotKnown); !ok {
t.Fatalf("wrong error type from nonexist call:\ngot: %T\nwant: %T", err, want) 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} want = VersionList{version}
if err != nil { if err != nil {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
@ -64,8 +64,30 @@ func TestMemoizeSource(t *testing.T) {
t.Fatalf("unexpected call log\n%s", diff) 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) { t.Run("PackageMeta for existing provider", func(t *testing.T) {
mock := NewMockSource([]PackageMeta{meta}) mock := NewMockSource([]PackageMeta{meta}, nil)
source := NewMemoizeSource(mock) source := NewMemoizeSource(mock)
got, err := source.PackageMeta(provider, version, platform) 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) { t.Run("AvailableVersions for non-existing provider", func(t *testing.T) {
mock := NewMockSource([]PackageMeta{meta}) mock := NewMockSource([]PackageMeta{meta}, nil)
source := NewMemoizeSource(mock) source := NewMemoizeSource(mock)
_, err := source.AvailableVersions(nonexistProvider) _, _, err := source.AvailableVersions(nonexistProvider)
if want, ok := err.(ErrRegistryProviderNotKnown); !ok { if want, ok := err.(ErrRegistryProviderNotKnown); !ok {
t.Fatalf("wrong error type from first call:\ngot: %T\nwant: %T", err, want) 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 { if want, ok := err.(ErrRegistryProviderNotKnown); !ok {
t.Fatalf("wrong error type from second call:\ngot: %T\nwant: %T", err, want) 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) { t.Run("PackageMeta for non-existing provider", func(t *testing.T) {
mock := NewMockSource([]PackageMeta{meta}) mock := NewMockSource([]PackageMeta{meta}, nil)
source := NewMemoizeSource(mock) source := NewMemoizeSource(mock)
_, err := source.PackageMeta(nonexistProvider, version, platform) _, err := source.PackageMeta(nonexistProvider, version, platform)

View File

@ -20,6 +20,7 @@ import (
// This should not be used outside of unit test code. // This should not be used outside of unit test code.
type MockSource struct { type MockSource struct {
packages []PackageMeta packages []PackageMeta
warnings map[addrs.Provider]Warnings
calls [][]interface{} calls [][]interface{}
} }
@ -31,16 +32,17 @@ var _ Source = (*MockSource)(nil)
// exist on disk or over the network, unless the calling test is planning to // exist on disk or over the network, unless the calling test is planning to
// use (directly or indirectly) the results for further provider installation // use (directly or indirectly) the results for further provider installation
// actions. // actions.
func NewMockSource(packages []PackageMeta) *MockSource { func NewMockSource(packages []PackageMeta, warns map[addrs.Provider]Warnings) *MockSource {
return &MockSource{ return &MockSource{
packages: packages, packages: packages,
warnings: warns,
} }
} }
// AvailableVersions returns all of the versions of the given provider that // AvailableVersions returns all of the versions of the given provider that
// are available in the fixed set of packages that were passed to // are available in the fixed set of packages that were passed to
// NewMockSource when creating the receiving source. // 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}) s.calls = append(s.calls, []interface{}{"AvailableVersions", provider})
var ret VersionList var ret VersionList
for _, pkg := range s.packages { for _, pkg := range s.packages {
@ -48,13 +50,19 @@ func (s *MockSource) AvailableVersions(provider addrs.Provider) (VersionList, er
ret = append(ret, pkg.Version) 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 { if len(ret) == 0 {
// In this case, we'll behave like a registry that doesn't know about // 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. // this provider at all, rather than just returning an empty result.
return nil, ErrRegistryProviderNotKnown{provider} return nil, warns, ErrRegistryProviderNotKnown{provider}
} }
ret.Sort() ret.Sort()
return ret, nil return ret, warns, nil
} }
// PackageMeta returns the first package from the list given to NewMockSource // 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 // AvailableVersions retrieves all of the versions of the given provider
// that are available across all of the underlying selectors, while respecting // that are available across all of the underlying selectors, while respecting
// each selector's matching patterns. // 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 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 // We will return the union of all versions reported by the nested
// sources that have matching patterns that accept the given provider. // sources that have matching patterns that accept the given provider.
vs := make(map[Version]struct{}) vs := make(map[Version]struct{})
var registryError bool var registryError bool
var warnings []string
for _, selector := range s { for _, selector := range s {
if !selector.CanHandleProvider(provider) { if !selector.CanHandleProvider(provider) {
continue // doesn't match the given patterns continue // doesn't match the given patterns
} }
thisSourceVersions, err := selector.Source.AvailableVersions(provider) thisSourceVersions, warningsResp, err := selector.Source.AvailableVersions(provider)
switch err.(type) { switch err.(type) {
case nil: case nil:
// okay // okay
@ -51,18 +52,21 @@ func (s MultiSource) AvailableVersions(provider addrs.Provider) (VersionList, er
case ErrProviderNotFound: case ErrProviderNotFound:
continue // ignore, then continue // ignore, then
default: default:
return nil, err return nil, nil, err
} }
for _, v := range thisSourceVersions { for _, v := range thisSourceVersions {
vs[v] = struct{}{} vs[v] = struct{}{}
} }
if len(warningsResp) > 0 {
warnings = append(warnings, warningsResp...)
}
} }
if len(vs) == 0 { if len(vs) == 0 {
if registryError { if registryError {
return nil, ErrRegistryProviderNotKnown{provider} return nil, nil, ErrRegistryProviderNotKnown{provider}
} else { } else {
return nil, ErrProviderNotFound{provider, s.sourcesForProvider(provider)} return nil, nil, ErrProviderNotFound{provider, s.sourcesForProvider(provider)}
} }
} }
ret := make(VersionList, 0, len(vs)) ret := make(VersionList, 0, len(vs))
@ -71,7 +75,7 @@ func (s MultiSource) AvailableVersions(provider addrs.Provider) (VersionList, er
} }
ret.Sort() ret.Sort()
return ret, nil return ret, warnings, nil
} }
// PackageMeta retrieves the package metadata for the requested provider package // 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")}, VersionList{MustParseVersion("5.0")},
platform2, platform2,
), ),
}) },
nil,
)
s2 := NewMockSource([]PackageMeta{ s2 := NewMockSource([]PackageMeta{
FakePackageMeta( FakePackageMeta(
addrs.NewDefaultProvider("foo"), addrs.NewDefaultProvider("foo"),
@ -51,7 +53,9 @@ func TestMultiSourceAvailableVersions(t *testing.T) {
VersionList{MustParseVersion("5.0")}, VersionList{MustParseVersion("5.0")},
platform1, platform1,
), ),
}) },
nil,
)
multi := MultiSource{ multi := MultiSource{
{Source: s1}, {Source: s1},
{Source: s2}, {Source: s2},
@ -59,7 +63,7 @@ func TestMultiSourceAvailableVersions(t *testing.T) {
// AvailableVersions produces the union of all versions available // AvailableVersions produces the union of all versions available
// across all of the sources. // across all of the sources.
got, err := multi.AvailableVersions(addrs.NewDefaultProvider("foo")) got, _, err := multi.AvailableVersions(addrs.NewDefaultProvider("foo"))
if err != nil { if err != nil {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
} }
@ -72,7 +76,7 @@ func TestMultiSourceAvailableVersions(t *testing.T) {
t.Errorf("wrong result\n%s", diff) 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 { if want, ok := err.(ErrRegistryProviderNotKnown); !ok {
t.Fatalf("wrong error type:\ngot: %T\nwant: %T", err, want) t.Fatalf("wrong error type:\ngot: %T\nwant: %T", err, want)
} }
@ -96,7 +100,9 @@ func TestMultiSourceAvailableVersions(t *testing.T) {
VersionList{MustParseVersion("5.0")}, VersionList{MustParseVersion("5.0")},
platform1, platform1,
), ),
}) },
nil,
)
s2 := NewMockSource([]PackageMeta{ s2 := NewMockSource([]PackageMeta{
FakePackageMeta( FakePackageMeta(
addrs.NewDefaultProvider("foo"), addrs.NewDefaultProvider("foo"),
@ -110,7 +116,9 @@ func TestMultiSourceAvailableVersions(t *testing.T) {
VersionList{MustParseVersion("5.0")}, VersionList{MustParseVersion("5.0")},
platform1, platform1,
), ),
}) },
nil,
)
multi := MultiSource{ multi := MultiSource{
{ {
Source: s1, 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 { if err != nil {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
} }
@ -134,7 +142,7 @@ func TestMultiSourceAvailableVersions(t *testing.T) {
t.Errorf("wrong result\n%s", diff) t.Errorf("wrong result\n%s", diff)
} }
got, err = multi.AvailableVersions(addrs.NewDefaultProvider("bar")) got, _, err = multi.AvailableVersions(addrs.NewDefaultProvider("bar"))
if err != nil { if err != nil {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
} }
@ -146,21 +154,21 @@ func TestMultiSourceAvailableVersions(t *testing.T) {
t.Errorf("wrong result\n%s", diff) 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 { if want, ok := err.(ErrRegistryProviderNotKnown); !ok {
t.Fatalf("wrong error type:\ngot: %T\nwant: %T", err, want) t.Fatalf("wrong error type:\ngot: %T\nwant: %T", err, want)
} }
}) })
t.Run("provider not found", func(t *testing.T) { t.Run("provider not found", func(t *testing.T) {
s1 := NewMockSource(nil) s1 := NewMockSource(nil, nil)
s2 := NewMockSource(nil) s2 := NewMockSource(nil, nil)
multi := MultiSource{ multi := MultiSource{
{Source: s1}, {Source: s1},
{Source: s2}, {Source: s2},
} }
_, err := multi.AvailableVersions(addrs.NewDefaultProvider("foo")) _, _, err := multi.AvailableVersions(addrs.NewDefaultProvider("foo"))
if err == nil { if err == nil {
t.Fatal("expected error, got success") 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) { func TestMultiSourcePackageMeta(t *testing.T) {
@ -214,7 +273,9 @@ func TestMultiSourcePackageMeta(t *testing.T) {
VersionList{MustParseVersion("5.0")}, VersionList{MustParseVersion("5.0")},
platform2, platform2,
)), )),
}) },
nil,
)
s2 := NewMockSource([]PackageMeta{ s2 := NewMockSource([]PackageMeta{
inBothS2, inBothS2,
onlyInS2, onlyInS2,
@ -224,7 +285,7 @@ func TestMultiSourcePackageMeta(t *testing.T) {
VersionList{MustParseVersion("5.0")}, VersionList{MustParseVersion("5.0")},
platform1, platform1,
)), )),
}) }, nil)
multi := MultiSource{ multi := MultiSource{
{Source: s1}, {Source: s1},
{Source: s2}, {Source: s2},
@ -297,7 +358,7 @@ func TestMultiSourcePackageMeta(t *testing.T) {
} }
func TestMultiSourceSelector(t *testing.T) { func TestMultiSourceSelector(t *testing.T) {
emptySource := NewMockSource(nil) emptySource := NewMockSource(nil, nil)
tests := map[string]struct { tests := map[string]struct {
Selector MultiSourceSelector 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, // 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 // ErrUnauthorized if the registry responds with 401 or 403 status codes, or
// ErrQueryFailed for any other protocol or operational problem. // 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")) endpointPath, err := url.Parse(path.Join(addr.Namespace, addr.Type, "versions"))
if err != nil { if err != nil {
// Should never happen because we're constructing this from // Should never happen because we're constructing this from
// already-validated components. // already-validated components.
return nil, err return nil, nil, err
} }
endpointURL := c.baseURL.ResolveReference(endpointPath) endpointURL := c.baseURL.ResolveReference(endpointPath)
req, err := retryablehttp.NewRequest("GET", endpointURL.String(), nil) req, err := retryablehttp.NewRequest("GET", endpointURL.String(), nil)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
c.addHeadersToRequest(req.Request) c.addHeadersToRequest(req.Request)
resp, err := c.httpClient.Do(req) resp, err := c.httpClient.Do(req)
if err != nil { if err != nil {
return nil, c.errQueryFailed(addr, err) return nil, nil, c.errQueryFailed(addr, err)
} }
defer resp.Body.Close() defer resp.Body.Close()
@ -122,13 +121,13 @@ func (c *registryClient) ProviderVersions(addr addrs.Provider) (map[string][]str
case http.StatusOK: case http.StatusOK:
// Great! // Great!
case http.StatusNotFound: case http.StatusNotFound:
return nil, ErrRegistryProviderNotKnown{ return nil, nil, ErrRegistryProviderNotKnown{
Provider: addr, Provider: addr,
} }
case http.StatusUnauthorized, http.StatusForbidden: case http.StatusUnauthorized, http.StatusForbidden:
return nil, c.errUnauthorized(addr.Hostname) return nil, nil, c.errUnauthorized(addr.Hostname)
default: 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 // 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"` Version string `json:"version"`
Protocols []string `json:"protocols"` Protocols []string `json:"protocols"`
} `json:"versions"` } `json:"versions"`
Warnings []string `json:"warnings"`
} }
var body ResponseBody var body ResponseBody
dec := json.NewDecoder(resp.Body) dec := json.NewDecoder(resp.Body)
if err := dec.Decode(&body); err != nil { 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 { if len(body.Versions) == 0 {
return nil, nil return nil, body.Warnings, nil
} }
ret := make(map[string][]string, len(body.Versions)) ret := make(map[string][]string, len(body.Versions))
for _, v := range body.Versions { for _, v := range body.Versions {
ret[v.Version] = v.Protocols ret[v.Version] = v.Protocols
} }
return ret, nil
return ret, body.Warnings, nil
} }
// PackageMeta returns metadata about a distribution package for a provider. // 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. // findClosestProtocolCompatibleVersion searches for the provider version with the closest protocol match.
func (c *registryClient) findClosestProtocolCompatibleVersion(provider addrs.Provider, version Version) (Version, error) { func (c *registryClient) findClosestProtocolCompatibleVersion(provider addrs.Provider, version Version) (Version, error) {
var match Version var match Version
available, err := c.ProviderVersions(provider) available, _, err := c.ProviderVersions(provider)
if err != nil { if err != nil {
return UnspecifiedVersion, err return UnspecifiedVersion, err
} }

View File

@ -220,7 +220,7 @@ func fakeRegistryHandler(resp http.ResponseWriter, req *http.Request) {
case "weaksauce/no-versions": case "weaksauce/no-versions":
resp.Header().Set("Content-Type", "application/json") resp.Header().Set("Content-Type", "application/json")
resp.WriteHeader(200) resp.WriteHeader(200)
resp.Write([]byte(`{"versions":[]}`)) resp.Write([]byte(`{"versions":[],"warnings":["this provider is weaksauce"]}`))
case "-/legacy": case "-/legacy":
resp.Header().Set("Content-Type", "application/json") resp.Header().Set("Content-Type", "application/json")
resp.WriteHeader(200) resp.WriteHeader(200)
@ -335,7 +335,7 @@ func TestProviderVersions(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
gotVersions, err := client.ProviderVersions(test.provider) gotVersions, _, err := client.ProviderVersions(test.provider)
if err != nil { if err != nil {
if test.wantErr == "" { if test.wantErr == "" {

View File

@ -32,40 +32,39 @@ func NewRegistrySource(services *disco.Disco) *RegistrySource {
// ErrHostNoProviders, ErrHostUnreachable, ErrUnauthenticated, // ErrHostNoProviders, ErrHostUnreachable, ErrUnauthenticated,
// ErrProviderNotKnown, or ErrQueryFailed. Callers must be defensive and // ErrProviderNotKnown, or ErrQueryFailed. Callers must be defensive and
// expect errors of other types too, to allow for future expansion. // 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) client, err := s.registryClient(provider.Hostname)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
versionProtosMap, err := client.ProviderVersions(provider) versionsResponse, warnings, err := client.ProviderVersions(provider)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
if len(versionProtosMap) == 0 { if len(versionsResponse) == 0 {
return nil, nil return nil, warnings, nil
} }
// We ignore everything except the version numbers here because our goal // We ignore protocols here because our goal is to find out which versions
// is to find out which versions are available _at all_. Which ones are // are available _at all_. Which ones are compatible with the current
// compatible with the current Terraform becomes relevant only once we've // Terraform becomes relevant only once we've selected one, at which point
// selected one, at which point we'll return an error if the selected one // we'll return an error if the selected one is incompatible.
// is incompatible.
// //
// We intentionally produce an error on incompatibility, rather than // We intentionally produce an error on incompatibility, rather than
// silently ignoring an incompatible version, in order to give the user // silently ignoring an incompatible version, in order to give the user
// explicit feedback about why their selection wasn't valid and allow them // 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 // 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 // action such as upgrading Terraform, using a different OS to run
// Terraform, etc. Changes that affect compatibility are considered // Terraform, etc. Changes that affect compatibility are considered breaking
// breaking changes from a provider API standpoint, so provider teams // changes from a provider API standpoint, so provider teams should change
// should change compatibility only in new major versions. // compatibility only in new major versions.
ret := make(VersionList, 0, len(versionProtosMap)) ret := make(VersionList, 0, len(versionsResponse))
for str := range versionProtosMap { for str := range versionsResponse {
v, err := ParseVersion(str) v, err := ParseVersion(str)
if err != nil { if err != nil {
return nil, ErrQueryFailed{ return nil, nil, ErrQueryFailed{
Provider: provider, Provider: provider,
Wrapped: fmt.Errorf("registry response includes invalid version string %q: %s", str, err), 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 = append(ret, v)
} }
ret.Sort() // lowest precedence first, preserving order when equal precedence 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 // PackageMeta returns metadata about the location and capabilities of

View File

@ -58,16 +58,8 @@ func TestSourceAvailableVersions(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.provider, func(t *testing.T) { t.Run(test.provider, func(t *testing.T) {
// TEMP: We don't yet have a function for parsing provider provider := addrs.MustParseProviderSourceString(test.provider)
// source addresses so we'll just fake it in here for now. gotVersions, _, err := source.AvailableVersions(provider)
parts := strings.Split(test.provider, "/")
providerAddr := addrs.Provider{
Hostname: svchost.Hostname(parts[0]),
Namespace: parts[1],
Type: parts[2],
}
gotVersions, err := source.AvailableVersions(providerAddr)
if err != nil { if err != nil {
if test.wantErr == "" { 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 // A Source can query a particular source for information about providers
// that are available to install. // that are available to install.
type Source interface { 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) PackageMeta(provider addrs.Provider, version Version, target Platform) (PackageMeta, error)
ForDisplay(provider addrs.Provider) string ForDisplay(provider addrs.Provider) string
} }

View File

@ -32,6 +32,9 @@ type VersionSet = versions.Set
// define the membership of a VersionSet by exclusion. // define the membership of a VersionSet by exclusion.
type VersionConstraints = constraints.IntersectionSpec 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 // Requirements gathers together requirements for many different providers
// into a single data structure, as a convenient way to represent the full // into a single data structure, as a convenient way to represent the full
// set of requirements for a particular configuration or state or both. // set of requirements for a particular configuration or state or both.

View File

@ -237,7 +237,7 @@ NeedProvider:
if cb := evts.QueryPackagesBegin; cb != nil { if cb := evts.QueryPackagesBegin; cb != nil {
cb(provider, reqs[provider]) cb(provider, reqs[provider])
} }
available, err := i.source.AvailableVersions(provider) available, warnings, err := i.source.AvailableVersions(provider)
if err != nil { if err != nil {
// TODO: Consider retrying a few times for certain types of // TODO: Consider retrying a few times for certain types of
// source errors that seem likely to be transient. // source errors that seem likely to be transient.
@ -248,6 +248,11 @@ NeedProvider:
// We will take no further actions for this provider. // We will take no further actions for this provider.
continue continue
} }
if len(warnings) > 0 {
if cb := evts.QueryPackagesWarning; cb != nil {
cb(provider, warnings)
}
}
available.Sort() // put the versions in increasing order of precedence 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 for i := len(available) - 1; i >= 0; i-- { // walk backwards to consider newer versions first
if acceptableVersions.Has(available[i]) { 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 // The Begin, Success, and Failure events will each occur only once per
// distinct provider. // distinct provider.
//
// The Warning event is unique to the registry source
QueryPackagesBegin func(provider addrs.Provider, versionConstraints getproviders.VersionConstraints) QueryPackagesBegin func(provider addrs.Provider, versionConstraints getproviders.VersionConstraints)
QueryPackagesSuccess func(provider addrs.Provider, selectedVersion getproviders.Version) QueryPackagesSuccess func(provider addrs.Provider, selectedVersion getproviders.Version)
QueryPackagesFailure func(provider addrs.Provider, err error) QueryPackagesFailure func(provider addrs.Provider, err error)
QueryPackagesWarning func(provider addrs.Provider, warn []string)
// The LinkFromCache... family of events delimit the operation of linking // The LinkFromCache... family of events delimit the operation of linking
// a selected provider package from the system-wide shared cache into the // a selected provider package from the system-wide shared cache into the