mirror of
https://github.com/coder/coder.git
synced 2025-07-06 15:41:45 +00:00
Re-enables TestSSH/RemoteForward_Unix_Signal and addresses the underlying race: we were not closing the remote forward on context expiry, only the session and connection. However, there is still a more fundamental issue in that we don't have the ability to ensure that TCP sessions are properly terminated before tearing down the Tailnet conn. This is due to the assumption in the sockets API, that the underlying IP interface is long lived compared with the TCP socket, and thus closing a socket returns immediately and does not wait for the TCP termination handshake --- that is handled async in the tcpip stack. However, this assumption does not hold for us and tailnet, since on shutdown, we also tear down the tailnet connection, and this can race with the TCP termination. Closing the remote forward explicitly should prevent forward state from accumulating, since the Close() function waits for a reply from the remote SSH server. I've also attempted to workaround the TCP/tailnet issue for `--stdio` by using `CloseWrite()` instead of `Close()`. By closing the write side of the connection, half-close the TCP connection, and the server detects this and closes the other direction, which then triggers our read loop to exit only after the server has had a chance to process the close. TODO in a stacked PR is to implement this logic for `vscodessh` as well.
1066 lines
31 KiB
Go
1066 lines
31 KiB
Go
package cli_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/crypto/ssh"
|
|
gosshagent "golang.org/x/crypto/ssh/agent"
|
|
|
|
"cdr.dev/slog"
|
|
"cdr.dev/slog/sloggers/slogtest"
|
|
|
|
"github.com/coder/coder/v2/agent"
|
|
"github.com/coder/coder/v2/agent/agenttest"
|
|
"github.com/coder/coder/v2/cli/clitest"
|
|
"github.com/coder/coder/v2/cli/cliui"
|
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/coder/v2/provisioner/echo"
|
|
"github.com/coder/coder/v2/provisionersdk/proto"
|
|
"github.com/coder/coder/v2/pty"
|
|
"github.com/coder/coder/v2/pty/ptytest"
|
|
"github.com/coder/coder/v2/testutil"
|
|
)
|
|
|
|
const (
|
|
startupScriptPattern = "i-am-ready"
|
|
)
|
|
|
|
func setupWorkspaceForAgent(t *testing.T, mutate func([]*proto.Agent) []*proto.Agent) (*codersdk.Client, codersdk.Workspace, string) {
|
|
t.Helper()
|
|
if mutate == nil {
|
|
mutate = func(a []*proto.Agent) []*proto.Agent {
|
|
return a
|
|
}
|
|
}
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
client.SetLogger(slogtest.Make(t, nil).Named("client").Leveled(slog.LevelDebug))
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
agentToken := uuid.NewString()
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionPlan: echo.PlanComplete,
|
|
ProvisionApply: []*proto.Response{{
|
|
Type: &proto.Response_Apply{
|
|
Apply: &proto.ApplyComplete{
|
|
Resources: []*proto.Resource{{
|
|
Name: "dev",
|
|
Type: "google_compute_instance",
|
|
Agents: mutate([]*proto.Agent{{
|
|
Id: uuid.NewString(),
|
|
Auth: &proto.Agent_Token{
|
|
Token: agentToken,
|
|
},
|
|
Scripts: []*proto.Script{
|
|
{
|
|
Script: fmt.Sprintf("echo '%s'", startupScriptPattern),
|
|
RunOnStart: true,
|
|
},
|
|
},
|
|
}}),
|
|
}},
|
|
},
|
|
},
|
|
}},
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
workspace, err := client.Workspace(context.Background(), workspace.ID)
|
|
require.NoError(t, err)
|
|
|
|
return client, workspace, agentToken
|
|
}
|
|
|
|
func TestSSH(t *testing.T) {
|
|
t.Parallel()
|
|
t.Run("ImmediateExit", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client, workspace, agentToken := setupWorkspaceForAgent(t, nil)
|
|
inv, root := clitest.New(t, "ssh", workspace.Name)
|
|
clitest.SetupConfig(t, client, root)
|
|
pty := ptytest.New(t).Attach(inv)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
cmdDone := tGo(t, func() {
|
|
err := inv.WithContext(ctx).Run()
|
|
assert.NoError(t, err)
|
|
})
|
|
pty.ExpectMatch("Waiting")
|
|
|
|
_ = agenttest.New(t, client.URL, agentToken)
|
|
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
|
|
|
|
// Shells on Mac, Windows, and Linux all exit shells with the "exit" command.
|
|
pty.WriteLine("exit")
|
|
<-cmdDone
|
|
})
|
|
t.Run("ShowTroubleshootingURLAfterTimeout", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
wantURL := "https://example.com/troubleshoot"
|
|
client, workspace, _ := setupWorkspaceForAgent(t, func(a []*proto.Agent) []*proto.Agent {
|
|
// Unfortunately, one second is the lowest
|
|
// we can go because 0 disables the feature.
|
|
a[0].ConnectionTimeoutSeconds = 1
|
|
a[0].TroubleshootingUrl = wantURL
|
|
return a
|
|
})
|
|
inv, root := clitest.New(t, "ssh", workspace.Name)
|
|
clitest.SetupConfig(t, client, root)
|
|
pty := ptytest.New(t)
|
|
inv.Stdin = pty.Input()
|
|
inv.Stderr = pty.Output()
|
|
inv.Stdout = pty.Output()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
cmdDone := tGo(t, func() {
|
|
err := inv.WithContext(ctx).Run()
|
|
assert.ErrorIs(t, err, cliui.Canceled)
|
|
})
|
|
pty.ExpectMatch(wantURL)
|
|
cancel()
|
|
<-cmdDone
|
|
})
|
|
|
|
t.Run("ExitOnStop", func(t *testing.T) {
|
|
t.Parallel()
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("Windows doesn't seem to clean up the process, maybe #7100 will fix it")
|
|
}
|
|
|
|
client, workspace, agentToken := setupWorkspaceForAgent(t, nil)
|
|
inv, root := clitest.New(t, "ssh", workspace.Name)
|
|
clitest.SetupConfig(t, client, root)
|
|
pty := ptytest.New(t).Attach(inv)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
cmdDone := tGo(t, func() {
|
|
err := inv.WithContext(ctx).Run()
|
|
assert.Error(t, err)
|
|
})
|
|
pty.ExpectMatch("Waiting")
|
|
|
|
_ = agenttest.New(t, client.URL, agentToken)
|
|
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
|
|
|
|
// Ensure the agent is connected.
|
|
pty.WriteLine("echo hell'o'")
|
|
pty.ExpectMatchContext(ctx, "hello")
|
|
|
|
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
|
|
|
select {
|
|
case <-cmdDone:
|
|
case <-ctx.Done():
|
|
require.Fail(t, "command did not exit in time")
|
|
}
|
|
})
|
|
|
|
t.Run("Stdio", func(t *testing.T) {
|
|
t.Parallel()
|
|
client, workspace, agentToken := setupWorkspaceForAgent(t, nil)
|
|
_, _ = tGoContext(t, func(ctx context.Context) {
|
|
// Run this async so the SSH command has to wait for
|
|
// the build and agent to connect!
|
|
_ = agenttest.New(t, client.URL, agentToken)
|
|
<-ctx.Done()
|
|
})
|
|
|
|
clientOutput, clientInput := io.Pipe()
|
|
serverOutput, serverInput := io.Pipe()
|
|
defer func() {
|
|
for _, c := range []io.Closer{clientOutput, clientInput, serverOutput, serverInput} {
|
|
_ = c.Close()
|
|
}
|
|
}()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
inv, root := clitest.New(t, "ssh", "--stdio", workspace.Name)
|
|
clitest.SetupConfig(t, client, root)
|
|
inv.Stdin = clientOutput
|
|
inv.Stdout = serverInput
|
|
inv.Stderr = io.Discard
|
|
|
|
cmdDone := tGo(t, func() {
|
|
err := inv.WithContext(ctx).Run()
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
conn, channels, requests, err := ssh.NewClientConn(&stdioConn{
|
|
Reader: serverOutput,
|
|
Writer: clientInput,
|
|
}, "", &ssh.ClientConfig{
|
|
// #nosec
|
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
|
})
|
|
require.NoError(t, err)
|
|
defer conn.Close()
|
|
|
|
sshClient := ssh.NewClient(conn, channels, requests)
|
|
session, err := sshClient.NewSession()
|
|
require.NoError(t, err)
|
|
defer session.Close()
|
|
|
|
command := "sh -c exit"
|
|
if runtime.GOOS == "windows" {
|
|
command = "cmd.exe /c exit"
|
|
}
|
|
err = session.Run(command)
|
|
require.NoError(t, err)
|
|
err = sshClient.Close()
|
|
require.NoError(t, err)
|
|
_ = clientOutput.Close()
|
|
|
|
<-cmdDone
|
|
})
|
|
|
|
t.Run("Stdio_RemoteForward_Signal", func(t *testing.T) {
|
|
t.Parallel()
|
|
client, workspace, agentToken := setupWorkspaceForAgent(t, nil)
|
|
_, _ = tGoContext(t, func(ctx context.Context) {
|
|
// Run this async so the SSH command has to wait for
|
|
// the build and agent to connect!
|
|
_ = agenttest.New(t, client.URL, agentToken)
|
|
<-ctx.Done()
|
|
})
|
|
|
|
clientOutput, clientInput := io.Pipe()
|
|
serverOutput, serverInput := io.Pipe()
|
|
defer func() {
|
|
for _, c := range []io.Closer{clientOutput, clientInput, serverOutput, serverInput} {
|
|
_ = c.Close()
|
|
}
|
|
}()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
inv, root := clitest.New(t, "ssh", "--stdio", workspace.Name)
|
|
fsn := clitest.NewFakeSignalNotifier(t)
|
|
inv = inv.WithTestSignalNotifyContext(t, fsn.NotifyContext)
|
|
clitest.SetupConfig(t, client, root)
|
|
inv.Stdin = clientOutput
|
|
inv.Stdout = serverInput
|
|
inv.Stderr = io.Discard
|
|
|
|
cmdDone := tGo(t, func() {
|
|
err := inv.WithContext(ctx).Run()
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
conn, channels, requests, err := ssh.NewClientConn(&stdioConn{
|
|
Reader: serverOutput,
|
|
Writer: clientInput,
|
|
}, "", &ssh.ClientConfig{
|
|
// #nosec
|
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
|
})
|
|
require.NoError(t, err)
|
|
defer conn.Close()
|
|
|
|
sshClient := ssh.NewClient(conn, channels, requests)
|
|
|
|
tmpdir := tempDirUnixSocket(t)
|
|
|
|
remoteSock := path.Join(tmpdir, "remote.sock")
|
|
_, err = sshClient.ListenUnix(remoteSock)
|
|
require.NoError(t, err)
|
|
|
|
fsn.Notify()
|
|
<-cmdDone
|
|
fsn.AssertStopped()
|
|
require.Eventually(t, func() bool {
|
|
_, err = os.Stat(remoteSock)
|
|
return xerrors.Is(err, os.ErrNotExist)
|
|
}, testutil.WaitShort, testutil.IntervalFast)
|
|
})
|
|
|
|
t.Run("Stdio_BrokenConn", func(t *testing.T) {
|
|
t.Parallel()
|
|
client, workspace, agentToken := setupWorkspaceForAgent(t, nil)
|
|
_, _ = tGoContext(t, func(ctx context.Context) {
|
|
// Run this async so the SSH command has to wait for
|
|
// the build and agent to connect!
|
|
_ = agenttest.New(t, client.URL, agentToken)
|
|
<-ctx.Done()
|
|
})
|
|
|
|
clientOutput, clientInput := io.Pipe()
|
|
serverOutput, serverInput := io.Pipe()
|
|
defer func() {
|
|
for _, c := range []io.Closer{clientOutput, clientInput, serverOutput, serverInput} {
|
|
_ = c.Close()
|
|
}
|
|
}()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
inv, root := clitest.New(t, "ssh", "--stdio", workspace.Name)
|
|
clitest.SetupConfig(t, client, root)
|
|
inv.Stdin = clientOutput
|
|
inv.Stdout = serverInput
|
|
inv.Stderr = io.Discard
|
|
|
|
cmdDone := tGo(t, func() {
|
|
err := inv.WithContext(ctx).Run()
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
conn, channels, requests, err := ssh.NewClientConn(&stdioConn{
|
|
Reader: serverOutput,
|
|
Writer: clientInput,
|
|
}, "", &ssh.ClientConfig{
|
|
// #nosec
|
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
|
})
|
|
require.NoError(t, err)
|
|
defer conn.Close()
|
|
|
|
sshClient := ssh.NewClient(conn, channels, requests)
|
|
_ = serverOutput.Close()
|
|
_ = clientInput.Close()
|
|
select {
|
|
case <-cmdDone:
|
|
// OK
|
|
case <-time.After(testutil.WaitShort):
|
|
t.Error("timeout waiting for command to exit")
|
|
}
|
|
|
|
_ = sshClient.Close()
|
|
})
|
|
|
|
// Test that we handle OS signals properly while remote forwarding, and don't just leave the TCP
|
|
// socket hanging.
|
|
t.Run("RemoteForward_Unix_Signal", func(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("No unix sockets on windows")
|
|
}
|
|
t.Parallel()
|
|
ctx := testutil.Context(t, testutil.WaitSuperLong)
|
|
client, workspace, agentToken := setupWorkspaceForAgent(t, nil)
|
|
_, _ = tGoContext(t, func(ctx context.Context) {
|
|
// Run this async so the SSH command has to wait for
|
|
// the build and agent to connect!
|
|
_ = agenttest.New(t, client.URL, agentToken)
|
|
<-ctx.Done()
|
|
})
|
|
|
|
tmpdir := tempDirUnixSocket(t)
|
|
localSock := filepath.Join(tmpdir, "local.sock")
|
|
l, err := net.Listen("unix", localSock)
|
|
require.NoError(t, err)
|
|
defer l.Close()
|
|
remoteSock := path.Join(tmpdir, "remote.sock")
|
|
for i := 0; i < 2; i++ {
|
|
t.Logf("connect %d of 2", i+1)
|
|
inv, root := clitest.New(t,
|
|
"ssh",
|
|
workspace.Name,
|
|
"--remote-forward",
|
|
remoteSock+":"+localSock,
|
|
)
|
|
fsn := clitest.NewFakeSignalNotifier(t)
|
|
inv = inv.WithTestSignalNotifyContext(t, fsn.NotifyContext)
|
|
inv.Stdout = io.Discard
|
|
inv.Stderr = io.Discard
|
|
|
|
clitest.SetupConfig(t, client, root)
|
|
cmdDone := tGo(t, func() {
|
|
err := inv.WithContext(ctx).Run()
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
// accept a single connection
|
|
msgs := make(chan string, 1)
|
|
go func() {
|
|
conn, err := l.Accept()
|
|
if !assert.NoError(t, err) {
|
|
return
|
|
}
|
|
msg, err := io.ReadAll(conn)
|
|
if !assert.NoError(t, err) {
|
|
return
|
|
}
|
|
msgs <- string(msg)
|
|
}()
|
|
|
|
// Unfortunately, there is a race in crypto/ssh where it sends the request to forward
|
|
// unix sockets before it is prepared to receive the response, meaning that even after
|
|
// the socket exists on the file system, the client might not be ready to accept the
|
|
// channel.
|
|
//
|
|
// https://cs.opensource.google/go/x/crypto/+/master:ssh/streamlocal.go;drc=2fc4c88bf43f0ea5ea305eae2b7af24b2cc93287;l=33
|
|
//
|
|
// To work around this, we attempt to send messages in a loop until one succeeds
|
|
success := make(chan struct{})
|
|
go func() {
|
|
var (
|
|
conn net.Conn
|
|
err error
|
|
)
|
|
for {
|
|
time.Sleep(testutil.IntervalMedium)
|
|
select {
|
|
case <-ctx.Done():
|
|
t.Error("timeout")
|
|
return
|
|
case <-success:
|
|
return
|
|
default:
|
|
// Ok
|
|
}
|
|
conn, err = net.Dial("unix", remoteSock)
|
|
if err != nil {
|
|
t.Logf("dial error: %s", err)
|
|
continue
|
|
}
|
|
_, err = conn.Write([]byte("test"))
|
|
if err != nil {
|
|
t.Logf("write error: %s", err)
|
|
}
|
|
err = conn.Close()
|
|
if err != nil {
|
|
t.Logf("close error: %s", err)
|
|
}
|
|
}
|
|
}()
|
|
|
|
msg := testutil.RequireRecvCtx(ctx, t, msgs)
|
|
require.Equal(t, "test", msg)
|
|
close(success)
|
|
fsn.Notify()
|
|
<-cmdDone
|
|
fsn.AssertStopped()
|
|
|
|
// wait for the remote socket to get cleaned up before retrying,
|
|
// because cleaning up the socket happens asynchronously, and we
|
|
// might connect to an old listener on the agent side.
|
|
require.Eventually(t, func() bool {
|
|
_, err = os.Stat(remoteSock)
|
|
return xerrors.Is(err, os.ErrNotExist)
|
|
}, testutil.WaitShort, testutil.IntervalFast)
|
|
}
|
|
})
|
|
|
|
t.Run("StdioExitOnStop", func(t *testing.T) {
|
|
t.Parallel()
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("Windows doesn't seem to clean up the process, maybe #7100 will fix it")
|
|
}
|
|
client, workspace, agentToken := setupWorkspaceForAgent(t, nil)
|
|
_, _ = tGoContext(t, func(ctx context.Context) {
|
|
// Run this async so the SSH command has to wait for
|
|
// the build and agent to connect.
|
|
_ = agenttest.New(t, client.URL, agentToken)
|
|
<-ctx.Done()
|
|
})
|
|
|
|
clientOutput, clientInput := io.Pipe()
|
|
serverOutput, serverInput := io.Pipe()
|
|
defer func() {
|
|
for _, c := range []io.Closer{clientOutput, clientInput, serverOutput, serverInput} {
|
|
_ = c.Close()
|
|
}
|
|
}()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
inv, root := clitest.New(t, "ssh", "--stdio", workspace.Name)
|
|
clitest.SetupConfig(t, client, root)
|
|
inv.Stdin = clientOutput
|
|
inv.Stdout = serverInput
|
|
inv.Stderr = io.Discard
|
|
|
|
cmdDone := tGo(t, func() {
|
|
err := inv.WithContext(ctx).Run()
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
conn, channels, requests, err := ssh.NewClientConn(&stdioConn{
|
|
Reader: serverOutput,
|
|
Writer: clientInput,
|
|
}, "", &ssh.ClientConfig{
|
|
// #nosec
|
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
|
})
|
|
require.NoError(t, err)
|
|
defer conn.Close()
|
|
|
|
sshClient := ssh.NewClient(conn, channels, requests)
|
|
defer sshClient.Close()
|
|
|
|
session, err := sshClient.NewSession()
|
|
require.NoError(t, err)
|
|
defer session.Close()
|
|
|
|
err = session.Shell()
|
|
require.NoError(t, err)
|
|
|
|
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
|
|
|
select {
|
|
case <-cmdDone:
|
|
case <-ctx.Done():
|
|
require.Fail(t, "command did not exit in time")
|
|
}
|
|
})
|
|
|
|
t.Run("ForwardAgent", func(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("Test not supported on windows")
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
client, workspace, agentToken := setupWorkspaceForAgent(t, nil)
|
|
|
|
_ = agenttest.New(t, client.URL, agentToken)
|
|
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
|
|
|
|
// Generate private key.
|
|
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
require.NoError(t, err)
|
|
kr := gosshagent.NewKeyring()
|
|
kr.Add(gosshagent.AddedKey{
|
|
PrivateKey: privateKey,
|
|
})
|
|
|
|
// Start up ssh agent listening on unix socket.
|
|
tmpdir := tempDirUnixSocket(t)
|
|
agentSock := filepath.Join(tmpdir, "agent.sock")
|
|
l, err := net.Listen("unix", agentSock)
|
|
require.NoError(t, err)
|
|
defer l.Close()
|
|
_ = tGo(t, func() {
|
|
for {
|
|
fd, err := l.Accept()
|
|
if err != nil {
|
|
if !errors.Is(err, net.ErrClosed) {
|
|
assert.NoError(t, err, "listener accept failed")
|
|
}
|
|
return
|
|
}
|
|
|
|
err = gosshagent.ServeAgent(kr, fd)
|
|
if !errors.Is(err, io.EOF) {
|
|
assert.NoError(t, err, "serve agent failed")
|
|
}
|
|
_ = fd.Close()
|
|
}
|
|
})
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
inv, root := clitest.New(t,
|
|
"ssh",
|
|
workspace.Name,
|
|
"--forward-agent",
|
|
"--identity-agent", agentSock, // Overrides $SSH_AUTH_SOCK.
|
|
)
|
|
clitest.SetupConfig(t, client, root)
|
|
pty := ptytest.New(t).Attach(inv)
|
|
inv.Stderr = pty.Output()
|
|
cmdDone := tGo(t, func() {
|
|
err := inv.WithContext(ctx).Run()
|
|
assert.NoError(t, err, "ssh command failed")
|
|
})
|
|
|
|
// Wait for the prompt or any output really to indicate the command has
|
|
// started and accepting input on stdin.
|
|
_ = pty.Peek(ctx, 1)
|
|
|
|
// Ensure that SSH_AUTH_SOCK is set.
|
|
// Linux: /tmp/auth-agent3167016167/listener.sock
|
|
// macOS: /var/folders/ng/m1q0wft14hj0t3rtjxrdnzsr0000gn/T/auth-agent3245553419/listener.sock
|
|
pty.WriteLine(`env | grep SSH_AUTH_SOCK=`)
|
|
pty.ExpectMatch("SSH_AUTH_SOCK=")
|
|
// Ensure that ssh-add lists our key.
|
|
pty.WriteLine("ssh-add -L")
|
|
keys, err := kr.List()
|
|
require.NoError(t, err, "list keys failed")
|
|
pty.ExpectMatch(keys[0].String())
|
|
|
|
// And we're done.
|
|
pty.WriteLine("exit")
|
|
<-cmdDone
|
|
})
|
|
|
|
t.Run("RemoteForward", func(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("Test not supported on windows")
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
httpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte("hello world"))
|
|
}))
|
|
defer httpServer.Close()
|
|
|
|
client, workspace, agentToken := setupWorkspaceForAgent(t, nil)
|
|
|
|
inv, root := clitest.New(t,
|
|
"ssh",
|
|
workspace.Name,
|
|
"--remote-forward",
|
|
"8222:"+httpServer.Listener.Addr().String(),
|
|
)
|
|
clitest.SetupConfig(t, client, root)
|
|
pty := ptytest.New(t).Attach(inv)
|
|
inv.Stderr = pty.Output()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
cmdDone := tGo(t, func() {
|
|
err := inv.WithContext(ctx).Run()
|
|
assert.NoError(t, err, "ssh command failed")
|
|
})
|
|
|
|
// Agent is still starting
|
|
pty.ExpectMatch("Waiting")
|
|
|
|
_ = agenttest.New(t, client.URL, agentToken)
|
|
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
|
|
|
|
// Startup script has just finished
|
|
pty.ExpectMatch(startupScriptPattern)
|
|
|
|
// Download the test page
|
|
pty.WriteLine("curl localhost:8222")
|
|
pty.ExpectMatch("hello world")
|
|
|
|
// And we're done.
|
|
pty.WriteLine("exit")
|
|
<-cmdDone
|
|
})
|
|
|
|
t.Run("RemoteForwardUnixSocket", func(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("Test not supported on windows")
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
client, workspace, agentToken := setupWorkspaceForAgent(t, nil)
|
|
|
|
_ = agenttest.New(t, client.URL, agentToken)
|
|
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
tmpdir := tempDirUnixSocket(t)
|
|
agentSock := filepath.Join(tmpdir, "agent.sock")
|
|
l, err := net.Listen("unix", agentSock)
|
|
require.NoError(t, err)
|
|
defer l.Close()
|
|
remoteSock := filepath.Join(tmpdir, "remote.sock")
|
|
|
|
inv, root := clitest.New(t,
|
|
"ssh",
|
|
workspace.Name,
|
|
"--remote-forward",
|
|
fmt.Sprintf("%s:%s", remoteSock, agentSock),
|
|
)
|
|
clitest.SetupConfig(t, client, root)
|
|
pty := ptytest.New(t).Attach(inv)
|
|
inv.Stderr = pty.Output()
|
|
cmdDone := tGo(t, func() {
|
|
err := inv.WithContext(ctx).Run()
|
|
assert.NoError(t, err, "ssh command failed")
|
|
})
|
|
|
|
// Wait for the prompt or any output really to indicate the command has
|
|
// started and accepting input on stdin.
|
|
_ = pty.Peek(ctx, 1)
|
|
|
|
// Download the test page
|
|
pty.WriteLine(fmt.Sprintf("ss -xl state listening src %s | wc -l", remoteSock))
|
|
pty.ExpectMatch("2")
|
|
|
|
// And we're done.
|
|
pty.WriteLine("exit")
|
|
<-cmdDone
|
|
})
|
|
|
|
t.Run("FileLogging", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
logDir := t.TempDir()
|
|
|
|
client, workspace, agentToken := setupWorkspaceForAgent(t, nil)
|
|
inv, root := clitest.New(t, "ssh", "-l", logDir, workspace.Name)
|
|
clitest.SetupConfig(t, client, root)
|
|
pty := ptytest.New(t).Attach(inv)
|
|
w := clitest.StartWithWaiter(t, inv)
|
|
|
|
pty.ExpectMatch("Waiting")
|
|
|
|
agenttest.New(t, client.URL, agentToken)
|
|
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
|
|
|
|
// Shells on Mac, Windows, and Linux all exit shells with the "exit" command.
|
|
pty.WriteLine("exit")
|
|
w.RequireSuccess()
|
|
|
|
ents, err := os.ReadDir(logDir)
|
|
require.NoError(t, err)
|
|
require.Len(t, ents, 1, "expected one file in logdir %s", logDir)
|
|
})
|
|
}
|
|
|
|
//nolint:paralleltest // This test uses t.Setenv, parent test MUST NOT be parallel.
|
|
func TestSSH_ForwardGPG(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
// While GPG forwarding from a Windows client works, we currently do
|
|
// not support forwarding to a Windows workspace. Our tests use the
|
|
// same platform for the "client" and "workspace" as they run in the
|
|
// same process.
|
|
t.Skip("Test not supported on windows")
|
|
}
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
}
|
|
|
|
// This key is for dean@coder.com.
|
|
const randPublicKeyFingerprint = "7BDFBA0CC7F5A96537C806C427BC6335EB5117F1"
|
|
const randPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
|
|
mQINBF6SWkEBEADB8sAhBaT36VQ6HEhAmtKexLldu1HUdXNw16rdF+1wiBzSFfJN
|
|
aPeX4Y9iFIZgC2wU0wOjJ04BpioyOLtJngbThI5WpeoQ/1yQZOpnDaCMPPLp+uJ+
|
|
Gy4tMZYWQq21PukrFm3XDRGKjVN58QN6uCPb1S/YzteP8Epmq590GYIYLiAHnMt6
|
|
5iyxIFhXj/fq5Fddp2+efI7QWvNl2wTNnCaTziOSKYcbNmQpn9gy0WvKktWYtB8E
|
|
JJtWES0DzgCnDpm/hYx79Wkb+F7qY54y2uauDx+z97QXrON47lsIyGm8/T59ZfSd
|
|
/yrBqDLHYrHlt9RkFpAnBzO402y2eHsKTB6/EAHv9H2apxahyJlcxGbE5QE+fOJk
|
|
LdPlako0cSljz0g9Icesr2nZL0MhWwLnwk7DHkg/PUUijkbuR/TD9dti2/yOTFrf
|
|
Y7DdZpoZ0ZkcGu9lMh2vOTWc96RNCyIZfE5WNDKKo+u5Txzndsc/qIgKohwDSxTC
|
|
3hAulG5Wt05UeyHBEAAvGV2szG88VsGwd1juqXAbEzk+kLQzNyoQX188/4V4X+MV
|
|
pY9Wz7JudmQpB/3+YTcA/ziK/+wu3c2wNlr7gMZYMOwDWTLfW64nux7zHWDytrP0
|
|
HfgJIgqP7F7SnChpTFdb1hr1WDox99ZG+/eDkwxnuXYWm9xx5/crqQ0POQARAQAB
|
|
tClEZWFuIFNoZWF0aGVyICh3b3JrIGtleSkgPGRlYW5AY29kZXIuY29tPokCVAQT
|
|
AQgAPhYhBHvfugzH9allN8gGxCe8YzXrURfxBQJeklpBAhsDBQkJZgGABQsJCAcC
|
|
BhUKCQgLAgQWAgMBAh4BAheAAAoJECe8YzXrURfxIVkP/3UJMzvIjTNF63WiK4xk
|
|
TXlBbPKodnzUmAJ+8DVXmJMJpNsSI2czw6eFUXMcrT3JMlviOXhRWMLHr2FsQhyS
|
|
AJOQo0x9z7nntPIkvj96ihCdgRn7VN1WzaMwOOesGPr57StWLE84bg9/R0aSsxtX
|
|
LgfBCyNkv6FFlruhnw8+JdZJEjvIXQ9swvwD6L68ZLWIWcdnj/CjQmnmgFA+O4UO
|
|
SFXMUjklbrq8mJ0sAPUUATJK0SOTyqkZPkhqjlTZa8p0XoJF25trhwLhzDi4GPR6
|
|
SK/9SkqB/go9ZwkNZOjs2tP7eMExy4zQ21MFH09JMKQB7H5CG8GwdMwz4+VKc9aP
|
|
y9Ncova/p7Y8kJ7oQPWhACJT1jMP6620oC2N/7wwS0Vtc6E9LoPrfXC2TtvOA9qx
|
|
aOf6riWSjo8BEcXDuMtlW4g6IQFNd0+wcgcKrAd+vPLZnG4rtYL0Etdd1ymBT4pi
|
|
5E5uT8oUT9rLHX+2tD/E8SE5PzsaKEOJKzcOB8ESb3YBGic7+VvX/AuJuSFsuWnZ
|
|
FqAUENqfdz6+0dEJe1pfWyje+Q+o7B7u+ffMT4dOQOC8NfHFnz1kU+DA3VDE6xsu
|
|
3YN1L8KlYON92s9VWDA8VuvmU2d9pq5ysUeg133ftDSwj3X+5GYcBv4VFcSRCBW5
|
|
w0hDpMDun1t8xcXdo1LQ4R4NuQINBF6SWkEBEADF4Nrhlqc5M3Sz9sNHDJZR68zb
|
|
4CjkoOpYwsKj/ZCukzRCGKpT5Agn0zOycUjbAyCZVjREeIRRURyAhfpOmZY5yF6b
|
|
PD93+04OzWk1AaDRmMfvi1Crn/WUEVHIbDaisxDzNuAJgLrt93I/lOz06GczhCb6
|
|
sPBeKuaXCLl/5LSwTahGWsweeSCmfyrYsOc11T+SjdyWXWXEpzFNNIhvqiEoJCw3
|
|
IcdktTBJYuHsN4jh5kVemi/ttqRN3z7rBMKR1sPG3ux1MfCfSTSCeZLTN9eVvqm9
|
|
ne8brk8ZC6sdwlZ9IofPbmSaAh+F5Kfcnd3KjmyQ63t+8plpJ2YH3Fx6IwTwVEQ8
|
|
Ii3WQInTpBSPqf0EwnzRBvhYeKusRpcmX3JSmosLbd5uhvJdgotzuwZYzgay/6DL
|
|
OlwElZ//ecXNhU8iYmx1BwNuquvGcGVpkP5eaaT6O9qDznB7TT0xztfAK0LaAuRJ
|
|
HOFCc8iiHtQ4o0OkRhg/0KkUGBU5Iw5SIDimkgwJMtD3ZiYOqLaXS6kmmVw2u6YD
|
|
LB8rTpegz/tcX+4uyfnIZ28JCOYFTeaDT4FixFW2hrfo/VJzMI5IIv9XAAmtAiEU
|
|
f+CY2BT6kg9NkQuke0p4/W8yTaScapYZa5I2bzFpJJyzh1TKE6x3qcbBs9vVX+6E
|
|
vK4FflNwu9WSWojO2wARAQABiQI8BBgBCAAmFiEEe9+6DMf1qWU3yAbEJ7xjNetR
|
|
F/EFAl6SWkECGwwFCQlmAYAACgkQJ7xjNetRF/FpnQ//SIYePQzhvWj9drnT2krG
|
|
dUGSxCN0pA2UQZNkreAaKmyxn2/6xEdxYSz0iUEk+I0HKay+NLCxJ5PDoDBypFtM
|
|
f0yOnbWRObhim8HmED4JRw678G4hRU7KEN0L/9SUYlsBNbgr1xYM/CUX/Ih9NT+P
|
|
eApxs2VgjKii6m81nfBCFpWSxAs+TOnbshp8dlDZk9kxjFH9+h1ffgZjntqeyiWe
|
|
F1UE1Wh32MbJdtc2Y3mrA6i+7+3OXmqMHoiG1obhISgdpaCJ/ub3ywnAmeXSiAKE
|
|
IuS6CriR71Wqv8LMQ8kPM8On9Q26d1dsKKBnlFop9oexxf1AFsbbf9gkcgb+uNno
|
|
1Qr/R6l2H1TcV1gmiyQLzVnkgLRORosLvSlFrisrsLv9uTYYgcGvwKiU/o3PTdQg
|
|
fv0D7LB+a3C9KsCBFjihW3bTOcHKX2sAWEQXZMtKGf5aNTBmWQ+eKWUGpudXIvLE
|
|
od5lgfk9p8T1R50KDieG/+2X95zxFSYBoPRAfp7JNT7h+TZ55qUmQXZGI1VqhWiq
|
|
b6y/yqfI17JCm4oWpXYbgeruLuye2c/ptDc3S3d26hbWYiWKVT4bLtUGR0wuE6lS
|
|
DK0u4LK+mnrYfIvRDYJGx18/nbLpR+ivWLIssJT2Jyyj8w9+hk10XkODySNjHCxj
|
|
p7KeSZdlk47pMBGOfnvEmoQ=
|
|
=OxHv
|
|
-----END PGP PUBLIC KEY BLOCK-----`
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
gpgPath, err := exec.LookPath("gpg")
|
|
if err != nil {
|
|
t.Skip("gpg not found")
|
|
}
|
|
gpgConfPath, err := exec.LookPath("gpgconf")
|
|
if err != nil {
|
|
t.Skip("gpgconf not found")
|
|
}
|
|
gpgAgentPath, err := exec.LookPath("gpg-agent")
|
|
if err != nil {
|
|
t.Skip("gpg-agent not found")
|
|
}
|
|
|
|
// Setup GPG home directory on the "client".
|
|
gnupgHomeClient := tempDirUnixSocket(t)
|
|
t.Setenv("GNUPGHOME", gnupgHomeClient)
|
|
|
|
// Get the agent extra socket path.
|
|
var (
|
|
stdout = bytes.NewBuffer(nil)
|
|
stderr = bytes.NewBuffer(nil)
|
|
)
|
|
c := exec.CommandContext(ctx, gpgConfPath, "--list-dir", "agent-extra-socket")
|
|
c.Stdout = stdout
|
|
c.Stderr = stderr
|
|
err = c.Run()
|
|
require.NoError(t, err, "get extra socket path failed: %s", stderr.String())
|
|
extraSocketPath := strings.TrimSpace(stdout.String())
|
|
|
|
// Generate private key non-interactively.
|
|
genKeyScript := `
|
|
Key-Type: 1
|
|
Key-Length: 2048
|
|
Subkey-Type: 1
|
|
Subkey-Length: 2048
|
|
Name-Real: Coder Test
|
|
Name-Email: test@coder.com
|
|
Expire-Date: 0
|
|
%no-protection
|
|
`
|
|
c = exec.CommandContext(ctx, gpgPath, "--batch", "--gen-key")
|
|
c.Stdin = strings.NewReader(genKeyScript)
|
|
out, err := c.CombinedOutput()
|
|
require.NoError(t, err, "generate key failed: %s", out)
|
|
|
|
// Import a random public key.
|
|
stdin := strings.NewReader(randPublicKey + "\n")
|
|
c = exec.CommandContext(ctx, gpgPath, "--import", "-")
|
|
c.Stdin = stdin
|
|
out, err = c.CombinedOutput()
|
|
require.NoError(t, err, "import key failed: %s", out)
|
|
|
|
// Set ultimate trust on imported key.
|
|
stdin = strings.NewReader(randPublicKeyFingerprint + ":6:\n")
|
|
c = exec.CommandContext(ctx, gpgPath, "--import-ownertrust")
|
|
c.Stdin = stdin
|
|
out, err = c.CombinedOutput()
|
|
require.NoError(t, err, "import ownertrust failed: %s", out)
|
|
|
|
// Start the GPG agent.
|
|
agentCmd := pty.CommandContext(ctx, gpgAgentPath, "--no-detach", "--extra-socket", extraSocketPath)
|
|
agentCmd.Env = append(agentCmd.Env, "GNUPGHOME="+gnupgHomeClient)
|
|
agentPTY, agentProc, err := pty.Start(agentCmd, pty.WithPTYOption(pty.WithGPGTTY()))
|
|
require.NoError(t, err, "launch agent failed")
|
|
defer func() {
|
|
_ = agentProc.Kill()
|
|
_ = agentPTY.Close()
|
|
}()
|
|
|
|
// Get the agent socket path in the "workspace".
|
|
gnupgHomeWorkspace := tempDirUnixSocket(t)
|
|
|
|
stdout = bytes.NewBuffer(nil)
|
|
stderr = bytes.NewBuffer(nil)
|
|
c = exec.CommandContext(ctx, gpgConfPath, "--list-dir", "agent-socket")
|
|
c.Env = append(c.Env, "GNUPGHOME="+gnupgHomeWorkspace)
|
|
c.Stdout = stdout
|
|
c.Stderr = stderr
|
|
err = c.Run()
|
|
require.NoError(t, err, "get agent socket path in workspace failed: %s", stderr.String())
|
|
workspaceAgentSocketPath := strings.TrimSpace(stdout.String())
|
|
require.NotEqual(t, extraSocketPath, workspaceAgentSocketPath, "socket path should be different")
|
|
|
|
client, workspace, agentToken := setupWorkspaceForAgent(t, nil)
|
|
|
|
_ = agenttest.New(t, client.URL, agentToken, func(o *agent.Options) {
|
|
o.EnvironmentVariables = map[string]string{
|
|
"GNUPGHOME": gnupgHomeWorkspace,
|
|
}
|
|
})
|
|
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
|
|
|
|
inv, root := clitest.New(t,
|
|
"ssh",
|
|
workspace.Name,
|
|
"--forward-gpg",
|
|
)
|
|
clitest.SetupConfig(t, client, root)
|
|
tpty := ptytest.New(t)
|
|
inv.Stdin = tpty.Input()
|
|
inv.Stdout = tpty.Output()
|
|
inv.Stderr = tpty.Output()
|
|
cmdDone := tGo(t, func() {
|
|
err := inv.WithContext(ctx).Run()
|
|
assert.NoError(t, err, "ssh command failed")
|
|
})
|
|
// Prevent the test from hanging if the asserts below kill the test
|
|
// early. This will cause the command to exit with an error, which will
|
|
// let the t.Cleanup'd `<-done` inside of `tGo` exit and not hang.
|
|
// Without this, the test will hang forever on failure, preventing the
|
|
// real error from being printed.
|
|
t.Cleanup(cancel)
|
|
|
|
// Wait for the prompt or any output really to indicate the command has
|
|
// started and accepting input on stdin.
|
|
_ = tpty.Peek(ctx, 1)
|
|
|
|
tpty.WriteLine("echo hello 'world'")
|
|
tpty.ExpectMatch("hello world")
|
|
|
|
// Check the GNUPGHOME was correctly inherited via shell.
|
|
tpty.WriteLine("env && echo env-''-command-done")
|
|
match := tpty.ExpectMatch("env--command-done")
|
|
require.Contains(t, match, "GNUPGHOME="+gnupgHomeWorkspace, match)
|
|
|
|
// Get the agent extra socket path in the "workspace" via shell.
|
|
tpty.WriteLine("gpgconf --list-dir agent-socket && echo gpgconf-''-agentsocket-command-done")
|
|
tpty.ExpectMatch(workspaceAgentSocketPath)
|
|
tpty.ExpectMatch("gpgconf--agentsocket-command-done")
|
|
|
|
// List the keys in the "workspace".
|
|
tpty.WriteLine("gpg --list-keys && echo gpg-''-listkeys-command-done")
|
|
listKeysOutput := tpty.ExpectMatch("gpg--listkeys-command-done")
|
|
require.Contains(t, listKeysOutput, "[ultimate] Coder Test <test@coder.com>")
|
|
require.Contains(t, listKeysOutput, "[ultimate] Dean Sheather (work key) <dean@coder.com>")
|
|
|
|
// Try to sign something. This demonstrates that the forwarding is
|
|
// working as expected, since the workspace doesn't have access to the
|
|
// private key directly and must use the forwarded agent.
|
|
tpty.WriteLine("echo 'hello world' | gpg --clearsign && echo gpg-''-sign-command-done")
|
|
tpty.ExpectMatch("BEGIN PGP SIGNED MESSAGE")
|
|
tpty.ExpectMatch("Hash:")
|
|
tpty.ExpectMatch("hello world")
|
|
tpty.ExpectMatch("gpg--sign-command-done")
|
|
|
|
// And we're done.
|
|
tpty.WriteLine("exit")
|
|
<-cmdDone
|
|
}
|
|
|
|
// tGoContext runs fn in a goroutine passing a context that will be
|
|
// canceled on test completion and wait until fn has finished executing.
|
|
// Done and cancel are returned for optionally waiting until completion
|
|
// or early cancellation.
|
|
//
|
|
// NOTE(mafredri): This could be moved to a helper library.
|
|
func tGoContext(t *testing.T, fn func(context.Context)) (done <-chan struct{}, cancel context.CancelFunc) {
|
|
t.Helper()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
doneC := make(chan struct{})
|
|
t.Cleanup(func() {
|
|
cancel()
|
|
<-done
|
|
})
|
|
go func() {
|
|
fn(ctx)
|
|
close(doneC)
|
|
}()
|
|
|
|
return doneC, cancel
|
|
}
|
|
|
|
// tGo runs fn in a goroutine and waits until fn has completed before
|
|
// test completion. Done is returned for optionally waiting for fn to
|
|
// exit.
|
|
//
|
|
// NOTE(mafredri): This could be moved to a helper library.
|
|
func tGo(t *testing.T, fn func()) (done <-chan struct{}) {
|
|
t.Helper()
|
|
|
|
doneC := make(chan struct{})
|
|
t.Cleanup(func() {
|
|
<-doneC
|
|
})
|
|
go func() {
|
|
fn()
|
|
close(doneC)
|
|
}()
|
|
|
|
return doneC
|
|
}
|
|
|
|
type stdioConn struct {
|
|
io.Reader
|
|
io.Writer
|
|
}
|
|
|
|
func (*stdioConn) Close() (err error) {
|
|
return nil
|
|
}
|
|
|
|
func (*stdioConn) LocalAddr() net.Addr {
|
|
return nil
|
|
}
|
|
|
|
func (*stdioConn) RemoteAddr() net.Addr {
|
|
return nil
|
|
}
|
|
|
|
func (*stdioConn) SetDeadline(_ time.Time) error {
|
|
return nil
|
|
}
|
|
|
|
func (*stdioConn) SetReadDeadline(_ time.Time) error {
|
|
return nil
|
|
}
|
|
|
|
func (*stdioConn) SetWriteDeadline(_ time.Time) error {
|
|
return nil
|
|
}
|
|
|
|
// tempDirUnixSocket returns a temporary directory that can safely hold unix
|
|
// sockets (probably).
|
|
//
|
|
// During tests on darwin we hit the max path length limit for unix sockets
|
|
// pretty easily in the default location, so this function uses /tmp instead to
|
|
// get shorter paths.
|
|
func tempDirUnixSocket(t *testing.T) string {
|
|
t.Helper()
|
|
if runtime.GOOS == "darwin" {
|
|
testName := strings.ReplaceAll(t.Name(), "/", "_")
|
|
dir, err := os.MkdirTemp("/tmp", fmt.Sprintf("coder-test-%s-", testName))
|
|
require.NoError(t, err, "create temp dir for gpg test")
|
|
|
|
t.Cleanup(func() {
|
|
err := os.RemoveAll(dir)
|
|
assert.NoError(t, err, "remove temp dir", dir)
|
|
})
|
|
return dir
|
|
}
|
|
|
|
return t.TempDir()
|
|
}
|