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