github.com/opentofu/opentofu@v1.7.1/internal/getproviders/multi_source_test.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package getproviders
     7  
     8  import (
     9  	"context"
    10  	"testing"
    11  
    12  	"github.com/google/go-cmp/cmp"
    13  	"github.com/opentofu/opentofu/internal/addrs"
    14  )
    15  
    16  func TestMultiSourceAvailableVersions(t *testing.T) {
    17  	platform1 := Platform{OS: "amigaos", Arch: "m68k"}
    18  	platform2 := Platform{OS: "aros", Arch: "arm"}
    19  
    20  	t.Run("unfiltered merging", func(t *testing.T) {
    21  		s1 := NewMockSource([]PackageMeta{
    22  			FakePackageMeta(
    23  				addrs.NewDefaultProvider("foo"),
    24  				MustParseVersion("1.0.0"),
    25  				VersionList{MustParseVersion("5.0")},
    26  				platform1,
    27  			),
    28  			FakePackageMeta(
    29  				addrs.NewDefaultProvider("foo"),
    30  				MustParseVersion("1.0.0"),
    31  				VersionList{MustParseVersion("5.0")},
    32  				platform2,
    33  			),
    34  			FakePackageMeta(
    35  				addrs.NewDefaultProvider("bar"),
    36  				MustParseVersion("1.0.0"),
    37  				VersionList{MustParseVersion("5.0")},
    38  				platform2,
    39  			),
    40  		},
    41  			nil,
    42  		)
    43  		s2 := NewMockSource([]PackageMeta{
    44  			FakePackageMeta(
    45  				addrs.NewDefaultProvider("foo"),
    46  				MustParseVersion("1.0.0"),
    47  				VersionList{MustParseVersion("5.0")},
    48  				platform1,
    49  			),
    50  			FakePackageMeta(
    51  				addrs.NewDefaultProvider("foo"),
    52  				MustParseVersion("1.2.0"),
    53  				VersionList{MustParseVersion("5.0")},
    54  				platform1,
    55  			),
    56  			FakePackageMeta(
    57  				addrs.NewDefaultProvider("bar"),
    58  				MustParseVersion("1.0.0"),
    59  				VersionList{MustParseVersion("5.0")},
    60  				platform1,
    61  			),
    62  		},
    63  			nil,
    64  		)
    65  		multi := MultiSource{
    66  			{Source: s1},
    67  			{Source: s2},
    68  		}
    69  
    70  		// AvailableVersions produces the union of all versions available
    71  		// across all of the sources.
    72  		got, _, err := multi.AvailableVersions(context.Background(), addrs.NewDefaultProvider("foo"))
    73  		if err != nil {
    74  			t.Fatalf("unexpected error: %s", err)
    75  		}
    76  		want := VersionList{
    77  			MustParseVersion("1.0.0"),
    78  			MustParseVersion("1.2.0"),
    79  		}
    80  
    81  		if diff := cmp.Diff(want, got); diff != "" {
    82  			t.Errorf("wrong result\n%s", diff)
    83  		}
    84  
    85  		_, _, err = multi.AvailableVersions(context.Background(), addrs.NewDefaultProvider("baz"))
    86  		if want, ok := err.(ErrRegistryProviderNotKnown); !ok {
    87  			t.Fatalf("wrong error type:\ngot:  %T\nwant: %T", err, want)
    88  		}
    89  	})
    90  
    91  	t.Run("merging with filters", func(t *testing.T) {
    92  		// This is just testing that filters are being honored at all, using a
    93  		// specific pair of filters. The different filter combinations
    94  		// themselves are tested in TestMultiSourceSelector.
    95  
    96  		s1 := NewMockSource([]PackageMeta{
    97  			FakePackageMeta(
    98  				addrs.NewDefaultProvider("foo"),
    99  				MustParseVersion("1.0.0"),
   100  				VersionList{MustParseVersion("5.0")},
   101  				platform1,
   102  			),
   103  			FakePackageMeta(
   104  				addrs.NewDefaultProvider("bar"),
   105  				MustParseVersion("1.0.0"),
   106  				VersionList{MustParseVersion("5.0")},
   107  				platform1,
   108  			),
   109  		},
   110  			nil,
   111  		)
   112  		s2 := NewMockSource([]PackageMeta{
   113  			FakePackageMeta(
   114  				addrs.NewDefaultProvider("foo"),
   115  				MustParseVersion("1.2.0"),
   116  				VersionList{MustParseVersion("5.0")},
   117  				platform1,
   118  			),
   119  			FakePackageMeta(
   120  				addrs.NewDefaultProvider("bar"),
   121  				MustParseVersion("1.2.0"),
   122  				VersionList{MustParseVersion("5.0")},
   123  				platform1,
   124  			),
   125  		},
   126  			nil,
   127  		)
   128  		multi := MultiSource{
   129  			{
   130  				Source:  s1,
   131  				Include: mustParseMultiSourceMatchingPatterns("hashicorp/*"),
   132  			},
   133  			{
   134  				Source:  s2,
   135  				Include: mustParseMultiSourceMatchingPatterns("hashicorp/bar"),
   136  			},
   137  		}
   138  
   139  		got, _, err := multi.AvailableVersions(context.Background(), addrs.NewDefaultProvider("foo"))
   140  		if err != nil {
   141  			t.Fatalf("unexpected error: %s", err)
   142  		}
   143  		want := VersionList{
   144  			MustParseVersion("1.0.0"),
   145  			// 1.2.0 isn't present because s3 doesn't include "foo"
   146  		}
   147  		if diff := cmp.Diff(want, got); diff != "" {
   148  			t.Errorf("wrong result\n%s", diff)
   149  		}
   150  
   151  		got, _, err = multi.AvailableVersions(context.Background(), addrs.NewDefaultProvider("bar"))
   152  		if err != nil {
   153  			t.Fatalf("unexpected error: %s", err)
   154  		}
   155  		want = VersionList{
   156  			MustParseVersion("1.0.0"),
   157  			MustParseVersion("1.2.0"), // included because s2 matches "bar"
   158  		}
   159  		if diff := cmp.Diff(want, got); diff != "" {
   160  			t.Errorf("wrong result\n%s", diff)
   161  		}
   162  
   163  		_, _, err = multi.AvailableVersions(context.Background(), addrs.NewDefaultProvider("baz"))
   164  		if want, ok := err.(ErrRegistryProviderNotKnown); !ok {
   165  			t.Fatalf("wrong error type:\ngot:  %T\nwant: %T", err, want)
   166  		}
   167  	})
   168  
   169  	t.Run("provider not found", func(t *testing.T) {
   170  		s1 := NewMockSource(nil, nil)
   171  		s2 := NewMockSource(nil, nil)
   172  		multi := MultiSource{
   173  			{Source: s1},
   174  			{Source: s2},
   175  		}
   176  
   177  		_, _, err := multi.AvailableVersions(context.Background(), addrs.NewDefaultProvider("foo"))
   178  		if err == nil {
   179  			t.Fatal("expected error, got success")
   180  		}
   181  
   182  		wantErr := `provider registry registry.opentofu.org does not have a provider named registry.opentofu.org/hashicorp/foo`
   183  
   184  		if err.Error() != wantErr {
   185  			t.Fatalf("wrong error.\ngot:  %s\nwant: %s\n", err, wantErr)
   186  		}
   187  
   188  	})
   189  
   190  	t.Run("merging with warnings", func(t *testing.T) {
   191  		platform1 := Platform{OS: "amigaos", Arch: "m68k"}
   192  		platform2 := Platform{OS: "aros", Arch: "arm"}
   193  		s1 := NewMockSource([]PackageMeta{
   194  			FakePackageMeta(
   195  				addrs.NewDefaultProvider("bar"),
   196  				MustParseVersion("1.0.0"),
   197  				VersionList{MustParseVersion("5.0")},
   198  				platform2,
   199  			),
   200  		},
   201  			map[addrs.Provider]Warnings{
   202  				addrs.NewDefaultProvider("bar"): {"WARNING!"},
   203  			},
   204  		)
   205  		s2 := NewMockSource([]PackageMeta{
   206  			FakePackageMeta(
   207  				addrs.NewDefaultProvider("bar"),
   208  				MustParseVersion("1.0.0"),
   209  				VersionList{MustParseVersion("5.0")},
   210  				platform1,
   211  			),
   212  		},
   213  			nil,
   214  		)
   215  		multi := MultiSource{
   216  			{Source: s1},
   217  			{Source: s2},
   218  		}
   219  
   220  		// AvailableVersions produces the union of all versions available
   221  		// across all of the sources.
   222  		got, warns, err := multi.AvailableVersions(context.Background(), addrs.NewDefaultProvider("bar"))
   223  		if err != nil {
   224  			t.Fatalf("unexpected error: %s", err)
   225  		}
   226  		want := VersionList{
   227  			MustParseVersion("1.0.0"),
   228  		}
   229  		if diff := cmp.Diff(want, got); diff != "" {
   230  			t.Errorf("wrong result\n%s", diff)
   231  		}
   232  
   233  		if len(warns) != 1 {
   234  			t.Fatalf("wrong number of warnings. Got %d, wanted 1", len(warns))
   235  		}
   236  		if warns[0] != "WARNING!" {
   237  			t.Fatalf("wrong warnings. Got %s, wanted \"WARNING!\"", warns[0])
   238  		}
   239  	})
   240  }
   241  
   242  func TestMultiSourcePackageMeta(t *testing.T) {
   243  	platform1 := Platform{OS: "amigaos", Arch: "m68k"}
   244  	platform2 := Platform{OS: "aros", Arch: "arm"}
   245  
   246  	// We'll use the Filename field of the fake PackageMetas we created above
   247  	// to create a difference between the packages in s1 and the ones in s2,
   248  	// so we can test where individual packages came from below.
   249  	fakeFilename := func(fn string, meta PackageMeta) PackageMeta {
   250  		meta.Filename = fn
   251  		return meta
   252  	}
   253  
   254  	onlyInS1 := fakeFilename("s1", FakePackageMeta(
   255  		addrs.NewDefaultProvider("foo"),
   256  		MustParseVersion("1.0.0"),
   257  		VersionList{MustParseVersion("5.0")},
   258  		platform2,
   259  	))
   260  	onlyInS2 := fakeFilename("s2", FakePackageMeta(
   261  		addrs.NewDefaultProvider("foo"),
   262  		MustParseVersion("1.2.0"),
   263  		VersionList{MustParseVersion("5.0")},
   264  		platform1,
   265  	))
   266  	inBothS1 := fakeFilename("s1", FakePackageMeta(
   267  		addrs.NewDefaultProvider("foo"),
   268  		MustParseVersion("1.0.0"),
   269  		VersionList{MustParseVersion("5.0")},
   270  		platform1,
   271  	))
   272  	inBothS2 := fakeFilename("s2", inBothS1)
   273  	s1 := NewMockSource([]PackageMeta{
   274  		inBothS1,
   275  		onlyInS1,
   276  		fakeFilename("s1", FakePackageMeta(
   277  			addrs.NewDefaultProvider("bar"),
   278  			MustParseVersion("1.0.0"),
   279  			VersionList{MustParseVersion("5.0")},
   280  			platform2,
   281  		)),
   282  	},
   283  		nil,
   284  	)
   285  	s2 := NewMockSource([]PackageMeta{
   286  		inBothS2,
   287  		onlyInS2,
   288  		fakeFilename("s2", FakePackageMeta(
   289  			addrs.NewDefaultProvider("bar"),
   290  			MustParseVersion("1.0.0"),
   291  			VersionList{MustParseVersion("5.0")},
   292  			platform1,
   293  		)),
   294  	}, nil)
   295  	multi := MultiSource{
   296  		{Source: s1},
   297  		{Source: s2},
   298  	}
   299  
   300  	t.Run("only in s1", func(t *testing.T) {
   301  		got, err := multi.PackageMeta(
   302  			context.Background(),
   303  			addrs.NewDefaultProvider("foo"),
   304  			MustParseVersion("1.0.0"),
   305  			platform2,
   306  		)
   307  		want := onlyInS1
   308  		if err != nil {
   309  			t.Fatalf("unexpected error: %s", err)
   310  		}
   311  		if diff := cmp.Diff(want, got); diff != "" {
   312  			t.Errorf("wrong result\n%s", diff)
   313  		}
   314  	})
   315  	t.Run("only in s2", func(t *testing.T) {
   316  		got, err := multi.PackageMeta(
   317  			context.Background(),
   318  			addrs.NewDefaultProvider("foo"),
   319  			MustParseVersion("1.2.0"),
   320  			platform1,
   321  		)
   322  		want := onlyInS2
   323  		if err != nil {
   324  			t.Fatalf("unexpected error: %s", err)
   325  		}
   326  		if diff := cmp.Diff(want, got); diff != "" {
   327  			t.Errorf("wrong result\n%s", diff)
   328  		}
   329  	})
   330  	t.Run("in both", func(t *testing.T) {
   331  		got, err := multi.PackageMeta(
   332  			context.Background(),
   333  			addrs.NewDefaultProvider("foo"),
   334  			MustParseVersion("1.0.0"),
   335  			platform1,
   336  		)
   337  		want := inBothS1 // S1 "wins" because it's earlier in the MultiSource
   338  		if err != nil {
   339  			t.Fatalf("unexpected error: %s", err)
   340  		}
   341  		if diff := cmp.Diff(want, got); diff != "" {
   342  			t.Errorf("wrong result\n%s", diff)
   343  		}
   344  
   345  		// Make sure inBothS1 and inBothS2 really are different; if not then
   346  		// that's a test bug which we'd rather catch than have this test
   347  		// accidentally passing without actually checking anything.
   348  		if diff := cmp.Diff(inBothS1, inBothS2); diff == "" {
   349  			t.Fatalf("test bug: inBothS1 and inBothS2 are indistinguishable")
   350  		}
   351  	})
   352  	t.Run("in neither", func(t *testing.T) {
   353  		_, err := multi.PackageMeta(
   354  			context.Background(),
   355  			addrs.NewDefaultProvider("nonexist"),
   356  			MustParseVersion("1.0.0"),
   357  			platform1,
   358  		)
   359  		// This case reports "platform not supported" because it assumes that
   360  		// a caller would only pass to it package versions that were returned
   361  		// by a previousc all to AvailableVersions, and therefore a missing
   362  		// object ought to be valid provider/version but an unsupported
   363  		// platform.
   364  		if want, ok := err.(ErrPlatformNotSupported); !ok {
   365  			t.Fatalf("wrong error type:\ngot:  %T\nwant: %T", err, want)
   366  		}
   367  	})
   368  }
   369  
   370  func TestMultiSourceSelector(t *testing.T) {
   371  	emptySource := NewMockSource(nil, nil)
   372  
   373  	tests := map[string]struct {
   374  		Selector  MultiSourceSelector
   375  		Provider  addrs.Provider
   376  		WantMatch bool
   377  	}{
   378  		"default provider with no constraints": {
   379  			MultiSourceSelector{
   380  				Source: emptySource,
   381  			},
   382  			addrs.NewDefaultProvider("foo"),
   383  			true,
   384  		},
   385  		"built-in provider with no constraints": {
   386  			MultiSourceSelector{
   387  				Source: emptySource,
   388  			},
   389  			addrs.NewBuiltInProvider("bar"),
   390  			true,
   391  		},
   392  
   393  		// Include constraints
   394  		"default provider with include constraint that matches it exactly": {
   395  			MultiSourceSelector{
   396  				Source:  emptySource,
   397  				Include: mustParseMultiSourceMatchingPatterns("hashicorp/foo"),
   398  			},
   399  			addrs.NewDefaultProvider("foo"),
   400  			true,
   401  		},
   402  		"default provider with include constraint that matches it via type wildcard": {
   403  			MultiSourceSelector{
   404  				Source:  emptySource,
   405  				Include: mustParseMultiSourceMatchingPatterns("hashicorp/*"),
   406  			},
   407  			addrs.NewDefaultProvider("foo"),
   408  			true,
   409  		},
   410  		"default provider with include constraint that matches it via namespace wildcard": {
   411  			MultiSourceSelector{
   412  				Source:  emptySource,
   413  				Include: mustParseMultiSourceMatchingPatterns("*/*"),
   414  			},
   415  			addrs.NewDefaultProvider("foo"),
   416  			true,
   417  		},
   418  		"default provider with non-normalized include constraint that matches it via type wildcard": {
   419  			MultiSourceSelector{
   420  				Source:  emptySource,
   421  				Include: mustParseMultiSourceMatchingPatterns("HashiCorp/*"),
   422  			},
   423  			addrs.NewDefaultProvider("foo"),
   424  			true,
   425  		},
   426  		"built-in provider with exact include constraint that does not match it": {
   427  			MultiSourceSelector{
   428  				Source:  emptySource,
   429  				Include: mustParseMultiSourceMatchingPatterns("hashicorp/foo"),
   430  			},
   431  			addrs.NewBuiltInProvider("bar"),
   432  			false,
   433  		},
   434  		"built-in provider with type-wild include constraint that does not match it": {
   435  			MultiSourceSelector{
   436  				Source:  emptySource,
   437  				Include: mustParseMultiSourceMatchingPatterns("hashicorp/*"),
   438  			},
   439  			addrs.NewBuiltInProvider("bar"),
   440  			false,
   441  		},
   442  		"built-in provider with namespace-wild include constraint that does not match it": {
   443  			MultiSourceSelector{
   444  				Source:  emptySource,
   445  				Include: mustParseMultiSourceMatchingPatterns("*/*"),
   446  			},
   447  			// Doesn't match because builtin providers are in "terraform.io",
   448  			// but a pattern with no hostname is for registry.opentofu.org.
   449  			addrs.NewBuiltInProvider("bar"),
   450  			false,
   451  		},
   452  		"built-in provider with include constraint that matches it via type wildcard": {
   453  			MultiSourceSelector{
   454  				Source:  emptySource,
   455  				Include: mustParseMultiSourceMatchingPatterns("terraform.io/builtin/*"),
   456  			},
   457  			addrs.NewBuiltInProvider("bar"),
   458  			true,
   459  		},
   460  
   461  		// Exclude constraints
   462  		"default provider with exclude constraint that matches it exactly": {
   463  			MultiSourceSelector{
   464  				Source:  emptySource,
   465  				Exclude: mustParseMultiSourceMatchingPatterns("hashicorp/foo"),
   466  			},
   467  			addrs.NewDefaultProvider("foo"),
   468  			false,
   469  		},
   470  		"default provider with exclude constraint that matches it via type wildcard": {
   471  			MultiSourceSelector{
   472  				Source:  emptySource,
   473  				Exclude: mustParseMultiSourceMatchingPatterns("hashicorp/*"),
   474  			},
   475  			addrs.NewDefaultProvider("foo"),
   476  			false,
   477  		},
   478  		"default provider with exact exclude constraint that doesn't match it": {
   479  			MultiSourceSelector{
   480  				Source:  emptySource,
   481  				Exclude: mustParseMultiSourceMatchingPatterns("hashicorp/bar"),
   482  			},
   483  			addrs.NewDefaultProvider("foo"),
   484  			true,
   485  		},
   486  		"default provider with non-normalized exclude constraint that matches it via type wildcard": {
   487  			MultiSourceSelector{
   488  				Source:  emptySource,
   489  				Exclude: mustParseMultiSourceMatchingPatterns("HashiCorp/*"),
   490  			},
   491  			addrs.NewDefaultProvider("foo"),
   492  			false,
   493  		},
   494  
   495  		// Both include and exclude in a single selector
   496  		"default provider with exclude wildcard overriding include exact": {
   497  			MultiSourceSelector{
   498  				Source:  emptySource,
   499  				Include: mustParseMultiSourceMatchingPatterns("hashicorp/foo"),
   500  				Exclude: mustParseMultiSourceMatchingPatterns("hashicorp/*"),
   501  			},
   502  			addrs.NewDefaultProvider("foo"),
   503  			false,
   504  		},
   505  		"default provider with exclude wildcard overriding irrelevant include exact": {
   506  			MultiSourceSelector{
   507  				Source:  emptySource,
   508  				Include: mustParseMultiSourceMatchingPatterns("hashicorp/bar"),
   509  				Exclude: mustParseMultiSourceMatchingPatterns("hashicorp/*"),
   510  			},
   511  			addrs.NewDefaultProvider("foo"),
   512  			false,
   513  		},
   514  		"default provider with exclude exact overriding include wildcard": {
   515  			MultiSourceSelector{
   516  				Source:  emptySource,
   517  				Include: mustParseMultiSourceMatchingPatterns("hashicorp/*"),
   518  				Exclude: mustParseMultiSourceMatchingPatterns("hashicorp/foo"),
   519  			},
   520  			addrs.NewDefaultProvider("foo"),
   521  			false,
   522  		},
   523  		"default provider with irrelevant exclude exact overriding include wildcard": {
   524  			MultiSourceSelector{
   525  				Source:  emptySource,
   526  				Include: mustParseMultiSourceMatchingPatterns("hashicorp/*"),
   527  				Exclude: mustParseMultiSourceMatchingPatterns("hashicorp/bar"),
   528  			},
   529  			addrs.NewDefaultProvider("foo"),
   530  			true,
   531  		},
   532  	}
   533  
   534  	for name, test := range tests {
   535  		t.Run(name, func(t *testing.T) {
   536  			t.Logf("include:  %s", test.Selector.Include)
   537  			t.Logf("exclude:  %s", test.Selector.Exclude)
   538  			t.Logf("provider: %s", test.Provider)
   539  			got := test.Selector.CanHandleProvider(test.Provider)
   540  			want := test.WantMatch
   541  			if got != want {
   542  				t.Errorf("wrong result %t; want %t", got, want)
   543  			}
   544  		})
   545  	}
   546  }
   547  
   548  func mustParseMultiSourceMatchingPatterns(strs ...string) MultiSourceMatchingPatterns {
   549  	ret, err := ParseMultiSourceMatchingPatterns(strs)
   550  	if err != nil {
   551  		panic(err)
   552  	}
   553  	return ret
   554  }