github.com/hashicorp/go-plugin@v1.6.0/client_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package plugin 5 6 import ( 7 "bytes" 8 "context" 9 "crypto/sha256" 10 "encoding/json" 11 "errors" 12 "fmt" 13 "io" 14 "log" 15 "net" 16 "os" 17 "os/exec" 18 "path/filepath" 19 "strings" 20 "sync" 21 "testing" 22 "time" 23 24 "github.com/hashicorp/go-hclog" 25 "github.com/hashicorp/go-plugin/internal/cmdrunner" 26 "github.com/hashicorp/go-plugin/runner" 27 ) 28 29 func TestClient(t *testing.T) { 30 process := helperProcess("mock") 31 c := NewClient(&ClientConfig{ 32 Cmd: process, 33 HandshakeConfig: testHandshake, 34 Plugins: testPluginMap, 35 }) 36 defer c.Kill() 37 38 // Test that it parses the proper address 39 addr, err := c.Start() 40 if err != nil { 41 t.Fatalf("err should be nil, got %s", err) 42 } 43 44 if addr.Network() != "tcp" { 45 t.Fatalf("bad: %#v", addr) 46 } 47 48 if addr.String() != ":1234" { 49 t.Fatalf("bad: %#v", addr) 50 } 51 52 // Test that it exits properly if killed 53 c.Kill() 54 55 // Test that it knows it is exited 56 if !c.Exited() { 57 t.Fatal("should say client has exited") 58 } 59 60 // this test isn't expected to get a client 61 if !c.killed() { 62 t.Fatal("Client should have failed") 63 } 64 } 65 66 // This tests a bug where Kill would start 67 func TestClient_killStart(t *testing.T) { 68 // Create a temporary dir to store the result file 69 td := t.TempDir() 70 defer os.RemoveAll(td) 71 72 // Start the client 73 path := filepath.Join(td, "booted") 74 process := helperProcess("bad-version", path) 75 c := NewClient(&ClientConfig{Cmd: process, HandshakeConfig: testHandshake}) 76 defer c.Kill() 77 78 // Verify our path doesn't exist 79 if _, err := os.Stat(path); err == nil || !os.IsNotExist(err) { 80 t.Fatalf("bad: %s", err) 81 } 82 83 // Test that it parses the proper address 84 if _, err := c.Start(); err == nil { 85 t.Fatal("expected error") 86 } 87 88 // Verify we started 89 if _, err := os.Stat(path); err != nil { 90 t.Fatalf("bad: %s", err) 91 } 92 if err := os.Remove(path); err != nil { 93 t.Fatalf("bad: %s", err) 94 } 95 96 // Test that Kill does nothing really 97 c.Kill() 98 99 // Test that it knows it is exited 100 if !c.Exited() { 101 t.Fatal("should say client has exited") 102 } 103 104 if !c.killed() { 105 t.Fatal("process should have failed") 106 } 107 108 // Verify our path doesn't exist 109 if _, err := os.Stat(path); err == nil || !os.IsNotExist(err) { 110 t.Fatalf("bad: %s", err) 111 } 112 } 113 114 func TestClient_testCleanup(t *testing.T) { 115 // Create a temporary dir to store the result file 116 td := t.TempDir() 117 defer os.RemoveAll(td) 118 119 // Create a path that the helper process will write on cleanup 120 path := filepath.Join(td, "output") 121 122 // Test the cleanup 123 process := helperProcess("cleanup", path) 124 c := NewClient(&ClientConfig{ 125 Cmd: process, 126 HandshakeConfig: testHandshake, 127 Plugins: testPluginMap, 128 }) 129 130 // Grab the client so the process starts 131 if _, err := c.Client(); err != nil { 132 c.Kill() 133 t.Fatalf("err: %s", err) 134 } 135 136 // Kill it gracefully 137 c.Kill() 138 139 // Test for the file 140 if _, err := os.Stat(path); err != nil { 141 t.Fatalf("err: %s", err) 142 } 143 } 144 145 func TestClient_testInterface(t *testing.T) { 146 process := helperProcess("test-interface") 147 c := NewClient(&ClientConfig{ 148 Cmd: process, 149 HandshakeConfig: testHandshake, 150 Plugins: testPluginMap, 151 }) 152 defer c.Kill() 153 154 // Grab the RPC client 155 client, err := c.Client() 156 if err != nil { 157 t.Fatalf("err should be nil, got %s", err) 158 } 159 160 // Grab the impl 161 raw, err := client.Dispense("test") 162 if err != nil { 163 t.Fatalf("err should be nil, got %s", err) 164 } 165 166 impl, ok := raw.(testInterface) 167 if !ok { 168 t.Fatalf("bad: %#v", raw) 169 } 170 171 result := impl.Double(21) 172 if result != 42 { 173 t.Fatalf("bad: %#v", result) 174 } 175 176 // Kill it 177 c.Kill() 178 179 // Test that it knows it is exited 180 if !c.Exited() { 181 t.Fatal("should say client has exited") 182 } 183 184 if c.killed() { 185 t.Fatal("process failed to exit gracefully") 186 } 187 } 188 189 func TestClient_grpc_servercrash(t *testing.T) { 190 process := helperProcess("test-grpc") 191 c := NewClient(&ClientConfig{ 192 Cmd: process, 193 HandshakeConfig: testHandshake, 194 Plugins: testGRPCPluginMap, 195 AllowedProtocols: []Protocol{ProtocolGRPC}, 196 }) 197 defer c.Kill() 198 199 if _, err := c.Start(); err != nil { 200 t.Fatalf("err: %s", err) 201 } 202 203 if v := c.Protocol(); v != ProtocolGRPC { 204 t.Fatalf("bad: %s", v) 205 } 206 207 // Grab the RPC client 208 client, err := c.Client() 209 if err != nil { 210 t.Fatalf("err should be nil, got %s", err) 211 } 212 213 // Grab the impl 214 raw, err := client.Dispense("test") 215 if err != nil { 216 t.Fatalf("err should be nil, got %s", err) 217 } 218 219 _, ok := raw.(testInterface) 220 if !ok { 221 t.Fatalf("bad: %#v", raw) 222 } 223 224 c.runner.Kill(context.Background()) 225 226 select { 227 case <-c.doneCtx.Done(): 228 case <-time.After(time.Second * 2): 229 t.Fatal("Context was not closed") 230 } 231 } 232 233 func TestClient_grpc(t *testing.T) { 234 process := helperProcess("test-grpc") 235 c := NewClient(&ClientConfig{ 236 Cmd: process, 237 HandshakeConfig: testHandshake, 238 Plugins: testGRPCPluginMap, 239 AllowedProtocols: []Protocol{ProtocolGRPC}, 240 }) 241 defer c.Kill() 242 243 if _, err := c.Start(); err != nil { 244 t.Fatalf("err: %s", err) 245 } 246 247 if v := c.Protocol(); v != ProtocolGRPC { 248 t.Fatalf("bad: %s", v) 249 } 250 251 // Grab the RPC client 252 client, err := c.Client() 253 if err != nil { 254 t.Fatalf("err should be nil, got %s", err) 255 } 256 257 // Grab the impl 258 raw, err := client.Dispense("test") 259 if err != nil { 260 t.Fatalf("err should be nil, got %s", err) 261 } 262 263 impl, ok := raw.(testInterface) 264 if !ok { 265 t.Fatalf("bad: %#v", raw) 266 } 267 268 result := impl.Double(21) 269 if result != 42 { 270 t.Fatalf("bad: %#v", result) 271 } 272 273 // Kill it 274 c.Kill() 275 276 // Test that it knows it is exited 277 if !c.Exited() { 278 t.Fatal("should say client has exited") 279 } 280 281 if c.killed() { 282 t.Fatal("process failed to exit gracefully") 283 } 284 } 285 286 func TestClient_grpcNotAllowed(t *testing.T) { 287 process := helperProcess("test-grpc") 288 c := NewClient(&ClientConfig{ 289 Cmd: process, 290 HandshakeConfig: testHandshake, 291 Plugins: testPluginMap, 292 }) 293 defer c.Kill() 294 295 if _, err := c.Start(); err == nil { 296 t.Fatal("should error") 297 } 298 } 299 300 func TestClient_grpcSyncStdio(t *testing.T) { 301 for name, tc := range map[string]struct { 302 useRunnerFunc bool 303 }{ 304 "default": {false}, 305 "use RunnerFunc": {true}, 306 } { 307 t.Run(name, func(t *testing.T) { 308 testClient_grpcSyncStdio(t, tc.useRunnerFunc) 309 }) 310 } 311 } 312 313 func testClient_grpcSyncStdio(t *testing.T, useRunnerFunc bool) { 314 var syncOut, syncErr safeBuffer 315 316 process := helperProcess("test-grpc") 317 cfg := &ClientConfig{ 318 HandshakeConfig: testHandshake, 319 Plugins: testGRPCPluginMap, 320 AllowedProtocols: []Protocol{ProtocolGRPC}, 321 SyncStdout: &syncOut, 322 SyncStderr: &syncErr, 323 } 324 325 if useRunnerFunc { 326 cfg.RunnerFunc = func(l hclog.Logger, cmd *exec.Cmd, _ string) (runner.Runner, error) { 327 process.Env = append(process.Env, cmd.Env...) 328 return cmdrunner.NewCmdRunner(l, process) 329 } 330 } else { 331 cfg.Cmd = process 332 } 333 c := NewClient(cfg) 334 defer c.Kill() 335 336 if _, err := c.Start(); err != nil { 337 t.Fatalf("err: %s", err) 338 } 339 340 if v := c.Protocol(); v != ProtocolGRPC { 341 t.Fatalf("bad: %s", v) 342 } 343 344 // Grab the RPC client 345 client, err := c.Client() 346 if err != nil { 347 t.Fatalf("err should be nil, got %s", err) 348 } 349 350 // Grab the impl 351 raw, err := client.Dispense("test") 352 if err != nil { 353 t.Fatalf("err should be nil, got %s", err) 354 } 355 356 impl, ok := raw.(testInterface) 357 if !ok { 358 t.Fatalf("bad: %#v", raw) 359 } 360 361 // Check reattach config is sensible. 362 reattach := c.ReattachConfig() 363 if useRunnerFunc { 364 if reattach.Pid != 0 { 365 t.Fatal(reattach.Pid) 366 } 367 } else { 368 if reattach.Pid == 0 { 369 t.Fatal(reattach.Pid) 370 } 371 } 372 373 // Print the data 374 stdout := []byte("hello\nworld!") 375 stderr := []byte("and some error\n messages!") 376 impl.PrintStdio(stdout, stderr) 377 378 // Wait for it to be copied 379 for syncOut.String() == "" || syncErr.String() == "" { 380 time.Sleep(10 * time.Millisecond) 381 } 382 383 // We should get the data 384 if syncOut.String() != string(stdout) { 385 t.Fatalf("stdout didn't match: %s", syncOut.String()) 386 } 387 if syncErr.String() != string(stderr) { 388 t.Fatalf("stderr didn't match: %s", syncErr.String()) 389 } 390 } 391 392 func TestClient_cmdAndReattach(t *testing.T) { 393 config := &ClientConfig{ 394 Cmd: helperProcess("start-timeout"), 395 Reattach: &ReattachConfig{}, 396 } 397 398 c := NewClient(config) 399 defer c.Kill() 400 401 _, err := c.Start() 402 if err == nil { 403 t.Fatal("err should not be nil") 404 } 405 } 406 407 func TestClient_reattach(t *testing.T) { 408 process := helperProcess("test-interface") 409 c := NewClient(&ClientConfig{ 410 Cmd: process, 411 HandshakeConfig: testHandshake, 412 Plugins: testPluginMap, 413 }) 414 defer c.Kill() 415 416 // Grab the RPC client 417 _, err := c.Client() 418 if err != nil { 419 t.Fatalf("err should be nil, got %s", err) 420 } 421 422 // Get the reattach configuration 423 reattach := c.ReattachConfig() 424 425 // Create a new client 426 c = NewClient(&ClientConfig{ 427 Reattach: reattach, 428 HandshakeConfig: testHandshake, 429 Plugins: testPluginMap, 430 }) 431 432 // Grab the RPC client 433 client, err := c.Client() 434 if err != nil { 435 t.Fatalf("err should be nil, got %s", err) 436 } 437 438 // Grab the impl 439 raw, err := client.Dispense("test") 440 if err != nil { 441 t.Fatalf("err should be nil, got %s", err) 442 } 443 444 impl, ok := raw.(testInterface) 445 if !ok { 446 t.Fatalf("bad: %#v", raw) 447 } 448 449 result := impl.Double(21) 450 if result != 42 { 451 t.Fatalf("bad: %#v", result) 452 } 453 454 // Kill it 455 c.Kill() 456 457 // Test that it knows it is exited 458 if !c.Exited() { 459 t.Fatal("should say client has exited") 460 } 461 462 if c.killed() { 463 t.Fatal("process failed to exit gracefully") 464 } 465 } 466 467 func TestClient_reattachNoProtocol(t *testing.T) { 468 process := helperProcess("test-interface") 469 c := NewClient(&ClientConfig{ 470 Cmd: process, 471 HandshakeConfig: testHandshake, 472 Plugins: testPluginMap, 473 }) 474 defer c.Kill() 475 476 // Grab the RPC client 477 _, err := c.Client() 478 if err != nil { 479 t.Fatalf("err should be nil, got %s", err) 480 } 481 482 // Get the reattach configuration 483 reattach := c.ReattachConfig() 484 reattach.Protocol = "" 485 486 // Create a new client 487 c = NewClient(&ClientConfig{ 488 Reattach: reattach, 489 HandshakeConfig: testHandshake, 490 Plugins: testPluginMap, 491 }) 492 493 // Grab the RPC client 494 client, err := c.Client() 495 if err != nil { 496 t.Fatalf("err should be nil, got %s", err) 497 } 498 499 // Grab the impl 500 raw, err := client.Dispense("test") 501 if err != nil { 502 t.Fatalf("err should be nil, got %s", err) 503 } 504 505 impl, ok := raw.(testInterface) 506 if !ok { 507 t.Fatalf("bad: %#v", raw) 508 } 509 510 result := impl.Double(21) 511 if result != 42 { 512 t.Fatalf("bad: %#v", result) 513 } 514 515 // Kill it 516 c.Kill() 517 518 // Test that it knows it is exited 519 if !c.Exited() { 520 t.Fatal("should say client has exited") 521 } 522 523 if c.killed() { 524 t.Fatal("process failed to exit gracefully") 525 } 526 } 527 528 func TestClient_reattachGRPC(t *testing.T) { 529 for name, tc := range map[string]struct { 530 useReattachFunc bool 531 }{ 532 "default": {false}, 533 "use ReattachFunc": {true}, 534 } { 535 t.Run(name, func(t *testing.T) { 536 testClient_reattachGRPC(t, tc.useReattachFunc) 537 }) 538 } 539 } 540 541 func testClient_reattachGRPC(t *testing.T, useReattachFunc bool) { 542 process := helperProcess("test-grpc") 543 c := NewClient(&ClientConfig{ 544 Cmd: process, 545 HandshakeConfig: testHandshake, 546 Plugins: testGRPCPluginMap, 547 AllowedProtocols: []Protocol{ProtocolGRPC}, 548 }) 549 defer c.Kill() 550 551 // Grab the RPC client 552 _, err := c.Client() 553 if err != nil { 554 t.Fatalf("err should be nil, got %s", err) 555 } 556 557 // Get the reattach configuration 558 reattach := c.ReattachConfig() 559 560 if useReattachFunc { 561 pid := reattach.Pid 562 reattach.Pid = 0 563 reattach.ReattachFunc = cmdrunner.ReattachFunc(pid, reattach.Addr) 564 } 565 566 // Create a new client 567 c = NewClient(&ClientConfig{ 568 Reattach: reattach, 569 HandshakeConfig: testHandshake, 570 Plugins: testGRPCPluginMap, 571 AllowedProtocols: []Protocol{ProtocolGRPC}, 572 }) 573 574 // Grab the RPC client 575 client, err := c.Client() 576 if err != nil { 577 t.Fatalf("err should be nil, got %s", err) 578 } 579 580 // Grab the impl 581 raw, err := client.Dispense("test") 582 if err != nil { 583 t.Fatalf("err should be nil, got %s", err) 584 } 585 586 impl, ok := raw.(testInterface) 587 if !ok { 588 t.Fatalf("bad: %#v", raw) 589 } 590 591 result := impl.Double(21) 592 if result != 42 { 593 t.Fatalf("bad: %#v", result) 594 } 595 596 // Kill it 597 c.Kill() 598 599 // Test that it knows it is exited 600 if !c.Exited() { 601 t.Fatal("should say client has exited") 602 } 603 604 if c.killed() { 605 t.Fatal("process failed to exit gracefully") 606 } 607 } 608 609 func TestClient_reattachNotFound(t *testing.T) { 610 // Find a bad pid 611 var pid int = 5000 612 for i := pid; i < 32000; i++ { 613 if _, err := os.FindProcess(i); err != nil { 614 pid = i 615 break 616 } 617 } 618 619 // Addr that won't work 620 l, err := net.Listen("tcp", "127.0.0.1:0") 621 if err != nil { 622 t.Fatalf("err: %s", err) 623 } 624 addr := l.Addr() 625 l.Close() 626 627 // Reattach 628 c := NewClient(&ClientConfig{ 629 Reattach: &ReattachConfig{ 630 Addr: addr, 631 Pid: pid, 632 }, 633 HandshakeConfig: testHandshake, 634 Plugins: testPluginMap, 635 }) 636 637 if _, err := c.Start(); err == nil { 638 t.Fatal("should error") 639 } else if err != ErrProcessNotFound { 640 t.Fatalf("err: %s", err) 641 } 642 } 643 644 func TestClientStart_badVersion(t *testing.T) { 645 config := &ClientConfig{ 646 Cmd: helperProcess("bad-version"), 647 StartTimeout: 50 * time.Millisecond, 648 HandshakeConfig: testHandshake, 649 Plugins: testPluginMap, 650 } 651 652 c := NewClient(config) 653 defer c.Kill() 654 655 _, err := c.Start() 656 if err == nil { 657 t.Fatal("err should not be nil") 658 } 659 } 660 661 func TestClientStart_badNegotiatedVersion(t *testing.T) { 662 config := &ClientConfig{ 663 Cmd: helperProcess("test-versioned-plugins"), 664 StartTimeout: 50 * time.Millisecond, 665 // test-versioned-plugins only has version 2 666 HandshakeConfig: testHandshake, 667 Plugins: testPluginMap, 668 } 669 670 c := NewClient(config) 671 defer c.Kill() 672 673 _, err := c.Start() 674 if err == nil { 675 t.Fatal("err should not be nil") 676 } 677 fmt.Println(err) 678 } 679 680 func TestClient_Start_Timeout(t *testing.T) { 681 config := &ClientConfig{ 682 Cmd: helperProcess("start-timeout"), 683 StartTimeout: 50 * time.Millisecond, 684 HandshakeConfig: testHandshake, 685 Plugins: testPluginMap, 686 } 687 688 c := NewClient(config) 689 defer c.Kill() 690 691 _, err := c.Start() 692 if err == nil { 693 t.Fatal("err should not be nil") 694 } 695 } 696 697 func TestClient_Stderr(t *testing.T) { 698 stderr := new(bytes.Buffer) 699 process := helperProcess("stderr") 700 c := NewClient(&ClientConfig{ 701 Cmd: process, 702 Stderr: stderr, 703 HandshakeConfig: testHandshake, 704 Plugins: testPluginMap, 705 }) 706 defer c.Kill() 707 708 if _, err := c.Start(); err != nil { 709 t.Fatalf("err: %s", err) 710 } 711 712 for !c.Exited() { 713 time.Sleep(10 * time.Millisecond) 714 } 715 716 if c.killed() { 717 t.Fatal("process failed to exit gracefully") 718 } 719 720 if !strings.Contains(stderr.String(), "HELLO\n") { 721 t.Fatalf("bad log data: '%s'", stderr.String()) 722 } 723 724 if !strings.Contains(stderr.String(), "WORLD\n") { 725 t.Fatalf("bad log data: '%s'", stderr.String()) 726 } 727 } 728 729 func TestClient_StderrJSON(t *testing.T) { 730 stderr := new(bytes.Buffer) 731 process := helperProcess("stderr-json") 732 733 var logBuf bytes.Buffer 734 mutex := new(sync.Mutex) 735 // Custom hclog.Logger 736 testLogger := hclog.New(&hclog.LoggerOptions{ 737 Name: "test-logger", 738 Level: hclog.Trace, 739 Output: &logBuf, 740 Mutex: mutex, 741 }) 742 743 c := NewClient(&ClientConfig{ 744 Cmd: process, 745 Stderr: stderr, 746 HandshakeConfig: testHandshake, 747 Logger: testLogger, 748 Plugins: testPluginMap, 749 }) 750 defer c.Kill() 751 752 if _, err := c.Start(); err != nil { 753 t.Fatalf("err: %s", err) 754 } 755 756 for !c.Exited() { 757 time.Sleep(10 * time.Millisecond) 758 } 759 760 if c.killed() { 761 t.Fatal("process failed to exit gracefully") 762 } 763 764 logOut := logBuf.String() 765 766 if !strings.Contains(logOut, "[\"HELLO\"]\n") { 767 t.Fatalf("missing json list: '%s'", logOut) 768 } 769 770 if !strings.Contains(logOut, "12345\n") { 771 t.Fatalf("missing line with raw number: '%s'", logOut) 772 } 773 774 if !strings.Contains(logOut, "{\"a\":1}") { 775 t.Fatalf("missing json object: '%s'", logOut) 776 } 777 } 778 779 func TestClient_textLogLevel(t *testing.T) { 780 stderr := new(bytes.Buffer) 781 process := helperProcess("level-warn-text") 782 783 var logBuf bytes.Buffer 784 mutex := new(sync.Mutex) 785 // Custom hclog.Logger 786 testLogger := hclog.New(&hclog.LoggerOptions{ 787 Name: "test-logger", 788 Level: hclog.Warn, 789 Output: &logBuf, 790 Mutex: mutex, 791 }) 792 793 c := NewClient(&ClientConfig{ 794 Cmd: process, 795 Stderr: stderr, 796 HandshakeConfig: testHandshake, 797 Logger: testLogger, 798 Plugins: testPluginMap, 799 }) 800 defer c.Kill() 801 802 if _, err := c.Start(); err != nil { 803 t.Fatalf("err: %s", err) 804 } 805 806 for !c.Exited() { 807 time.Sleep(10 * time.Millisecond) 808 } 809 810 if c.killed() { 811 t.Fatal("process failed to exit gracefully") 812 } 813 814 logOut := logBuf.String() 815 816 if !strings.Contains(logOut, "test line 98765") { 817 log.Fatalf("test string not found in log: %q\n", logOut) 818 } 819 } 820 821 func TestClient_Stdin(t *testing.T) { 822 // Overwrite stdin for this test with a temporary file 823 tf, err := os.CreateTemp("", "terraform") 824 if err != nil { 825 t.Fatalf("err: %s", err) 826 } 827 defer os.Remove(tf.Name()) 828 defer tf.Close() 829 830 if _, err = tf.WriteString("hello"); err != nil { 831 t.Fatalf("error: %s", err) 832 } 833 834 if err = tf.Sync(); err != nil { 835 t.Fatalf("error: %s", err) 836 } 837 838 if _, err = tf.Seek(0, 0); err != nil { 839 t.Fatalf("error: %s", err) 840 } 841 842 oldStdin := os.Stdin 843 defer func() { os.Stdin = oldStdin }() 844 os.Stdin = tf 845 846 process := helperProcess("stdin") 847 c := NewClient(&ClientConfig{ 848 Cmd: process, 849 HandshakeConfig: testHandshake, 850 Plugins: testPluginMap, 851 }) 852 defer c.Kill() 853 854 _, err = c.Start() 855 if err != nil { 856 t.Fatalf("error: %s", err) 857 } 858 859 for { 860 if c.Exited() { 861 break 862 } 863 864 time.Sleep(50 * time.Millisecond) 865 } 866 867 if !process.ProcessState.Success() { 868 t.Fatal("process didn't exit cleanly") 869 } 870 } 871 872 func TestClient_SkipHostEnv(t *testing.T) { 873 for _, tc := range []struct { 874 helper string 875 skip bool 876 }{ 877 {"test-skip-host-env-true", true}, 878 {"test-skip-host-env-false", false}, 879 } { 880 t.Run(tc.helper, func(t *testing.T) { 881 process := helperProcess(tc.helper) 882 // Set env in the host process, which we'll look for in the plugin. 883 t.Setenv("PLUGIN_TEST_SKIP_HOST_ENV", "foo") 884 c := NewClient(&ClientConfig{ 885 Cmd: process, 886 HandshakeConfig: testHandshake, 887 Plugins: testPluginMap, 888 SkipHostEnv: tc.skip, 889 }) 890 defer c.Kill() 891 892 _, err := c.Start() 893 if err != nil { 894 t.Fatalf("error: %s", err) 895 } 896 897 for { 898 if c.Exited() { 899 break 900 } 901 902 time.Sleep(50 * time.Millisecond) 903 } 904 905 if !process.ProcessState.Success() { 906 t.Fatal("process didn't exit cleanly") 907 } 908 }) 909 } 910 } 911 912 func TestClient_RequestGRPCMultiplexing_UnsupportedByPlugin(t *testing.T) { 913 for _, name := range []string{ 914 "mux-grpc-with-old-plugin", 915 "mux-grpc-with-unsupported-plugin", 916 } { 917 t.Run(name, func(t *testing.T) { 918 process := helperProcess(name) 919 c := NewClient(&ClientConfig{ 920 Cmd: process, 921 HandshakeConfig: testHandshake, 922 Plugins: testGRPCPluginMap, 923 AllowedProtocols: []Protocol{ProtocolGRPC}, 924 GRPCBrokerMultiplex: true, 925 }) 926 defer c.Kill() 927 928 _, err := c.Start() 929 if err == nil { 930 t.Fatal("expected error") 931 } 932 933 if !errors.Is(err, ErrGRPCBrokerMuxNotSupported) { 934 t.Fatalf("expected %s, but got %s", ErrGRPCBrokerMuxNotSupported, err) 935 } 936 }) 937 } 938 } 939 940 func TestClient_SecureConfig(t *testing.T) { 941 // Test failure case 942 secureConfig := &SecureConfig{ 943 Checksum: []byte{'1'}, 944 Hash: sha256.New(), 945 } 946 process := helperProcess("test-interface") 947 c := NewClient(&ClientConfig{ 948 Cmd: process, 949 HandshakeConfig: testHandshake, 950 Plugins: testPluginMap, 951 SecureConfig: secureConfig, 952 }) 953 954 // Grab the RPC client, should error 955 _, err := c.Client() 956 c.Kill() 957 if err != ErrChecksumsDoNotMatch { 958 t.Fatalf("err should be %s, got %s", ErrChecksumsDoNotMatch, err) 959 } 960 961 // Get the checksum of the executable 962 file, err := os.Open(os.Args[0]) 963 if err != nil { 964 t.Fatal(err) 965 } 966 defer file.Close() 967 968 hash := sha256.New() 969 970 _, err = io.Copy(hash, file) 971 if err != nil { 972 t.Fatal(err) 973 } 974 975 sum := hash.Sum(nil) 976 977 secureConfig = &SecureConfig{ 978 Checksum: sum, 979 Hash: sha256.New(), 980 } 981 982 process = helperProcess("test-interface") 983 c = NewClient(&ClientConfig{ 984 Cmd: process, 985 HandshakeConfig: testHandshake, 986 Plugins: testPluginMap, 987 SecureConfig: secureConfig, 988 }) 989 defer c.Kill() 990 991 // Grab the RPC client 992 _, err = c.Client() 993 if err != nil { 994 t.Fatalf("err should be nil, got %s", err) 995 } 996 } 997 998 func TestClient_TLS(t *testing.T) { 999 // Test failure case 1000 process := helperProcess("test-interface-tls") 1001 cBad := NewClient(&ClientConfig{ 1002 Cmd: process, 1003 HandshakeConfig: testHandshake, 1004 Plugins: testPluginMap, 1005 }) 1006 defer cBad.Kill() 1007 1008 // Grab the RPC client 1009 clientBad, err := cBad.Client() 1010 if err != nil { 1011 t.Fatalf("err should be nil, got %s", err) 1012 } 1013 1014 // Grab the impl 1015 raw, err := clientBad.Dispense("test") 1016 if err == nil { 1017 t.Fatal("expected error, got nil") 1018 } 1019 1020 cBad.Kill() 1021 1022 // Add TLS config to client 1023 tlsConfig, err := helperTLSProvider() 1024 if err != nil { 1025 t.Fatalf("err should be nil, got %s", err) 1026 } 1027 1028 process = helperProcess("test-interface-tls") 1029 c := NewClient(&ClientConfig{ 1030 Cmd: process, 1031 HandshakeConfig: testHandshake, 1032 Plugins: testPluginMap, 1033 TLSConfig: tlsConfig, 1034 }) 1035 defer c.Kill() 1036 1037 // Grab the RPC client 1038 client, err := c.Client() 1039 if err != nil { 1040 t.Fatalf("err should be nil, got %s", err) 1041 } 1042 1043 // Grab the impl 1044 raw, err = client.Dispense("test") 1045 if err != nil { 1046 t.Fatalf("err should be nil, got %s", err) 1047 } 1048 1049 impl, ok := raw.(testInterface) 1050 if !ok { 1051 t.Fatalf("bad: %#v", raw) 1052 } 1053 1054 result := impl.Double(21) 1055 if result != 42 { 1056 t.Fatalf("bad: %#v", result) 1057 } 1058 1059 // Kill it 1060 c.Kill() 1061 1062 // Test that it knows it is exited 1063 if !c.Exited() { 1064 t.Fatal("should say client has exited") 1065 } 1066 1067 if c.killed() { 1068 t.Fatal("process failed to exit gracefully") 1069 } 1070 } 1071 1072 func TestClient_TLS_grpc(t *testing.T) { 1073 // Add TLS config to client 1074 tlsConfig, err := helperTLSProvider() 1075 if err != nil { 1076 t.Fatalf("err should be nil, got %s", err) 1077 } 1078 1079 process := helperProcess("test-grpc-tls") 1080 c := NewClient(&ClientConfig{ 1081 Cmd: process, 1082 HandshakeConfig: testHandshake, 1083 Plugins: testGRPCPluginMap, 1084 TLSConfig: tlsConfig, 1085 AllowedProtocols: []Protocol{ProtocolGRPC}, 1086 }) 1087 defer c.Kill() 1088 1089 // Grab the RPC client 1090 client, err := c.Client() 1091 if err != nil { 1092 t.Fatalf("err should be nil, got %s", err) 1093 } 1094 1095 // Grab the impl 1096 raw, err := client.Dispense("test") 1097 if err != nil { 1098 t.Fatalf("err should be nil, got %s", err) 1099 } 1100 1101 impl, ok := raw.(testInterface) 1102 if !ok { 1103 t.Fatalf("bad: %#v", raw) 1104 } 1105 1106 result := impl.Double(21) 1107 if result != 42 { 1108 t.Fatalf("bad: %#v", result) 1109 } 1110 1111 // Kill it 1112 c.Kill() 1113 1114 if !c.Exited() { 1115 t.Fatal("should say client has exited") 1116 } 1117 1118 if c.killed() { 1119 t.Fatal("process failed to exit gracefully") 1120 } 1121 } 1122 1123 func TestClient_secureConfigAndReattach(t *testing.T) { 1124 config := &ClientConfig{ 1125 SecureConfig: &SecureConfig{}, 1126 Reattach: &ReattachConfig{}, 1127 } 1128 1129 c := NewClient(config) 1130 defer c.Kill() 1131 1132 _, err := c.Start() 1133 if err != ErrSecureConfigAndReattach { 1134 t.Fatalf("err should not be %s, got %s", ErrSecureConfigAndReattach, err) 1135 } 1136 } 1137 1138 func TestClient_ping(t *testing.T) { 1139 process := helperProcess("test-interface") 1140 c := NewClient(&ClientConfig{ 1141 Cmd: process, 1142 HandshakeConfig: testHandshake, 1143 Plugins: testPluginMap, 1144 }) 1145 defer c.Kill() 1146 1147 // Get the client 1148 client, err := c.Client() 1149 if err != nil { 1150 t.Fatalf("err: %s", err) 1151 } 1152 1153 // Ping, should work 1154 if err := client.Ping(); err != nil { 1155 t.Fatalf("err: %s", err) 1156 } 1157 1158 // Kill it 1159 c.Kill() 1160 if err := client.Ping(); err == nil { 1161 t.Fatal("should error") 1162 } 1163 } 1164 1165 func TestClient_wrongVersion(t *testing.T) { 1166 process := helperProcess("test-proto-upgraded-plugin") 1167 c := NewClient(&ClientConfig{ 1168 Cmd: process, 1169 HandshakeConfig: testHandshake, 1170 Plugins: testGRPCPluginMap, 1171 AllowedProtocols: []Protocol{ProtocolGRPC}, 1172 }) 1173 defer c.Kill() 1174 1175 // Get the client 1176 _, err := c.Client() 1177 if err == nil { 1178 t.Fatal("expected incorrect protocol version server") 1179 } 1180 1181 } 1182 1183 func TestClient_legacyClient(t *testing.T) { 1184 process := helperProcess("test-proto-upgraded-plugin") 1185 c := NewClient(&ClientConfig{ 1186 Cmd: process, 1187 HandshakeConfig: testVersionedHandshake, 1188 VersionedPlugins: map[int]PluginSet{ 1189 1: testPluginMap, 1190 }, 1191 }) 1192 defer c.Kill() 1193 1194 // Get the client 1195 client, err := c.Client() 1196 if err != nil { 1197 t.Fatalf("err: %s", err) 1198 } 1199 1200 if c.NegotiatedVersion() != 1 { 1201 t.Fatal("using incorrect version", c.NegotiatedVersion()) 1202 } 1203 1204 // Ping, should work 1205 if err := client.Ping(); err == nil { 1206 t.Fatal("expected error, should negotiate wrong plugin") 1207 } 1208 } 1209 1210 func TestClient_legacyServer(t *testing.T) { 1211 // test using versioned plugins version when the server supports only 1212 // supports one 1213 process := helperProcess("test-proto-upgraded-client") 1214 c := NewClient(&ClientConfig{ 1215 Cmd: process, 1216 HandshakeConfig: testVersionedHandshake, 1217 VersionedPlugins: map[int]PluginSet{ 1218 2: testGRPCPluginMap, 1219 }, 1220 AllowedProtocols: []Protocol{ProtocolGRPC}, 1221 }) 1222 defer c.Kill() 1223 1224 // Get the client 1225 client, err := c.Client() 1226 if err != nil { 1227 t.Fatalf("err: %s", err) 1228 } 1229 1230 if c.NegotiatedVersion() != 2 { 1231 t.Fatal("using incorrect version", c.NegotiatedVersion()) 1232 } 1233 1234 // Ping, should work 1235 if err := client.Ping(); err == nil { 1236 t.Fatal("expected error, should negotiate wrong plugin") 1237 } 1238 } 1239 1240 func TestClient_versionedClient(t *testing.T) { 1241 process := helperProcess("test-versioned-plugins") 1242 c := NewClient(&ClientConfig{ 1243 Cmd: process, 1244 HandshakeConfig: testVersionedHandshake, 1245 VersionedPlugins: map[int]PluginSet{ 1246 2: testGRPCPluginMap, 1247 }, 1248 AllowedProtocols: []Protocol{ProtocolGRPC}, 1249 }) 1250 defer c.Kill() 1251 1252 if _, err := c.Start(); err != nil { 1253 t.Fatalf("err: %s", err) 1254 } 1255 1256 if v := c.Protocol(); v != ProtocolGRPC { 1257 t.Fatalf("bad: %s", v) 1258 } 1259 1260 // Grab the RPC client 1261 client, err := c.Client() 1262 if err != nil { 1263 t.Fatalf("err should be nil, got %s", err) 1264 } 1265 1266 if c.NegotiatedVersion() != 2 { 1267 t.Fatal("using incorrect version", c.NegotiatedVersion()) 1268 } 1269 1270 // Grab the impl 1271 raw, err := client.Dispense("test") 1272 if err != nil { 1273 t.Fatalf("err should be nil, got %s", err) 1274 } 1275 1276 _, ok := raw.(testInterface) 1277 if !ok { 1278 t.Fatalf("bad: %#v", raw) 1279 } 1280 1281 c.runner.Kill(context.Background()) 1282 1283 select { 1284 case <-c.doneCtx.Done(): 1285 case <-time.After(time.Second * 2): 1286 t.Fatal("Context was not closed") 1287 } 1288 } 1289 1290 func TestClient_mtlsClient(t *testing.T) { 1291 process := helperProcess("test-mtls") 1292 c := NewClient(&ClientConfig{ 1293 AutoMTLS: true, 1294 Cmd: process, 1295 HandshakeConfig: testVersionedHandshake, 1296 VersionedPlugins: map[int]PluginSet{ 1297 2: testGRPCPluginMap, 1298 }, 1299 AllowedProtocols: []Protocol{ProtocolGRPC}, 1300 }) 1301 defer c.Kill() 1302 1303 if _, err := c.Start(); err != nil { 1304 t.Fatalf("err: %s", err) 1305 } 1306 1307 if v := c.Protocol(); v != ProtocolGRPC { 1308 t.Fatalf("bad: %s", v) 1309 } 1310 1311 // Grab the RPC client 1312 client, err := c.Client() 1313 if err != nil { 1314 t.Fatalf("err should be nil, got %s", err) 1315 } 1316 1317 if c.NegotiatedVersion() != 2 { 1318 t.Fatal("using incorrect version", c.NegotiatedVersion()) 1319 } 1320 1321 // Grab the impl 1322 raw, err := client.Dispense("test") 1323 if err != nil { 1324 t.Fatalf("err should be nil, got %s", err) 1325 } 1326 1327 tester, ok := raw.(testInterface) 1328 if !ok { 1329 t.Fatalf("bad: %#v", raw) 1330 } 1331 1332 n := tester.Double(3) 1333 if n != 6 { 1334 t.Fatal("invalid response", n) 1335 } 1336 1337 c.runner.Kill(context.Background()) 1338 1339 select { 1340 case <-c.doneCtx.Done(): 1341 case <-time.After(time.Second * 2): 1342 t.Fatal("Context was not closed") 1343 } 1344 } 1345 1346 func TestClient_mtlsNetRPCClient(t *testing.T) { 1347 process := helperProcess("test-interface-mtls") 1348 c := NewClient(&ClientConfig{ 1349 AutoMTLS: true, 1350 Cmd: process, 1351 HandshakeConfig: testVersionedHandshake, 1352 Plugins: testPluginMap, 1353 AllowedProtocols: []Protocol{ProtocolNetRPC}, 1354 }) 1355 defer c.Kill() 1356 1357 if _, err := c.Start(); err != nil { 1358 t.Fatalf("err: %s", err) 1359 } 1360 1361 // Grab the RPC client 1362 client, err := c.Client() 1363 if err != nil { 1364 t.Fatalf("err should be nil, got %s", err) 1365 } 1366 1367 // Grab the impl 1368 raw, err := client.Dispense("test") 1369 if err != nil { 1370 t.Fatalf("err should be nil, got %s", err) 1371 } 1372 1373 tester, ok := raw.(testInterface) 1374 if !ok { 1375 t.Fatalf("bad: %#v", raw) 1376 } 1377 1378 n := tester.Double(3) 1379 if n != 6 { 1380 t.Fatal("invalid response", n) 1381 } 1382 1383 c.runner.Kill(context.Background()) 1384 1385 select { 1386 case <-c.doneCtx.Done(): 1387 case <-time.After(time.Second * 2): 1388 t.Fatal("Context was not closed") 1389 } 1390 } 1391 1392 func TestClient_logger(t *testing.T) { 1393 t.Run("net/rpc", func(t *testing.T) { testClient_logger(t, "netrpc") }) 1394 t.Run("grpc", func(t *testing.T) { testClient_logger(t, "grpc") }) 1395 } 1396 1397 func testClient_logger(t *testing.T, proto string) { 1398 var buffer bytes.Buffer 1399 mutex := new(sync.Mutex) 1400 stderr := io.MultiWriter(os.Stderr, &buffer) 1401 // Custom hclog.Logger 1402 clientLogger := hclog.New(&hclog.LoggerOptions{ 1403 Name: "test-logger", 1404 Level: hclog.Trace, 1405 Output: stderr, 1406 Mutex: mutex, 1407 }) 1408 1409 process := helperProcess("test-interface-logger-" + proto) 1410 c := NewClient(&ClientConfig{ 1411 Cmd: process, 1412 HandshakeConfig: testHandshake, 1413 Plugins: testGRPCPluginMap, 1414 Logger: clientLogger, 1415 AllowedProtocols: []Protocol{ProtocolNetRPC, ProtocolGRPC}, 1416 }) 1417 defer c.Kill() 1418 1419 // Grab the RPC client 1420 client, err := c.Client() 1421 if err != nil { 1422 t.Fatalf("err should be nil, got %s", err) 1423 } 1424 1425 // Grab the impl 1426 raw, err := client.Dispense("test") 1427 if err != nil { 1428 t.Fatalf("err should be nil, got %s", err) 1429 } 1430 1431 impl, ok := raw.(testInterface) 1432 if !ok { 1433 t.Fatalf("bad: %#v", raw) 1434 } 1435 1436 { 1437 // Discard everything else, and capture the output we care about 1438 mutex.Lock() 1439 buffer.Reset() 1440 mutex.Unlock() 1441 impl.PrintKV("foo", "bar") 1442 time.Sleep(100 * time.Millisecond) 1443 mutex.Lock() 1444 line, err := buffer.ReadString('\n') 1445 mutex.Unlock() 1446 if err != nil { 1447 t.Fatal(err) 1448 } 1449 if !strings.Contains(line, "foo=bar") { 1450 t.Fatalf("bad: %q", line) 1451 } 1452 } 1453 1454 { 1455 // Try an integer type 1456 mutex.Lock() 1457 buffer.Reset() 1458 mutex.Unlock() 1459 impl.PrintKV("foo", 12) 1460 time.Sleep(100 * time.Millisecond) 1461 mutex.Lock() 1462 line, err := buffer.ReadString('\n') 1463 mutex.Unlock() 1464 if err != nil { 1465 t.Fatal(err) 1466 } 1467 if !strings.Contains(line, "foo=12") { 1468 t.Fatalf("bad: %q", line) 1469 } 1470 } 1471 1472 // Kill it 1473 c.Kill() 1474 1475 // Test that it knows it is exited 1476 if !c.Exited() { 1477 t.Fatal("should say client has exited") 1478 } 1479 1480 if c.killed() { 1481 t.Fatal("process failed to exit gracefully") 1482 } 1483 } 1484 1485 // Test that we continue to consume stderr over long lines. 1486 func TestClient_logStderr(t *testing.T) { 1487 stderr := bytes.Buffer{} 1488 c := NewClient(&ClientConfig{ 1489 Stderr: &stderr, 1490 Cmd: &exec.Cmd{ 1491 Path: "test", 1492 }, 1493 PluginLogBufferSize: 32, 1494 }) 1495 c.clientWaitGroup.Add(1) 1496 1497 msg := ` 1498 this line is more than 32 bytes long 1499 and this line is more than 32 bytes long 1500 {"a": "b", "@level": "debug"} 1501 this line is short 1502 ` 1503 1504 reader := strings.NewReader(msg) 1505 1506 c.stderrWaitGroup.Add(1) 1507 c.logStderr(c.config.Cmd.Path, reader) 1508 read := stderr.String() 1509 1510 if read != msg { 1511 t.Fatalf("\nexpected output: %q\ngot output: %q", msg, read) 1512 } 1513 } 1514 1515 func TestClient_logStderrParseJSON(t *testing.T) { 1516 logBuf := bytes.Buffer{} 1517 c := NewClient(&ClientConfig{ 1518 Stderr: bytes.NewBuffer(nil), 1519 Cmd: &exec.Cmd{Path: "test"}, 1520 PluginLogBufferSize: 64, 1521 Logger: hclog.New(&hclog.LoggerOptions{ 1522 Name: "test-logger", 1523 Level: hclog.Trace, 1524 Output: &logBuf, 1525 JSONFormat: true, 1526 }), 1527 }) 1528 c.clientWaitGroup.Add(1) 1529 1530 msg := `{"@message": "this is a message", "@level": "info"} 1531 {"@message": "this is a large message that is more than 64 bytes long", "@level": "info"}` 1532 reader := strings.NewReader(msg) 1533 1534 c.stderrWaitGroup.Add(1) 1535 c.logStderr(c.config.Cmd.Path, reader) 1536 logs := strings.Split(strings.TrimSpace(logBuf.String()), "\n") 1537 1538 wants := []struct { 1539 wantLevel string 1540 wantMessage string 1541 }{ 1542 {"info", "this is a message"}, 1543 {"debug", `{"@message": "this is a large message that is more than 64 bytes`}, 1544 {"debug", ` long", "@level": "info"}`}, 1545 } 1546 1547 if len(logs) != len(wants) { 1548 t.Fatalf("expected %d logs, got %d", len(wants), len(logs)) 1549 } 1550 1551 for i, tt := range wants { 1552 l := make(map[string]interface{}) 1553 if err := json.Unmarshal([]byte(logs[i]), &l); err != nil { 1554 t.Fatal(err) 1555 } 1556 1557 if l["@level"] != tt.wantLevel { 1558 t.Fatalf("expected level %q, got %q", tt.wantLevel, l["@level"]) 1559 } 1560 1561 if l["@message"] != tt.wantMessage { 1562 t.Fatalf("expected message %q, got %q", tt.wantMessage, l["@message"]) 1563 } 1564 } 1565 }