2022-02-19 05:13:32 +00:00
|
|
|
|
package agent_test
|
|
|
|
|
|
|
|
|
|
import (
|
2023-12-07 21:15:54 +00:00
|
|
|
|
"bufio"
|
2022-11-24 12:22:20 +00:00
|
|
|
|
"bytes"
|
2022-02-19 05:13:32 +00:00
|
|
|
|
"context"
|
2022-04-29 22:30:10 +00:00
|
|
|
|
"encoding/json"
|
2024-02-19 14:30:00 +00:00
|
|
|
|
"errors"
|
2022-04-11 23:54:30 +00:00
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"net"
|
2023-03-23 19:09:13 +00:00
|
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
2022-09-01 01:09:44 +00:00
|
|
|
|
"net/netip"
|
2022-04-12 00:17:18 +00:00
|
|
|
|
"os"
|
2023-08-14 19:19:13 +00:00
|
|
|
|
"os/exec"
|
2022-10-21 14:54:06 +00:00
|
|
|
|
"os/user"
|
2022-10-26 13:02:06 +00:00
|
|
|
|
"path"
|
2022-04-12 00:17:18 +00:00
|
|
|
|
"path/filepath"
|
2023-06-30 18:41:29 +00:00
|
|
|
|
"regexp"
|
2022-02-19 05:13:32 +00:00
|
|
|
|
"runtime"
|
|
|
|
|
"strings"
|
2022-09-01 01:09:44 +00:00
|
|
|
|
"sync"
|
2022-10-24 03:35:08 +00:00
|
|
|
|
"sync/atomic"
|
2023-09-15 00:45:05 +00:00
|
|
|
|
"syscall"
|
2022-02-19 05:13:32 +00:00
|
|
|
|
"testing"
|
2022-04-25 18:30:39 +00:00
|
|
|
|
"time"
|
2022-02-19 05:13:32 +00:00
|
|
|
|
|
2023-11-27 05:42:45 +00:00
|
|
|
|
"github.com/bramvdbogaerde/go-scp"
|
2022-04-29 22:30:10 +00:00
|
|
|
|
"github.com/google/uuid"
|
2022-05-18 14:10:40 +00:00
|
|
|
|
"github.com/pion/udp"
|
2022-04-12 00:17:18 +00:00
|
|
|
|
"github.com/pkg/sftp"
|
2023-05-25 10:52:36 +00:00
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
|
|
|
promgo "github.com/prometheus/client_model/go"
|
2022-10-25 00:46:24 +00:00
|
|
|
|
"github.com/spf13/afero"
|
2022-05-24 07:58:39 +00:00
|
|
|
|
"github.com/stretchr/testify/assert"
|
2022-02-19 05:13:32 +00:00
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
|
"go.uber.org/goleak"
|
2024-01-05 00:35:56 +00:00
|
|
|
|
"go.uber.org/mock/gomock"
|
2022-02-19 05:13:32 +00:00
|
|
|
|
"golang.org/x/crypto/ssh"
|
2023-06-27 12:44:16 +00:00
|
|
|
|
"golang.org/x/exp/slices"
|
2023-02-10 03:43:18 +00:00
|
|
|
|
"golang.org/x/xerrors"
|
|
|
|
|
"tailscale.com/net/speedtest"
|
|
|
|
|
"tailscale.com/tailcfg"
|
2022-02-19 05:13:32 +00:00
|
|
|
|
|
|
|
|
|
"cdr.dev/slog"
|
2023-09-15 00:45:05 +00:00
|
|
|
|
"cdr.dev/slog/sloggers/sloghuman"
|
2022-02-19 05:13:32 +00:00
|
|
|
|
"cdr.dev/slog/sloggers/slogtest"
|
2023-08-18 18:55:43 +00:00
|
|
|
|
"github.com/coder/coder/v2/agent"
|
2023-09-15 00:45:05 +00:00
|
|
|
|
"github.com/coder/coder/v2/agent/agentproc"
|
|
|
|
|
"github.com/coder/coder/v2/agent/agentproc/agentproctest"
|
2023-08-18 18:55:43 +00:00
|
|
|
|
"github.com/coder/coder/v2/agent/agentssh"
|
|
|
|
|
"github.com/coder/coder/v2/agent/agenttest"
|
2024-02-07 11:26:41 +00:00
|
|
|
|
"github.com/coder/coder/v2/agent/proto"
|
2023-08-18 18:55:43 +00:00
|
|
|
|
"github.com/coder/coder/v2/codersdk"
|
|
|
|
|
"github.com/coder/coder/v2/codersdk/agentsdk"
|
2024-03-26 17:44:31 +00:00
|
|
|
|
"github.com/coder/coder/v2/codersdk/workspacesdk"
|
2024-03-14 15:36:12 +00:00
|
|
|
|
"github.com/coder/coder/v2/cryptorand"
|
2023-08-18 18:55:43 +00:00
|
|
|
|
"github.com/coder/coder/v2/pty/ptytest"
|
|
|
|
|
"github.com/coder/coder/v2/tailnet"
|
|
|
|
|
"github.com/coder/coder/v2/tailnet/tailnettest"
|
|
|
|
|
"github.com/coder/coder/v2/testutil"
|
2022-02-19 05:13:32 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestMain(m *testing.M) {
|
|
|
|
|
goleak.VerifyTestMain(m)
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-10 03:43:18 +00:00
|
|
|
|
// NOTE: These tests only work when your default shell is bash for some reason.
|
|
|
|
|
|
2022-12-12 20:20:46 +00:00
|
|
|
|
func TestAgent_Stats_SSH(t *testing.T) {
|
2022-02-19 05:13:32 +00:00
|
|
|
|
t.Parallel()
|
2022-12-12 20:20:46 +00:00
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
|
|
|
defer cancel()
|
2022-09-01 19:58:23 +00:00
|
|
|
|
|
2023-03-06 19:34:00 +00:00
|
|
|
|
//nolint:dogsled
|
2023-07-12 22:37:31 +00:00
|
|
|
|
conn, _, stats, _, _ := setupAgent(t, agentsdk.Manifest{}, 0)
|
2022-11-08 22:10:48 +00:00
|
|
|
|
|
2022-12-12 20:20:46 +00:00
|
|
|
|
sshClient, err := conn.SSHClient(ctx)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer sshClient.Close()
|
|
|
|
|
session, err := sshClient.NewSession()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer session.Close()
|
2023-03-09 03:05:45 +00:00
|
|
|
|
stdin, err := session.StdinPipe()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
err = session.Shell()
|
|
|
|
|
require.NoError(t, err)
|
2022-12-12 20:20:46 +00:00
|
|
|
|
|
2024-02-07 11:26:41 +00:00
|
|
|
|
var s *proto.Stats
|
2022-12-12 20:20:46 +00:00
|
|
|
|
require.Eventuallyf(t, func() bool {
|
|
|
|
|
var ok bool
|
|
|
|
|
s, ok = <-stats
|
2024-02-07 11:26:41 +00:00
|
|
|
|
return ok && s.ConnectionCount > 0 && s.RxBytes > 0 && s.TxBytes > 0 && s.SessionCountSsh == 1
|
2022-12-12 20:20:46 +00:00
|
|
|
|
}, testutil.WaitLong, testutil.IntervalFast,
|
|
|
|
|
"never saw stats: %+v", s,
|
|
|
|
|
)
|
2023-03-09 03:05:45 +00:00
|
|
|
|
_ = stdin.Close()
|
|
|
|
|
err = session.Wait()
|
|
|
|
|
require.NoError(t, err)
|
2022-12-12 20:20:46 +00:00
|
|
|
|
}
|
2022-09-20 00:46:29 +00:00
|
|
|
|
|
2022-12-12 20:20:46 +00:00
|
|
|
|
func TestAgent_Stats_ReconnectingPTY(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
2022-09-20 00:46:29 +00:00
|
|
|
|
|
2022-12-12 20:20:46 +00:00
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
|
|
|
defer cancel()
|
2022-11-08 22:10:48 +00:00
|
|
|
|
|
2023-03-06 19:34:00 +00:00
|
|
|
|
//nolint:dogsled
|
2023-07-12 22:37:31 +00:00
|
|
|
|
conn, _, stats, _, _ := setupAgent(t, agentsdk.Manifest{}, 0)
|
2022-09-01 19:58:23 +00:00
|
|
|
|
|
2023-08-14 19:19:13 +00:00
|
|
|
|
ptyConn, err := conn.ReconnectingPTY(ctx, uuid.New(), 128, 128, "bash")
|
2022-12-12 20:20:46 +00:00
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer ptyConn.Close()
|
2022-09-20 00:46:29 +00:00
|
|
|
|
|
2024-03-26 17:44:31 +00:00
|
|
|
|
data, err := json.Marshal(workspacesdk.ReconnectingPTYRequest{
|
2022-12-12 20:20:46 +00:00
|
|
|
|
Data: "echo test\r\n",
|
2022-09-01 19:58:23 +00:00
|
|
|
|
})
|
2022-12-12 20:20:46 +00:00
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
_, err = ptyConn.Write(data)
|
|
|
|
|
require.NoError(t, err)
|
2022-09-01 19:58:23 +00:00
|
|
|
|
|
2024-02-07 11:26:41 +00:00
|
|
|
|
var s *proto.Stats
|
2022-12-12 20:20:46 +00:00
|
|
|
|
require.Eventuallyf(t, func() bool {
|
|
|
|
|
var ok bool
|
|
|
|
|
s, ok = <-stats
|
2024-02-07 11:26:41 +00:00
|
|
|
|
return ok && s.ConnectionCount > 0 && s.RxBytes > 0 && s.TxBytes > 0 && s.SessionCountReconnectingPty == 1
|
2023-03-02 14:06:00 +00:00
|
|
|
|
}, testutil.WaitLong, testutil.IntervalFast,
|
|
|
|
|
"never saw stats: %+v", s,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAgent_Stats_Magic(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
2023-03-09 03:05:45 +00:00
|
|
|
|
t.Run("StripsEnvironmentVariable", func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
|
|
|
defer cancel()
|
|
|
|
|
//nolint:dogsled
|
2023-07-12 22:37:31 +00:00
|
|
|
|
conn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0)
|
2023-03-09 03:05:45 +00:00
|
|
|
|
sshClient, err := conn.SSHClient(ctx)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer sshClient.Close()
|
|
|
|
|
session, err := sshClient.NewSession()
|
|
|
|
|
require.NoError(t, err)
|
2023-04-06 16:39:22 +00:00
|
|
|
|
session.Setenv(agentssh.MagicSessionTypeEnvironmentVariable, agentssh.MagicSessionTypeVSCode)
|
2023-03-09 03:05:45 +00:00
|
|
|
|
defer session.Close()
|
|
|
|
|
|
2023-04-06 16:39:22 +00:00
|
|
|
|
command := "sh -c 'echo $" + agentssh.MagicSessionTypeEnvironmentVariable + "'"
|
2023-03-09 03:05:45 +00:00
|
|
|
|
expected := ""
|
|
|
|
|
if runtime.GOOS == "windows" {
|
2023-04-06 16:39:22 +00:00
|
|
|
|
expected = "%" + agentssh.MagicSessionTypeEnvironmentVariable + "%"
|
2023-03-09 03:05:45 +00:00
|
|
|
|
command = "cmd.exe /c echo " + expected
|
|
|
|
|
}
|
|
|
|
|
output, err := session.Output(command)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.Equal(t, expected, strings.TrimSpace(string(output)))
|
|
|
|
|
})
|
2023-12-07 21:15:54 +00:00
|
|
|
|
t.Run("TracksVSCode", func(t *testing.T) {
|
2023-03-09 03:05:45 +00:00
|
|
|
|
t.Parallel()
|
|
|
|
|
if runtime.GOOS == "window" {
|
|
|
|
|
t.Skip("Sleeping for infinity doesn't work on Windows")
|
|
|
|
|
}
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
|
|
|
defer cancel()
|
|
|
|
|
//nolint:dogsled
|
2023-07-12 22:37:31 +00:00
|
|
|
|
conn, _, stats, _, _ := setupAgent(t, agentsdk.Manifest{}, 0)
|
2023-03-09 03:05:45 +00:00
|
|
|
|
sshClient, err := conn.SSHClient(ctx)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer sshClient.Close()
|
|
|
|
|
session, err := sshClient.NewSession()
|
|
|
|
|
require.NoError(t, err)
|
2023-04-06 16:39:22 +00:00
|
|
|
|
session.Setenv(agentssh.MagicSessionTypeEnvironmentVariable, agentssh.MagicSessionTypeVSCode)
|
2023-03-09 03:05:45 +00:00
|
|
|
|
defer session.Close()
|
|
|
|
|
stdin, err := session.StdinPipe()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
err = session.Shell()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.Eventuallyf(t, func() bool {
|
2024-01-02 11:53:52 +00:00
|
|
|
|
s, ok := <-stats
|
|
|
|
|
t.Logf("got stats: ok=%t, ConnectionCount=%d, RxBytes=%d, TxBytes=%d, SessionCountVSCode=%d, ConnectionMedianLatencyMS=%f",
|
2024-02-07 11:26:41 +00:00
|
|
|
|
ok, s.ConnectionCount, s.RxBytes, s.TxBytes, s.SessionCountVscode, s.ConnectionMedianLatencyMs)
|
2023-03-09 03:05:45 +00:00
|
|
|
|
return ok && s.ConnectionCount > 0 && s.RxBytes > 0 && s.TxBytes > 0 &&
|
|
|
|
|
// Ensure that the connection didn't count as a "normal" SSH session.
|
|
|
|
|
// This was a special one, so it should be labeled specially in the stats!
|
2024-02-07 11:26:41 +00:00
|
|
|
|
s.SessionCountVscode == 1 &&
|
2023-03-09 03:05:45 +00:00
|
|
|
|
// Ensure that connection latency is being counted!
|
|
|
|
|
// If it isn't, it's set to -1.
|
2024-02-07 11:26:41 +00:00
|
|
|
|
s.ConnectionMedianLatencyMs >= 0
|
2023-03-09 03:05:45 +00:00
|
|
|
|
}, testutil.WaitLong, testutil.IntervalFast,
|
2024-01-02 11:53:52 +00:00
|
|
|
|
"never saw stats",
|
2023-03-09 03:05:45 +00:00
|
|
|
|
)
|
|
|
|
|
// The shell will automatically exit if there is no stdin!
|
|
|
|
|
_ = stdin.Close()
|
|
|
|
|
err = session.Wait()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
})
|
2023-12-07 21:15:54 +00:00
|
|
|
|
|
|
|
|
|
t.Run("TracksJetBrains", func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
if runtime.GOOS != "linux" {
|
|
|
|
|
t.Skip("JetBrains tracking is only supported on Linux")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
|
|
|
|
|
|
|
|
// JetBrains tracking works by looking at the process name listening on the
|
|
|
|
|
// forwarded port. If the process's command line includes the magic string
|
|
|
|
|
// we are looking for, then we assume it is a JetBrains editor. So when we
|
|
|
|
|
// connect to the port we must ensure the process includes that magic string
|
|
|
|
|
// to fool the agent into thinking this is JetBrains. To do this we need to
|
|
|
|
|
// spawn an external process (in this case a simple echo server) so we can
|
|
|
|
|
// control the process name. The -D here is just to mimic how Java options
|
|
|
|
|
// are set but is not necessary as the agent looks only for the magic
|
|
|
|
|
// string itself anywhere in the command.
|
|
|
|
|
_, b, _, ok := runtime.Caller(0)
|
|
|
|
|
require.True(t, ok)
|
|
|
|
|
dir := filepath.Join(filepath.Dir(b), "../scripts/echoserver/main.go")
|
2024-01-17 11:38:39 +00:00
|
|
|
|
echoServerCmd := exec.Command("go", "run", dir,
|
|
|
|
|
"-D", agentssh.MagicProcessCmdlineJetBrains)
|
|
|
|
|
stdout, err := echoServerCmd.StdoutPipe()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
err = echoServerCmd.Start()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer echoServerCmd.Process.Kill()
|
2023-12-07 21:15:54 +00:00
|
|
|
|
|
2024-01-17 11:38:39 +00:00
|
|
|
|
// The echo server prints its port as the first line.
|
|
|
|
|
sc := bufio.NewScanner(stdout)
|
|
|
|
|
sc.Scan()
|
|
|
|
|
remotePort := sc.Text()
|
2023-12-07 21:15:54 +00:00
|
|
|
|
|
|
|
|
|
//nolint:dogsled
|
|
|
|
|
conn, _, stats, _, _ := setupAgent(t, agentsdk.Manifest{}, 0)
|
|
|
|
|
sshClient, err := conn.SSHClient(ctx)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
2024-01-17 11:38:39 +00:00
|
|
|
|
tunneledConn, err := sshClient.Dial("tcp", fmt.Sprintf("127.0.0.1:%s", remotePort))
|
2023-12-07 21:15:54 +00:00
|
|
|
|
require.NoError(t, err)
|
2024-01-17 11:38:39 +00:00
|
|
|
|
t.Cleanup(func() {
|
|
|
|
|
// always close on failure of test
|
|
|
|
|
_ = conn.Close()
|
|
|
|
|
_ = tunneledConn.Close()
|
|
|
|
|
})
|
2023-12-07 21:15:54 +00:00
|
|
|
|
|
|
|
|
|
require.Eventuallyf(t, func() bool {
|
2024-01-02 11:53:52 +00:00
|
|
|
|
s, ok := <-stats
|
|
|
|
|
t.Logf("got stats with conn open: ok=%t, ConnectionCount=%d, SessionCountJetBrains=%d",
|
2024-02-07 11:26:41 +00:00
|
|
|
|
ok, s.ConnectionCount, s.SessionCountJetbrains)
|
2023-12-07 21:15:54 +00:00
|
|
|
|
return ok && s.ConnectionCount > 0 &&
|
2024-02-07 11:26:41 +00:00
|
|
|
|
s.SessionCountJetbrains == 1
|
2023-12-07 21:15:54 +00:00
|
|
|
|
}, testutil.WaitLong, testutil.IntervalFast,
|
2024-01-02 11:53:52 +00:00
|
|
|
|
"never saw stats with conn open",
|
2023-12-07 21:15:54 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Kill the server and connection after checking for the echo.
|
2024-01-17 11:38:39 +00:00
|
|
|
|
requireEcho(t, tunneledConn)
|
|
|
|
|
_ = echoServerCmd.Process.Kill()
|
|
|
|
|
_ = tunneledConn.Close()
|
2023-12-07 21:15:54 +00:00
|
|
|
|
|
|
|
|
|
require.Eventuallyf(t, func() bool {
|
2024-01-02 11:53:52 +00:00
|
|
|
|
s, ok := <-stats
|
|
|
|
|
t.Logf("got stats after disconnect %t, %d",
|
2024-02-07 11:26:41 +00:00
|
|
|
|
ok, s.SessionCountJetbrains)
|
2024-01-02 11:53:52 +00:00
|
|
|
|
return ok &&
|
2024-02-07 11:26:41 +00:00
|
|
|
|
s.SessionCountJetbrains == 0
|
2023-12-07 21:15:54 +00:00
|
|
|
|
}, testutil.WaitLong, testutil.IntervalFast,
|
2024-01-02 11:53:52 +00:00
|
|
|
|
"never saw stats after conn closes",
|
2023-12-07 21:15:54 +00:00
|
|
|
|
)
|
|
|
|
|
})
|
2022-12-12 20:20:46 +00:00
|
|
|
|
}
|
2022-02-19 05:13:32 +00:00
|
|
|
|
|
2022-12-12 20:20:46 +00:00
|
|
|
|
func TestAgent_SessionExec(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
2023-07-06 07:57:51 +00:00
|
|
|
|
session := setupSSHSession(t, agentsdk.Manifest{}, codersdk.ServiceBannerConfig{}, nil)
|
2022-04-07 22:40:27 +00:00
|
|
|
|
|
2022-12-12 20:20:46 +00:00
|
|
|
|
command := "echo test"
|
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
|
command = "cmd.exe /c echo test"
|
|
|
|
|
}
|
|
|
|
|
output, err := session.Output(command)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.Equal(t, "test", strings.TrimSpace(string(output)))
|
|
|
|
|
}
|
2022-04-11 23:54:30 +00:00
|
|
|
|
|
2024-02-19 14:30:00 +00:00
|
|
|
|
//nolint:tparallel // Sub tests need to run sequentially.
|
|
|
|
|
func TestAgent_Session_EnvironmentVariables(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
2024-02-20 11:26:18 +00:00
|
|
|
|
tmpdir := t.TempDir()
|
|
|
|
|
|
|
|
|
|
// Defined by the coder script runner, hardcoded here since we don't
|
|
|
|
|
// have a reference to it.
|
|
|
|
|
scriptBinDir := filepath.Join(tmpdir, "coder-script-data", "bin")
|
|
|
|
|
|
2024-02-19 14:30:00 +00:00
|
|
|
|
manifest := agentsdk.Manifest{
|
|
|
|
|
EnvironmentVariables: map[string]string{
|
|
|
|
|
"MY_MANIFEST": "true",
|
|
|
|
|
"MY_OVERRIDE": "false",
|
|
|
|
|
"MY_SESSION_MANIFEST": "false",
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
banner := codersdk.ServiceBannerConfig{}
|
|
|
|
|
session := setupSSHSession(t, manifest, banner, nil, func(_ *agenttest.Client, opts *agent.Options) {
|
2024-02-20 11:26:18 +00:00
|
|
|
|
opts.ScriptDataDir = tmpdir
|
2024-02-19 14:30:00 +00:00
|
|
|
|
opts.EnvironmentVariables["MY_OVERRIDE"] = "true"
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
err := session.Setenv("MY_SESSION_MANIFEST", "true")
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
err = session.Setenv("MY_SESSION", "true")
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
command := "sh"
|
|
|
|
|
echoEnv := func(t *testing.T, w io.Writer, env string) {
|
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
|
_, err := fmt.Fprintf(w, "echo %%%s%%\r\n", env)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
} else {
|
|
|
|
|
_, err := fmt.Fprintf(w, "echo $%s\n", env)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
|
command = "cmd.exe"
|
|
|
|
|
}
|
|
|
|
|
stdin, err := session.StdinPipe()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer stdin.Close()
|
|
|
|
|
stdout, err := session.StdoutPipe()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
err = session.Start(command)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
// Context is fine here since we're not doing a parallel subtest.
|
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
|
|
|
go func() {
|
|
|
|
|
<-ctx.Done()
|
|
|
|
|
_ = session.Close()
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
s := bufio.NewScanner(stdout)
|
|
|
|
|
|
|
|
|
|
//nolint:paralleltest // These tests need to run sequentially.
|
|
|
|
|
for k, partialV := range map[string]string{
|
|
|
|
|
"CODER": "true", // From the agent.
|
|
|
|
|
"MY_MANIFEST": "true", // From the manifest.
|
|
|
|
|
"MY_OVERRIDE": "true", // From the agent environment variables option, overrides manifest.
|
|
|
|
|
"MY_SESSION_MANIFEST": "false", // From the manifest, overrides session env.
|
|
|
|
|
"MY_SESSION": "true", // From the session.
|
2024-02-20 11:26:18 +00:00
|
|
|
|
"PATH": scriptBinDir + string(filepath.ListSeparator),
|
2024-02-19 14:30:00 +00:00
|
|
|
|
} {
|
|
|
|
|
t.Run(k, func(t *testing.T) {
|
|
|
|
|
echoEnv(t, stdin, k)
|
|
|
|
|
// Windows is unreliable, so keep scanning until we find a match.
|
|
|
|
|
for s.Scan() {
|
|
|
|
|
got := strings.TrimSpace(s.Text())
|
|
|
|
|
t.Logf("%s=%s", k, got)
|
|
|
|
|
if strings.Contains(got, partialV) {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if err := s.Err(); !errors.Is(err, io.EOF) {
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-12 20:20:46 +00:00
|
|
|
|
func TestAgent_GitSSH(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
2023-07-06 07:57:51 +00:00
|
|
|
|
session := setupSSHSession(t, agentsdk.Manifest{}, codersdk.ServiceBannerConfig{}, nil)
|
2022-12-12 20:20:46 +00:00
|
|
|
|
command := "sh -c 'echo $GIT_SSH_COMMAND'"
|
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
|
command = "cmd.exe /c echo %GIT_SSH_COMMAND%"
|
|
|
|
|
}
|
|
|
|
|
output, err := session.Output(command)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.True(t, strings.HasSuffix(strings.TrimSpace(string(output)), "gitssh --"))
|
|
|
|
|
}
|
2022-07-27 19:23:28 +00:00
|
|
|
|
|
2022-12-12 20:20:46 +00:00
|
|
|
|
func TestAgent_SessionTTYShell(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
2023-01-23 11:23:25 +00:00
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
|
|
|
t.Cleanup(cancel)
|
2022-12-12 20:20:46 +00:00
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
|
// This might be our implementation, or ConPTY itself.
|
|
|
|
|
// It's difficult to find extensive tests for it, so
|
|
|
|
|
// it seems like it could be either.
|
|
|
|
|
t.Skip("ConPTY appears to be inconsistent on Windows.")
|
|
|
|
|
}
|
2023-07-06 07:57:51 +00:00
|
|
|
|
session := setupSSHSession(t, agentsdk.Manifest{}, codersdk.ServiceBannerConfig{}, nil)
|
2023-01-23 11:23:25 +00:00
|
|
|
|
command := "sh"
|
2022-12-12 20:20:46 +00:00
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
|
command = "cmd.exe"
|
|
|
|
|
}
|
|
|
|
|
err := session.RequestPty("xterm", 128, 128, ssh.TerminalModes{})
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
ptty := ptytest.New(t)
|
|
|
|
|
session.Stdout = ptty.Output()
|
|
|
|
|
session.Stderr = ptty.Output()
|
|
|
|
|
session.Stdin = ptty.Input()
|
|
|
|
|
err = session.Start(command)
|
|
|
|
|
require.NoError(t, err)
|
2023-01-23 11:23:25 +00:00
|
|
|
|
_ = ptty.Peek(ctx, 1) // wait for the prompt
|
2022-12-12 20:20:46 +00:00
|
|
|
|
ptty.WriteLine("echo test")
|
|
|
|
|
ptty.ExpectMatch("test")
|
|
|
|
|
ptty.WriteLine("exit")
|
|
|
|
|
err = session.Wait()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
}
|
2022-11-24 12:22:20 +00:00
|
|
|
|
|
2022-12-12 20:20:46 +00:00
|
|
|
|
func TestAgent_SessionTTYExitCode(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
2023-07-06 07:57:51 +00:00
|
|
|
|
session := setupSSHSession(t, agentsdk.Manifest{}, codersdk.ServiceBannerConfig{}, nil)
|
2022-12-12 20:20:46 +00:00
|
|
|
|
command := "areallynotrealcommand"
|
|
|
|
|
err := session.RequestPty("xterm", 128, 128, ssh.TerminalModes{})
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
ptty := ptytest.New(t)
|
|
|
|
|
session.Stdout = ptty.Output()
|
|
|
|
|
session.Stderr = ptty.Output()
|
|
|
|
|
session.Stdin = ptty.Input()
|
|
|
|
|
err = session.Start(command)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
err = session.Wait()
|
|
|
|
|
exitErr := &ssh.ExitError{}
|
|
|
|
|
require.True(t, xerrors.As(err, &exitErr))
|
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
|
assert.Equal(t, 1, exitErr.ExitStatus())
|
|
|
|
|
} else {
|
|
|
|
|
assert.Equal(t, 127, exitErr.ExitStatus())
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-11-24 12:22:20 +00:00
|
|
|
|
|
2022-12-12 20:20:46 +00:00
|
|
|
|
func TestAgent_Session_TTY_MOTD(t *testing.T) {
|
2023-07-06 07:57:51 +00:00
|
|
|
|
t.Parallel()
|
2022-12-12 20:20:46 +00:00
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
|
// This might be our implementation, or ConPTY itself.
|
|
|
|
|
// It's difficult to find extensive tests for it, so
|
|
|
|
|
// it seems like it could be either.
|
|
|
|
|
t.Skip("ConPTY appears to be inconsistent on Windows.")
|
|
|
|
|
}
|
2022-11-24 12:22:20 +00:00
|
|
|
|
|
2023-07-06 07:57:51 +00:00
|
|
|
|
u, err := user.Current()
|
|
|
|
|
require.NoError(t, err, "get current user")
|
2022-11-24 12:22:20 +00:00
|
|
|
|
|
2023-07-06 07:57:51 +00:00
|
|
|
|
name := filepath.Join(u.HomeDir, "motd")
|
2022-11-24 12:22:20 +00:00
|
|
|
|
|
2023-07-06 07:57:51 +00:00
|
|
|
|
wantMOTD := "Welcome to your Coder workspace!"
|
|
|
|
|
wantServiceBanner := "Service banner text goes here"
|
2022-11-24 12:22:20 +00:00
|
|
|
|
|
2023-06-30 18:41:29 +00:00
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
manifest agentsdk.Manifest
|
|
|
|
|
banner codersdk.ServiceBannerConfig
|
|
|
|
|
expected []string
|
|
|
|
|
unexpected []string
|
|
|
|
|
expectedRe *regexp.Regexp
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "WithoutServiceBanner",
|
|
|
|
|
manifest: agentsdk.Manifest{MOTDFile: name},
|
|
|
|
|
banner: codersdk.ServiceBannerConfig{},
|
|
|
|
|
expected: []string{wantMOTD},
|
|
|
|
|
unexpected: []string{wantServiceBanner},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "WithServiceBanner",
|
|
|
|
|
manifest: agentsdk.Manifest{MOTDFile: name},
|
|
|
|
|
banner: codersdk.ServiceBannerConfig{
|
|
|
|
|
Enabled: true,
|
|
|
|
|
Message: wantServiceBanner,
|
|
|
|
|
},
|
|
|
|
|
expected: []string{wantMOTD, wantServiceBanner},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "ServiceBannerDisabled",
|
|
|
|
|
manifest: agentsdk.Manifest{MOTDFile: name},
|
|
|
|
|
banner: codersdk.ServiceBannerConfig{
|
|
|
|
|
Enabled: false,
|
|
|
|
|
Message: wantServiceBanner,
|
|
|
|
|
},
|
|
|
|
|
expected: []string{wantMOTD},
|
|
|
|
|
unexpected: []string{wantServiceBanner},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "ServiceBannerOnly",
|
|
|
|
|
manifest: agentsdk.Manifest{},
|
|
|
|
|
banner: codersdk.ServiceBannerConfig{
|
|
|
|
|
Enabled: true,
|
|
|
|
|
Message: wantServiceBanner,
|
|
|
|
|
},
|
|
|
|
|
expected: []string{wantServiceBanner},
|
|
|
|
|
unexpected: []string{wantMOTD},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "None",
|
|
|
|
|
manifest: agentsdk.Manifest{},
|
|
|
|
|
banner: codersdk.ServiceBannerConfig{},
|
|
|
|
|
unexpected: []string{wantServiceBanner, wantMOTD},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "CarriageReturns",
|
|
|
|
|
manifest: agentsdk.Manifest{},
|
|
|
|
|
banner: codersdk.ServiceBannerConfig{
|
|
|
|
|
Enabled: true,
|
|
|
|
|
Message: "service\n\nbanner\nhere",
|
|
|
|
|
},
|
|
|
|
|
expected: []string{"service\r\n\r\nbanner\r\nhere\r\n\r\n"},
|
|
|
|
|
unexpected: []string{},
|
|
|
|
|
},
|
|
|
|
|
{
|
2023-10-23 15:06:59 +00:00
|
|
|
|
name: "Trim",
|
|
|
|
|
// Enable motd since it will be printed after the banner,
|
|
|
|
|
// this ensures that we can test for an exact mount of
|
|
|
|
|
// newlines.
|
|
|
|
|
manifest: agentsdk.Manifest{
|
|
|
|
|
MOTDFile: name,
|
|
|
|
|
},
|
2023-06-30 18:41:29 +00:00
|
|
|
|
banner: codersdk.ServiceBannerConfig{
|
|
|
|
|
Enabled: true,
|
|
|
|
|
Message: "\n\n\n\n\n\nbanner\n\n\n\n\n\n",
|
|
|
|
|
},
|
2023-10-10 04:42:39 +00:00
|
|
|
|
expectedRe: regexp.MustCompile(`([^\n\r]|^)banner\r\n\r\n[^\r\n]`),
|
2023-06-30 18:41:29 +00:00
|
|
|
|
},
|
|
|
|
|
}
|
2022-11-24 12:22:20 +00:00
|
|
|
|
|
2023-06-30 18:41:29 +00:00
|
|
|
|
for _, test := range tests {
|
|
|
|
|
test := test
|
|
|
|
|
t.Run(test.name, func(t *testing.T) {
|
2023-07-06 07:57:51 +00:00
|
|
|
|
t.Parallel()
|
|
|
|
|
session := setupSSHSession(t, test.manifest, test.banner, func(fs afero.Fs) {
|
|
|
|
|
err := fs.MkdirAll(filepath.Dir(name), 0o700)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
err = afero.WriteFile(fs, name, []byte(wantMOTD), 0o600)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
})
|
2023-06-30 18:41:29 +00:00
|
|
|
|
testSessionOutput(t, session, test.expected, test.unexpected, test.expectedRe)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-11-24 12:22:20 +00:00
|
|
|
|
|
2023-10-23 17:32:28 +00:00
|
|
|
|
//nolint:tparallel // Sub tests need to run sequentially.
|
2023-06-30 18:41:29 +00:00
|
|
|
|
func TestAgent_Session_TTY_MOTD_Update(t *testing.T) {
|
2023-07-06 07:57:51 +00:00
|
|
|
|
t.Parallel()
|
2023-06-30 18:41:29 +00:00
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
|
// This might be our implementation, or ConPTY itself.
|
|
|
|
|
// It's difficult to find extensive tests for it, so
|
|
|
|
|
// it seems like it could be either.
|
|
|
|
|
t.Skip("ConPTY appears to be inconsistent on Windows.")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Only the banner updates dynamically; the MOTD file does not.
|
|
|
|
|
wantServiceBanner := "Service banner text goes here"
|
|
|
|
|
|
|
|
|
|
tests := []struct {
|
|
|
|
|
banner codersdk.ServiceBannerConfig
|
|
|
|
|
expected []string
|
|
|
|
|
unexpected []string
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
banner: codersdk.ServiceBannerConfig{},
|
|
|
|
|
expected: []string{},
|
|
|
|
|
unexpected: []string{wantServiceBanner},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
banner: codersdk.ServiceBannerConfig{
|
|
|
|
|
Enabled: true,
|
|
|
|
|
Message: wantServiceBanner,
|
|
|
|
|
},
|
|
|
|
|
expected: []string{wantServiceBanner},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
banner: codersdk.ServiceBannerConfig{
|
|
|
|
|
Enabled: false,
|
|
|
|
|
Message: wantServiceBanner,
|
|
|
|
|
},
|
|
|
|
|
expected: []string{},
|
|
|
|
|
unexpected: []string{wantServiceBanner},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
banner: codersdk.ServiceBannerConfig{
|
|
|
|
|
Enabled: true,
|
|
|
|
|
Message: wantServiceBanner,
|
|
|
|
|
},
|
|
|
|
|
expected: []string{wantServiceBanner},
|
|
|
|
|
unexpected: []string{},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
banner: codersdk.ServiceBannerConfig{},
|
|
|
|
|
unexpected: []string{wantServiceBanner},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
|
|
|
defer cancel()
|
2023-07-14 13:10:26 +00:00
|
|
|
|
|
|
|
|
|
setSBInterval := func(_ *agenttest.Client, opts *agent.Options) {
|
|
|
|
|
opts.ServiceBannerRefreshInterval = 5 * time.Millisecond
|
|
|
|
|
}
|
2023-06-30 18:41:29 +00:00
|
|
|
|
//nolint:dogsled // Allow the blank identifiers.
|
2023-07-14 13:10:26 +00:00
|
|
|
|
conn, client, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0, setSBInterval)
|
2023-10-23 17:32:28 +00:00
|
|
|
|
|
|
|
|
|
sshClient, err := conn.SSHClient(ctx)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
t.Cleanup(func() {
|
|
|
|
|
_ = sshClient.Close()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
//nolint:paralleltest // These tests need to swap the banner func.
|
|
|
|
|
for i, test := range tests {
|
2023-06-30 18:41:29 +00:00
|
|
|
|
test := test
|
2023-10-23 17:32:28 +00:00
|
|
|
|
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
|
|
|
|
// Set new banner func and wait for the agent to call it to update the
|
|
|
|
|
// banner.
|
|
|
|
|
ready := make(chan struct{}, 2)
|
2024-05-08 21:40:43 +00:00
|
|
|
|
client.SetNotificationBannersFunc(func() ([]codersdk.BannerConfig, error) {
|
2023-10-23 17:32:28 +00:00
|
|
|
|
select {
|
|
|
|
|
case ready <- struct{}{}:
|
|
|
|
|
default:
|
|
|
|
|
}
|
2024-05-08 21:40:43 +00:00
|
|
|
|
return []codersdk.BannerConfig{test.banner}, nil
|
2023-10-23 17:32:28 +00:00
|
|
|
|
})
|
|
|
|
|
<-ready
|
|
|
|
|
<-ready // Wait for two updates to ensure the value has propagated.
|
2023-06-30 18:41:29 +00:00
|
|
|
|
|
2023-10-23 17:32:28 +00:00
|
|
|
|
session, err := sshClient.NewSession()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
t.Cleanup(func() {
|
|
|
|
|
_ = session.Close()
|
|
|
|
|
})
|
2022-11-24 12:22:20 +00:00
|
|
|
|
|
2023-10-23 17:32:28 +00:00
|
|
|
|
testSessionOutput(t, session, test.expected, test.unexpected, nil)
|
|
|
|
|
})
|
2023-06-30 18:41:29 +00:00
|
|
|
|
}
|
2022-12-12 20:20:46 +00:00
|
|
|
|
}
|
2022-11-24 12:22:20 +00:00
|
|
|
|
|
2022-12-12 20:20:46 +00:00
|
|
|
|
//nolint:paralleltest // This test sets an environment variable.
|
2023-06-30 18:41:29 +00:00
|
|
|
|
func TestAgent_Session_TTY_QuietLogin(t *testing.T) {
|
2022-12-12 20:20:46 +00:00
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
|
// This might be our implementation, or ConPTY itself.
|
|
|
|
|
// It's difficult to find extensive tests for it, so
|
|
|
|
|
// it seems like it could be either.
|
|
|
|
|
t.Skip("ConPTY appears to be inconsistent on Windows.")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
wantNotMOTD := "Welcome to your Coder workspace!"
|
2023-07-06 07:57:51 +00:00
|
|
|
|
wantMaybeServiceBanner := "Service banner text goes here"
|
2022-12-12 20:20:46 +00:00
|
|
|
|
|
2023-07-06 07:57:51 +00:00
|
|
|
|
u, err := user.Current()
|
|
|
|
|
require.NoError(t, err, "get current user")
|
2022-12-12 20:20:46 +00:00
|
|
|
|
|
2023-07-06 07:57:51 +00:00
|
|
|
|
name := filepath.Join(u.HomeDir, "motd")
|
2022-12-12 20:20:46 +00:00
|
|
|
|
|
2023-06-30 18:41:29 +00:00
|
|
|
|
// Neither banner nor MOTD should show if not a login shell.
|
|
|
|
|
t.Run("NotLogin", func(t *testing.T) {
|
|
|
|
|
session := setupSSHSession(t, agentsdk.Manifest{
|
|
|
|
|
MOTDFile: name,
|
|
|
|
|
}, codersdk.ServiceBannerConfig{
|
|
|
|
|
Enabled: true,
|
2023-07-06 07:57:51 +00:00
|
|
|
|
Message: wantMaybeServiceBanner,
|
|
|
|
|
}, func(fs afero.Fs) {
|
|
|
|
|
err := afero.WriteFile(fs, name, []byte(wantNotMOTD), 0o600)
|
|
|
|
|
require.NoError(t, err, "write motd file")
|
2023-06-30 18:41:29 +00:00
|
|
|
|
})
|
|
|
|
|
err = session.RequestPty("xterm", 128, 128, ssh.TerminalModes{})
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
2023-07-06 07:57:51 +00:00
|
|
|
|
wantEcho := "foobar"
|
2023-06-30 18:41:29 +00:00
|
|
|
|
command := "echo " + wantEcho
|
|
|
|
|
output, err := session.Output(command)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
require.Contains(t, string(output), wantEcho, "should show echo")
|
|
|
|
|
require.NotContains(t, string(output), wantNotMOTD, "should not show motd")
|
2023-07-06 07:57:51 +00:00
|
|
|
|
require.NotContains(t, string(output), wantMaybeServiceBanner, "should not show service banner")
|
2022-11-24 12:22:20 +00:00
|
|
|
|
})
|
|
|
|
|
|
2023-07-06 07:57:51 +00:00
|
|
|
|
// Only the MOTD should be silenced when hushlogin is present.
|
2023-06-30 18:41:29 +00:00
|
|
|
|
t.Run("Hushlogin", func(t *testing.T) {
|
|
|
|
|
session := setupSSHSession(t, agentsdk.Manifest{
|
|
|
|
|
MOTDFile: name,
|
|
|
|
|
}, codersdk.ServiceBannerConfig{
|
|
|
|
|
Enabled: true,
|
2023-07-06 07:57:51 +00:00
|
|
|
|
Message: wantMaybeServiceBanner,
|
|
|
|
|
}, func(fs afero.Fs) {
|
|
|
|
|
err := afero.WriteFile(fs, name, []byte(wantNotMOTD), 0o600)
|
|
|
|
|
require.NoError(t, err, "write motd file")
|
|
|
|
|
|
|
|
|
|
// Create hushlogin to silence motd.
|
|
|
|
|
err = afero.WriteFile(fs, name, []byte{}, 0o600)
|
|
|
|
|
require.NoError(t, err, "write hushlogin file")
|
2023-06-30 18:41:29 +00:00
|
|
|
|
})
|
|
|
|
|
err = session.RequestPty("xterm", 128, 128, ssh.TerminalModes{})
|
|
|
|
|
require.NoError(t, err)
|
2022-04-11 23:54:30 +00:00
|
|
|
|
|
2023-06-30 18:41:29 +00:00
|
|
|
|
ptty := ptytest.New(t)
|
|
|
|
|
var stdout bytes.Buffer
|
|
|
|
|
session.Stdout = &stdout
|
|
|
|
|
session.Stderr = ptty.Output()
|
|
|
|
|
session.Stdin = ptty.Input()
|
|
|
|
|
err = session.Shell()
|
|
|
|
|
require.NoError(t, err)
|
2022-04-11 23:54:30 +00:00
|
|
|
|
|
2023-06-30 18:41:29 +00:00
|
|
|
|
ptty.WriteLine("exit 0")
|
|
|
|
|
err = session.Wait()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
require.NotContains(t, stdout.String(), wantNotMOTD, "should not show motd")
|
2023-07-06 07:57:51 +00:00
|
|
|
|
require.Contains(t, stdout.String(), wantMaybeServiceBanner, "should show service banner")
|
2023-06-30 18:41:29 +00:00
|
|
|
|
})
|
2022-12-12 20:20:46 +00:00
|
|
|
|
}
|
2022-04-12 00:17:18 +00:00
|
|
|
|
|
2023-03-24 18:23:41 +00:00
|
|
|
|
func TestAgent_Session_TTY_FastCommandHasOutput(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
// This test is here to prevent regressions where quickly executing
|
2023-03-29 18:58:38 +00:00
|
|
|
|
// commands (with TTY) don't sync their output to the SSH session.
|
2023-03-24 18:23:41 +00:00
|
|
|
|
//
|
|
|
|
|
// See: https://github.com/coder/coder/issues/6656
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
|
|
|
defer cancel()
|
|
|
|
|
//nolint:dogsled
|
2023-07-12 22:37:31 +00:00
|
|
|
|
conn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0)
|
2023-03-24 18:23:41 +00:00
|
|
|
|
sshClient, err := conn.SSHClient(ctx)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer sshClient.Close()
|
|
|
|
|
|
|
|
|
|
ptty := ptytest.New(t)
|
|
|
|
|
|
|
|
|
|
var stdout bytes.Buffer
|
|
|
|
|
// NOTE(mafredri): Increase iterations to increase chance of failure,
|
2023-03-28 09:11:15 +00:00
|
|
|
|
// assuming bug is present. Limiting GOMAXPROCS further
|
|
|
|
|
// increases the chance of failure.
|
2023-03-24 18:23:41 +00:00
|
|
|
|
// Using 1000 iterations is basically a guaranteed failure (but let's
|
|
|
|
|
// not increase test times needlessly).
|
2023-03-28 09:11:15 +00:00
|
|
|
|
// Limit GOMAXPROCS (e.g. `export GOMAXPROCS=1`) to further increase
|
|
|
|
|
// chance of failure. Also -race helps.
|
2023-03-24 18:23:41 +00:00
|
|
|
|
for i := 0; i < 5; i++ {
|
|
|
|
|
func() {
|
|
|
|
|
stdout.Reset()
|
|
|
|
|
|
|
|
|
|
session, err := sshClient.NewSession()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer session.Close()
|
|
|
|
|
err = session.RequestPty("xterm", 128, 128, ssh.TerminalModes{})
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
session.Stdout = &stdout
|
|
|
|
|
session.Stderr = ptty.Output()
|
|
|
|
|
session.Stdin = ptty.Input()
|
|
|
|
|
err = session.Start("echo wazzup")
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
err = session.Wait()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.Contains(t, stdout.String(), "wazzup", "should output greeting")
|
|
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-28 09:11:15 +00:00
|
|
|
|
func TestAgent_Session_TTY_HugeOutputIsNotLost(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
2023-03-29 18:58:38 +00:00
|
|
|
|
|
|
|
|
|
// This test is here to prevent regressions where a command (with or
|
|
|
|
|
// without) a large amount of output would not be fully copied to the
|
|
|
|
|
// SSH session. On unix systems, this was fixed by duplicating the file
|
|
|
|
|
// descriptor of the PTY master and using it for copying the output.
|
|
|
|
|
//
|
|
|
|
|
// See: https://github.com/coder/coder/issues/6656
|
2023-03-28 09:11:15 +00:00
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
|
|
|
defer cancel()
|
|
|
|
|
//nolint:dogsled
|
2023-07-12 22:37:31 +00:00
|
|
|
|
conn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0)
|
2023-03-28 09:11:15 +00:00
|
|
|
|
sshClient, err := conn.SSHClient(ctx)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer sshClient.Close()
|
|
|
|
|
|
|
|
|
|
ptty := ptytest.New(t)
|
|
|
|
|
|
|
|
|
|
var stdout bytes.Buffer
|
|
|
|
|
// NOTE(mafredri): Increase iterations to increase chance of failure,
|
|
|
|
|
// assuming bug is present.
|
|
|
|
|
// Using 10 iterations is basically a guaranteed failure (but let's
|
|
|
|
|
// not increase test times needlessly). Run with -race and do not
|
|
|
|
|
// limit parallelism (`export GOMAXPROCS=10`) to increase the chance
|
|
|
|
|
// of failure.
|
|
|
|
|
for i := 0; i < 1; i++ {
|
|
|
|
|
func() {
|
|
|
|
|
stdout.Reset()
|
|
|
|
|
|
|
|
|
|
session, err := sshClient.NewSession()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer session.Close()
|
|
|
|
|
err = session.RequestPty("xterm", 128, 128, ssh.TerminalModes{})
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
session.Stdout = &stdout
|
|
|
|
|
session.Stderr = ptty.Output()
|
|
|
|
|
session.Stdin = ptty.Input()
|
|
|
|
|
want := strings.Repeat("wazzup", 1024+1) // ~6KB, +1 because 1024 is a common buffer size.
|
|
|
|
|
err = session.Start("echo " + want)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
err = session.Wait()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.Contains(t, stdout.String(), want, "should output entire greeting")
|
|
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-06 07:52:19 +00:00
|
|
|
|
func TestAgent_TCPLocalForwarding(t *testing.T) {
|
2023-11-27 05:42:45 +00:00
|
|
|
|
t.Parallel()
|
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
2022-12-12 20:20:46 +00:00
|
|
|
|
|
2023-11-27 05:42:45 +00:00
|
|
|
|
rl, err := net.Listen("tcp", "127.0.0.1:0")
|
2022-12-12 20:20:46 +00:00
|
|
|
|
require.NoError(t, err)
|
2023-11-27 05:42:45 +00:00
|
|
|
|
defer rl.Close()
|
|
|
|
|
tcpAddr, valid := rl.Addr().(*net.TCPAddr)
|
2022-12-12 20:20:46 +00:00
|
|
|
|
require.True(t, valid)
|
2023-01-06 07:52:19 +00:00
|
|
|
|
remotePort := tcpAddr.Port
|
2023-11-27 05:42:45 +00:00
|
|
|
|
go echoOnce(t, rl)
|
2022-04-25 18:30:39 +00:00
|
|
|
|
|
2023-11-27 05:42:45 +00:00
|
|
|
|
sshClient := setupAgentSSHClient(ctx, t)
|
2023-01-06 07:52:19 +00:00
|
|
|
|
|
2023-11-27 05:42:45 +00:00
|
|
|
|
conn, err := sshClient.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", remotePort))
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer conn.Close()
|
|
|
|
|
requireEcho(t, conn)
|
2023-01-06 07:52:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAgent_TCPRemoteForwarding(t *testing.T) {
|
2023-11-27 05:42:45 +00:00
|
|
|
|
t.Parallel()
|
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
|
|
|
sshClient := setupAgentSSHClient(ctx, t)
|
|
|
|
|
|
|
|
|
|
localhost := netip.MustParseAddr("127.0.0.1")
|
|
|
|
|
var randomPort uint16
|
|
|
|
|
var ll net.Listener
|
|
|
|
|
var err error
|
|
|
|
|
for {
|
2024-02-29 12:51:44 +00:00
|
|
|
|
randomPort = testutil.RandomPortNoListen(t)
|
2023-11-27 05:42:45 +00:00
|
|
|
|
addr := net.TCPAddrFromAddrPort(netip.AddrPortFrom(localhost, randomPort))
|
|
|
|
|
ll, err = sshClient.ListenTCP(addr)
|
2023-01-06 07:52:19 +00:00
|
|
|
|
if err != nil {
|
2023-11-27 05:42:45 +00:00
|
|
|
|
t.Logf("error remote forwarding: %s", err.Error())
|
|
|
|
|
select {
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
t.Fatal("timed out getting random listener")
|
|
|
|
|
default:
|
|
|
|
|
continue
|
|
|
|
|
}
|
2023-01-06 07:52:19 +00:00
|
|
|
|
}
|
2023-11-27 05:42:45 +00:00
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
defer ll.Close()
|
|
|
|
|
go echoOnce(t, ll)
|
2023-01-06 07:52:19 +00:00
|
|
|
|
|
2023-11-27 05:42:45 +00:00
|
|
|
|
conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", randomPort))
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer conn.Close()
|
|
|
|
|
requireEcho(t, conn)
|
2023-01-06 07:52:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAgent_UnixLocalForwarding(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
|
t.Skip("unix domain sockets are not fully supported on Windows")
|
|
|
|
|
}
|
2023-11-27 05:42:45 +00:00
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
2023-01-06 07:52:19 +00:00
|
|
|
|
tmpdir := tempDirUnixSocket(t)
|
|
|
|
|
remoteSocketPath := filepath.Join(tmpdir, "remote-socket")
|
|
|
|
|
|
|
|
|
|
l, err := net.Listen("unix", remoteSocketPath)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer l.Close()
|
2023-11-27 05:42:45 +00:00
|
|
|
|
go echoOnce(t, l)
|
2023-01-06 07:52:19 +00:00
|
|
|
|
|
2023-11-27 05:42:45 +00:00
|
|
|
|
sshClient := setupAgentSSHClient(ctx, t)
|
2023-01-06 07:52:19 +00:00
|
|
|
|
|
2023-11-27 05:42:45 +00:00
|
|
|
|
conn, err := sshClient.Dial("unix", remoteSocketPath)
|
2023-01-06 07:52:19 +00:00
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer conn.Close()
|
|
|
|
|
_, err = conn.Write([]byte("test"))
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
b := make([]byte, 4)
|
|
|
|
|
_, err = conn.Read(b)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.Equal(t, "test", string(b))
|
|
|
|
|
_ = conn.Close()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAgent_UnixRemoteForwarding(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
|
t.Skip("unix domain sockets are not fully supported on Windows")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tmpdir := tempDirUnixSocket(t)
|
|
|
|
|
remoteSocketPath := filepath.Join(tmpdir, "remote-socket")
|
|
|
|
|
|
2023-11-27 05:42:45 +00:00
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
|
|
|
sshClient := setupAgentSSHClient(ctx, t)
|
|
|
|
|
|
|
|
|
|
l, err := sshClient.ListenUnix(remoteSocketPath)
|
2023-01-06 07:52:19 +00:00
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer l.Close()
|
2023-11-27 05:42:45 +00:00
|
|
|
|
go echoOnce(t, l)
|
2023-01-06 07:52:19 +00:00
|
|
|
|
|
2023-11-27 05:42:45 +00:00
|
|
|
|
conn, err := net.Dial("unix", remoteSocketPath)
|
2023-01-06 07:52:19 +00:00
|
|
|
|
require.NoError(t, err)
|
2023-11-27 05:42:45 +00:00
|
|
|
|
defer conn.Close()
|
|
|
|
|
requireEcho(t, conn)
|
2022-12-12 20:20:46 +00:00
|
|
|
|
}
|
2022-04-25 18:30:39 +00:00
|
|
|
|
|
2022-12-12 20:20:46 +00:00
|
|
|
|
func TestAgent_SFTP(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
|
|
|
defer cancel()
|
|
|
|
|
u, err := user.Current()
|
|
|
|
|
require.NoError(t, err, "get current user")
|
|
|
|
|
home := u.HomeDir
|
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
|
home = "/" + strings.ReplaceAll(home, "\\", "/")
|
|
|
|
|
}
|
2023-01-24 12:24:27 +00:00
|
|
|
|
//nolint:dogsled
|
2023-07-12 22:37:31 +00:00
|
|
|
|
conn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0)
|
2022-12-12 20:20:46 +00:00
|
|
|
|
sshClient, err := conn.SSHClient(ctx)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer sshClient.Close()
|
|
|
|
|
client, err := sftp.NewClient(sshClient)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer client.Close()
|
|
|
|
|
wd, err := client.Getwd()
|
|
|
|
|
require.NoError(t, err, "get working directory")
|
|
|
|
|
require.Equal(t, home, wd, "working directory should be home user home")
|
|
|
|
|
tempFile := filepath.Join(t.TempDir(), "sftp")
|
|
|
|
|
// SFTP only accepts unix-y paths.
|
|
|
|
|
remoteFile := filepath.ToSlash(tempFile)
|
|
|
|
|
if !path.IsAbs(remoteFile) {
|
|
|
|
|
// On Windows, e.g. "/C:/Users/...".
|
|
|
|
|
remoteFile = path.Join("/", remoteFile)
|
|
|
|
|
}
|
|
|
|
|
file, err := client.Create(remoteFile)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
err = file.Close()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
_, err = os.Stat(tempFile)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
}
|
2022-06-17 05:54:45 +00:00
|
|
|
|
|
2022-12-12 20:20:46 +00:00
|
|
|
|
func TestAgent_SCP(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
2022-08-23 11:29:01 +00:00
|
|
|
|
|
2022-12-12 20:20:46 +00:00
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
|
|
|
defer cancel()
|
2022-08-23 11:29:01 +00:00
|
|
|
|
|
2023-01-24 12:24:27 +00:00
|
|
|
|
//nolint:dogsled
|
2023-07-12 22:37:31 +00:00
|
|
|
|
conn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0)
|
2022-12-12 20:20:46 +00:00
|
|
|
|
sshClient, err := conn.SSHClient(ctx)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer sshClient.Close()
|
|
|
|
|
scpClient, err := scp.NewClientBySSH(sshClient)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer scpClient.Close()
|
|
|
|
|
tempFile := filepath.Join(t.TempDir(), "scp")
|
|
|
|
|
content := "hello world"
|
|
|
|
|
err = scpClient.CopyFile(context.Background(), strings.NewReader(content), tempFile, "0755")
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
_, err = os.Stat(tempFile)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAgent_EnvironmentVariables(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
key := "EXAMPLE"
|
|
|
|
|
value := "value"
|
2023-03-31 20:26:19 +00:00
|
|
|
|
session := setupSSHSession(t, agentsdk.Manifest{
|
2022-12-12 20:20:46 +00:00
|
|
|
|
EnvironmentVariables: map[string]string{
|
|
|
|
|
key: value,
|
|
|
|
|
},
|
2023-07-06 07:57:51 +00:00
|
|
|
|
}, codersdk.ServiceBannerConfig{}, nil)
|
2022-12-12 20:20:46 +00:00
|
|
|
|
command := "sh -c 'echo $" + key + "'"
|
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
|
command = "cmd.exe /c echo %" + key + "%"
|
|
|
|
|
}
|
|
|
|
|
output, err := session.Output(command)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.Equal(t, value, strings.TrimSpace(string(output)))
|
|
|
|
|
}
|
2022-08-23 18:19:57 +00:00
|
|
|
|
|
2022-12-12 20:20:46 +00:00
|
|
|
|
func TestAgent_EnvironmentVariableExpansion(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
key := "EXAMPLE"
|
2023-03-31 20:26:19 +00:00
|
|
|
|
session := setupSSHSession(t, agentsdk.Manifest{
|
2022-12-12 20:20:46 +00:00
|
|
|
|
EnvironmentVariables: map[string]string{
|
|
|
|
|
key: "$SOMETHINGNOTSET",
|
|
|
|
|
},
|
2023-07-06 07:57:51 +00:00
|
|
|
|
}, codersdk.ServiceBannerConfig{}, nil)
|
2022-12-12 20:20:46 +00:00
|
|
|
|
command := "sh -c 'echo $" + key + "'"
|
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
|
command = "cmd.exe /c echo %" + key + "%"
|
|
|
|
|
}
|
|
|
|
|
output, err := session.Output(command)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
expect := ""
|
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
|
expect = "%EXAMPLE%"
|
|
|
|
|
}
|
|
|
|
|
// Output should be empty, because the variable is not set!
|
|
|
|
|
require.Equal(t, expect, strings.TrimSpace(string(output)))
|
|
|
|
|
}
|
2022-08-23 11:29:01 +00:00
|
|
|
|
|
2022-12-12 20:20:46 +00:00
|
|
|
|
func TestAgent_CoderEnvVars(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
2024-01-02 18:46:18 +00:00
|
|
|
|
for _, key := range []string{"CODER", "CODER_WORKSPACE_NAME", "CODER_WORKSPACE_AGENT_NAME"} {
|
2022-12-12 20:20:46 +00:00
|
|
|
|
key := key
|
|
|
|
|
t.Run(key, func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
2023-07-06 07:57:51 +00:00
|
|
|
|
session := setupSSHSession(t, agentsdk.Manifest{}, codersdk.ServiceBannerConfig{}, nil)
|
2022-12-12 20:20:46 +00:00
|
|
|
|
command := "sh -c 'echo $" + key + "'"
|
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
|
command = "cmd.exe /c echo %" + key + "%"
|
2022-04-25 19:01:49 +00:00
|
|
|
|
}
|
2022-12-12 20:20:46 +00:00
|
|
|
|
output, err := session.Output(command)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.NotEmpty(t, strings.TrimSpace(string(output)))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAgent_SSHConnectionEnvVars(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
// Note: the SSH_TTY environment variable should only be set for TTYs.
|
|
|
|
|
// For some reason this test produces a TTY locally and a non-TTY in CI
|
|
|
|
|
// so we don't test for the absence of SSH_TTY.
|
|
|
|
|
for _, key := range []string{"SSH_CONNECTION", "SSH_CLIENT"} {
|
|
|
|
|
key := key
|
|
|
|
|
t.Run(key, func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
2023-07-06 07:57:51 +00:00
|
|
|
|
session := setupSSHSession(t, agentsdk.Manifest{}, codersdk.ServiceBannerConfig{}, nil)
|
2022-12-12 20:20:46 +00:00
|
|
|
|
command := "sh -c 'echo $" + key + "'"
|
2022-04-25 18:30:39 +00:00
|
|
|
|
if runtime.GOOS == "windows" {
|
2022-12-12 20:20:46 +00:00
|
|
|
|
command = "cmd.exe /c echo %" + key + "%"
|
2022-04-25 18:30:39 +00:00
|
|
|
|
}
|
2022-12-12 20:20:46 +00:00
|
|
|
|
output, err := session.Output(command)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.NotEmpty(t, strings.TrimSpace(string(output)))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-04-29 22:30:10 +00:00
|
|
|
|
|
2023-03-31 20:26:19 +00:00
|
|
|
|
func TestAgent_Metadata(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
2023-05-02 10:41:41 +00:00
|
|
|
|
echoHello := "echo 'hello'"
|
|
|
|
|
|
2023-03-31 20:26:19 +00:00
|
|
|
|
t.Run("Once", func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
2023-10-13 14:48:25 +00:00
|
|
|
|
|
2023-03-31 20:26:19 +00:00
|
|
|
|
//nolint:dogsled
|
2023-07-12 22:37:31 +00:00
|
|
|
|
_, client, _, _, _ := setupAgent(t, agentsdk.Manifest{
|
|
|
|
|
Metadata: []codersdk.WorkspaceAgentMetadataDescription{
|
|
|
|
|
{
|
2023-10-13 14:48:25 +00:00
|
|
|
|
Key: "greeting1",
|
2023-07-12 22:37:31 +00:00
|
|
|
|
Interval: 0,
|
|
|
|
|
Script: echoHello,
|
2023-03-31 20:26:19 +00:00
|
|
|
|
},
|
2023-10-13 14:48:25 +00:00
|
|
|
|
{
|
|
|
|
|
Key: "greeting2",
|
|
|
|
|
Interval: 1,
|
|
|
|
|
Script: echoHello,
|
|
|
|
|
},
|
2023-03-31 20:26:19 +00:00
|
|
|
|
},
|
2023-07-14 13:10:26 +00:00
|
|
|
|
}, 0, func(_ *agenttest.Client, opts *agent.Options) {
|
2023-10-13 14:48:25 +00:00
|
|
|
|
opts.ReportMetadataInterval = testutil.IntervalFast
|
2023-07-14 13:10:26 +00:00
|
|
|
|
})
|
2023-03-31 20:26:19 +00:00
|
|
|
|
|
2023-10-13 14:32:28 +00:00
|
|
|
|
var gotMd map[string]agentsdk.Metadata
|
2023-03-31 20:26:19 +00:00
|
|
|
|
require.Eventually(t, func() bool {
|
2023-07-12 22:37:31 +00:00
|
|
|
|
gotMd = client.GetMetadata()
|
2023-10-13 14:48:25 +00:00
|
|
|
|
return len(gotMd) == 2
|
|
|
|
|
}, testutil.WaitShort, testutil.IntervalFast/2)
|
2023-03-31 20:26:19 +00:00
|
|
|
|
|
2023-10-13 14:48:25 +00:00
|
|
|
|
collectedAt1 := gotMd["greeting1"].CollectedAt
|
|
|
|
|
collectedAt2 := gotMd["greeting2"].CollectedAt
|
2023-03-31 20:26:19 +00:00
|
|
|
|
|
2023-10-13 14:48:25 +00:00
|
|
|
|
require.Eventually(t, func() bool {
|
2023-07-12 22:37:31 +00:00
|
|
|
|
gotMd = client.GetMetadata()
|
2023-10-13 14:48:25 +00:00
|
|
|
|
if len(gotMd) != 2 {
|
2023-03-31 20:26:19 +00:00
|
|
|
|
panic("unexpected number of metadata")
|
|
|
|
|
}
|
2023-10-13 14:48:25 +00:00
|
|
|
|
return !gotMd["greeting2"].CollectedAt.Equal(collectedAt2)
|
|
|
|
|
}, testutil.WaitShort, testutil.IntervalFast/2)
|
|
|
|
|
|
|
|
|
|
require.Equal(t, gotMd["greeting1"].CollectedAt, collectedAt1, "metadata should not be collected again")
|
2023-03-31 20:26:19 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("Many", func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
2023-05-02 10:41:41 +00:00
|
|
|
|
//nolint:dogsled
|
2023-07-12 22:37:31 +00:00
|
|
|
|
_, client, _, _, _ := setupAgent(t, agentsdk.Manifest{
|
|
|
|
|
Metadata: []codersdk.WorkspaceAgentMetadataDescription{
|
|
|
|
|
{
|
|
|
|
|
Key: "greeting",
|
|
|
|
|
Interval: 1,
|
|
|
|
|
Timeout: 100,
|
|
|
|
|
Script: echoHello,
|
2023-03-31 20:26:19 +00:00
|
|
|
|
},
|
|
|
|
|
},
|
2023-07-14 13:10:26 +00:00
|
|
|
|
}, 0, func(_ *agenttest.Client, opts *agent.Options) {
|
|
|
|
|
opts.ReportMetadataInterval = testutil.IntervalFast
|
|
|
|
|
})
|
2023-03-31 20:26:19 +00:00
|
|
|
|
|
2023-10-13 14:32:28 +00:00
|
|
|
|
var gotMd map[string]agentsdk.Metadata
|
2023-03-31 20:26:19 +00:00
|
|
|
|
require.Eventually(t, func() bool {
|
2023-07-12 22:37:31 +00:00
|
|
|
|
gotMd = client.GetMetadata()
|
2023-05-02 10:41:41 +00:00
|
|
|
|
return len(gotMd) == 1
|
2023-07-14 13:10:26 +00:00
|
|
|
|
}, testutil.WaitShort, testutil.IntervalFast/2)
|
2023-03-31 20:26:19 +00:00
|
|
|
|
|
2023-05-02 10:41:41 +00:00
|
|
|
|
collectedAt1 := gotMd["greeting"].CollectedAt
|
2023-07-14 13:10:26 +00:00
|
|
|
|
require.Equal(t, "hello", strings.TrimSpace(gotMd["greeting"].Value))
|
2023-03-31 20:26:19 +00:00
|
|
|
|
|
2023-05-02 10:41:41 +00:00
|
|
|
|
if !assert.Eventually(t, func() bool {
|
2023-07-12 22:37:31 +00:00
|
|
|
|
gotMd = client.GetMetadata()
|
2023-05-02 10:41:41 +00:00
|
|
|
|
return gotMd["greeting"].CollectedAt.After(collectedAt1)
|
2023-07-14 13:10:26 +00:00
|
|
|
|
}, testutil.WaitShort, testutil.IntervalFast/2) {
|
2023-05-02 10:41:41 +00:00
|
|
|
|
t.Fatalf("expected metadata to be collected again")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
2023-03-31 20:26:19 +00:00
|
|
|
|
|
2023-05-02 10:41:41 +00:00
|
|
|
|
func TestAgentMetadata_Timing(t *testing.T) {
|
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
|
// Shell scripting in Windows is a pain, and we have already tested
|
|
|
|
|
// that the OS logic works in the simpler tests.
|
2023-05-15 00:37:00 +00:00
|
|
|
|
t.SkipNow()
|
2023-05-02 10:41:41 +00:00
|
|
|
|
}
|
|
|
|
|
testutil.SkipIfNotTiming(t)
|
|
|
|
|
t.Parallel()
|
2023-03-31 20:26:19 +00:00
|
|
|
|
|
2023-05-02 10:41:41 +00:00
|
|
|
|
dir := t.TempDir()
|
2023-03-31 20:26:19 +00:00
|
|
|
|
|
2023-05-02 10:41:41 +00:00
|
|
|
|
const reportInterval = 2
|
|
|
|
|
const intervalUnit = 100 * time.Millisecond
|
|
|
|
|
var (
|
|
|
|
|
greetingPath = filepath.Join(dir, "greeting")
|
|
|
|
|
script = "echo hello | tee -a " + greetingPath
|
|
|
|
|
)
|
|
|
|
|
//nolint:dogsled
|
2023-07-12 22:37:31 +00:00
|
|
|
|
_, client, _, _, _ := setupAgent(t, agentsdk.Manifest{
|
|
|
|
|
Metadata: []codersdk.WorkspaceAgentMetadataDescription{
|
|
|
|
|
{
|
|
|
|
|
Key: "greeting",
|
|
|
|
|
Interval: reportInterval,
|
|
|
|
|
Script: script,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Key: "bad",
|
|
|
|
|
Interval: reportInterval,
|
|
|
|
|
Script: "exit 1",
|
2023-05-02 10:41:41 +00:00
|
|
|
|
},
|
|
|
|
|
},
|
2023-07-14 13:10:26 +00:00
|
|
|
|
}, 0, func(_ *agenttest.Client, opts *agent.Options) {
|
|
|
|
|
opts.ReportMetadataInterval = intervalUnit
|
|
|
|
|
})
|
2023-05-02 10:41:41 +00:00
|
|
|
|
|
|
|
|
|
require.Eventually(t, func() bool {
|
2023-07-12 22:37:31 +00:00
|
|
|
|
return len(client.GetMetadata()) == 2
|
2023-05-02 10:41:41 +00:00
|
|
|
|
}, testutil.WaitShort, testutil.IntervalMedium)
|
|
|
|
|
|
|
|
|
|
for start := time.Now(); time.Since(start) < testutil.WaitMedium; time.Sleep(testutil.IntervalMedium) {
|
2023-07-12 22:37:31 +00:00
|
|
|
|
md := client.GetMetadata()
|
2023-05-02 10:41:41 +00:00
|
|
|
|
require.Len(t, md, 2, "got: %+v", md)
|
|
|
|
|
|
|
|
|
|
require.Equal(t, "hello\n", md["greeting"].Value)
|
|
|
|
|
require.Equal(t, "run cmd: exit status 1", md["bad"].Error)
|
|
|
|
|
|
|
|
|
|
greetingByt, err := os.ReadFile(greetingPath)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
numGreetings = bytes.Count(greetingByt, []byte("hello"))
|
|
|
|
|
idealNumGreetings = time.Since(start) / (reportInterval * intervalUnit)
|
|
|
|
|
// We allow a 50% error margin because the report loop may backlog
|
|
|
|
|
// in CI and other toasters. In production, there is no hard
|
|
|
|
|
// guarantee on timing either, and the frontend gives similar
|
|
|
|
|
// wiggle room to the staleness of the value.
|
|
|
|
|
upperBound = int(idealNumGreetings) + 1
|
|
|
|
|
lowerBound = (int(idealNumGreetings) / 2)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if idealNumGreetings < 50 {
|
|
|
|
|
// There is an insufficient sample size.
|
|
|
|
|
continue
|
2023-03-31 20:26:19 +00:00
|
|
|
|
}
|
2023-05-02 10:41:41 +00:00
|
|
|
|
|
|
|
|
|
t.Logf("numGreetings: %d, idealNumGreetings: %d", numGreetings, idealNumGreetings)
|
|
|
|
|
// The report loop may slow down on load, but it should never, ever
|
|
|
|
|
// speed up.
|
|
|
|
|
if numGreetings > upperBound {
|
|
|
|
|
t.Fatalf("too many greetings: %d > %d in %v", numGreetings, upperBound, time.Since(start))
|
|
|
|
|
} else if numGreetings < lowerBound {
|
|
|
|
|
t.Fatalf("too few greetings: %d < %d", numGreetings, lowerBound)
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-03-31 20:26:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-01-24 12:24:27 +00:00
|
|
|
|
func TestAgent_Lifecycle(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
2023-03-06 19:34:00 +00:00
|
|
|
|
t.Run("StartTimeout", func(t *testing.T) {
|
2023-01-24 12:24:27 +00:00
|
|
|
|
t.Parallel()
|
|
|
|
|
|
2023-07-12 22:37:31 +00:00
|
|
|
|
_, client, _, _, _ := setupAgent(t, agentsdk.Manifest{
|
2023-09-25 21:47:17 +00:00
|
|
|
|
Scripts: []codersdk.WorkspaceAgentScript{{
|
|
|
|
|
Script: "sleep 3",
|
|
|
|
|
Timeout: time.Millisecond,
|
|
|
|
|
RunOnStart: true,
|
|
|
|
|
}},
|
2023-01-24 12:24:27 +00:00
|
|
|
|
}, 0)
|
|
|
|
|
|
|
|
|
|
want := []codersdk.WorkspaceAgentLifecycle{
|
|
|
|
|
codersdk.WorkspaceAgentLifecycleStarting,
|
|
|
|
|
codersdk.WorkspaceAgentLifecycleStartTimeout,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var got []codersdk.WorkspaceAgentLifecycle
|
|
|
|
|
assert.Eventually(t, func() bool {
|
2023-07-12 22:37:31 +00:00
|
|
|
|
got = client.GetLifecycleStates()
|
2023-06-27 12:44:16 +00:00
|
|
|
|
return slices.Contains(got, want[len(want)-1])
|
2023-01-24 12:24:27 +00:00
|
|
|
|
}, testutil.WaitShort, testutil.IntervalMedium)
|
2023-06-20 11:41:55 +00:00
|
|
|
|
|
2023-06-27 12:44:16 +00:00
|
|
|
|
require.Equal(t, want, got[:len(want)])
|
2023-01-24 12:24:27 +00:00
|
|
|
|
})
|
|
|
|
|
|
2023-03-06 19:34:00 +00:00
|
|
|
|
t.Run("StartError", func(t *testing.T) {
|
2023-01-24 12:24:27 +00:00
|
|
|
|
t.Parallel()
|
|
|
|
|
|
2023-07-12 22:37:31 +00:00
|
|
|
|
_, client, _, _, _ := setupAgent(t, agentsdk.Manifest{
|
2023-09-25 21:47:17 +00:00
|
|
|
|
Scripts: []codersdk.WorkspaceAgentScript{{
|
|
|
|
|
Script: "false",
|
|
|
|
|
Timeout: 30 * time.Second,
|
|
|
|
|
RunOnStart: true,
|
|
|
|
|
}},
|
2023-01-24 12:24:27 +00:00
|
|
|
|
}, 0)
|
|
|
|
|
|
|
|
|
|
want := []codersdk.WorkspaceAgentLifecycle{
|
|
|
|
|
codersdk.WorkspaceAgentLifecycleStarting,
|
|
|
|
|
codersdk.WorkspaceAgentLifecycleStartError,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var got []codersdk.WorkspaceAgentLifecycle
|
|
|
|
|
assert.Eventually(t, func() bool {
|
2023-07-12 22:37:31 +00:00
|
|
|
|
got = client.GetLifecycleStates()
|
2023-06-27 12:44:16 +00:00
|
|
|
|
return slices.Contains(got, want[len(want)-1])
|
2023-01-24 12:24:27 +00:00
|
|
|
|
}, testutil.WaitShort, testutil.IntervalMedium)
|
2023-06-20 11:41:55 +00:00
|
|
|
|
|
2023-06-27 12:44:16 +00:00
|
|
|
|
require.Equal(t, want, got[:len(want)])
|
2023-01-24 12:24:27 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("Ready", func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
2023-07-12 22:37:31 +00:00
|
|
|
|
_, client, _, _, _ := setupAgent(t, agentsdk.Manifest{
|
2023-09-25 21:47:17 +00:00
|
|
|
|
Scripts: []codersdk.WorkspaceAgentScript{{
|
|
|
|
|
Script: "true",
|
|
|
|
|
Timeout: 30 * time.Second,
|
|
|
|
|
RunOnStart: true,
|
|
|
|
|
}},
|
2023-01-24 12:24:27 +00:00
|
|
|
|
}, 0)
|
|
|
|
|
|
|
|
|
|
want := []codersdk.WorkspaceAgentLifecycle{
|
|
|
|
|
codersdk.WorkspaceAgentLifecycleStarting,
|
|
|
|
|
codersdk.WorkspaceAgentLifecycleReady,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var got []codersdk.WorkspaceAgentLifecycle
|
|
|
|
|
assert.Eventually(t, func() bool {
|
2023-07-12 22:37:31 +00:00
|
|
|
|
got = client.GetLifecycleStates()
|
2023-01-24 12:24:27 +00:00
|
|
|
|
return len(got) > 0 && got[len(got)-1] == want[len(want)-1]
|
|
|
|
|
}, testutil.WaitShort, testutil.IntervalMedium)
|
2023-06-20 11:41:55 +00:00
|
|
|
|
|
|
|
|
|
require.Equal(t, want, got)
|
2023-01-24 12:24:27 +00:00
|
|
|
|
})
|
2023-03-06 19:34:00 +00:00
|
|
|
|
|
|
|
|
|
t.Run("ShuttingDown", func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
2023-07-12 22:37:31 +00:00
|
|
|
|
_, client, _, _, closer := setupAgent(t, agentsdk.Manifest{
|
2023-09-25 21:47:17 +00:00
|
|
|
|
Scripts: []codersdk.WorkspaceAgentScript{{
|
|
|
|
|
Script: "sleep 3",
|
|
|
|
|
Timeout: 30 * time.Second,
|
|
|
|
|
RunOnStop: true,
|
|
|
|
|
}},
|
2023-03-06 19:34:00 +00:00
|
|
|
|
}, 0)
|
|
|
|
|
|
|
|
|
|
assert.Eventually(t, func() bool {
|
2023-07-12 22:37:31 +00:00
|
|
|
|
return slices.Contains(client.GetLifecycleStates(), codersdk.WorkspaceAgentLifecycleReady)
|
2023-03-06 19:34:00 +00:00
|
|
|
|
}, testutil.WaitShort, testutil.IntervalMedium)
|
|
|
|
|
|
|
|
|
|
// Start close asynchronously so that we an inspect the state.
|
|
|
|
|
done := make(chan struct{})
|
|
|
|
|
go func() {
|
|
|
|
|
defer close(done)
|
|
|
|
|
err := closer.Close()
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
}()
|
|
|
|
|
t.Cleanup(func() {
|
|
|
|
|
<-done
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
want := []codersdk.WorkspaceAgentLifecycle{
|
2023-06-27 12:44:16 +00:00
|
|
|
|
codersdk.WorkspaceAgentLifecycleStarting,
|
|
|
|
|
codersdk.WorkspaceAgentLifecycleReady,
|
2023-03-06 19:34:00 +00:00
|
|
|
|
codersdk.WorkspaceAgentLifecycleShuttingDown,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var got []codersdk.WorkspaceAgentLifecycle
|
|
|
|
|
assert.Eventually(t, func() bool {
|
2023-07-12 22:37:31 +00:00
|
|
|
|
got = client.GetLifecycleStates()
|
2023-06-27 12:44:16 +00:00
|
|
|
|
return slices.Contains(got, want[len(want)-1])
|
2023-03-06 19:34:00 +00:00
|
|
|
|
}, testutil.WaitShort, testutil.IntervalMedium)
|
|
|
|
|
|
2023-06-27 12:44:16 +00:00
|
|
|
|
require.Equal(t, want, got[:len(want)])
|
2023-03-06 19:34:00 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("ShutdownTimeout", func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
2023-07-12 22:37:31 +00:00
|
|
|
|
_, client, _, _, closer := setupAgent(t, agentsdk.Manifest{
|
2023-09-25 21:47:17 +00:00
|
|
|
|
Scripts: []codersdk.WorkspaceAgentScript{{
|
|
|
|
|
Script: "sleep 3",
|
|
|
|
|
Timeout: time.Millisecond,
|
|
|
|
|
RunOnStop: true,
|
|
|
|
|
}},
|
2023-03-06 19:34:00 +00:00
|
|
|
|
}, 0)
|
|
|
|
|
|
|
|
|
|
assert.Eventually(t, func() bool {
|
2023-07-12 22:37:31 +00:00
|
|
|
|
return slices.Contains(client.GetLifecycleStates(), codersdk.WorkspaceAgentLifecycleReady)
|
2023-03-06 19:34:00 +00:00
|
|
|
|
}, testutil.WaitShort, testutil.IntervalMedium)
|
|
|
|
|
|
|
|
|
|
// Start close asynchronously so that we an inspect the state.
|
|
|
|
|
done := make(chan struct{})
|
|
|
|
|
go func() {
|
|
|
|
|
defer close(done)
|
|
|
|
|
err := closer.Close()
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
}()
|
|
|
|
|
t.Cleanup(func() {
|
|
|
|
|
<-done
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
want := []codersdk.WorkspaceAgentLifecycle{
|
2023-06-27 12:44:16 +00:00
|
|
|
|
codersdk.WorkspaceAgentLifecycleStarting,
|
|
|
|
|
codersdk.WorkspaceAgentLifecycleReady,
|
2023-03-06 19:34:00 +00:00
|
|
|
|
codersdk.WorkspaceAgentLifecycleShuttingDown,
|
|
|
|
|
codersdk.WorkspaceAgentLifecycleShutdownTimeout,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var got []codersdk.WorkspaceAgentLifecycle
|
|
|
|
|
assert.Eventually(t, func() bool {
|
2023-07-12 22:37:31 +00:00
|
|
|
|
got = client.GetLifecycleStates()
|
2023-06-27 12:44:16 +00:00
|
|
|
|
return slices.Contains(got, want[len(want)-1])
|
2023-03-06 19:34:00 +00:00
|
|
|
|
}, testutil.WaitShort, testutil.IntervalMedium)
|
|
|
|
|
|
2023-06-27 12:44:16 +00:00
|
|
|
|
require.Equal(t, want, got[:len(want)])
|
2023-03-06 19:34:00 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("ShutdownError", func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
2023-07-12 22:37:31 +00:00
|
|
|
|
_, client, _, _, closer := setupAgent(t, agentsdk.Manifest{
|
2023-09-25 21:47:17 +00:00
|
|
|
|
Scripts: []codersdk.WorkspaceAgentScript{{
|
|
|
|
|
Script: "false",
|
|
|
|
|
Timeout: 30 * time.Second,
|
|
|
|
|
RunOnStop: true,
|
|
|
|
|
}},
|
2023-03-06 19:34:00 +00:00
|
|
|
|
}, 0)
|
|
|
|
|
|
|
|
|
|
assert.Eventually(t, func() bool {
|
2023-07-12 22:37:31 +00:00
|
|
|
|
return slices.Contains(client.GetLifecycleStates(), codersdk.WorkspaceAgentLifecycleReady)
|
2023-03-06 19:34:00 +00:00
|
|
|
|
}, testutil.WaitShort, testutil.IntervalMedium)
|
|
|
|
|
|
|
|
|
|
// Start close asynchronously so that we an inspect the state.
|
|
|
|
|
done := make(chan struct{})
|
|
|
|
|
go func() {
|
|
|
|
|
defer close(done)
|
|
|
|
|
err := closer.Close()
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
}()
|
|
|
|
|
t.Cleanup(func() {
|
|
|
|
|
<-done
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
want := []codersdk.WorkspaceAgentLifecycle{
|
2023-06-27 12:44:16 +00:00
|
|
|
|
codersdk.WorkspaceAgentLifecycleStarting,
|
|
|
|
|
codersdk.WorkspaceAgentLifecycleReady,
|
2023-03-06 19:34:00 +00:00
|
|
|
|
codersdk.WorkspaceAgentLifecycleShuttingDown,
|
|
|
|
|
codersdk.WorkspaceAgentLifecycleShutdownError,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var got []codersdk.WorkspaceAgentLifecycle
|
|
|
|
|
assert.Eventually(t, func() bool {
|
2023-07-12 22:37:31 +00:00
|
|
|
|
got = client.GetLifecycleStates()
|
2023-06-27 12:44:16 +00:00
|
|
|
|
return slices.Contains(got, want[len(want)-1])
|
2023-03-06 19:34:00 +00:00
|
|
|
|
}, testutil.WaitShort, testutil.IntervalMedium)
|
|
|
|
|
|
2023-06-27 12:44:16 +00:00
|
|
|
|
require.Equal(t, want, got[:len(want)])
|
2023-03-06 19:34:00 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("ShutdownScriptOnce", func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
2023-04-27 09:59:01 +00:00
|
|
|
|
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
|
2023-03-06 19:34:00 +00:00
|
|
|
|
expected := "this-is-shutdown"
|
2023-07-12 22:37:31 +00:00
|
|
|
|
derpMap, _ := tailnettest.RunDERPAndSTUN(t)
|
|
|
|
|
|
|
|
|
|
client := agenttest.NewClient(t,
|
2023-07-20 18:49:44 +00:00
|
|
|
|
logger,
|
2023-07-12 22:37:31 +00:00
|
|
|
|
uuid.New(),
|
|
|
|
|
agentsdk.Manifest{
|
2023-09-25 21:47:17 +00:00
|
|
|
|
DERPMap: derpMap,
|
|
|
|
|
Scripts: []codersdk.WorkspaceAgentScript{{
|
|
|
|
|
LogPath: "coder-startup-script.log",
|
|
|
|
|
Script: "echo 1",
|
|
|
|
|
RunOnStart: true,
|
|
|
|
|
}, {
|
|
|
|
|
LogPath: "coder-shutdown-script.log",
|
|
|
|
|
Script: "echo " + expected,
|
|
|
|
|
RunOnStop: true,
|
|
|
|
|
}},
|
2023-03-06 19:34:00 +00:00
|
|
|
|
},
|
2024-02-07 11:26:41 +00:00
|
|
|
|
make(chan *proto.Stats, 50),
|
2023-07-12 22:37:31 +00:00
|
|
|
|
tailnet.NewCoordinator(logger),
|
|
|
|
|
)
|
2024-01-23 10:42:07 +00:00
|
|
|
|
defer client.Close()
|
2023-03-06 19:34:00 +00:00
|
|
|
|
|
|
|
|
|
fs := afero.NewMemMapFs()
|
|
|
|
|
agent := agent.New(agent.Options{
|
|
|
|
|
Client: client,
|
2023-04-27 09:59:01 +00:00
|
|
|
|
Logger: logger.Named("agent"),
|
2023-03-06 19:34:00 +00:00
|
|
|
|
Filesystem: fs,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// agent.Close() loads the shutdown script from the agent metadata.
|
|
|
|
|
// The metadata is populated just before execution of the startup script, so it's mandatory to wait
|
|
|
|
|
// until the startup starts.
|
|
|
|
|
require.Eventually(t, func() bool {
|
|
|
|
|
outputPath := filepath.Join(os.TempDir(), "coder-startup-script.log")
|
|
|
|
|
content, err := afero.ReadFile(fs, outputPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Logf("read file %q: %s", outputPath, err)
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return len(content) > 0 // something is in the startup log file
|
|
|
|
|
}, testutil.WaitShort, testutil.IntervalMedium)
|
|
|
|
|
|
|
|
|
|
err := agent.Close()
|
|
|
|
|
require.NoError(t, err, "agent should be closed successfully")
|
|
|
|
|
|
|
|
|
|
outputPath := filepath.Join(os.TempDir(), "coder-shutdown-script.log")
|
|
|
|
|
logFirstRead, err := afero.ReadFile(fs, outputPath)
|
|
|
|
|
require.NoError(t, err, "log file should be present")
|
|
|
|
|
require.Equal(t, expected, string(bytes.TrimSpace(logFirstRead)))
|
|
|
|
|
|
|
|
|
|
// Make sure that script can't be executed twice.
|
|
|
|
|
err = agent.Close()
|
|
|
|
|
require.NoError(t, err, "don't need to close the agent twice, no effect")
|
|
|
|
|
|
|
|
|
|
logSecondRead, err := afero.ReadFile(fs, outputPath)
|
|
|
|
|
require.NoError(t, err, "log file should be present")
|
|
|
|
|
require.Equal(t, string(bytes.TrimSpace(logFirstRead)), string(bytes.TrimSpace(logSecondRead)))
|
|
|
|
|
})
|
2023-01-24 12:24:27 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-02-07 21:35:09 +00:00
|
|
|
|
func TestAgent_Startup(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
t.Run("EmptyDirectory", func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
2024-01-30 07:23:28 +00:00
|
|
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
2023-02-07 21:35:09 +00:00
|
|
|
|
|
2023-07-12 22:37:31 +00:00
|
|
|
|
_, client, _, _, _ := setupAgent(t, agentsdk.Manifest{
|
2023-09-25 21:47:17 +00:00
|
|
|
|
Directory: "",
|
2023-02-07 21:35:09 +00:00
|
|
|
|
}, 0)
|
2024-01-30 07:23:28 +00:00
|
|
|
|
startup := testutil.RequireRecvCtx(ctx, t, client.GetStartup())
|
|
|
|
|
require.Equal(t, "", startup.GetExpandedDirectory())
|
2023-02-07 21:35:09 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("HomeDirectory", func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
2024-01-30 07:23:28 +00:00
|
|
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
2023-02-07 21:35:09 +00:00
|
|
|
|
|
2023-07-12 22:37:31 +00:00
|
|
|
|
_, client, _, _, _ := setupAgent(t, agentsdk.Manifest{
|
2023-09-25 21:47:17 +00:00
|
|
|
|
Directory: "~",
|
2023-02-07 21:35:09 +00:00
|
|
|
|
}, 0)
|
2024-01-30 07:23:28 +00:00
|
|
|
|
startup := testutil.RequireRecvCtx(ctx, t, client.GetStartup())
|
2023-02-07 21:35:09 +00:00
|
|
|
|
homeDir, err := os.UserHomeDir()
|
|
|
|
|
require.NoError(t, err)
|
2024-01-30 07:23:28 +00:00
|
|
|
|
require.Equal(t, homeDir, startup.GetExpandedDirectory())
|
2023-02-07 21:35:09 +00:00
|
|
|
|
})
|
|
|
|
|
|
2023-04-14 14:32:18 +00:00
|
|
|
|
t.Run("NotAbsoluteDirectory", func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
2024-01-30 07:23:28 +00:00
|
|
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
2023-04-14 14:32:18 +00:00
|
|
|
|
|
2023-07-12 22:37:31 +00:00
|
|
|
|
_, client, _, _, _ := setupAgent(t, agentsdk.Manifest{
|
2023-09-25 21:47:17 +00:00
|
|
|
|
Directory: "coder/coder",
|
2023-04-14 14:32:18 +00:00
|
|
|
|
}, 0)
|
2024-01-30 07:23:28 +00:00
|
|
|
|
startup := testutil.RequireRecvCtx(ctx, t, client.GetStartup())
|
2023-04-14 14:32:18 +00:00
|
|
|
|
homeDir, err := os.UserHomeDir()
|
|
|
|
|
require.NoError(t, err)
|
2024-01-30 07:23:28 +00:00
|
|
|
|
require.Equal(t, filepath.Join(homeDir, "coder/coder"), startup.GetExpandedDirectory())
|
2023-04-14 14:32:18 +00:00
|
|
|
|
})
|
|
|
|
|
|
2023-02-07 21:35:09 +00:00
|
|
|
|
t.Run("HomeEnvironmentVariable", func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
2024-01-30 07:23:28 +00:00
|
|
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
2023-02-07 21:35:09 +00:00
|
|
|
|
|
2023-07-12 22:37:31 +00:00
|
|
|
|
_, client, _, _, _ := setupAgent(t, agentsdk.Manifest{
|
2023-09-25 21:47:17 +00:00
|
|
|
|
Directory: "$HOME",
|
2023-02-07 21:35:09 +00:00
|
|
|
|
}, 0)
|
2024-01-30 07:23:28 +00:00
|
|
|
|
startup := testutil.RequireRecvCtx(ctx, t, client.GetStartup())
|
2023-02-07 21:35:09 +00:00
|
|
|
|
homeDir, err := os.UserHomeDir()
|
|
|
|
|
require.NoError(t, err)
|
2024-01-30 07:23:28 +00:00
|
|
|
|
require.Equal(t, homeDir, startup.GetExpandedDirectory())
|
2023-02-07 21:35:09 +00:00
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-14 19:19:13 +00:00
|
|
|
|
//nolint:paralleltest // This test sets an environment variable.
|
2022-12-12 20:20:46 +00:00
|
|
|
|
func TestAgent_ReconnectingPTY(t *testing.T) {
|
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
|
// This might be our implementation, or ConPTY itself.
|
|
|
|
|
// It's difficult to find extensive tests for it, so
|
|
|
|
|
// it seems like it could be either.
|
|
|
|
|
t.Skip("ConPTY appears to be inconsistent on Windows.")
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-14 19:19:13 +00:00
|
|
|
|
backends := []string{"Buffered", "Screen"}
|
2022-12-12 20:20:46 +00:00
|
|
|
|
|
2023-08-14 19:19:13 +00:00
|
|
|
|
_, err := exec.LookPath("screen")
|
|
|
|
|
hasScreen := err == nil
|
2022-11-08 22:10:48 +00:00
|
|
|
|
|
2023-10-11 21:25:04 +00:00
|
|
|
|
// Make sure UTF-8 works even with LANG set to something like C.
|
|
|
|
|
t.Setenv("LANG", "C")
|
|
|
|
|
|
2023-08-14 19:19:13 +00:00
|
|
|
|
for _, backendType := range backends {
|
|
|
|
|
backendType := backendType
|
|
|
|
|
t.Run(backendType, func(t *testing.T) {
|
|
|
|
|
if backendType == "Screen" {
|
|
|
|
|
if runtime.GOOS != "linux" {
|
|
|
|
|
t.Skipf("`screen` is not supported on %s", runtime.GOOS)
|
|
|
|
|
} else if !hasScreen {
|
|
|
|
|
t.Skip("`screen` not found")
|
|
|
|
|
}
|
|
|
|
|
} else if hasScreen && runtime.GOOS == "linux" {
|
|
|
|
|
// Set up a PATH that does not have screen in it.
|
|
|
|
|
bashPath, err := exec.LookPath("bash")
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
dir, err := os.MkdirTemp("/tmp", "coder-test-reconnecting-pty-PATH")
|
|
|
|
|
require.NoError(t, err, "create temp dir for reconnecting pty PATH")
|
|
|
|
|
err = os.Symlink(bashPath, filepath.Join(dir, "bash"))
|
|
|
|
|
require.NoError(t, err, "symlink bash into reconnecting pty PATH")
|
|
|
|
|
t.Setenv("PATH", dir)
|
|
|
|
|
}
|
2022-12-09 10:22:27 +00:00
|
|
|
|
|
2023-08-14 19:19:13 +00:00
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
|
|
|
defer cancel()
|
2022-05-18 17:06:17 +00:00
|
|
|
|
|
2023-08-14 19:19:13 +00:00
|
|
|
|
//nolint:dogsled
|
|
|
|
|
conn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0)
|
|
|
|
|
id := uuid.New()
|
2023-09-27 08:26:24 +00:00
|
|
|
|
// --norc disables executing .bashrc, which is often used to customize the bash prompt
|
|
|
|
|
netConn1, err := conn.ReconnectingPTY(ctx, id, 80, 80, "bash --norc")
|
2023-08-14 19:19:13 +00:00
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer netConn1.Close()
|
2023-09-27 08:26:24 +00:00
|
|
|
|
tr1 := testutil.NewTerminalReader(t, netConn1)
|
2023-08-14 19:19:13 +00:00
|
|
|
|
|
|
|
|
|
// A second simultaneous connection.
|
2023-09-27 08:26:24 +00:00
|
|
|
|
netConn2, err := conn.ReconnectingPTY(ctx, id, 80, 80, "bash --norc")
|
2022-12-12 20:20:46 +00:00
|
|
|
|
require.NoError(t, err)
|
2023-08-14 19:19:13 +00:00
|
|
|
|
defer netConn2.Close()
|
2023-09-27 08:26:24 +00:00
|
|
|
|
tr2 := testutil.NewTerminalReader(t, netConn2)
|
2023-08-14 19:19:13 +00:00
|
|
|
|
|
2023-09-27 08:26:24 +00:00
|
|
|
|
matchPrompt := func(line string) bool {
|
|
|
|
|
return strings.Contains(line, "$ ") || strings.Contains(line, "# ")
|
|
|
|
|
}
|
2023-08-14 19:19:13 +00:00
|
|
|
|
matchEchoCommand := func(line string) bool {
|
|
|
|
|
return strings.Contains(line, "echo test")
|
|
|
|
|
}
|
|
|
|
|
matchEchoOutput := func(line string) bool {
|
|
|
|
|
return strings.Contains(line, "test") && !strings.Contains(line, "echo")
|
|
|
|
|
}
|
|
|
|
|
matchExitCommand := func(line string) bool {
|
|
|
|
|
return strings.Contains(line, "exit")
|
|
|
|
|
}
|
|
|
|
|
matchExitOutput := func(line string) bool {
|
|
|
|
|
return strings.Contains(line, "exit") || strings.Contains(line, "logout")
|
|
|
|
|
}
|
2022-12-12 20:20:46 +00:00
|
|
|
|
|
2023-09-27 08:26:24 +00:00
|
|
|
|
// Wait for the prompt before writing commands. If the command arrives before the prompt is written, screen
|
|
|
|
|
// will sometimes put the command output on the same line as the command and the test will flake
|
|
|
|
|
require.NoError(t, tr1.ReadUntil(ctx, matchPrompt), "find prompt")
|
|
|
|
|
require.NoError(t, tr2.ReadUntil(ctx, matchPrompt), "find prompt")
|
|
|
|
|
|
2024-03-26 17:44:31 +00:00
|
|
|
|
data, err := json.Marshal(workspacesdk.ReconnectingPTYRequest{
|
2023-09-27 08:26:24 +00:00
|
|
|
|
Data: "echo test\r",
|
|
|
|
|
})
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
_, err = netConn1.Write(data)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
2023-08-14 19:19:13 +00:00
|
|
|
|
// Once for typing the command...
|
2023-09-19 17:57:30 +00:00
|
|
|
|
require.NoError(t, tr1.ReadUntil(ctx, matchEchoCommand), "find echo command")
|
2023-08-14 19:19:13 +00:00
|
|
|
|
// And another time for the actual output.
|
2023-09-19 17:57:30 +00:00
|
|
|
|
require.NoError(t, tr1.ReadUntil(ctx, matchEchoOutput), "find echo output")
|
2022-12-12 20:20:46 +00:00
|
|
|
|
|
2023-08-14 19:19:13 +00:00
|
|
|
|
// Same for the other connection.
|
2023-09-19 17:57:30 +00:00
|
|
|
|
require.NoError(t, tr2.ReadUntil(ctx, matchEchoCommand), "find echo command")
|
|
|
|
|
require.NoError(t, tr2.ReadUntil(ctx, matchEchoOutput), "find echo output")
|
2023-08-14 19:19:13 +00:00
|
|
|
|
|
|
|
|
|
_ = netConn1.Close()
|
|
|
|
|
_ = netConn2.Close()
|
2023-09-27 08:26:24 +00:00
|
|
|
|
netConn3, err := conn.ReconnectingPTY(ctx, id, 80, 80, "bash --norc")
|
2023-08-14 19:19:13 +00:00
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer netConn3.Close()
|
2023-09-27 08:26:24 +00:00
|
|
|
|
tr3 := testutil.NewTerminalReader(t, netConn3)
|
2023-08-14 19:19:13 +00:00
|
|
|
|
|
|
|
|
|
// Same output again!
|
2023-09-19 17:57:30 +00:00
|
|
|
|
require.NoError(t, tr3.ReadUntil(ctx, matchEchoCommand), "find echo command")
|
|
|
|
|
require.NoError(t, tr3.ReadUntil(ctx, matchEchoOutput), "find echo output")
|
2022-12-09 10:22:27 +00:00
|
|
|
|
|
2023-08-14 19:19:13 +00:00
|
|
|
|
// Exit should cause the connection to close.
|
2024-03-26 17:44:31 +00:00
|
|
|
|
data, err = json.Marshal(workspacesdk.ReconnectingPTYRequest{
|
2023-09-27 08:26:24 +00:00
|
|
|
|
Data: "exit\r",
|
2023-08-14 19:19:13 +00:00
|
|
|
|
})
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
_, err = netConn3.Write(data)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
// Once for the input and again for the output.
|
2023-09-19 17:57:30 +00:00
|
|
|
|
require.NoError(t, tr3.ReadUntil(ctx, matchExitCommand), "find exit command")
|
|
|
|
|
require.NoError(t, tr3.ReadUntil(ctx, matchExitOutput), "find exit output")
|
2023-08-14 19:19:13 +00:00
|
|
|
|
|
|
|
|
|
// Wait for the connection to close.
|
2023-09-19 17:57:30 +00:00
|
|
|
|
require.ErrorIs(t, tr3.ReadUntil(ctx, nil), io.EOF)
|
2023-08-14 19:19:13 +00:00
|
|
|
|
|
|
|
|
|
// Try a non-shell command. It should output then immediately exit.
|
2023-08-16 21:02:03 +00:00
|
|
|
|
netConn4, err := conn.ReconnectingPTY(ctx, uuid.New(), 80, 80, "echo test")
|
2023-08-14 19:19:13 +00:00
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer netConn4.Close()
|
|
|
|
|
|
2023-09-19 17:57:30 +00:00
|
|
|
|
tr4 := testutil.NewTerminalReader(t, netConn4)
|
|
|
|
|
require.NoError(t, tr4.ReadUntil(ctx, matchEchoOutput), "find echo output")
|
|
|
|
|
require.ErrorIs(t, tr4.ReadUntil(ctx, nil), io.EOF)
|
2023-10-11 21:25:04 +00:00
|
|
|
|
|
|
|
|
|
// Ensure that UTF-8 is supported. Avoid the terminal emulator because it
|
|
|
|
|
// does not appear to support UTF-8, just make sure the bytes that come
|
|
|
|
|
// back have the character in it.
|
|
|
|
|
netConn5, err := conn.ReconnectingPTY(ctx, uuid.New(), 80, 80, "echo ❯")
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer netConn5.Close()
|
|
|
|
|
|
|
|
|
|
bytes, err := io.ReadAll(netConn5)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.Contains(t, string(bytes), "❯")
|
2023-08-14 19:19:13 +00:00
|
|
|
|
})
|
|
|
|
|
}
|
2022-12-12 20:20:46 +00:00
|
|
|
|
}
|
2022-04-29 22:30:10 +00:00
|
|
|
|
|
2022-12-12 20:20:46 +00:00
|
|
|
|
func TestAgent_Dial(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
2022-05-18 14:10:40 +00:00
|
|
|
|
|
2022-12-12 20:20:46 +00:00
|
|
|
|
cases := []struct {
|
|
|
|
|
name string
|
|
|
|
|
setup func(t *testing.T) net.Listener
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "TCP",
|
|
|
|
|
setup: func(t *testing.T) net.Listener {
|
|
|
|
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
|
|
|
require.NoError(t, err, "create TCP listener")
|
|
|
|
|
return l
|
2022-05-18 14:10:40 +00:00
|
|
|
|
},
|
2022-12-12 20:20:46 +00:00
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "UDP",
|
|
|
|
|
setup: func(t *testing.T) net.Listener {
|
|
|
|
|
addr := net.UDPAddr{
|
|
|
|
|
IP: net.ParseIP("127.0.0.1"),
|
|
|
|
|
Port: 0,
|
|
|
|
|
}
|
|
|
|
|
l, err := udp.Listen("udp", &addr)
|
|
|
|
|
require.NoError(t, err, "create UDP listener")
|
|
|
|
|
return l
|
2022-05-18 14:10:40 +00:00
|
|
|
|
},
|
2022-12-12 20:20:46 +00:00
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, c := range cases {
|
|
|
|
|
c := c
|
|
|
|
|
t.Run(c.name, func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
2022-05-18 14:10:40 +00:00
|
|
|
|
|
2023-12-04 11:11:30 +00:00
|
|
|
|
// The purpose of this test is to ensure that a client can dial a
|
|
|
|
|
// listener in the workspace over tailnet.
|
2022-12-12 20:20:46 +00:00
|
|
|
|
l := c.setup(t)
|
2023-12-04 11:11:30 +00:00
|
|
|
|
done := make(chan struct{})
|
|
|
|
|
defer func() {
|
|
|
|
|
l.Close()
|
|
|
|
|
<-done
|
|
|
|
|
}()
|
2022-05-18 14:10:40 +00:00
|
|
|
|
|
2023-12-04 11:11:30 +00:00
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
|
defer close(done)
|
|
|
|
|
c, err := l.Accept()
|
2023-12-21 10:26:11 +00:00
|
|
|
|
if assert.NoError(t, err, "accept connection") {
|
|
|
|
|
defer c.Close()
|
|
|
|
|
testAccept(ctx, t, c)
|
|
|
|
|
}
|
2022-12-12 20:20:46 +00:00
|
|
|
|
}()
|
2022-10-24 03:35:08 +00:00
|
|
|
|
|
2023-01-24 12:24:27 +00:00
|
|
|
|
//nolint:dogsled
|
2023-12-04 11:11:30 +00:00
|
|
|
|
agentConn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0)
|
|
|
|
|
require.True(t, agentConn.AwaitReachable(ctx))
|
|
|
|
|
conn, err := agentConn.DialContext(ctx, l.Addr().Network(), l.Addr().String())
|
2022-12-12 20:20:46 +00:00
|
|
|
|
require.NoError(t, err)
|
2023-12-04 11:11:30 +00:00
|
|
|
|
defer conn.Close()
|
|
|
|
|
testDial(ctx, t, conn)
|
2022-10-24 03:35:08 +00:00
|
|
|
|
})
|
2022-12-12 20:20:46 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2022-10-25 00:46:24 +00:00
|
|
|
|
|
2023-07-26 16:21:04 +00:00
|
|
|
|
// TestAgent_UpdatedDERP checks that agents can handle their DERP map being
|
|
|
|
|
// updated, and that clients can also handle it.
|
|
|
|
|
func TestAgent_UpdatedDERP(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
|
|
|
|
|
|
|
|
|
|
originalDerpMap, _ := tailnettest.RunDERPAndSTUN(t)
|
|
|
|
|
require.NotNil(t, originalDerpMap)
|
|
|
|
|
|
|
|
|
|
coordinator := tailnet.NewCoordinator(logger)
|
2024-01-22 07:07:50 +00:00
|
|
|
|
// use t.Cleanup so the coordinator closing doesn't deadlock with in-memory
|
|
|
|
|
// coordination
|
|
|
|
|
t.Cleanup(func() {
|
2023-07-26 16:21:04 +00:00
|
|
|
|
_ = coordinator.Close()
|
2024-01-22 07:07:50 +00:00
|
|
|
|
})
|
2023-07-26 16:21:04 +00:00
|
|
|
|
agentID := uuid.New()
|
2024-02-07 11:26:41 +00:00
|
|
|
|
statsCh := make(chan *proto.Stats, 50)
|
2023-07-26 16:21:04 +00:00
|
|
|
|
fs := afero.NewMemMapFs()
|
|
|
|
|
client := agenttest.NewClient(t,
|
|
|
|
|
logger.Named("agent"),
|
|
|
|
|
agentID,
|
|
|
|
|
agentsdk.Manifest{
|
|
|
|
|
DERPMap: originalDerpMap,
|
|
|
|
|
// Force DERP.
|
|
|
|
|
DisableDirectConnections: true,
|
|
|
|
|
},
|
|
|
|
|
statsCh,
|
|
|
|
|
coordinator,
|
|
|
|
|
)
|
2024-01-23 10:42:07 +00:00
|
|
|
|
t.Cleanup(func() {
|
|
|
|
|
t.Log("closing client")
|
|
|
|
|
client.Close()
|
|
|
|
|
})
|
2024-01-22 07:07:50 +00:00
|
|
|
|
uut := agent.New(agent.Options{
|
2023-07-26 16:21:04 +00:00
|
|
|
|
Client: client,
|
|
|
|
|
Filesystem: fs,
|
|
|
|
|
Logger: logger.Named("agent"),
|
|
|
|
|
ReconnectingPTYTimeout: time.Minute,
|
|
|
|
|
})
|
2024-01-22 07:07:50 +00:00
|
|
|
|
t.Cleanup(func() {
|
2024-01-23 10:42:07 +00:00
|
|
|
|
t.Log("closing agent")
|
2024-01-22 07:07:50 +00:00
|
|
|
|
_ = uut.Close()
|
|
|
|
|
})
|
2023-07-26 16:21:04 +00:00
|
|
|
|
|
|
|
|
|
// Setup a client connection.
|
2024-03-26 17:44:31 +00:00
|
|
|
|
newClientConn := func(derpMap *tailcfg.DERPMap, name string) *workspacesdk.AgentConn {
|
2023-07-26 16:21:04 +00:00
|
|
|
|
conn, err := tailnet.NewConn(&tailnet.Options{
|
|
|
|
|
Addresses: []netip.Prefix{netip.PrefixFrom(tailnet.IP(), 128)},
|
|
|
|
|
DERPMap: derpMap,
|
2024-01-22 07:07:50 +00:00
|
|
|
|
Logger: logger.Named(name),
|
2023-07-26 16:21:04 +00:00
|
|
|
|
})
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
t.Cleanup(func() {
|
2024-01-22 07:07:50 +00:00
|
|
|
|
t.Logf("closing conn %s", name)
|
2023-07-26 16:21:04 +00:00
|
|
|
|
_ = conn.Close()
|
|
|
|
|
})
|
2024-01-22 07:07:50 +00:00
|
|
|
|
testCtx, testCtxCancel := context.WithCancel(context.Background())
|
|
|
|
|
t.Cleanup(testCtxCancel)
|
|
|
|
|
clientID := uuid.New()
|
|
|
|
|
coordination := tailnet.NewInMemoryCoordination(
|
|
|
|
|
testCtx, logger,
|
|
|
|
|
clientID, agentID,
|
|
|
|
|
coordinator, conn)
|
|
|
|
|
t.Cleanup(func() {
|
|
|
|
|
t.Logf("closing coordination %s", name)
|
|
|
|
|
err := coordination.Close()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Logf("error closing in-memory coordination: %s", err.Error())
|
|
|
|
|
}
|
2024-01-23 10:42:07 +00:00
|
|
|
|
t.Logf("closed coordination %s", name)
|
2023-07-26 16:21:04 +00:00
|
|
|
|
})
|
|
|
|
|
// Force DERP.
|
|
|
|
|
conn.SetBlockEndpoints(true)
|
|
|
|
|
|
2024-03-26 17:44:31 +00:00
|
|
|
|
sdkConn := workspacesdk.NewAgentConn(conn, workspacesdk.AgentConnOptions{
|
2023-07-26 16:21:04 +00:00
|
|
|
|
AgentID: agentID,
|
2024-03-26 17:44:31 +00:00
|
|
|
|
CloseFunc: func() error { return workspacesdk.ErrSkipClose },
|
2023-07-26 16:21:04 +00:00
|
|
|
|
})
|
|
|
|
|
t.Cleanup(func() {
|
2024-01-22 07:07:50 +00:00
|
|
|
|
t.Logf("closing sdkConn %s", name)
|
2023-07-26 16:21:04 +00:00
|
|
|
|
_ = sdkConn.Close()
|
|
|
|
|
})
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
|
|
|
defer cancel()
|
|
|
|
|
if !sdkConn.AwaitReachable(ctx) {
|
|
|
|
|
t.Fatal("agent not reachable")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return sdkConn
|
|
|
|
|
}
|
2024-01-22 07:07:50 +00:00
|
|
|
|
conn1 := newClientConn(originalDerpMap, "client1")
|
2023-07-26 16:21:04 +00:00
|
|
|
|
|
|
|
|
|
// Change the DERP map.
|
|
|
|
|
newDerpMap, _ := tailnettest.RunDERPAndSTUN(t)
|
|
|
|
|
require.NotNil(t, newDerpMap)
|
|
|
|
|
|
|
|
|
|
// Change the region ID.
|
|
|
|
|
newDerpMap.Regions[2] = newDerpMap.Regions[1]
|
|
|
|
|
delete(newDerpMap.Regions, 1)
|
|
|
|
|
newDerpMap.Regions[2].RegionID = 2
|
|
|
|
|
for _, node := range newDerpMap.Regions[2].Nodes {
|
|
|
|
|
node.RegionID = 2
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Push a new DERP map to the agent.
|
2024-01-23 10:42:07 +00:00
|
|
|
|
err := client.PushDERPMapUpdate(newDerpMap)
|
2023-07-26 16:21:04 +00:00
|
|
|
|
require.NoError(t, err)
|
2024-01-23 10:42:07 +00:00
|
|
|
|
t.Logf("pushed DERPMap update to agent")
|
2023-07-26 16:21:04 +00:00
|
|
|
|
|
2023-08-01 15:50:43 +00:00
|
|
|
|
require.Eventually(t, func() bool {
|
2024-01-22 07:07:50 +00:00
|
|
|
|
conn := uut.TailnetConn()
|
2023-08-01 15:50:43 +00:00
|
|
|
|
if conn == nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
regionIDs := conn.DERPMap().RegionIDs()
|
2024-01-22 07:07:50 +00:00
|
|
|
|
preferredDERP := conn.Node().PreferredDERP
|
|
|
|
|
t.Logf("agent Conn DERPMap with regionIDs %v, PreferredDERP %d", regionIDs, preferredDERP)
|
|
|
|
|
return len(regionIDs) == 1 && regionIDs[0] == 2 && preferredDERP == 2
|
2023-08-01 15:50:43 +00:00
|
|
|
|
}, testutil.WaitLong, testutil.IntervalFast)
|
2024-01-22 07:07:50 +00:00
|
|
|
|
t.Logf("agent got the new DERPMap")
|
2023-08-01 15:50:43 +00:00
|
|
|
|
|
2023-07-26 16:21:04 +00:00
|
|
|
|
// Connect from a second client and make sure it uses the new DERP map.
|
2024-01-22 07:07:50 +00:00
|
|
|
|
conn2 := newClientConn(newDerpMap, "client2")
|
2023-07-26 16:21:04 +00:00
|
|
|
|
require.Equal(t, []int{2}, conn2.DERPMap().RegionIDs())
|
2024-01-22 07:07:50 +00:00
|
|
|
|
t.Log("conn2 got the new DERPMap")
|
2023-07-26 16:21:04 +00:00
|
|
|
|
|
|
|
|
|
// If the first client gets a DERP map update, it should be able to
|
|
|
|
|
// reconnect just fine.
|
|
|
|
|
conn1.SetDERPMap(newDerpMap)
|
|
|
|
|
require.Equal(t, []int{2}, conn1.DERPMap().RegionIDs())
|
2024-01-22 07:07:50 +00:00
|
|
|
|
t.Log("set the new DERPMap on conn1")
|
2023-07-26 16:21:04 +00:00
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
|
|
|
defer cancel()
|
|
|
|
|
require.True(t, conn1.AwaitReachable(ctx))
|
2024-01-22 07:07:50 +00:00
|
|
|
|
t.Log("conn1 reached agent with new DERP")
|
2023-07-26 16:21:04 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-12-12 20:20:46 +00:00
|
|
|
|
func TestAgent_Speedtest(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
t.Skip("This test is relatively flakey because of Tailscale's speedtest code...")
|
2023-10-09 16:48:28 +00:00
|
|
|
|
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
|
2022-12-12 20:20:46 +00:00
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
|
|
|
defer cancel()
|
2023-07-12 22:37:31 +00:00
|
|
|
|
derpMap, _ := tailnettest.RunDERPAndSTUN(t)
|
2023-01-24 12:24:27 +00:00
|
|
|
|
//nolint:dogsled
|
2023-07-12 22:37:31 +00:00
|
|
|
|
conn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{
|
|
|
|
|
DERPMap: derpMap,
|
2023-10-09 16:48:28 +00:00
|
|
|
|
}, 0, func(client *agenttest.Client, options *agent.Options) {
|
|
|
|
|
options.Logger = logger.Named("agent")
|
|
|
|
|
})
|
2022-12-12 20:20:46 +00:00
|
|
|
|
defer conn.Close()
|
|
|
|
|
res, err := conn.Speedtest(ctx, speedtest.Upload, 250*time.Millisecond)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
t.Logf("%.2f MBits/s", res[len(res)-1].MBitsPerSecond())
|
|
|
|
|
}
|
2022-12-09 10:22:27 +00:00
|
|
|
|
|
2022-12-12 20:20:46 +00:00
|
|
|
|
func TestAgent_Reconnect(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
2023-04-27 09:59:01 +00:00
|
|
|
|
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
|
2022-12-12 20:20:46 +00:00
|
|
|
|
// After the agent is disconnected from a coordinator, it's supposed
|
|
|
|
|
// to reconnect!
|
2023-04-27 09:59:01 +00:00
|
|
|
|
coordinator := tailnet.NewCoordinator(logger)
|
2022-12-12 20:20:46 +00:00
|
|
|
|
defer coordinator.Close()
|
|
|
|
|
|
|
|
|
|
agentID := uuid.New()
|
2024-02-07 11:26:41 +00:00
|
|
|
|
statsCh := make(chan *proto.Stats, 50)
|
2023-07-12 22:37:31 +00:00
|
|
|
|
derpMap, _ := tailnettest.RunDERPAndSTUN(t)
|
|
|
|
|
client := agenttest.NewClient(t,
|
2023-07-20 18:49:44 +00:00
|
|
|
|
logger,
|
2023-07-12 22:37:31 +00:00
|
|
|
|
agentID,
|
|
|
|
|
agentsdk.Manifest{
|
2022-12-12 20:20:46 +00:00
|
|
|
|
DERPMap: derpMap,
|
|
|
|
|
},
|
2023-07-12 22:37:31 +00:00
|
|
|
|
statsCh,
|
|
|
|
|
coordinator,
|
|
|
|
|
)
|
2024-01-23 10:42:07 +00:00
|
|
|
|
defer client.Close()
|
2022-12-12 20:20:46 +00:00
|
|
|
|
initialized := atomic.Int32{}
|
|
|
|
|
closer := agent.New(agent.Options{
|
|
|
|
|
ExchangeToken: func(ctx context.Context) (string, error) {
|
|
|
|
|
initialized.Add(1)
|
|
|
|
|
return "", nil
|
|
|
|
|
},
|
|
|
|
|
Client: client,
|
2023-04-27 09:59:01 +00:00
|
|
|
|
Logger: logger.Named("agent"),
|
2022-12-12 20:20:46 +00:00
|
|
|
|
})
|
|
|
|
|
defer closer.Close()
|
|
|
|
|
|
|
|
|
|
require.Eventually(t, func() bool {
|
|
|
|
|
return coordinator.Node(agentID) != nil
|
|
|
|
|
}, testutil.WaitShort, testutil.IntervalFast)
|
2023-07-12 22:37:31 +00:00
|
|
|
|
client.LastWorkspaceAgent()
|
2022-12-12 20:20:46 +00:00
|
|
|
|
require.Eventually(t, func() bool {
|
|
|
|
|
return initialized.Load() == 2
|
|
|
|
|
}, testutil.WaitShort, testutil.IntervalFast)
|
|
|
|
|
}
|
2022-12-09 10:22:27 +00:00
|
|
|
|
|
2022-12-12 20:20:46 +00:00
|
|
|
|
func TestAgent_WriteVSCodeConfigs(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
2023-04-27 09:59:01 +00:00
|
|
|
|
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
|
|
|
|
|
coordinator := tailnet.NewCoordinator(logger)
|
2022-12-12 20:20:46 +00:00
|
|
|
|
defer coordinator.Close()
|
|
|
|
|
|
2023-07-12 22:37:31 +00:00
|
|
|
|
client := agenttest.NewClient(t,
|
2023-07-20 18:49:44 +00:00
|
|
|
|
logger,
|
2023-07-12 22:37:31 +00:00
|
|
|
|
uuid.New(),
|
|
|
|
|
agentsdk.Manifest{
|
2022-12-12 20:20:46 +00:00
|
|
|
|
GitAuthConfigs: 1,
|
|
|
|
|
DERPMap: &tailcfg.DERPMap{},
|
|
|
|
|
},
|
2024-02-07 11:26:41 +00:00
|
|
|
|
make(chan *proto.Stats, 50),
|
2023-07-12 22:37:31 +00:00
|
|
|
|
coordinator,
|
|
|
|
|
)
|
2024-01-23 10:42:07 +00:00
|
|
|
|
defer client.Close()
|
2022-12-12 20:20:46 +00:00
|
|
|
|
filesystem := afero.NewMemMapFs()
|
|
|
|
|
closer := agent.New(agent.Options{
|
|
|
|
|
ExchangeToken: func(ctx context.Context) (string, error) {
|
|
|
|
|
return "", nil
|
|
|
|
|
},
|
|
|
|
|
Client: client,
|
2023-04-27 09:59:01 +00:00
|
|
|
|
Logger: logger.Named("agent"),
|
2022-12-12 20:20:46 +00:00
|
|
|
|
Filesystem: filesystem,
|
2022-10-25 00:46:24 +00:00
|
|
|
|
})
|
2022-12-12 20:20:46 +00:00
|
|
|
|
defer closer.Close()
|
|
|
|
|
|
|
|
|
|
home, err := os.UserHomeDir()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
name := filepath.Join(home, ".vscode-server", "data", "Machine", "settings.json")
|
|
|
|
|
require.Eventually(t, func() bool {
|
|
|
|
|
_, err := filesystem.Stat(name)
|
|
|
|
|
return err == nil
|
|
|
|
|
}, testutil.WaitShort, testutil.IntervalFast)
|
2022-02-19 05:13:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-08-08 17:56:08 +00:00
|
|
|
|
func TestAgent_DebugServer(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
2024-03-14 15:36:12 +00:00
|
|
|
|
logDir := t.TempDir()
|
|
|
|
|
logPath := filepath.Join(logDir, "coder-agent.log")
|
|
|
|
|
randLogStr, err := cryptorand.String(32)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.NoError(t, os.WriteFile(logPath, []byte(randLogStr), 0o600))
|
2023-08-08 17:56:08 +00:00
|
|
|
|
derpMap, _ := tailnettest.RunDERPAndSTUN(t)
|
|
|
|
|
//nolint:dogsled
|
|
|
|
|
conn, _, _, _, agnt := setupAgent(t, agentsdk.Manifest{
|
|
|
|
|
DERPMap: derpMap,
|
2024-03-14 15:36:12 +00:00
|
|
|
|
}, 0, func(c *agenttest.Client, o *agent.Options) {
|
|
|
|
|
o.ExchangeToken = func(context.Context) (string, error) {
|
|
|
|
|
return "token", nil
|
|
|
|
|
}
|
|
|
|
|
o.LogDir = logDir
|
|
|
|
|
})
|
2023-08-08 17:56:08 +00:00
|
|
|
|
|
|
|
|
|
awaitReachableCtx := testutil.Context(t, testutil.WaitLong)
|
|
|
|
|
ok := conn.AwaitReachable(awaitReachableCtx)
|
|
|
|
|
require.True(t, ok)
|
|
|
|
|
_ = conn.Close()
|
|
|
|
|
|
|
|
|
|
srv := httptest.NewServer(agnt.HTTPDebug())
|
|
|
|
|
t.Cleanup(srv.Close)
|
|
|
|
|
|
|
|
|
|
t.Run("MagicsockDebug", func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+"/debug/magicsock", nil)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
res, err := srv.Client().Do(req)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
require.Equal(t, http.StatusOK, res.StatusCode)
|
|
|
|
|
|
|
|
|
|
resBody, err := io.ReadAll(res.Body)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.Contains(t, string(resBody), "<h1>magicsock</h1>")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("MagicsockDebugLogging", func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
t.Run("Enable", func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+"/debug/magicsock/debug-logging/t", nil)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
res, err := srv.Client().Do(req)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
require.Equal(t, http.StatusOK, res.StatusCode)
|
|
|
|
|
|
|
|
|
|
resBody, err := io.ReadAll(res.Body)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.Contains(t, string(resBody), "updated magicsock debug logging to true")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("Disable", func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+"/debug/magicsock/debug-logging/0", nil)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
res, err := srv.Client().Do(req)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
require.Equal(t, http.StatusOK, res.StatusCode)
|
|
|
|
|
|
|
|
|
|
resBody, err := io.ReadAll(res.Body)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.Contains(t, string(resBody), "updated magicsock debug logging to false")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("Invalid", func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+"/debug/magicsock/debug-logging/blah", nil)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
res, err := srv.Client().Do(req)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
require.Equal(t, http.StatusBadRequest, res.StatusCode)
|
|
|
|
|
|
|
|
|
|
resBody, err := io.ReadAll(res.Body)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.Contains(t, string(resBody), `invalid state "blah", must be a boolean`)
|
|
|
|
|
})
|
|
|
|
|
})
|
2024-03-14 15:36:12 +00:00
|
|
|
|
|
|
|
|
|
t.Run("Manifest", func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+"/debug/manifest", nil)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
res, err := srv.Client().Do(req)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
require.Equal(t, http.StatusOK, res.StatusCode)
|
|
|
|
|
|
|
|
|
|
var v agentsdk.Manifest
|
|
|
|
|
require.NoError(t, json.NewDecoder(res.Body).Decode(&v))
|
|
|
|
|
require.NotNil(t, v)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("Logs", func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+"/debug/logs", nil)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
res, err := srv.Client().Do(req)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.Equal(t, http.StatusOK, res.StatusCode)
|
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
resBody, err := io.ReadAll(res.Body)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.NotEmpty(t, string(resBody))
|
|
|
|
|
require.Contains(t, string(resBody), randLogStr)
|
|
|
|
|
})
|
2023-08-08 17:56:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
2024-02-23 07:27:15 +00:00
|
|
|
|
func TestAgent_ScriptLogging(t *testing.T) {
|
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
|
t.Skip("bash scripts only")
|
|
|
|
|
}
|
|
|
|
|
t.Parallel()
|
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
|
|
|
|
|
|
|
|
derpMap, _ := tailnettest.RunDERPAndSTUN(t)
|
|
|
|
|
logsCh := make(chan *proto.BatchCreateLogsRequest, 100)
|
|
|
|
|
lsStart := uuid.UUID{0x11}
|
|
|
|
|
lsStop := uuid.UUID{0x22}
|
|
|
|
|
//nolint:dogsled
|
|
|
|
|
_, _, _, _, agnt := setupAgent(
|
|
|
|
|
t,
|
|
|
|
|
agentsdk.Manifest{
|
|
|
|
|
DERPMap: derpMap,
|
|
|
|
|
Scripts: []codersdk.WorkspaceAgentScript{
|
|
|
|
|
{
|
|
|
|
|
LogSourceID: lsStart,
|
|
|
|
|
RunOnStart: true,
|
|
|
|
|
Script: `#!/bin/sh
|
|
|
|
|
i=0
|
|
|
|
|
while [ $i -ne 5 ]
|
|
|
|
|
do
|
|
|
|
|
i=$(($i+1))
|
|
|
|
|
echo "start $i"
|
|
|
|
|
done
|
|
|
|
|
`,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
LogSourceID: lsStop,
|
|
|
|
|
RunOnStop: true,
|
|
|
|
|
Script: `#!/bin/sh
|
|
|
|
|
i=0
|
|
|
|
|
while [ $i -ne 3000 ]
|
|
|
|
|
do
|
|
|
|
|
i=$(($i+1))
|
|
|
|
|
echo "stop $i"
|
|
|
|
|
done
|
|
|
|
|
`, // send a lot of stop logs to make sure we don't truncate shutdown logs before closing the API conn
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
0,
|
|
|
|
|
func(cl *agenttest.Client, _ *agent.Options) {
|
|
|
|
|
cl.SetLogsChannel(logsCh)
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
n := 1
|
|
|
|
|
for n <= 5 {
|
|
|
|
|
logs := testutil.RequireRecvCtx(ctx, t, logsCh)
|
|
|
|
|
require.NotNil(t, logs)
|
|
|
|
|
for _, l := range logs.GetLogs() {
|
|
|
|
|
require.Equal(t, fmt.Sprintf("start %d", n), l.GetOutput())
|
|
|
|
|
n++
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err := agnt.Close()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
n = 1
|
|
|
|
|
for n <= 3000 {
|
|
|
|
|
logs := testutil.RequireRecvCtx(ctx, t, logsCh)
|
|
|
|
|
require.NotNil(t, logs)
|
|
|
|
|
for _, l := range logs.GetLogs() {
|
|
|
|
|
require.Equal(t, fmt.Sprintf("stop %d", n), l.GetOutput())
|
|
|
|
|
n++
|
|
|
|
|
}
|
|
|
|
|
t.Logf("got %d stop logs", n-1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-27 05:42:45 +00:00
|
|
|
|
// setupAgentSSHClient creates an agent, dials it, and sets up an ssh.Client for it
|
|
|
|
|
func setupAgentSSHClient(ctx context.Context, t *testing.T) *ssh.Client {
|
|
|
|
|
//nolint: dogsled
|
2023-07-12 22:37:31 +00:00
|
|
|
|
agentConn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0)
|
2023-11-27 05:42:45 +00:00
|
|
|
|
sshClient, err := agentConn.SSHClient(ctx)
|
2022-04-11 23:54:30 +00:00
|
|
|
|
require.NoError(t, err)
|
2023-11-27 05:42:45 +00:00
|
|
|
|
t.Cleanup(func() { sshClient.Close() })
|
|
|
|
|
return sshClient
|
2022-04-11 23:54:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-06-30 18:41:29 +00:00
|
|
|
|
func setupSSHSession(
|
|
|
|
|
t *testing.T,
|
2023-07-14 13:10:26 +00:00
|
|
|
|
manifest agentsdk.Manifest,
|
2024-05-08 21:40:43 +00:00
|
|
|
|
banner codersdk.BannerConfig,
|
2023-07-06 07:57:51 +00:00
|
|
|
|
prepareFS func(fs afero.Fs),
|
2024-02-19 14:30:00 +00:00
|
|
|
|
opts ...func(*agenttest.Client, *agent.Options),
|
2023-06-30 18:41:29 +00:00
|
|
|
|
) *ssh.Session {
|
2022-11-08 22:10:48 +00:00
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
|
|
|
defer cancel()
|
2024-02-19 14:30:00 +00:00
|
|
|
|
opts = append(opts, func(c *agenttest.Client, o *agent.Options) {
|
2024-05-08 21:40:43 +00:00
|
|
|
|
c.SetNotificationBannersFunc(func() ([]codersdk.BannerConfig, error) {
|
|
|
|
|
return []codersdk.BannerConfig{banner}, nil
|
2023-07-12 22:37:31 +00:00
|
|
|
|
})
|
|
|
|
|
})
|
2024-02-19 14:30:00 +00:00
|
|
|
|
//nolint:dogsled
|
|
|
|
|
conn, _, _, fs, _ := setupAgent(t, manifest, 0, opts...)
|
2023-07-06 07:57:51 +00:00
|
|
|
|
if prepareFS != nil {
|
|
|
|
|
prepareFS(fs)
|
|
|
|
|
}
|
2022-11-08 22:10:48 +00:00
|
|
|
|
sshClient, err := conn.SSHClient(ctx)
|
2022-04-11 23:54:30 +00:00
|
|
|
|
require.NoError(t, err)
|
2022-09-01 01:09:44 +00:00
|
|
|
|
t.Cleanup(func() {
|
|
|
|
|
_ = sshClient.Close()
|
|
|
|
|
})
|
2022-04-11 23:54:30 +00:00
|
|
|
|
session, err := sshClient.NewSession()
|
|
|
|
|
require.NoError(t, err)
|
2022-12-09 10:22:27 +00:00
|
|
|
|
t.Cleanup(func() {
|
|
|
|
|
_ = session.Close()
|
|
|
|
|
})
|
2022-04-11 23:54:30 +00:00
|
|
|
|
return session
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-12 22:37:31 +00:00
|
|
|
|
func setupAgent(t *testing.T, metadata agentsdk.Manifest, ptyTimeout time.Duration, opts ...func(*agenttest.Client, *agent.Options)) (
|
2024-03-26 17:44:31 +00:00
|
|
|
|
*workspacesdk.AgentConn,
|
2023-07-12 22:37:31 +00:00
|
|
|
|
*agenttest.Client,
|
2024-02-07 11:26:41 +00:00
|
|
|
|
<-chan *proto.Stats,
|
2022-11-13 20:23:23 +00:00
|
|
|
|
afero.Fs,
|
2023-08-08 17:56:08 +00:00
|
|
|
|
agent.Agent,
|
2022-09-01 19:58:23 +00:00
|
|
|
|
) {
|
2024-01-30 03:44:47 +00:00
|
|
|
|
logger := slogtest.Make(t, &slogtest.Options{
|
2024-01-30 06:04:01 +00:00
|
|
|
|
// Agent can drop errors when shutting down, and some, like the
|
|
|
|
|
// fasthttplistener connection closed error, are unexported.
|
|
|
|
|
IgnoreErrors: true,
|
2024-01-30 03:44:47 +00:00
|
|
|
|
}).Leveled(slog.LevelDebug)
|
2023-07-12 22:37:31 +00:00
|
|
|
|
if metadata.DERPMap == nil {
|
|
|
|
|
metadata.DERPMap, _ = tailnettest.RunDERPAndSTUN(t)
|
2022-09-20 00:46:29 +00:00
|
|
|
|
}
|
2023-07-12 22:37:31 +00:00
|
|
|
|
if metadata.AgentID == uuid.Nil {
|
|
|
|
|
metadata.AgentID = uuid.New()
|
|
|
|
|
}
|
2024-01-02 18:46:18 +00:00
|
|
|
|
if metadata.AgentName == "" {
|
|
|
|
|
metadata.AgentName = "test-agent"
|
|
|
|
|
}
|
|
|
|
|
if metadata.WorkspaceName == "" {
|
|
|
|
|
metadata.WorkspaceName = "test-workspace"
|
|
|
|
|
}
|
2024-01-30 06:11:28 +00:00
|
|
|
|
if metadata.WorkspaceID == uuid.Nil {
|
|
|
|
|
metadata.WorkspaceID = uuid.New()
|
|
|
|
|
}
|
2023-07-12 22:37:31 +00:00
|
|
|
|
coordinator := tailnet.NewCoordinator(logger)
|
2022-12-09 10:22:27 +00:00
|
|
|
|
t.Cleanup(func() {
|
2023-07-12 22:37:31 +00:00
|
|
|
|
_ = coordinator.Close()
|
2022-12-09 10:22:27 +00:00
|
|
|
|
})
|
2024-02-07 11:26:41 +00:00
|
|
|
|
statsCh := make(chan *proto.Stats, 50)
|
2022-11-13 20:23:23 +00:00
|
|
|
|
fs := afero.NewMemMapFs()
|
2024-02-23 07:27:15 +00:00
|
|
|
|
c := agenttest.NewClient(t, logger.Named("agenttest"), metadata.AgentID, metadata, statsCh, coordinator)
|
2024-01-23 10:42:07 +00:00
|
|
|
|
t.Cleanup(c.Close)
|
2023-07-12 22:37:31 +00:00
|
|
|
|
|
2023-05-25 10:52:36 +00:00
|
|
|
|
options := agent.Options{
|
2023-01-24 12:24:27 +00:00
|
|
|
|
Client: c,
|
2022-11-13 20:23:23 +00:00
|
|
|
|
Filesystem: fs,
|
2023-04-27 09:59:01 +00:00
|
|
|
|
Logger: logger.Named("agent"),
|
2022-04-29 22:30:10 +00:00
|
|
|
|
ReconnectingPTYTimeout: ptyTimeout,
|
2024-02-19 14:30:00 +00:00
|
|
|
|
EnvironmentVariables: map[string]string{},
|
2023-05-25 10:52:36 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, opt := range opts {
|
2023-07-12 22:37:31 +00:00
|
|
|
|
opt(c, &options)
|
2023-05-25 10:52:36 +00:00
|
|
|
|
}
|
|
|
|
|
|
2024-02-23 07:27:15 +00:00
|
|
|
|
agnt := agent.New(options)
|
2022-02-19 05:13:32 +00:00
|
|
|
|
t.Cleanup(func() {
|
2024-02-23 07:27:15 +00:00
|
|
|
|
_ = agnt.Close()
|
2022-02-19 05:13:32 +00:00
|
|
|
|
})
|
2022-09-20 00:46:29 +00:00
|
|
|
|
conn, err := tailnet.NewConn(&tailnet.Options{
|
2023-02-10 03:43:18 +00:00
|
|
|
|
Addresses: []netip.Prefix{netip.PrefixFrom(tailnet.IP(), 128)},
|
2023-07-12 22:37:31 +00:00
|
|
|
|
DERPMap: metadata.DERPMap,
|
2023-04-27 09:59:01 +00:00
|
|
|
|
Logger: logger.Named("client"),
|
2022-04-07 22:40:27 +00:00
|
|
|
|
})
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
t.Cleanup(func() {
|
|
|
|
|
_ = conn.Close()
|
|
|
|
|
})
|
2024-01-22 07:07:50 +00:00
|
|
|
|
testCtx, testCtxCancel := context.WithCancel(context.Background())
|
|
|
|
|
t.Cleanup(testCtxCancel)
|
|
|
|
|
clientID := uuid.New()
|
|
|
|
|
coordination := tailnet.NewInMemoryCoordination(
|
|
|
|
|
testCtx, logger,
|
|
|
|
|
clientID, metadata.AgentID,
|
|
|
|
|
coordinator, conn)
|
|
|
|
|
t.Cleanup(func() {
|
|
|
|
|
err := coordination.Close()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Logf("error closing in-mem coordination: %s", err.Error())
|
|
|
|
|
}
|
2022-09-20 00:46:29 +00:00
|
|
|
|
})
|
2024-03-26 17:44:31 +00:00
|
|
|
|
agentConn := workspacesdk.NewAgentConn(conn, workspacesdk.AgentConnOptions{
|
2023-07-12 22:37:31 +00:00
|
|
|
|
AgentID: metadata.AgentID,
|
|
|
|
|
})
|
2023-02-24 16:16:29 +00:00
|
|
|
|
t.Cleanup(func() {
|
|
|
|
|
_ = agentConn.Close()
|
|
|
|
|
})
|
2023-04-21 19:40:17 +00:00
|
|
|
|
// Ideally we wouldn't wait too long here, but sometimes the the
|
|
|
|
|
// networking needs more time to resolve itself.
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
2023-02-24 16:16:29 +00:00
|
|
|
|
defer cancel()
|
|
|
|
|
if !agentConn.AwaitReachable(ctx) {
|
|
|
|
|
t.Fatal("agent not reachable")
|
|
|
|
|
}
|
2024-02-23 07:27:15 +00:00
|
|
|
|
return agentConn, c, statsCh, fs, agnt
|
2022-02-19 05:13:32 +00:00
|
|
|
|
}
|
2022-05-18 14:10:40 +00:00
|
|
|
|
|
|
|
|
|
var dialTestPayload = []byte("dean-was-here123")
|
|
|
|
|
|
2023-12-04 11:11:30 +00:00
|
|
|
|
func testDial(ctx context.Context, t *testing.T, c net.Conn) {
|
2022-05-18 14:10:40 +00:00
|
|
|
|
t.Helper()
|
|
|
|
|
|
2023-12-04 11:11:30 +00:00
|
|
|
|
if deadline, ok := ctx.Deadline(); ok {
|
|
|
|
|
err := c.SetDeadline(deadline)
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
defer func() {
|
|
|
|
|
err := c.SetDeadline(time.Time{})
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-18 14:10:40 +00:00
|
|
|
|
assertWritePayload(t, c, dialTestPayload)
|
|
|
|
|
assertReadPayload(t, c, dialTestPayload)
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-04 11:11:30 +00:00
|
|
|
|
func testAccept(ctx context.Context, t *testing.T, c net.Conn) {
|
2022-05-18 14:10:40 +00:00
|
|
|
|
t.Helper()
|
|
|
|
|
defer c.Close()
|
|
|
|
|
|
2023-12-04 11:11:30 +00:00
|
|
|
|
if deadline, ok := ctx.Deadline(); ok {
|
|
|
|
|
err := c.SetDeadline(deadline)
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
defer func() {
|
|
|
|
|
err := c.SetDeadline(time.Time{})
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-18 14:10:40 +00:00
|
|
|
|
assertReadPayload(t, c, dialTestPayload)
|
|
|
|
|
assertWritePayload(t, c, dialTestPayload)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func assertReadPayload(t *testing.T, r io.Reader, payload []byte) {
|
2023-12-04 11:11:30 +00:00
|
|
|
|
t.Helper()
|
2022-05-18 14:10:40 +00:00
|
|
|
|
b := make([]byte, len(payload)+16)
|
|
|
|
|
n, err := r.Read(b)
|
2022-05-24 07:58:39 +00:00
|
|
|
|
assert.NoError(t, err, "read payload")
|
|
|
|
|
assert.Equal(t, len(payload), n, "read payload length does not match")
|
|
|
|
|
assert.Equal(t, payload, b[:n])
|
2022-05-18 14:10:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func assertWritePayload(t *testing.T, w io.Writer, payload []byte) {
|
2023-12-04 11:11:30 +00:00
|
|
|
|
t.Helper()
|
2022-05-18 14:10:40 +00:00
|
|
|
|
n, err := w.Write(payload)
|
2022-05-24 07:58:39 +00:00
|
|
|
|
assert.NoError(t, err, "write payload")
|
|
|
|
|
assert.Equal(t, len(payload), n, "payload length does not match")
|
2022-05-18 14:10:40 +00:00
|
|
|
|
}
|
2022-10-24 03:35:08 +00:00
|
|
|
|
|
2023-06-30 18:41:29 +00:00
|
|
|
|
func testSessionOutput(t *testing.T, session *ssh.Session, expected, unexpected []string, expectedRe *regexp.Regexp) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
|
|
err := session.RequestPty("xterm", 128, 128, ssh.TerminalModes{})
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
ptty := ptytest.New(t)
|
|
|
|
|
var stdout bytes.Buffer
|
|
|
|
|
session.Stdout = &stdout
|
|
|
|
|
session.Stderr = ptty.Output()
|
|
|
|
|
session.Stdin = ptty.Input()
|
|
|
|
|
err = session.Shell()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
ptty.WriteLine("exit 0")
|
|
|
|
|
err = session.Wait()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
for _, unexpected := range unexpected {
|
|
|
|
|
require.NotContains(t, stdout.String(), unexpected, "should not show output")
|
|
|
|
|
}
|
|
|
|
|
for _, expect := range expected {
|
|
|
|
|
require.Contains(t, stdout.String(), expect, "should show output")
|
|
|
|
|
}
|
|
|
|
|
if expectedRe != nil {
|
|
|
|
|
require.Regexp(t, expectedRe, stdout.String())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-06 07:52:19 +00:00
|
|
|
|
// 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()
|
|
|
|
|
}
|
2023-05-25 10:52:36 +00:00
|
|
|
|
|
|
|
|
|
func TestAgent_Metrics_SSH(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
registry := prometheus.NewRegistry()
|
|
|
|
|
|
|
|
|
|
//nolint:dogsled
|
2023-07-12 22:37:31 +00:00
|
|
|
|
conn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0, func(_ *agenttest.Client, o *agent.Options) {
|
2023-05-25 10:52:36 +00:00
|
|
|
|
o.PrometheusRegistry = registry
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
sshClient, err := conn.SSHClient(ctx)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer sshClient.Close()
|
|
|
|
|
session, err := sshClient.NewSession()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer session.Close()
|
|
|
|
|
stdin, err := session.StdinPipe()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
err = session.Shell()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
expected := []agentsdk.AgentMetric{
|
|
|
|
|
{
|
|
|
|
|
Name: "agent_reconnecting_pty_connections_total",
|
|
|
|
|
Type: agentsdk.AgentMetricTypeCounter,
|
|
|
|
|
Value: 0,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Name: "agent_sessions_total",
|
|
|
|
|
Type: agentsdk.AgentMetricTypeCounter,
|
|
|
|
|
Value: 1,
|
|
|
|
|
Labels: []agentsdk.AgentMetricLabel{
|
|
|
|
|
{
|
|
|
|
|
Name: "magic_type",
|
|
|
|
|
Value: "ssh",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Name: "pty",
|
|
|
|
|
Value: "no",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Name: "agent_ssh_server_failed_connections_total",
|
|
|
|
|
Type: agentsdk.AgentMetricTypeCounter,
|
|
|
|
|
Value: 0,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Name: "agent_ssh_server_sftp_connections_total",
|
|
|
|
|
Type: agentsdk.AgentMetricTypeCounter,
|
|
|
|
|
Value: 0,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Name: "agent_ssh_server_sftp_server_errors_total",
|
|
|
|
|
Type: agentsdk.AgentMetricTypeCounter,
|
|
|
|
|
Value: 0,
|
|
|
|
|
},
|
2023-12-13 17:45:43 +00:00
|
|
|
|
{
|
|
|
|
|
Name: "coderd_agentstats_startup_script_seconds",
|
|
|
|
|
Type: agentsdk.AgentMetricTypeGauge,
|
|
|
|
|
Value: 0,
|
|
|
|
|
Labels: []agentsdk.AgentMetricLabel{
|
|
|
|
|
{
|
|
|
|
|
Name: "success",
|
|
|
|
|
Value: "true",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
2023-05-25 10:52:36 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var actual []*promgo.MetricFamily
|
|
|
|
|
assert.Eventually(t, func() bool {
|
|
|
|
|
actual, err = registry.Gather()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(expected) != len(actual) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return verifyCollectedMetrics(t, expected, actual)
|
|
|
|
|
}, testutil.WaitLong, testutil.IntervalFast)
|
|
|
|
|
|
|
|
|
|
require.Len(t, actual, len(expected))
|
|
|
|
|
collected := verifyCollectedMetrics(t, expected, actual)
|
|
|
|
|
require.True(t, collected, "expected metrics were not collected")
|
|
|
|
|
|
|
|
|
|
_ = stdin.Close()
|
|
|
|
|
err = session.Wait()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-15 00:45:05 +00:00
|
|
|
|
func TestAgent_ManageProcessPriority(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
t.Run("OK", func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
if runtime.GOOS != "linux" {
|
|
|
|
|
t.Skip("Skipping non-linux environment")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
expectedProcs = map[int32]agentproc.Process{}
|
|
|
|
|
fs = afero.NewMemMapFs()
|
|
|
|
|
syscaller = agentproctest.NewMockSyscaller(gomock.NewController(t))
|
|
|
|
|
ticker = make(chan time.Time)
|
|
|
|
|
modProcs = make(chan []*agentproc.Process)
|
|
|
|
|
logger = slog.Make(sloghuman.Sink(io.Discard))
|
|
|
|
|
)
|
|
|
|
|
|
2024-04-03 14:42:03 +00:00
|
|
|
|
requireFileWrite(t, fs, "/proc/self/oom_score_adj", "-500")
|
|
|
|
|
|
2023-09-15 00:45:05 +00:00
|
|
|
|
// Create some processes.
|
|
|
|
|
for i := 0; i < 4; i++ {
|
2024-04-03 14:42:03 +00:00
|
|
|
|
// Create a prioritized process.
|
2023-09-15 00:45:05 +00:00
|
|
|
|
var proc agentproc.Process
|
|
|
|
|
if i == 0 {
|
|
|
|
|
proc = agentproctest.GenerateProcess(t, fs,
|
|
|
|
|
func(p *agentproc.Process) {
|
|
|
|
|
p.CmdLine = "./coder\x00agent\x00--no-reap"
|
|
|
|
|
p.PID = int32(i)
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
proc = agentproctest.GenerateProcess(t, fs,
|
|
|
|
|
func(p *agentproc.Process) {
|
|
|
|
|
// Make the cmd something similar to a prioritized
|
|
|
|
|
// process but differentiate the arguments.
|
|
|
|
|
p.CmdLine = "./coder\x00stat"
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
syscaller.EXPECT().GetPriority(proc.PID).Return(20, nil)
|
2024-04-03 14:42:03 +00:00
|
|
|
|
syscaller.EXPECT().SetPriority(proc.PID, 10).Return(nil)
|
2023-09-15 00:45:05 +00:00
|
|
|
|
}
|
|
|
|
|
syscaller.EXPECT().
|
|
|
|
|
Kill(proc.PID, syscall.Signal(0)).
|
|
|
|
|
Return(nil)
|
|
|
|
|
|
|
|
|
|
expectedProcs[proc.PID] = proc
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_, _, _, _, _ = setupAgent(t, agentsdk.Manifest{}, 0, func(c *agenttest.Client, o *agent.Options) {
|
|
|
|
|
o.Syscaller = syscaller
|
|
|
|
|
o.ModifiedProcesses = modProcs
|
|
|
|
|
o.EnvironmentVariables = map[string]string{agent.EnvProcPrioMgmt: "1"}
|
|
|
|
|
o.Filesystem = fs
|
|
|
|
|
o.Logger = logger
|
|
|
|
|
o.ProcessManagementTick = ticker
|
|
|
|
|
})
|
|
|
|
|
actualProcs := <-modProcs
|
|
|
|
|
require.Len(t, actualProcs, len(expectedProcs)-1)
|
2024-04-03 14:42:03 +00:00
|
|
|
|
for _, proc := range actualProcs {
|
|
|
|
|
requireFileEquals(t, fs, fmt.Sprintf("/proc/%d/oom_score_adj", proc.PID), "0")
|
|
|
|
|
}
|
2023-09-15 00:45:05 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("IgnoreCustomNice", func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
if runtime.GOOS != "linux" {
|
|
|
|
|
t.Skip("Skipping non-linux environment")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
expectedProcs = map[int32]agentproc.Process{}
|
|
|
|
|
fs = afero.NewMemMapFs()
|
|
|
|
|
ticker = make(chan time.Time)
|
|
|
|
|
syscaller = agentproctest.NewMockSyscaller(gomock.NewController(t))
|
|
|
|
|
modProcs = make(chan []*agentproc.Process)
|
|
|
|
|
logger = slog.Make(sloghuman.Sink(io.Discard))
|
|
|
|
|
)
|
|
|
|
|
|
2024-04-03 14:42:03 +00:00
|
|
|
|
err := afero.WriteFile(fs, "/proc/self/oom_score_adj", []byte("0"), 0o644)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
2023-09-15 00:45:05 +00:00
|
|
|
|
// Create some processes.
|
2024-04-03 14:42:03 +00:00
|
|
|
|
for i := 0; i < 3; i++ {
|
2023-09-15 00:45:05 +00:00
|
|
|
|
proc := agentproctest.GenerateProcess(t, fs)
|
|
|
|
|
syscaller.EXPECT().
|
|
|
|
|
Kill(proc.PID, syscall.Signal(0)).
|
|
|
|
|
Return(nil)
|
|
|
|
|
|
|
|
|
|
if i == 0 {
|
|
|
|
|
// Set a random nice score. This one should not be adjusted by
|
|
|
|
|
// our management loop.
|
|
|
|
|
syscaller.EXPECT().GetPriority(proc.PID).Return(25, nil)
|
|
|
|
|
} else {
|
|
|
|
|
syscaller.EXPECT().GetPriority(proc.PID).Return(20, nil)
|
|
|
|
|
syscaller.EXPECT().SetPriority(proc.PID, 10).Return(nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
expectedProcs[proc.PID] = proc
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_, _, _, _, _ = setupAgent(t, agentsdk.Manifest{}, 0, func(c *agenttest.Client, o *agent.Options) {
|
|
|
|
|
o.Syscaller = syscaller
|
|
|
|
|
o.ModifiedProcesses = modProcs
|
|
|
|
|
o.EnvironmentVariables = map[string]string{agent.EnvProcPrioMgmt: "1"}
|
|
|
|
|
o.Filesystem = fs
|
|
|
|
|
o.Logger = logger
|
|
|
|
|
o.ProcessManagementTick = ticker
|
|
|
|
|
})
|
|
|
|
|
actualProcs := <-modProcs
|
|
|
|
|
// We should ignore the process with a custom nice score.
|
2024-04-03 14:42:03 +00:00
|
|
|
|
require.Len(t, actualProcs, 2)
|
|
|
|
|
for _, proc := range actualProcs {
|
|
|
|
|
_, ok := expectedProcs[proc.PID]
|
|
|
|
|
require.True(t, ok)
|
|
|
|
|
requireFileEquals(t, fs, fmt.Sprintf("/proc/%d/oom_score_adj", proc.PID), "998")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("CustomOOMScore", func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
if runtime.GOOS != "linux" {
|
|
|
|
|
t.Skip("Skipping non-linux environment")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
fs = afero.NewMemMapFs()
|
|
|
|
|
ticker = make(chan time.Time)
|
|
|
|
|
syscaller = agentproctest.NewMockSyscaller(gomock.NewController(t))
|
|
|
|
|
modProcs = make(chan []*agentproc.Process)
|
|
|
|
|
logger = slog.Make(sloghuman.Sink(io.Discard))
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
err := afero.WriteFile(fs, "/proc/self/oom_score_adj", []byte("0"), 0o644)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
// Create some processes.
|
|
|
|
|
for i := 0; i < 3; i++ {
|
|
|
|
|
proc := agentproctest.GenerateProcess(t, fs)
|
|
|
|
|
syscaller.EXPECT().
|
|
|
|
|
Kill(proc.PID, syscall.Signal(0)).
|
|
|
|
|
Return(nil)
|
|
|
|
|
syscaller.EXPECT().GetPriority(proc.PID).Return(20, nil)
|
|
|
|
|
syscaller.EXPECT().SetPriority(proc.PID, 10).Return(nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_, _, _, _, _ = setupAgent(t, agentsdk.Manifest{}, 0, func(c *agenttest.Client, o *agent.Options) {
|
|
|
|
|
o.Syscaller = syscaller
|
|
|
|
|
o.ModifiedProcesses = modProcs
|
|
|
|
|
o.EnvironmentVariables = map[string]string{
|
|
|
|
|
agent.EnvProcPrioMgmt: "1",
|
|
|
|
|
agent.EnvProcOOMScore: "-567",
|
|
|
|
|
}
|
|
|
|
|
o.Filesystem = fs
|
|
|
|
|
o.Logger = logger
|
|
|
|
|
o.ProcessManagementTick = ticker
|
|
|
|
|
})
|
|
|
|
|
actualProcs := <-modProcs
|
|
|
|
|
// We should ignore the process with a custom nice score.
|
|
|
|
|
require.Len(t, actualProcs, 3)
|
|
|
|
|
for _, proc := range actualProcs {
|
|
|
|
|
requireFileEquals(t, fs, fmt.Sprintf("/proc/%d/oom_score_adj", proc.PID), "-567")
|
|
|
|
|
}
|
2023-09-15 00:45:05 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("DisabledByDefault", func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
if runtime.GOOS != "linux" {
|
|
|
|
|
t.Skip("Skipping non-linux environment")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
buf bytes.Buffer
|
|
|
|
|
wr = &syncWriter{
|
|
|
|
|
w: &buf,
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
log := slog.Make(sloghuman.Sink(wr)).Leveled(slog.LevelDebug)
|
|
|
|
|
|
|
|
|
|
_, _, _, _, _ = setupAgent(t, agentsdk.Manifest{}, 0, func(c *agenttest.Client, o *agent.Options) {
|
|
|
|
|
o.Logger = log
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
require.Eventually(t, func() bool {
|
|
|
|
|
wr.mu.Lock()
|
|
|
|
|
defer wr.mu.Unlock()
|
|
|
|
|
return strings.Contains(buf.String(), "process priority not enabled")
|
|
|
|
|
}, testutil.WaitLong, testutil.IntervalFast)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("DisabledForNonLinux", func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
if runtime.GOOS == "linux" {
|
|
|
|
|
t.Skip("Skipping linux environment")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
buf bytes.Buffer
|
|
|
|
|
wr = &syncWriter{
|
|
|
|
|
w: &buf,
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
log := slog.Make(sloghuman.Sink(wr)).Leveled(slog.LevelDebug)
|
|
|
|
|
|
|
|
|
|
_, _, _, _, _ = setupAgent(t, agentsdk.Manifest{}, 0, func(c *agenttest.Client, o *agent.Options) {
|
|
|
|
|
o.Logger = log
|
|
|
|
|
// Try to enable it so that we can assert that non-linux
|
|
|
|
|
// environments are truly disabled.
|
|
|
|
|
o.EnvironmentVariables = map[string]string{agent.EnvProcPrioMgmt: "1"}
|
|
|
|
|
})
|
|
|
|
|
require.Eventually(t, func() bool {
|
|
|
|
|
wr.mu.Lock()
|
|
|
|
|
defer wr.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
return strings.Contains(buf.String(), "process priority not enabled")
|
|
|
|
|
}, testutil.WaitLong, testutil.IntervalFast)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-25 10:52:36 +00:00
|
|
|
|
func verifyCollectedMetrics(t *testing.T, expected []agentsdk.AgentMetric, actual []*promgo.MetricFamily) bool {
|
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
|
|
for i, e := range expected {
|
|
|
|
|
assert.Equal(t, e.Name, actual[i].GetName())
|
|
|
|
|
assert.Equal(t, string(e.Type), strings.ToLower(actual[i].GetType().String()))
|
|
|
|
|
|
|
|
|
|
for _, m := range actual[i].GetMetric() {
|
|
|
|
|
assert.Equal(t, e.Value, m.Counter.GetValue())
|
|
|
|
|
|
|
|
|
|
if len(m.GetLabel()) > 0 {
|
|
|
|
|
for j, lbl := range m.GetLabel() {
|
|
|
|
|
assert.Equal(t, e.Labels[j].Name, lbl.GetName())
|
|
|
|
|
assert.Equal(t, e.Labels[j].Value, lbl.GetValue())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
m.GetLabel()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
2023-09-15 00:45:05 +00:00
|
|
|
|
|
|
|
|
|
type syncWriter struct {
|
|
|
|
|
mu sync.Mutex
|
|
|
|
|
w io.Writer
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *syncWriter) Write(p []byte) (int, error) {
|
|
|
|
|
s.mu.Lock()
|
|
|
|
|
defer s.mu.Unlock()
|
|
|
|
|
return s.w.Write(p)
|
|
|
|
|
}
|
2023-11-27 05:42:45 +00:00
|
|
|
|
|
|
|
|
|
// echoOnce accepts a single connection, reads 4 bytes and echos them back
|
|
|
|
|
func echoOnce(t *testing.T, ll net.Listener) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
conn, err := ll.Accept()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer conn.Close()
|
|
|
|
|
b := make([]byte, 4)
|
|
|
|
|
_, err = conn.Read(b)
|
|
|
|
|
if !assert.NoError(t, err) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
_, err = conn.Write(b)
|
|
|
|
|
if !assert.NoError(t, err) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// requireEcho sends 4 bytes and requires the read response to match what was sent.
|
|
|
|
|
func requireEcho(t *testing.T, conn net.Conn) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
_, err := conn.Write([]byte("test"))
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
b := make([]byte, 4)
|
|
|
|
|
_, err = conn.Read(b)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.Equal(t, "test", string(b))
|
|
|
|
|
}
|
2024-04-03 14:42:03 +00:00
|
|
|
|
|
|
|
|
|
func requireFileWrite(t testing.TB, fs afero.Fs, fp, data string) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
err := afero.WriteFile(fs, fp, []byte(data), 0o600)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func requireFileEquals(t testing.TB, fs afero.Fs, fp, expect string) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
actual, err := afero.ReadFile(fs, fp)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
require.Equal(t, expect, string(actual))
|
|
|
|
|
}
|