opentofu/internal/getproviders/multi_source_test.go
Martin Atkins 0b734a2803 command: Make provider installation interruptible
In earlier commits we started to make the installation codepath
context-aware so that it could be canceled in the event of a SIGINT, but
we didn't complete wiring that through the API of the getproviders
package.

Here we make the getproviders.Source interface methods, along with some
other functions that can make network requests, take a context.Context
argument and act appropriately if that context is cancelled.

The main providercache.Installer.EnsureProviderVersions method now also
has some context-awareness so that it can abort its work early if its
context reports any sort of error. That avoids waiting for the process
to wind through all of the remaining iterations of the various loops,
logging each request failure separately, and instead returns just
a single aggregate "canceled" error.

We can then set things up in the "terraform init" and
"terraform providers mirror" commands so that the context will be
cancelled if we get an interrupt signal, allowing provider installation
to abort early while still atomically completing any local-side effects
that may have started.
2020-09-29 10:00:35 -07:00

550 lines
15 KiB
Go

package getproviders
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform/addrs"
)
func TestMultiSourceAvailableVersions(t *testing.T) {
platform1 := Platform{OS: "amigaos", Arch: "m68k"}
platform2 := Platform{OS: "aros", Arch: "arm"}
t.Run("unfiltered merging", func(t *testing.T) {
s1 := NewMockSource([]PackageMeta{
FakePackageMeta(
addrs.NewDefaultProvider("foo"),
MustParseVersion("1.0.0"),
VersionList{MustParseVersion("5.0")},
platform1,
),
FakePackageMeta(
addrs.NewDefaultProvider("foo"),
MustParseVersion("1.0.0"),
VersionList{MustParseVersion("5.0")},
platform2,
),
FakePackageMeta(
addrs.NewDefaultProvider("bar"),
MustParseVersion("1.0.0"),
VersionList{MustParseVersion("5.0")},
platform2,
),
},
nil,
)
s2 := NewMockSource([]PackageMeta{
FakePackageMeta(
addrs.NewDefaultProvider("foo"),
MustParseVersion("1.0.0"),
VersionList{MustParseVersion("5.0")},
platform1,
),
FakePackageMeta(
addrs.NewDefaultProvider("foo"),
MustParseVersion("1.2.0"),
VersionList{MustParseVersion("5.0")},
platform1,
),
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, _, err := multi.AvailableVersions(context.Background(), addrs.NewDefaultProvider("foo"))
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
want := VersionList{
MustParseVersion("1.0.0"),
MustParseVersion("1.2.0"),
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("wrong result\n%s", diff)
}
_, _, err = multi.AvailableVersions(context.Background(), addrs.NewDefaultProvider("baz"))
if want, ok := err.(ErrRegistryProviderNotKnown); !ok {
t.Fatalf("wrong error type:\ngot: %T\nwant: %T", err, want)
}
})
t.Run("merging with filters", func(t *testing.T) {
// This is just testing that filters are being honored at all, using a
// specific pair of filters. The different filter combinations
// themselves are tested in TestMultiSourceSelector.
s1 := NewMockSource([]PackageMeta{
FakePackageMeta(
addrs.NewDefaultProvider("foo"),
MustParseVersion("1.0.0"),
VersionList{MustParseVersion("5.0")},
platform1,
),
FakePackageMeta(
addrs.NewDefaultProvider("bar"),
MustParseVersion("1.0.0"),
VersionList{MustParseVersion("5.0")},
platform1,
),
},
nil,
)
s2 := NewMockSource([]PackageMeta{
FakePackageMeta(
addrs.NewDefaultProvider("foo"),
MustParseVersion("1.2.0"),
VersionList{MustParseVersion("5.0")},
platform1,
),
FakePackageMeta(
addrs.NewDefaultProvider("bar"),
MustParseVersion("1.2.0"),
VersionList{MustParseVersion("5.0")},
platform1,
),
},
nil,
)
multi := MultiSource{
{
Source: s1,
Include: mustParseMultiSourceMatchingPatterns("hashicorp/*"),
},
{
Source: s2,
Include: mustParseMultiSourceMatchingPatterns("hashicorp/bar"),
},
}
got, _, err := multi.AvailableVersions(context.Background(), addrs.NewDefaultProvider("foo"))
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
want := VersionList{
MustParseVersion("1.0.0"),
// 1.2.0 isn't present because s3 doesn't include "foo"
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("wrong result\n%s", diff)
}
got, _, err = multi.AvailableVersions(context.Background(), addrs.NewDefaultProvider("bar"))
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
want = VersionList{
MustParseVersion("1.0.0"),
MustParseVersion("1.2.0"), // included because s2 matches "bar"
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("wrong result\n%s", diff)
}
_, _, err = multi.AvailableVersions(context.Background(), 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, nil)
s2 := NewMockSource(nil, nil)
multi := MultiSource{
{Source: s1},
{Source: s2},
}
_, _, err := multi.AvailableVersions(context.Background(), addrs.NewDefaultProvider("foo"))
if err == nil {
t.Fatal("expected error, got success")
}
wantErr := `provider registry registry.terraform.io does not have a provider named registry.terraform.io/hashicorp/foo`
if err.Error() != wantErr {
t.Fatalf("wrong error.\ngot: %s\nwant: %s\n", err, wantErr)
}
})
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(context.Background(), 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) {
platform1 := Platform{OS: "amigaos", Arch: "m68k"}
platform2 := Platform{OS: "aros", Arch: "arm"}
// We'll use the Filename field of the fake PackageMetas we created above
// to create a difference between the packages in s1 and the ones in s2,
// so we can test where individual packages came from below.
fakeFilename := func(fn string, meta PackageMeta) PackageMeta {
meta.Filename = fn
return meta
}
onlyInS1 := fakeFilename("s1", FakePackageMeta(
addrs.NewDefaultProvider("foo"),
MustParseVersion("1.0.0"),
VersionList{MustParseVersion("5.0")},
platform2,
))
onlyInS2 := fakeFilename("s2", FakePackageMeta(
addrs.NewDefaultProvider("foo"),
MustParseVersion("1.2.0"),
VersionList{MustParseVersion("5.0")},
platform1,
))
inBothS1 := fakeFilename("s1", FakePackageMeta(
addrs.NewDefaultProvider("foo"),
MustParseVersion("1.0.0"),
VersionList{MustParseVersion("5.0")},
platform1,
))
inBothS2 := fakeFilename("s2", inBothS1)
s1 := NewMockSource([]PackageMeta{
inBothS1,
onlyInS1,
fakeFilename("s1", FakePackageMeta(
addrs.NewDefaultProvider("bar"),
MustParseVersion("1.0.0"),
VersionList{MustParseVersion("5.0")},
platform2,
)),
},
nil,
)
s2 := NewMockSource([]PackageMeta{
inBothS2,
onlyInS2,
fakeFilename("s2", FakePackageMeta(
addrs.NewDefaultProvider("bar"),
MustParseVersion("1.0.0"),
VersionList{MustParseVersion("5.0")},
platform1,
)),
}, nil)
multi := MultiSource{
{Source: s1},
{Source: s2},
}
t.Run("only in s1", func(t *testing.T) {
got, err := multi.PackageMeta(
context.Background(),
addrs.NewDefaultProvider("foo"),
MustParseVersion("1.0.0"),
platform2,
)
want := onlyInS1
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("wrong result\n%s", diff)
}
})
t.Run("only in s2", func(t *testing.T) {
got, err := multi.PackageMeta(
context.Background(),
addrs.NewDefaultProvider("foo"),
MustParseVersion("1.2.0"),
platform1,
)
want := onlyInS2
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("wrong result\n%s", diff)
}
})
t.Run("in both", func(t *testing.T) {
got, err := multi.PackageMeta(
context.Background(),
addrs.NewDefaultProvider("foo"),
MustParseVersion("1.0.0"),
platform1,
)
want := inBothS1 // S1 "wins" because it's earlier in the MultiSource
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("wrong result\n%s", diff)
}
// Make sure inBothS1 and inBothS2 really are different; if not then
// that's a test bug which we'd rather catch than have this test
// accidentally passing without actually checking anything.
if diff := cmp.Diff(inBothS1, inBothS2); diff == "" {
t.Fatalf("test bug: inBothS1 and inBothS2 are indistinguishable")
}
})
t.Run("in neither", func(t *testing.T) {
_, err := multi.PackageMeta(
context.Background(),
addrs.NewDefaultProvider("nonexist"),
MustParseVersion("1.0.0"),
platform1,
)
// This case reports "platform not supported" because it assumes that
// a caller would only pass to it package versions that were returned
// by a previousc all to AvailableVersions, and therefore a missing
// object ought to be valid provider/version but an unsupported
// platform.
if want, ok := err.(ErrPlatformNotSupported); !ok {
t.Fatalf("wrong error type:\ngot: %T\nwant: %T", err, want)
}
})
}
func TestMultiSourceSelector(t *testing.T) {
emptySource := NewMockSource(nil, nil)
tests := map[string]struct {
Selector MultiSourceSelector
Provider addrs.Provider
WantMatch bool
}{
"default provider with no constraints": {
MultiSourceSelector{
Source: emptySource,
},
addrs.NewDefaultProvider("foo"),
true,
},
"built-in provider with no constraints": {
MultiSourceSelector{
Source: emptySource,
},
addrs.NewBuiltInProvider("bar"),
true,
},
// Include constraints
"default provider with include constraint that matches it exactly": {
MultiSourceSelector{
Source: emptySource,
Include: mustParseMultiSourceMatchingPatterns("hashicorp/foo"),
},
addrs.NewDefaultProvider("foo"),
true,
},
"default provider with include constraint that matches it via type wildcard": {
MultiSourceSelector{
Source: emptySource,
Include: mustParseMultiSourceMatchingPatterns("hashicorp/*"),
},
addrs.NewDefaultProvider("foo"),
true,
},
"default provider with include constraint that matches it via namespace wildcard": {
MultiSourceSelector{
Source: emptySource,
Include: mustParseMultiSourceMatchingPatterns("*/*"),
},
addrs.NewDefaultProvider("foo"),
true,
},
"default provider with non-normalized include constraint that matches it via type wildcard": {
MultiSourceSelector{
Source: emptySource,
Include: mustParseMultiSourceMatchingPatterns("HashiCorp/*"),
},
addrs.NewDefaultProvider("foo"),
true,
},
"built-in provider with exact include constraint that does not match it": {
MultiSourceSelector{
Source: emptySource,
Include: mustParseMultiSourceMatchingPatterns("hashicorp/foo"),
},
addrs.NewBuiltInProvider("bar"),
false,
},
"built-in provider with type-wild include constraint that does not match it": {
MultiSourceSelector{
Source: emptySource,
Include: mustParseMultiSourceMatchingPatterns("hashicorp/*"),
},
addrs.NewBuiltInProvider("bar"),
false,
},
"built-in provider with namespace-wild include constraint that does not match it": {
MultiSourceSelector{
Source: emptySource,
Include: mustParseMultiSourceMatchingPatterns("*/*"),
},
// Doesn't match because builtin providers are in "terraform.io",
// but a pattern with no hostname is for registry.terraform.io.
addrs.NewBuiltInProvider("bar"),
false,
},
"built-in provider with include constraint that matches it via type wildcard": {
MultiSourceSelector{
Source: emptySource,
Include: mustParseMultiSourceMatchingPatterns("terraform.io/builtin/*"),
},
addrs.NewBuiltInProvider("bar"),
true,
},
// Exclude constraints
"default provider with exclude constraint that matches it exactly": {
MultiSourceSelector{
Source: emptySource,
Exclude: mustParseMultiSourceMatchingPatterns("hashicorp/foo"),
},
addrs.NewDefaultProvider("foo"),
false,
},
"default provider with exclude constraint that matches it via type wildcard": {
MultiSourceSelector{
Source: emptySource,
Exclude: mustParseMultiSourceMatchingPatterns("hashicorp/*"),
},
addrs.NewDefaultProvider("foo"),
false,
},
"default provider with exact exclude constraint that doesn't match it": {
MultiSourceSelector{
Source: emptySource,
Exclude: mustParseMultiSourceMatchingPatterns("hashicorp/bar"),
},
addrs.NewDefaultProvider("foo"),
true,
},
"default provider with non-normalized exclude constraint that matches it via type wildcard": {
MultiSourceSelector{
Source: emptySource,
Exclude: mustParseMultiSourceMatchingPatterns("HashiCorp/*"),
},
addrs.NewDefaultProvider("foo"),
false,
},
// Both include and exclude in a single selector
"default provider with exclude wildcard overriding include exact": {
MultiSourceSelector{
Source: emptySource,
Include: mustParseMultiSourceMatchingPatterns("hashicorp/foo"),
Exclude: mustParseMultiSourceMatchingPatterns("hashicorp/*"),
},
addrs.NewDefaultProvider("foo"),
false,
},
"default provider with exclude wildcard overriding irrelevant include exact": {
MultiSourceSelector{
Source: emptySource,
Include: mustParseMultiSourceMatchingPatterns("hashicorp/bar"),
Exclude: mustParseMultiSourceMatchingPatterns("hashicorp/*"),
},
addrs.NewDefaultProvider("foo"),
false,
},
"default provider with exclude exact overriding include wildcard": {
MultiSourceSelector{
Source: emptySource,
Include: mustParseMultiSourceMatchingPatterns("hashicorp/*"),
Exclude: mustParseMultiSourceMatchingPatterns("hashicorp/foo"),
},
addrs.NewDefaultProvider("foo"),
false,
},
"default provider with irrelevant exclude exact overriding include wildcard": {
MultiSourceSelector{
Source: emptySource,
Include: mustParseMultiSourceMatchingPatterns("hashicorp/*"),
Exclude: mustParseMultiSourceMatchingPatterns("hashicorp/bar"),
},
addrs.NewDefaultProvider("foo"),
true,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
t.Logf("include: %s", test.Selector.Include)
t.Logf("exclude: %s", test.Selector.Exclude)
t.Logf("provider: %s", test.Provider)
got := test.Selector.CanHandleProvider(test.Provider)
want := test.WantMatch
if got != want {
t.Errorf("wrong result %t; want %t", got, want)
}
})
}
}
func mustParseMultiSourceMatchingPatterns(strs ...string) MultiSourceMatchingPatterns {
ret, err := ParseMultiSourceMatchingPatterns(strs)
if err != nil {
panic(err)
}
return ret
}