@@ -25,24 +25,28 @@ import (
2525"testing"
2626"time"
2727
28+ "go.uber.org/goleak"
29+ "tailscale.com/net/speedtest"
30+ "tailscale.com/tailcfg"
31+
2832"github.com/bramvdbogaerde/go-scp"
2933"github.com/google/uuid"
34+ "github.com/ory/dockertest/v3"
35+ "github.com/ory/dockertest/v3/docker"
3036"github.com/pion/udp"
3137"github.com/pkg/sftp"
3238"github.com/prometheus/client_golang/prometheus"
3339promgo"github.com/prometheus/client_model/go"
3440"github.com/spf13/afero"
3541"github.com/stretchr/testify/assert"
3642"github.com/stretchr/testify/require"
37- "go.uber.org/goleak"
3843"golang.org/x/crypto/ssh"
3944"golang.org/x/exp/slices"
4045"golang.org/x/xerrors"
41- "tailscale.com/net/speedtest"
42- "tailscale.com/tailcfg"
4346
4447"cdr.dev/slog"
4548"cdr.dev/slog/sloggers/slogtest"
49+
4650"github.com/coder/coder/v2/agent"
4751"github.com/coder/coder/v2/agent/agentssh"
4852"github.com/coder/coder/v2/agent/agenttest"
@@ -1761,6 +1765,69 @@ func TestAgent_ReconnectingPTY(t *testing.T) {
17611765}
17621766}
17631767
1768+ // This tests end-to-end functionality of connecting to a running container
1769+ // and executing a command. It creates a real Docker container and runs a
1770+ // command. As such, it does not run by default in CI.
1771+ // You can run it manually as follows:
1772+ //
1773+ // CODER_TEST_USE_DOCKER=1 go test -count=1 ./agent -run TestAgent_ReconnectingPTYContainer
1774+ func TestAgent_ReconnectingPTYContainer (t * testing.T ) {
1775+ t .Parallel ()
1776+ if ctud ,ok := os .LookupEnv ("CODER_TEST_USE_DOCKER" );! ok || ctud != "1" {
1777+ t .Skip ("Set CODER_TEST_USE_DOCKER=1 to run this test" )
1778+ }
1779+
1780+ ctx ,cancel := context .WithTimeout (context .Background (),testutil .WaitLong )
1781+ defer cancel ()
1782+
1783+ pool ,err := dockertest .NewPool ("" )
1784+ require .NoError (t ,err ,"Could not connect to docker" )
1785+ ct ,err := pool .RunWithOptions (& dockertest.RunOptions {
1786+ Repository :"busybox" ,
1787+ Tag :"latest" ,
1788+ Cmd : []string {"sleep" ,"infnity" },
1789+ },func (config * docker.HostConfig ) {
1790+ config .AutoRemove = true
1791+ config .RestartPolicy = docker.RestartPolicy {Name :"no" }
1792+ })
1793+ require .NoError (t ,err ,"Could not start container" )
1794+ // Wait for container to start
1795+ require .Eventually (t ,func ()bool {
1796+ ct ,ok := pool .ContainerByName (ct .Container .Name )
1797+ return ok && ct .Container .State .Running
1798+ },testutil .WaitShort ,testutil .IntervalSlow ,"Container did not start in time" )
1799+
1800+ // nolint: dogsled
1801+ conn ,_ ,_ ,_ ,_ := setupAgent (t , agentsdk.Manifest {},0 )
1802+ ac ,err := conn .ReconnectingPTY (ctx ,uuid .New (),80 ,80 ,"/bin/sh" ,func (arp * workspacesdk.AgentReconnectingPTYInit ) {
1803+ arp .Container = ct .Container .ID
1804+ })
1805+ require .NoError (t ,err ,"failed to create ReconnectingPTY" )
1806+ defer ac .Close ()
1807+ tr := testutil .NewTerminalReader (t ,ac )
1808+
1809+ require .NoError (t ,tr .ReadUntil (ctx ,func (line string )bool {
1810+ return strings .Contains (line ,"#" )|| strings .Contains (line ,"$" )
1811+ }),"find prompt" )
1812+
1813+ require .NoError (t ,json .NewEncoder (ac ).Encode (workspacesdk.ReconnectingPTYRequest {
1814+ Data :"hostname\r " ,
1815+ }),"write hostname" )
1816+ require .NoError (t ,tr .ReadUntil (ctx ,func (line string )bool {
1817+ return strings .Contains (line ,"hostname" )
1818+ }),"find hostname command" )
1819+
1820+ require .NoError (t ,tr .ReadUntil (ctx ,func (line string )bool {
1821+ return strings .Contains (line ,ct .Container .Config .Hostname )
1822+ }),"find hostname output" )
1823+ require .NoError (t ,json .NewEncoder (ac ).Encode (workspacesdk.ReconnectingPTYRequest {
1824+ Data :"exit\r " ,
1825+ }),"write exit command" )
1826+
1827+ // Wait for the connection to close.
1828+ require .ErrorIs (t ,tr .ReadUntil (ctx ,nil ),io .EOF )
1829+ }
1830+
17641831func TestAgent_Dial (t * testing.T ) {
17651832t .Parallel ()
17661833