|
3 | 3 | package jsonproxy_test |
4 | 4 |
|
5 | 5 | import ( |
| 6 | + "bufio" |
6 | 7 | "encoding/json" |
7 | 8 | "fmt" |
8 | 9 | "io" |
9 | 10 | "net" |
10 | 11 | "os" |
11 | 12 | "os/exec" |
| 13 | + "path/filepath" |
12 | 14 | "strings" |
13 | 15 | "sync" |
14 | 16 | "syscall" |
@@ -560,3 +562,128 @@ func TestProxyPolicyVerification(t *testing.T) { |
560 | 562 | }) |
561 | 563 | } |
562 | 564 | } |
| 565 | + |
| 566 | +// newProxyWithStore spawns the test binary with a local containers-storage |
| 567 | +// store seeded with the given image. It returns the proxy and the |
| 568 | +// containers-storage:// reference string for the seeded image. |
| 569 | +func newProxyWithStore(t *testing.T, seedImage string) (*proxy, string) { |
| 570 | + t.Helper() |
| 571 | + |
| 572 | + proxyBinary := os.Getenv("JSON_PROXY_TEST_BINARY") |
| 573 | + if proxyBinary == "" { |
| 574 | + t.Skip("JSON_PROXY_TEST_BINARY is not set; skipping integration test") |
| 575 | + } |
| 576 | + |
| 577 | + wd := t.TempDir() |
| 578 | + graphRoot := filepath.Join(wd, "root") |
| 579 | + runRoot := filepath.Join(wd, "run") |
| 580 | + |
| 581 | + fds, err := syscall.Socketpair(syscall.AF_LOCAL, syscall.SOCK_SEQPACKET, 0) |
| 582 | + require.NoError(t, err) |
| 583 | + myfd := os.NewFile(uintptr(fds[0]), "myfd") |
| 584 | + defer myfd.Close() |
| 585 | + theirfd := os.NewFile(uintptr(fds[1]), "theirfd") |
| 586 | + defer theirfd.Close() |
| 587 | + |
| 588 | + mysock, err := net.FileConn(myfd) |
| 589 | + require.NoError(t, err) |
| 590 | + unixConn, ok := mysock.(*net.UnixConn) |
| 591 | + require.True(t, ok, "expected *net.UnixConn, got %T", mysock) |
| 592 | + |
| 593 | + proc := exec.Command(proxyBinary, //nolint:gosec |
| 594 | + "--sockfd", "3", |
| 595 | + "--graph-root", graphRoot, |
| 596 | + "--run-root", runRoot, |
| 597 | + "--seed-image", seedImage, |
| 598 | + ) |
| 599 | + proc.Stderr = os.Stderr |
| 600 | + proc.ExtraFiles = append(proc.ExtraFiles, theirfd) |
| 601 | + |
| 602 | + stdoutPipe, err := proc.StdoutPipe() |
| 603 | + require.NoError(t, err) |
| 604 | + |
| 605 | + err = proc.Start() |
| 606 | + require.NoError(t, err) |
| 607 | + |
| 608 | + // Read the containers-storage reference from stdout. |
| 609 | + scanner := bufio.NewScanner(stdoutPipe) |
| 610 | + require.True(t, scanner.Scan(), "expected storage reference on stdout") |
| 611 | + storageRef := strings.TrimSpace(scanner.Text()) |
| 612 | + require.True(t, strings.HasPrefix(storageRef, "containers-storage:"), "unexpected ref: %s", storageRef) |
| 613 | + |
| 614 | + p := &proxy{ |
| 615 | + c: unixConn, |
| 616 | + proc: proc, |
| 617 | + } |
| 618 | + t.Cleanup(p.close) |
| 619 | + |
| 620 | + v, err := p.callNoFd("Initialize", nil) |
| 621 | + require.NoError(t, err) |
| 622 | + semver, ok := v.(string) |
| 623 | + require.True(t, ok, "proxy Initialize: Unexpected value %T", v) |
| 624 | + require.True(t, strings.HasPrefix(semver, expectedProxySemverMajor), "Unexpected semver %s", semver) |
| 625 | + |
| 626 | + return p, storageRef |
| 627 | +} |
| 628 | + |
| 629 | +func TestOpenJSONRPCFdPass(t *testing.T) { |
| 630 | + p, storageRef := newProxyWithStore(t, knownListImage) |
| 631 | + |
| 632 | + // Open the containers-storage image to trigger auto-discovery. |
| 633 | + imgidVal, err := p.callNoFd("OpenImage", []any{storageRef}) |
| 634 | + require.NoError(t, err) |
| 635 | + imgid, ok := imgidVal.(float64) |
| 636 | + require.True(t, ok) |
| 637 | + require.NotZero(t, imgid) |
| 638 | + |
| 639 | + // OpenJSONRPCFdPass should return a valid FD. |
| 640 | + _, fd, err := p.call("OpenJSONRPCFdPass", nil) |
| 641 | + require.NoError(t, err) |
| 642 | + require.NotNil(t, fd, "expected an FD from OpenJSONRPCFdPass") |
| 643 | + |
| 644 | + // Verify the received FD is a unix socket. |
| 645 | + var stat syscall.Stat_t |
| 646 | + err = syscall.Fstat(int(fd.datafd.Fd()), &stat) |
| 647 | + require.NoError(t, err) |
| 648 | + require.True(t, stat.Mode&syscall.S_IFMT == syscall.S_IFSOCK, "expected socket, got mode %o", stat.Mode) |
| 649 | + |
| 650 | + // Validate the socket speaks the splitfdstream jsonrpc-fdpass protocol. |
| 651 | + // Send a JSON-RPC request for a bogus method and expect a method-not-found error. |
| 652 | + conn, err := net.FileConn(fd.datafd) |
| 653 | + fd.datafd.Close() |
| 654 | + require.NoError(t, err) |
| 655 | + unixSock, ok := conn.(*net.UnixConn) |
| 656 | + require.True(t, ok) |
| 657 | + defer unixSock.Close() |
| 658 | + |
| 659 | + rpcReq := []byte("{\"jsonrpc\":\"2.0\",\"method\":\"NoSuchMethod\",\"id\":1}\n") |
| 660 | + _, err = unixSock.Write(rpcReq) |
| 661 | + require.NoError(t, err) |
| 662 | + |
| 663 | + respBuf := make([]byte, 4096) |
| 664 | + n, err := unixSock.Read(respBuf) |
| 665 | + require.NoError(t, err) |
| 666 | + var rpcResp map[string]any |
| 667 | + err = json.Unmarshal(respBuf[:n], &rpcResp) |
| 668 | + require.NoError(t, err) |
| 669 | + // A valid JSON-RPC server returns an error object for unknown methods. |
| 670 | + rpcErr, ok := rpcResp["error"].(map[string]any) |
| 671 | + require.True(t, ok, "expected JSON-RPC error object, got %v", rpcResp) |
| 672 | + require.Contains(t, rpcErr["message"], "not found") |
| 673 | + |
| 674 | + _, err = p.callNoFd("CloseImage", []any{imgid}) |
| 675 | + require.NoError(t, err) |
| 676 | +} |
| 677 | + |
| 678 | +func TestOpenJSONRPCFdPassNotAvailable(t *testing.T) { |
| 679 | + p := newProxy(t) |
| 680 | + |
| 681 | + // Open a docker:// image (no splitfdstream support). |
| 682 | + _, err := p.callNoFd("OpenImage", []any{knownNotManifestListedImageX8664}) |
| 683 | + require.NoError(t, err) |
| 684 | + |
| 685 | + // OpenJSONRPCFdPass should fail since no containers-storage source was opened. |
| 686 | + _, _, err = p.call("OpenJSONRPCFdPass", nil) |
| 687 | + require.Error(t, err) |
| 688 | + require.Contains(t, err.Error(), "splitfdstream store not configured") |
| 689 | +} |
0 commit comments