github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/helper/pluginutils/loader/loader_test.go (about) 1 package loader 2 3 import ( 4 "io" 5 "io/ioutil" 6 "os" 7 "path/filepath" 8 "runtime" 9 "sort" 10 "strings" 11 "testing" 12 13 log "github.com/hashicorp/go-hclog" 14 version "github.com/hashicorp/go-version" 15 "github.com/hashicorp/nomad/ci" 16 "github.com/hashicorp/nomad/helper/testlog" 17 "github.com/hashicorp/nomad/nomad/structs/config" 18 "github.com/hashicorp/nomad/plugins/base" 19 "github.com/hashicorp/nomad/plugins/device" 20 "github.com/stretchr/testify/require" 21 ) 22 23 var ( 24 // supportedApiVersions is the set of api versions that the "client" can 25 // support 26 supportedApiVersions = map[string][]string{ 27 base.PluginTypeDevice: {device.ApiVersion010}, 28 } 29 ) 30 31 // harness is used to build a temp directory and copy our own test executable 32 // into it, allowing the plugin loader to scan for plugins. 33 type harness struct { 34 t *testing.T 35 tmpDir string 36 } 37 38 // newHarness returns a harness and copies our test binary to the temp directory 39 // with the passed plugin names. 40 func newHarness(t *testing.T, plugins []string) *harness { 41 t.Helper() 42 43 h := &harness{ 44 t: t, 45 } 46 47 // Build a temp directory 48 h.tmpDir = t.TempDir() 49 50 // Get our own executable path 51 selfExe, err := os.Executable() 52 if err != nil { 53 t.Fatalf("failed to get self executable path: %v", err) 54 } 55 56 exeSuffix := "" 57 if runtime.GOOS == "windows" { 58 exeSuffix = ".exe" 59 } 60 for _, p := range plugins { 61 dest := filepath.Join(h.tmpDir, p) + exeSuffix 62 if err := copyFile(selfExe, dest); err != nil { 63 t.Fatalf("failed to copy file: %v", err) 64 } 65 } 66 67 return h 68 } 69 70 // copyFile copies the src file to dst. 71 func copyFile(src, dst string) error { 72 in, err := os.Open(src) 73 if err != nil { 74 return err 75 } 76 defer in.Close() 77 78 out, err := os.Create(dst) 79 if err != nil { 80 return err 81 } 82 defer out.Close() 83 84 if _, err = io.Copy(out, in); err != nil { 85 return err 86 } 87 if err := out.Close(); err != nil { 88 return err 89 } 90 91 return os.Chmod(dst, 0777) 92 } 93 94 // pluginDir returns the plugin directory. 95 func (h *harness) pluginDir() string { 96 return h.tmpDir 97 } 98 99 func TestPluginLoader_External(t *testing.T) { 100 ci.Parallel(t) 101 require := require.New(t) 102 103 // Create two plugins 104 plugins := []string{"mock-device", "mock-device-2"} 105 pluginVersions := []string{"v0.0.1", "v0.0.2"} 106 h := newHarness(t, plugins) 107 108 logger := testlog.HCLogger(t) 109 logger.SetLevel(log.Trace) 110 lconfig := &PluginLoaderConfig{ 111 Logger: logger, 112 PluginDir: h.pluginDir(), 113 SupportedVersions: supportedApiVersions, 114 Configs: []*config.PluginConfig{ 115 { 116 Name: plugins[0], 117 Args: []string{"-plugin", "-name", plugins[0], 118 "-type", base.PluginTypeDevice, "-version", pluginVersions[0], 119 "-api-version", device.ApiVersion010}, 120 }, 121 { 122 Name: plugins[1], 123 Args: []string{"-plugin", "-name", plugins[1], 124 "-type", base.PluginTypeDevice, "-version", pluginVersions[1], 125 "-api-version", device.ApiVersion010, "-api-version", "v0.2.0"}, 126 }, 127 }, 128 } 129 130 l, err := NewPluginLoader(lconfig) 131 require.NoError(err) 132 133 // Get the catalog and assert we have the two plugins 134 c := l.Catalog() 135 require.Len(c, 1) 136 require.Contains(c, base.PluginTypeDevice) 137 detected := c[base.PluginTypeDevice] 138 require.Len(detected, 2) 139 sort.Slice(detected, func(i, j int) bool { return detected[i].Name < detected[j].Name }) 140 141 expected := []*base.PluginInfoResponse{ 142 { 143 Name: plugins[0], 144 Type: base.PluginTypeDevice, 145 PluginVersion: pluginVersions[0], 146 PluginApiVersions: []string{"v0.1.0"}, 147 }, 148 { 149 Name: plugins[1], 150 Type: base.PluginTypeDevice, 151 PluginVersion: pluginVersions[1], 152 PluginApiVersions: []string{"v0.1.0", "v0.2.0"}, 153 }, 154 } 155 require.EqualValues(expected, detected) 156 } 157 158 func TestPluginLoader_External_ApiVersions(t *testing.T) { 159 ci.Parallel(t) 160 require := require.New(t) 161 162 // Create two plugins 163 plugins := []string{"mock-device", "mock-device-2", "mock-device-3"} 164 pluginVersions := []string{"v0.0.1", "v0.0.2"} 165 h := newHarness(t, plugins) 166 167 logger := testlog.HCLogger(t) 168 logger.SetLevel(log.Trace) 169 lconfig := &PluginLoaderConfig{ 170 Logger: logger, 171 PluginDir: h.pluginDir(), 172 SupportedVersions: map[string][]string{ 173 base.PluginTypeDevice: {"0.2.0", "0.2.1", "0.3.0"}, 174 }, 175 Configs: []*config.PluginConfig{ 176 { 177 // No supporting version 178 Name: plugins[0], 179 Args: []string{"-plugin", "-name", plugins[0], 180 "-type", base.PluginTypeDevice, "-version", pluginVersions[0], 181 "-api-version", "v0.1.0"}, 182 }, 183 { 184 // Pick highest matching 185 Name: plugins[1], 186 Args: []string{"-plugin", "-name", plugins[1], 187 "-type", base.PluginTypeDevice, "-version", pluginVersions[1], 188 "-api-version", "v0.1.0", 189 "-api-version", "v0.2.0", 190 "-api-version", "v0.2.1", 191 "-api-version", "v0.2.2", 192 }, 193 }, 194 { 195 // Pick highest matching 196 Name: plugins[2], 197 Args: []string{"-plugin", "-name", plugins[2], 198 "-type", base.PluginTypeDevice, "-version", pluginVersions[1], 199 "-api-version", "v0.1.0", 200 "-api-version", "v0.2.0", 201 "-api-version", "v0.2.1", 202 "-api-version", "v0.3.0", 203 }, 204 }, 205 }, 206 } 207 208 l, err := NewPluginLoader(lconfig) 209 require.NoError(err) 210 211 // Get the catalog and assert we have the two plugins 212 c := l.Catalog() 213 require.Len(c, 1) 214 require.Contains(c, base.PluginTypeDevice) 215 detected := c[base.PluginTypeDevice] 216 require.Len(detected, 2) 217 sort.Slice(detected, func(i, j int) bool { return detected[i].Name < detected[j].Name }) 218 219 expected := []*base.PluginInfoResponse{ 220 { 221 Name: plugins[1], 222 Type: base.PluginTypeDevice, 223 PluginVersion: pluginVersions[1], 224 PluginApiVersions: []string{"v0.1.0", "v0.2.0", "v0.2.1", "v0.2.2"}, 225 }, 226 { 227 Name: plugins[2], 228 Type: base.PluginTypeDevice, 229 PluginVersion: pluginVersions[1], 230 PluginApiVersions: []string{"v0.1.0", "v0.2.0", "v0.2.1", "v0.3.0"}, 231 }, 232 } 233 require.EqualValues(expected, detected) 234 235 // Test we chose the correct versions by dispensing and checking and then 236 // reattaching and checking 237 p1, err := l.Dispense(plugins[1], base.PluginTypeDevice, nil, logger) 238 require.NoError(err) 239 defer p1.Kill() 240 require.Equal("v0.2.1", p1.ApiVersion()) 241 242 p2, err := l.Dispense(plugins[2], base.PluginTypeDevice, nil, logger) 243 require.NoError(err) 244 defer p2.Kill() 245 require.Equal("v0.3.0", p2.ApiVersion()) 246 247 // Test reattach api versions 248 rc1, ok := p1.ReattachConfig() 249 require.True(ok) 250 r1, err := l.Reattach(plugins[1], base.PluginTypeDriver, rc1) 251 require.NoError(err) 252 require.Equal("v0.2.1", r1.ApiVersion()) 253 254 rc2, ok := p2.ReattachConfig() 255 require.True(ok) 256 r2, err := l.Reattach(plugins[2], base.PluginTypeDriver, rc2) 257 require.NoError(err) 258 require.Equal("v0.3.0", r2.ApiVersion()) 259 } 260 261 func TestPluginLoader_External_NoApiVersion(t *testing.T) { 262 ci.Parallel(t) 263 require := require.New(t) 264 265 // Create two plugins 266 plugins := []string{"mock-device"} 267 pluginVersions := []string{"v0.0.1", "v0.0.2"} 268 h := newHarness(t, plugins) 269 270 logger := testlog.HCLogger(t) 271 logger.SetLevel(log.Trace) 272 lconfig := &PluginLoaderConfig{ 273 Logger: logger, 274 PluginDir: h.pluginDir(), 275 SupportedVersions: supportedApiVersions, 276 Configs: []*config.PluginConfig{ 277 { 278 Name: plugins[0], 279 Args: []string{"-plugin", "-name", plugins[0], 280 "-type", base.PluginTypeDevice, "-version", pluginVersions[0]}, 281 }, 282 }, 283 } 284 285 _, err := NewPluginLoader(lconfig) 286 require.Error(err) 287 require.Contains(err.Error(), "no compatible API versions") 288 } 289 290 func TestPluginLoader_External_Config(t *testing.T) { 291 ci.Parallel(t) 292 require := require.New(t) 293 294 // Create two plugins 295 plugins := []string{"mock-device", "mock-device-2"} 296 pluginVersions := []string{"v0.0.1", "v0.0.2"} 297 h := newHarness(t, plugins) 298 299 logger := testlog.HCLogger(t) 300 logger.SetLevel(log.Trace) 301 lconfig := &PluginLoaderConfig{ 302 Logger: logger, 303 PluginDir: h.pluginDir(), 304 SupportedVersions: supportedApiVersions, 305 Configs: []*config.PluginConfig{ 306 { 307 Name: plugins[0], 308 Args: []string{"-plugin", "-name", plugins[0], 309 "-type", base.PluginTypeDevice, "-version", pluginVersions[0], "-api-version", device.ApiVersion010}, 310 Config: map[string]interface{}{ 311 "foo": "1", 312 "bar": "2", 313 }, 314 }, 315 { 316 Name: plugins[1], 317 Args: []string{"-plugin", "-name", plugins[1], 318 "-type", base.PluginTypeDevice, "-version", pluginVersions[1], "-api-version", device.ApiVersion010}, 319 Config: map[string]interface{}{ 320 "foo": "3", 321 "bar": "4", 322 }, 323 }, 324 }, 325 } 326 327 l, err := NewPluginLoader(lconfig) 328 require.NoError(err) 329 330 // Get the catalog and assert we have the two plugins 331 c := l.Catalog() 332 require.Len(c, 1) 333 require.Contains(c, base.PluginTypeDevice) 334 detected := c[base.PluginTypeDevice] 335 require.Len(detected, 2) 336 sort.Slice(detected, func(i, j int) bool { return detected[i].Name < detected[j].Name }) 337 338 expected := []*base.PluginInfoResponse{ 339 { 340 Name: plugins[0], 341 Type: base.PluginTypeDevice, 342 PluginVersion: pluginVersions[0], 343 PluginApiVersions: []string{device.ApiVersion010}, 344 }, 345 { 346 Name: plugins[1], 347 Type: base.PluginTypeDevice, 348 PluginVersion: pluginVersions[1], 349 PluginApiVersions: []string{device.ApiVersion010}, 350 }, 351 } 352 require.EqualValues(expected, detected) 353 } 354 355 // Pass a config but make sure it is fatal 356 func TestPluginLoader_External_Config_Bad(t *testing.T) { 357 ci.Parallel(t) 358 require := require.New(t) 359 360 // Create a plugin 361 plugins := []string{"mock-device"} 362 pluginVersions := []string{"v0.0.1"} 363 h := newHarness(t, plugins) 364 365 logger := testlog.HCLogger(t) 366 logger.SetLevel(log.Trace) 367 lconfig := &PluginLoaderConfig{ 368 Logger: logger, 369 PluginDir: h.pluginDir(), 370 SupportedVersions: supportedApiVersions, 371 Configs: []*config.PluginConfig{ 372 { 373 Name: plugins[0], 374 Args: []string{"-plugin", "-name", plugins[0], 375 "-type", base.PluginTypeDevice, "-version", pluginVersions[0], "-api-version", device.ApiVersion010}, 376 Config: map[string]interface{}{ 377 "foo": "1", 378 "bar": "2", 379 "non-existent": "3", 380 }, 381 }, 382 }, 383 } 384 385 _, err := NewPluginLoader(lconfig) 386 require.Error(err) 387 require.Contains(err.Error(), "No argument or block type is named \"non-existent\"") 388 } 389 390 func TestPluginLoader_External_VersionOverlap(t *testing.T) { 391 ci.Parallel(t) 392 require := require.New(t) 393 394 // Create two plugins 395 plugins := []string{"mock-device", "mock-device-2"} 396 pluginVersions := []string{"v0.0.1", "v0.0.2"} 397 h := newHarness(t, plugins) 398 399 logger := testlog.HCLogger(t) 400 logger.SetLevel(log.Trace) 401 lconfig := &PluginLoaderConfig{ 402 Logger: logger, 403 PluginDir: h.pluginDir(), 404 SupportedVersions: supportedApiVersions, 405 Configs: []*config.PluginConfig{ 406 { 407 Name: plugins[0], 408 Args: []string{"-plugin", "-name", plugins[0], 409 "-type", base.PluginTypeDevice, "-version", pluginVersions[0], "-api-version", device.ApiVersion010}, 410 }, 411 { 412 Name: plugins[1], 413 Args: []string{"-plugin", "-name", plugins[0], 414 "-type", base.PluginTypeDevice, "-version", pluginVersions[1], "-api-version", device.ApiVersion010}, 415 }, 416 }, 417 } 418 419 l, err := NewPluginLoader(lconfig) 420 require.NoError(err) 421 422 // Get the catalog and assert we have the two plugins 423 c := l.Catalog() 424 require.Len(c, 1) 425 require.Contains(c, base.PluginTypeDevice) 426 detected := c[base.PluginTypeDevice] 427 require.Len(detected, 1) 428 sort.Slice(detected, func(i, j int) bool { return detected[i].Name < detected[j].Name }) 429 430 expected := []*base.PluginInfoResponse{ 431 { 432 Name: plugins[0], 433 Type: base.PluginTypeDevice, 434 PluginVersion: pluginVersions[1], 435 PluginApiVersions: []string{device.ApiVersion010}, 436 }, 437 } 438 require.EqualValues(expected, detected) 439 } 440 441 func TestPluginLoader_Internal(t *testing.T) { 442 ci.Parallel(t) 443 require := require.New(t) 444 445 // Create the harness 446 h := newHarness(t, nil) 447 448 plugins := []string{"mock-device", "mock-device-2"} 449 pluginVersions := []string{"v0.0.1", "v0.0.2"} 450 pluginApiVersions := []string{device.ApiVersion010} 451 452 logger := testlog.HCLogger(t) 453 logger.SetLevel(log.Trace) 454 lconfig := &PluginLoaderConfig{ 455 Logger: logger, 456 PluginDir: h.pluginDir(), 457 SupportedVersions: supportedApiVersions, 458 InternalPlugins: map[PluginID]*InternalPluginConfig{ 459 { 460 Name: plugins[0], 461 PluginType: base.PluginTypeDevice, 462 }: { 463 Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[0], pluginApiVersions, true), 464 }, 465 { 466 Name: plugins[1], 467 PluginType: base.PluginTypeDevice, 468 }: { 469 Factory: mockFactory(plugins[1], base.PluginTypeDevice, pluginVersions[1], pluginApiVersions, true), 470 }, 471 }, 472 } 473 474 l, err := NewPluginLoader(lconfig) 475 require.NoError(err) 476 477 // Get the catalog and assert we have the two plugins 478 c := l.Catalog() 479 require.Len(c, 1) 480 require.Contains(c, base.PluginTypeDevice) 481 detected := c[base.PluginTypeDevice] 482 require.Len(detected, 2) 483 sort.Slice(detected, func(i, j int) bool { return detected[i].Name < detected[j].Name }) 484 485 expected := []*base.PluginInfoResponse{ 486 { 487 Name: plugins[0], 488 Type: base.PluginTypeDevice, 489 PluginVersion: pluginVersions[0], 490 PluginApiVersions: []string{device.ApiVersion010}, 491 }, 492 { 493 Name: plugins[1], 494 Type: base.PluginTypeDevice, 495 PluginVersion: pluginVersions[1], 496 PluginApiVersions: []string{device.ApiVersion010}, 497 }, 498 } 499 require.EqualValues(expected, detected) 500 } 501 502 func TestPluginLoader_Internal_ApiVersions(t *testing.T) { 503 ci.Parallel(t) 504 require := require.New(t) 505 506 // Create two plugins 507 plugins := []string{"mock-device", "mock-device-2", "mock-device-3"} 508 pluginVersions := []string{"v0.0.1", "v0.0.2"} 509 h := newHarness(t, nil) 510 511 logger := testlog.HCLogger(t) 512 logger.SetLevel(log.Trace) 513 lconfig := &PluginLoaderConfig{ 514 Logger: logger, 515 PluginDir: h.pluginDir(), 516 SupportedVersions: map[string][]string{ 517 base.PluginTypeDevice: {"0.2.0", "0.2.1", "0.3.0"}, 518 }, 519 InternalPlugins: map[PluginID]*InternalPluginConfig{ 520 { 521 Name: plugins[0], 522 PluginType: base.PluginTypeDevice, 523 }: { 524 Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[0], []string{"v0.1.0"}, true), 525 }, 526 { 527 Name: plugins[1], 528 PluginType: base.PluginTypeDevice, 529 }: { 530 Factory: mockFactory(plugins[1], base.PluginTypeDevice, pluginVersions[1], 531 []string{"v0.1.0", "v0.2.0", "v0.2.1", "v0.2.2"}, true), 532 }, 533 { 534 Name: plugins[2], 535 PluginType: base.PluginTypeDevice, 536 }: { 537 Factory: mockFactory(plugins[2], base.PluginTypeDevice, pluginVersions[1], 538 []string{"v0.1.0", "v0.2.0", "v0.2.1", "v0.3.0"}, true), 539 }, 540 }, 541 } 542 543 l, err := NewPluginLoader(lconfig) 544 require.NoError(err) 545 546 // Get the catalog and assert we have the two plugins 547 c := l.Catalog() 548 require.Len(c, 1) 549 require.Contains(c, base.PluginTypeDevice) 550 detected := c[base.PluginTypeDevice] 551 require.Len(detected, 2) 552 sort.Slice(detected, func(i, j int) bool { return detected[i].Name < detected[j].Name }) 553 554 expected := []*base.PluginInfoResponse{ 555 { 556 Name: plugins[1], 557 Type: base.PluginTypeDevice, 558 PluginVersion: pluginVersions[1], 559 PluginApiVersions: []string{"v0.1.0", "v0.2.0", "v0.2.1", "v0.2.2"}, 560 }, 561 { 562 Name: plugins[2], 563 Type: base.PluginTypeDevice, 564 PluginVersion: pluginVersions[1], 565 PluginApiVersions: []string{"v0.1.0", "v0.2.0", "v0.2.1", "v0.3.0"}, 566 }, 567 } 568 require.EqualValues(expected, detected) 569 570 // Test we chose the correct versions by dispensing and checking and then 571 // reattaching and checking 572 p1, err := l.Dispense(plugins[1], base.PluginTypeDevice, nil, logger) 573 require.NoError(err) 574 defer p1.Kill() 575 require.Equal("v0.2.1", p1.ApiVersion()) 576 577 p2, err := l.Dispense(plugins[2], base.PluginTypeDevice, nil, logger) 578 require.NoError(err) 579 defer p2.Kill() 580 require.Equal("v0.3.0", p2.ApiVersion()) 581 } 582 583 func TestPluginLoader_Internal_NoApiVersion(t *testing.T) { 584 ci.Parallel(t) 585 require := require.New(t) 586 587 // Create two plugins 588 plugins := []string{"mock-device"} 589 pluginVersions := []string{"v0.0.1", "v0.0.2"} 590 h := newHarness(t, nil) 591 592 logger := testlog.HCLogger(t) 593 logger.SetLevel(log.Trace) 594 lconfig := &PluginLoaderConfig{ 595 Logger: logger, 596 PluginDir: h.pluginDir(), 597 SupportedVersions: supportedApiVersions, 598 InternalPlugins: map[PluginID]*InternalPluginConfig{ 599 { 600 Name: plugins[0], 601 PluginType: base.PluginTypeDevice, 602 }: { 603 Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[0], nil, true), 604 }, 605 }, 606 } 607 608 _, err := NewPluginLoader(lconfig) 609 require.Error(err) 610 require.Contains(err.Error(), "no compatible API versions") 611 } 612 613 func TestPluginLoader_Internal_Config(t *testing.T) { 614 ci.Parallel(t) 615 require := require.New(t) 616 617 // Create the harness 618 h := newHarness(t, nil) 619 620 plugins := []string{"mock-device", "mock-device-2"} 621 pluginVersions := []string{"v0.0.1", "v0.0.2"} 622 pluginApiVersions := []string{device.ApiVersion010} 623 624 logger := testlog.HCLogger(t) 625 logger.SetLevel(log.Trace) 626 lconfig := &PluginLoaderConfig{ 627 Logger: logger, 628 PluginDir: h.pluginDir(), 629 SupportedVersions: supportedApiVersions, 630 InternalPlugins: map[PluginID]*InternalPluginConfig{ 631 { 632 Name: plugins[0], 633 PluginType: base.PluginTypeDevice, 634 }: { 635 Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[0], pluginApiVersions, true), 636 Config: map[string]interface{}{ 637 "foo": "1", 638 "bar": "2", 639 }, 640 }, 641 { 642 Name: plugins[1], 643 PluginType: base.PluginTypeDevice, 644 }: { 645 Factory: mockFactory(plugins[1], base.PluginTypeDevice, pluginVersions[1], pluginApiVersions, true), 646 Config: map[string]interface{}{ 647 "foo": "3", 648 "bar": "4", 649 }, 650 }, 651 }, 652 } 653 654 l, err := NewPluginLoader(lconfig) 655 require.NoError(err) 656 657 // Get the catalog and assert we have the two plugins 658 c := l.Catalog() 659 require.Len(c, 1) 660 require.Contains(c, base.PluginTypeDevice) 661 detected := c[base.PluginTypeDevice] 662 require.Len(detected, 2) 663 sort.Slice(detected, func(i, j int) bool { return detected[i].Name < detected[j].Name }) 664 665 expected := []*base.PluginInfoResponse{ 666 { 667 Name: plugins[0], 668 Type: base.PluginTypeDevice, 669 PluginVersion: pluginVersions[0], 670 PluginApiVersions: []string{device.ApiVersion010}, 671 }, 672 { 673 Name: plugins[1], 674 Type: base.PluginTypeDevice, 675 PluginVersion: pluginVersions[1], 676 PluginApiVersions: []string{device.ApiVersion010}, 677 }, 678 } 679 require.EqualValues(expected, detected) 680 } 681 682 // Tests that an external config can override the config of an internal plugin 683 func TestPluginLoader_Internal_ExternalConfig(t *testing.T) { 684 ci.Parallel(t) 685 require := require.New(t) 686 687 // Create the harness 688 h := newHarness(t, nil) 689 690 plugin := "mock-device" 691 pluginVersion := "v0.0.1" 692 pluginApiVersions := []string{device.ApiVersion010} 693 694 id := PluginID{ 695 Name: plugin, 696 PluginType: base.PluginTypeDevice, 697 } 698 expectedConfig := map[string]interface{}{ 699 "foo": "2", 700 "bar": "3", 701 } 702 703 logger := testlog.HCLogger(t) 704 logger.SetLevel(log.Trace) 705 lconfig := &PluginLoaderConfig{ 706 Logger: logger, 707 PluginDir: h.pluginDir(), 708 SupportedVersions: supportedApiVersions, 709 InternalPlugins: map[PluginID]*InternalPluginConfig{ 710 id: { 711 Factory: mockFactory(plugin, base.PluginTypeDevice, pluginVersion, pluginApiVersions, true), 712 Config: map[string]interface{}{ 713 "foo": "1", 714 "bar": "2", 715 }, 716 }, 717 }, 718 Configs: []*config.PluginConfig{ 719 { 720 Name: plugin, 721 Config: expectedConfig, 722 }, 723 }, 724 } 725 726 l, err := NewPluginLoader(lconfig) 727 require.NoError(err) 728 729 // Get the catalog and assert we have the two plugins 730 c := l.Catalog() 731 require.Len(c, 1) 732 require.Contains(c, base.PluginTypeDevice) 733 detected := c[base.PluginTypeDevice] 734 require.Len(detected, 1) 735 736 expected := []*base.PluginInfoResponse{ 737 { 738 Name: plugin, 739 Type: base.PluginTypeDevice, 740 PluginVersion: pluginVersion, 741 PluginApiVersions: []string{device.ApiVersion010}, 742 }, 743 } 744 require.EqualValues(expected, detected) 745 746 // Check the config 747 loaded, ok := l.plugins[id] 748 require.True(ok) 749 require.EqualValues(expectedConfig, loaded.config) 750 } 751 752 // Pass a config but make sure it is fatal 753 func TestPluginLoader_Internal_Config_Bad(t *testing.T) { 754 ci.Parallel(t) 755 require := require.New(t) 756 757 // Create the harness 758 h := newHarness(t, nil) 759 760 plugins := []string{"mock-device"} 761 pluginVersions := []string{"v0.0.1"} 762 pluginApiVersions := []string{device.ApiVersion010} 763 764 logger := testlog.HCLogger(t) 765 logger.SetLevel(log.Trace) 766 lconfig := &PluginLoaderConfig{ 767 Logger: logger, 768 PluginDir: h.pluginDir(), 769 SupportedVersions: supportedApiVersions, 770 InternalPlugins: map[PluginID]*InternalPluginConfig{ 771 { 772 Name: plugins[0], 773 PluginType: base.PluginTypeDevice, 774 }: { 775 Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[0], pluginApiVersions, true), 776 Config: map[string]interface{}{ 777 "foo": "1", 778 "bar": "2", 779 "non-existent": "3", 780 }, 781 }, 782 }, 783 } 784 785 _, err := NewPluginLoader(lconfig) 786 require.Error(err) 787 require.Contains(err.Error(), "No argument or block type is named \"non-existent\"") 788 } 789 790 func TestPluginLoader_InternalOverrideExternal(t *testing.T) { 791 ci.Parallel(t) 792 require := require.New(t) 793 794 // Create two plugins 795 plugins := []string{"mock-device"} 796 pluginVersions := []string{"v0.0.1", "v0.0.2"} 797 pluginApiVersions := []string{device.ApiVersion010} 798 799 h := newHarness(t, plugins) 800 801 logger := testlog.HCLogger(t) 802 logger.SetLevel(log.Trace) 803 lconfig := &PluginLoaderConfig{ 804 Logger: logger, 805 PluginDir: h.pluginDir(), 806 SupportedVersions: supportedApiVersions, 807 Configs: []*config.PluginConfig{ 808 { 809 Name: plugins[0], 810 Args: []string{"-plugin", "-name", plugins[0], 811 "-type", base.PluginTypeDevice, "-version", pluginVersions[0], "-api-version", pluginApiVersions[0]}, 812 }, 813 }, 814 InternalPlugins: map[PluginID]*InternalPluginConfig{ 815 { 816 Name: plugins[0], 817 PluginType: base.PluginTypeDevice, 818 }: { 819 Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[1], pluginApiVersions, true), 820 }, 821 }, 822 } 823 824 l, err := NewPluginLoader(lconfig) 825 require.NoError(err) 826 827 // Get the catalog and assert we have the two plugins 828 c := l.Catalog() 829 require.Len(c, 1) 830 require.Contains(c, base.PluginTypeDevice) 831 detected := c[base.PluginTypeDevice] 832 require.Len(detected, 1) 833 sort.Slice(detected, func(i, j int) bool { return detected[i].Name < detected[j].Name }) 834 835 expected := []*base.PluginInfoResponse{ 836 { 837 Name: plugins[0], 838 Type: base.PluginTypeDevice, 839 PluginVersion: pluginVersions[1], 840 PluginApiVersions: []string{device.ApiVersion010}, 841 }, 842 } 843 require.EqualValues(expected, detected) 844 } 845 846 func TestPluginLoader_ExternalOverrideInternal(t *testing.T) { 847 ci.Parallel(t) 848 require := require.New(t) 849 850 // Create two plugins 851 plugins := []string{"mock-device"} 852 pluginVersions := []string{"v0.0.1", "v0.0.2"} 853 pluginApiVersions := []string{device.ApiVersion010} 854 855 h := newHarness(t, plugins) 856 857 logger := testlog.HCLogger(t) 858 logger.SetLevel(log.Trace) 859 lconfig := &PluginLoaderConfig{ 860 Logger: logger, 861 PluginDir: h.pluginDir(), 862 SupportedVersions: supportedApiVersions, 863 Configs: []*config.PluginConfig{ 864 { 865 Name: plugins[0], 866 Args: []string{"-plugin", "-name", plugins[0], 867 "-type", base.PluginTypeDevice, "-version", pluginVersions[1], "-api-version", pluginApiVersions[0]}, 868 }, 869 }, 870 InternalPlugins: map[PluginID]*InternalPluginConfig{ 871 { 872 Name: plugins[0], 873 PluginType: base.PluginTypeDevice, 874 }: { 875 Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[0], pluginApiVersions, true), 876 }, 877 }, 878 } 879 880 l, err := NewPluginLoader(lconfig) 881 require.NoError(err) 882 883 // Get the catalog and assert we have the two plugins 884 c := l.Catalog() 885 require.Len(c, 1) 886 require.Contains(c, base.PluginTypeDevice) 887 detected := c[base.PluginTypeDevice] 888 require.Len(detected, 1) 889 sort.Slice(detected, func(i, j int) bool { return detected[i].Name < detected[j].Name }) 890 891 expected := []*base.PluginInfoResponse{ 892 { 893 Name: plugins[0], 894 Type: base.PluginTypeDevice, 895 PluginVersion: pluginVersions[1], 896 PluginApiVersions: []string{device.ApiVersion010}, 897 }, 898 } 899 require.EqualValues(expected, detected) 900 } 901 902 func TestPluginLoader_Dispense_External(t *testing.T) { 903 ci.Parallel(t) 904 require := require.New(t) 905 906 // Create two plugins 907 plugin := "mock-device" 908 pluginVersion := "v0.0.1" 909 h := newHarness(t, []string{plugin}) 910 911 expKey := "set_config_worked" 912 913 logger := testlog.HCLogger(t) 914 logger.SetLevel(log.Trace) 915 lconfig := &PluginLoaderConfig{ 916 Logger: logger, 917 PluginDir: h.pluginDir(), 918 SupportedVersions: supportedApiVersions, 919 Configs: []*config.PluginConfig{ 920 { 921 Name: plugin, 922 Args: []string{"-plugin", "-name", plugin, 923 "-type", base.PluginTypeDevice, "-version", pluginVersion, "-api-version", device.ApiVersion010}, 924 Config: map[string]interface{}{ 925 "res_key": expKey, 926 }, 927 }, 928 }, 929 } 930 931 l, err := NewPluginLoader(lconfig) 932 require.NoError(err) 933 934 // Dispense a device plugin 935 p, err := l.Dispense(plugin, base.PluginTypeDevice, nil, logger) 936 require.NoError(err) 937 defer p.Kill() 938 939 instance, ok := p.Plugin().(device.DevicePlugin) 940 require.True(ok) 941 942 res, err := instance.Reserve([]string{"fake"}) 943 require.NoError(err) 944 require.NotNil(res) 945 require.Contains(res.Envs, expKey) 946 } 947 948 func TestPluginLoader_Dispense_Internal(t *testing.T) { 949 ci.Parallel(t) 950 require := require.New(t) 951 952 // Create two plugins 953 plugin := "mock-device" 954 pluginVersion := "v0.0.1" 955 pluginApiVersions := []string{device.ApiVersion010} 956 h := newHarness(t, nil) 957 958 expKey := "set_config_worked" 959 expNomadConfig := &base.AgentConfig{ 960 Driver: &base.ClientDriverConfig{ 961 ClientMinPort: 100, 962 }, 963 } 964 965 logger := testlog.HCLogger(t) 966 logger.SetLevel(log.Trace) 967 lconfig := &PluginLoaderConfig{ 968 Logger: logger, 969 PluginDir: h.pluginDir(), 970 SupportedVersions: supportedApiVersions, 971 InternalPlugins: map[PluginID]*InternalPluginConfig{ 972 { 973 Name: plugin, 974 PluginType: base.PluginTypeDevice, 975 }: { 976 Factory: mockFactory(plugin, base.PluginTypeDevice, pluginVersion, pluginApiVersions, true), 977 Config: map[string]interface{}{ 978 "res_key": expKey, 979 }, 980 }, 981 }, 982 } 983 984 l, err := NewPluginLoader(lconfig) 985 require.NoError(err) 986 987 // Dispense a device plugin 988 p, err := l.Dispense(plugin, base.PluginTypeDevice, expNomadConfig, logger) 989 require.NoError(err) 990 defer p.Kill() 991 992 instance, ok := p.Plugin().(device.DevicePlugin) 993 require.True(ok) 994 995 res, err := instance.Reserve([]string{"fake"}) 996 require.NoError(err) 997 require.NotNil(res) 998 require.Contains(res.Envs, expKey) 999 1000 mock, ok := p.Plugin().(*mockPlugin) 1001 require.True(ok) 1002 require.Exactly(expNomadConfig, mock.nomadConfig) 1003 require.Equal(device.ApiVersion010, mock.negotiatedApiVersion) 1004 } 1005 1006 func TestPluginLoader_Dispense_NoConfigSchema_External(t *testing.T) { 1007 ci.Parallel(t) 1008 require := require.New(t) 1009 1010 // Create two plugins 1011 plugin := "mock-device" 1012 pluginVersion := "v0.0.1" 1013 h := newHarness(t, []string{plugin}) 1014 1015 expKey := "set_config_worked" 1016 1017 logger := testlog.HCLogger(t) 1018 logger.SetLevel(log.Trace) 1019 lconfig := &PluginLoaderConfig{ 1020 Logger: logger, 1021 PluginDir: h.pluginDir(), 1022 SupportedVersions: supportedApiVersions, 1023 Configs: []*config.PluginConfig{ 1024 { 1025 Name: plugin, 1026 Args: []string{"-plugin", "-config-schema=false", "-name", plugin, 1027 "-type", base.PluginTypeDevice, "-version", pluginVersion, "-api-version", device.ApiVersion010}, 1028 Config: map[string]interface{}{ 1029 "res_key": expKey, 1030 }, 1031 }, 1032 }, 1033 } 1034 1035 _, err := NewPluginLoader(lconfig) 1036 require.Error(err) 1037 require.Contains(err.Error(), "configuration not allowed") 1038 1039 // Remove the config and try again 1040 lconfig.Configs[0].Config = nil 1041 l, err := NewPluginLoader(lconfig) 1042 require.NoError(err) 1043 1044 // Dispense a device plugin 1045 p, err := l.Dispense(plugin, base.PluginTypeDevice, nil, logger) 1046 require.NoError(err) 1047 defer p.Kill() 1048 1049 _, ok := p.Plugin().(device.DevicePlugin) 1050 require.True(ok) 1051 } 1052 1053 func TestPluginLoader_Dispense_NoConfigSchema_Internal(t *testing.T) { 1054 ci.Parallel(t) 1055 require := require.New(t) 1056 1057 // Create two plugins 1058 plugin := "mock-device" 1059 pluginVersion := "v0.0.1" 1060 pluginApiVersions := []string{device.ApiVersion010} 1061 h := newHarness(t, nil) 1062 1063 expKey := "set_config_worked" 1064 1065 logger := testlog.HCLogger(t) 1066 logger.SetLevel(log.Trace) 1067 pid := PluginID{ 1068 Name: plugin, 1069 PluginType: base.PluginTypeDevice, 1070 } 1071 lconfig := &PluginLoaderConfig{ 1072 Logger: logger, 1073 PluginDir: h.pluginDir(), 1074 SupportedVersions: supportedApiVersions, 1075 InternalPlugins: map[PluginID]*InternalPluginConfig{ 1076 pid: { 1077 Factory: mockFactory(plugin, base.PluginTypeDevice, pluginVersion, pluginApiVersions, false), 1078 Config: map[string]interface{}{ 1079 "res_key": expKey, 1080 }, 1081 }, 1082 }, 1083 } 1084 1085 _, err := NewPluginLoader(lconfig) 1086 require.Error(err) 1087 require.Contains(err.Error(), "configuration not allowed") 1088 1089 // Remove the config and try again 1090 lconfig.InternalPlugins[pid].Factory = mockFactory(plugin, base.PluginTypeDevice, pluginVersion, pluginApiVersions, true) 1091 l, err := NewPluginLoader(lconfig) 1092 require.NoError(err) 1093 1094 // Dispense a device plugin 1095 p, err := l.Dispense(plugin, base.PluginTypeDevice, nil, logger) 1096 require.NoError(err) 1097 defer p.Kill() 1098 1099 _, ok := p.Plugin().(device.DevicePlugin) 1100 require.True(ok) 1101 } 1102 1103 func TestPluginLoader_Reattach_External(t *testing.T) { 1104 ci.Parallel(t) 1105 require := require.New(t) 1106 1107 // Create a plugin 1108 plugin := "mock-device" 1109 pluginVersion := "v0.0.1" 1110 h := newHarness(t, []string{plugin}) 1111 1112 expKey := "set_config_worked" 1113 1114 logger := testlog.HCLogger(t) 1115 logger.SetLevel(log.Trace) 1116 lconfig := &PluginLoaderConfig{ 1117 Logger: logger, 1118 PluginDir: h.pluginDir(), 1119 SupportedVersions: supportedApiVersions, 1120 Configs: []*config.PluginConfig{ 1121 { 1122 Name: plugin, 1123 Args: []string{"-plugin", "-name", plugin, 1124 "-type", base.PluginTypeDevice, "-version", pluginVersion, "-api-version", device.ApiVersion010}, 1125 Config: map[string]interface{}{ 1126 "res_key": expKey, 1127 }, 1128 }, 1129 }, 1130 } 1131 1132 l, err := NewPluginLoader(lconfig) 1133 require.NoError(err) 1134 1135 // Dispense a device plugin 1136 p, err := l.Dispense(plugin, base.PluginTypeDevice, nil, logger) 1137 require.NoError(err) 1138 defer p.Kill() 1139 1140 instance, ok := p.Plugin().(device.DevicePlugin) 1141 require.True(ok) 1142 1143 res, err := instance.Reserve([]string{"fake"}) 1144 require.NoError(err) 1145 require.NotNil(res) 1146 require.Contains(res.Envs, expKey) 1147 1148 // Reattach to the plugin 1149 reattach, ok := p.ReattachConfig() 1150 require.True(ok) 1151 1152 p2, err := l.Reattach(plugin, base.PluginTypeDevice, reattach) 1153 require.NoError(err) 1154 1155 // Get the reattached plugin and ensure its the same 1156 instance2, ok := p2.Plugin().(device.DevicePlugin) 1157 require.True(ok) 1158 1159 res2, err := instance2.Reserve([]string{"fake"}) 1160 require.NoError(err) 1161 require.NotNil(res2) 1162 require.Contains(res2.Envs, expKey) 1163 } 1164 1165 // Test the loader trying to launch a non-plugin binary 1166 func TestPluginLoader_Bad_Executable(t *testing.T) { 1167 ci.Parallel(t) 1168 require := require.New(t) 1169 1170 // Create a plugin 1171 plugin := "mock-device" 1172 h := newHarness(t, []string{plugin}) 1173 1174 logger := testlog.HCLogger(t) 1175 logger.SetLevel(log.Trace) 1176 lconfig := &PluginLoaderConfig{ 1177 Logger: logger, 1178 PluginDir: h.pluginDir(), 1179 SupportedVersions: supportedApiVersions, 1180 Configs: []*config.PluginConfig{ 1181 { 1182 Name: plugin, 1183 Args: []string{"-bad-flag"}, 1184 }, 1185 }, 1186 } 1187 1188 _, err := NewPluginLoader(lconfig) 1189 require.Error(err) 1190 require.Contains(err.Error(), "failed to fingerprint plugin") 1191 } 1192 1193 // Test that we skip directories, non-executables and follow symlinks 1194 func TestPluginLoader_External_SkipBadFiles(t *testing.T) { 1195 ci.Parallel(t) 1196 if runtime.GOOS == "windows" { 1197 t.Skip("Windows currently does not skip non exe files") 1198 } 1199 require := require.New(t) 1200 1201 // Create two plugins 1202 plugins := []string{"mock-device"} 1203 pluginVersions := []string{"v0.0.1"} 1204 h := newHarness(t, nil) 1205 1206 // Create a folder inside our plugin dir 1207 require.NoError(os.Mkdir(filepath.Join(h.pluginDir(), "folder"), 0666)) 1208 1209 // Get our own executable path 1210 selfExe, err := os.Executable() 1211 require.NoError(err) 1212 1213 // Create a symlink from our own binary to the directory 1214 require.NoError(os.Symlink(selfExe, filepath.Join(h.pluginDir(), plugins[0]))) 1215 1216 // Create a non-executable file 1217 require.NoError(ioutil.WriteFile(filepath.Join(h.pluginDir(), "some.yaml"), []byte("hcl > yaml"), 0666)) 1218 1219 logger := testlog.HCLogger(t) 1220 logger.SetLevel(log.Trace) 1221 lconfig := &PluginLoaderConfig{ 1222 Logger: logger, 1223 PluginDir: h.pluginDir(), 1224 SupportedVersions: supportedApiVersions, 1225 Configs: []*config.PluginConfig{ 1226 { 1227 Name: plugins[0], 1228 Args: []string{"-plugin", "-name", plugins[0], 1229 "-type", base.PluginTypeDevice, "-version", pluginVersions[0], "-api-version", device.ApiVersion010}, 1230 }, 1231 }, 1232 } 1233 1234 l, err := NewPluginLoader(lconfig) 1235 require.NoError(err) 1236 1237 // Get the catalog and assert we have the two plugins 1238 c := l.Catalog() 1239 require.Len(c, 1) 1240 require.Contains(c, base.PluginTypeDevice) 1241 detected := c[base.PluginTypeDevice] 1242 require.Len(detected, 1) 1243 sort.Slice(detected, func(i, j int) bool { return detected[i].Name < detected[j].Name }) 1244 1245 expected := []*base.PluginInfoResponse{ 1246 { 1247 Name: plugins[0], 1248 Type: base.PluginTypeDevice, 1249 PluginVersion: pluginVersions[0], 1250 PluginApiVersions: []string{device.ApiVersion010}, 1251 }, 1252 } 1253 require.EqualValues(expected, detected) 1254 } 1255 1256 func TestPluginLoader_ConvertVersions(t *testing.T) { 1257 ci.Parallel(t) 1258 1259 v010 := version.Must(version.NewVersion("v0.1.0")) 1260 v020 := version.Must(version.NewVersion("v0.2.0")) 1261 v021 := version.Must(version.NewVersion("v0.2.1")) 1262 v030 := version.Must(version.NewVersion("v0.3.0")) 1263 1264 cases := []struct { 1265 in []string 1266 out []*version.Version 1267 err bool 1268 }{ 1269 { 1270 in: []string{"v0.1.0", "0.2.0", "v0.2.1"}, 1271 out: []*version.Version{v021, v020, v010}, 1272 }, 1273 { 1274 in: []string{"0.3.0", "v0.1.0", "0.2.0", "v0.2.1"}, 1275 out: []*version.Version{v030, v021, v020, v010}, 1276 }, 1277 { 1278 in: []string{"foo", "v0.1.0", "0.2.0", "v0.2.1"}, 1279 err: true, 1280 }, 1281 } 1282 1283 for _, c := range cases { 1284 t.Run(strings.Join(c.in, ","), func(t *testing.T) { 1285 act, err := convertVersions(c.in) 1286 if err != nil { 1287 if c.err { 1288 return 1289 } 1290 t.Fatalf("unexpected err: %v", err) 1291 } 1292 require.Len(t, act, len(c.out)) 1293 for i, v := range act { 1294 if !v.Equal(c.out[i]) { 1295 t.Fatalf("parsed version[%d] not equal: %v != %v", i, v, c.out[i]) 1296 } 1297 } 1298 }) 1299 } 1300 }