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