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 }