|
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" |
@@ -508,3 +510,128 @@ func TestProxyGetBlob(t *testing.T) { |
508 | 510 | } |
509 | 511 | assert.NoError(t, err) |
510 | 512 | } |
| 513 | + |
| 514 | +// newProxyWithStore spawns the test binary with a local containers-storage |
| 515 | +// store seeded with the given image. It returns the proxy and the |
| 516 | +// containers-storage:// reference string for the seeded image. |
| 517 | +func newProxyWithStore(t *testing.T, seedImage string) (*proxy, string) { |
| 518 | + t.Helper() |
| 519 | + |
| 520 | + proxyBinary := os.Getenv("JSON_PROXY_TEST_BINARY") |
| 521 | + if proxyBinary == "" { |
| 522 | + t.Skip("JSON_PROXY_TEST_BINARY is not set; skipping integration test") |
| 523 | + } |
| 524 | + |
| 525 | + wd := t.TempDir() |
| 526 | + graphRoot := filepath.Join(wd, "root") |
| 527 | + runRoot := filepath.Join(wd, "run") |
| 528 | + |
| 529 | + fds, err := syscall.Socketpair(syscall.AF_LOCAL, syscall.SOCK_SEQPACKET, 0) |
| 530 | + require.NoError(t, err) |
| 531 | + myfd := os.NewFile(uintptr(fds[0]), "myfd") |
| 532 | + defer myfd.Close() |
| 533 | + theirfd := os.NewFile(uintptr(fds[1]), "theirfd") |
| 534 | + defer theirfd.Close() |
| 535 | + |
| 536 | + mysock, err := net.FileConn(myfd) |
| 537 | + require.NoError(t, err) |
| 538 | + unixConn, ok := mysock.(*net.UnixConn) |
| 539 | + require.True(t, ok, "expected *net.UnixConn, got %T", mysock) |
| 540 | + |
| 541 | + proc := exec.Command(proxyBinary, //nolint:gosec |
| 542 | + "--sockfd", "3", |
| 543 | + "--graph-root", graphRoot, |
| 544 | + "--run-root", runRoot, |
| 545 | + "--seed-image", seedImage, |
| 546 | + ) |
| 547 | + proc.Stderr = os.Stderr |
| 548 | + proc.ExtraFiles = append(proc.ExtraFiles, theirfd) |
| 549 | + |
| 550 | + stdoutPipe, err := proc.StdoutPipe() |
| 551 | + require.NoError(t, err) |
| 552 | + |
| 553 | + err = proc.Start() |
| 554 | + require.NoError(t, err) |
| 555 | + |
| 556 | + // Read the containers-storage reference from stdout. |
| 557 | + scanner := bufio.NewScanner(stdoutPipe) |
| 558 | + require.True(t, scanner.Scan(), "expected storage reference on stdout") |
| 559 | + storageRef := strings.TrimSpace(scanner.Text()) |
| 560 | + require.True(t, strings.HasPrefix(storageRef, "containers-storage:"), "unexpected ref: %s", storageRef) |
| 561 | + |
| 562 | + p := &proxy{ |
| 563 | + c: unixConn, |
| 564 | + proc: proc, |
| 565 | + } |
| 566 | + t.Cleanup(p.close) |
| 567 | + |
| 568 | + v, err := p.callNoFd("Initialize", nil) |
| 569 | + require.NoError(t, err) |
| 570 | + semver, ok := v.(string) |
| 571 | + require.True(t, ok, "proxy Initialize: Unexpected value %T", v) |
| 572 | + require.True(t, strings.HasPrefix(semver, expectedProxySemverMajor), "Unexpected semver %s", semver) |
| 573 | + |
| 574 | + return p, storageRef |
| 575 | +} |
| 576 | + |
| 577 | +func TestGetSplitFDStreamSocket(t *testing.T) { |
| 578 | + p, storageRef := newProxyWithStore(t, knownListImage) |
| 579 | + |
| 580 | + // Open the containers-storage image to trigger auto-discovery. |
| 581 | + imgidVal, err := p.callNoFd("OpenImage", []any{storageRef}) |
| 582 | + require.NoError(t, err) |
| 583 | + imgid, ok := imgidVal.(float64) |
| 584 | + require.True(t, ok) |
| 585 | + require.NotZero(t, imgid) |
| 586 | + |
| 587 | + // GetSplitFDStreamSocket should return a valid FD. |
| 588 | + _, fd, err := p.call("GetSplitFDStreamSocket", nil) |
| 589 | + require.NoError(t, err) |
| 590 | + require.NotNil(t, fd, "expected an FD from GetSplitFDStreamSocket") |
| 591 | + |
| 592 | + // Verify the received FD is a unix socket. |
| 593 | + var stat syscall.Stat_t |
| 594 | + err = syscall.Fstat(int(fd.datafd.Fd()), &stat) |
| 595 | + require.NoError(t, err) |
| 596 | + require.True(t, stat.Mode&syscall.S_IFMT == syscall.S_IFSOCK, "expected socket, got mode %o", stat.Mode) |
| 597 | + |
| 598 | + // Validate the socket speaks the splitfdstream jsonrpc-fdpass protocol. |
| 599 | + // Send a JSON-RPC request for a bogus method and expect a method-not-found error. |
| 600 | + conn, err := net.FileConn(fd.datafd) |
| 601 | + fd.datafd.Close() |
| 602 | + require.NoError(t, err) |
| 603 | + unixSock, ok := conn.(*net.UnixConn) |
| 604 | + require.True(t, ok) |
| 605 | + defer unixSock.Close() |
| 606 | + |
| 607 | + rpcReq := []byte("{\"jsonrpc\":\"2.0\",\"method\":\"NoSuchMethod\",\"id\":1}\n") |
| 608 | + _, err = unixSock.Write(rpcReq) |
| 609 | + require.NoError(t, err) |
| 610 | + |
| 611 | + respBuf := make([]byte, 4096) |
| 612 | + n, err := unixSock.Read(respBuf) |
| 613 | + require.NoError(t, err) |
| 614 | + var rpcResp map[string]any |
| 615 | + err = json.Unmarshal(respBuf[:n], &rpcResp) |
| 616 | + require.NoError(t, err) |
| 617 | + // A valid JSON-RPC server returns an error object for unknown methods. |
| 618 | + rpcErr, ok := rpcResp["error"].(map[string]any) |
| 619 | + require.True(t, ok, "expected JSON-RPC error object, got %v", rpcResp) |
| 620 | + require.Contains(t, rpcErr["message"], "not found") |
| 621 | + |
| 622 | + _, err = p.callNoFd("CloseImage", []any{imgid}) |
| 623 | + require.NoError(t, err) |
| 624 | +} |
| 625 | + |
| 626 | +func TestGetSplitFDStreamSocketNotAvailable(t *testing.T) { |
| 627 | + p := newProxy(t) |
| 628 | + |
| 629 | + // Open a docker:// image (no splitfdstream support). |
| 630 | + _, err := p.callNoFd("OpenImage", []any{knownNotManifestListedImageX8664}) |
| 631 | + require.NoError(t, err) |
| 632 | + |
| 633 | + // GetSplitFDStreamSocket should fail since no containers-storage source was opened. |
| 634 | + _, _, err = p.call("GetSplitFDStreamSocket", nil) |
| 635 | + require.Error(t, err) |
| 636 | + require.Contains(t, err.Error(), "splitfdstream store not configured") |
| 637 | +} |
0 commit comments