Skip to content

Commit c3d08ef

Browse files
authored
Merge branch 'main' into jaaydenh/default-values-update
2 parents 0597d26 + 4de7661 commit c3d08ef

File tree

203 files changed

+1066
-1894
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

203 files changed

+1066
-1894
lines changed

cli/exp_mcp_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ func TestExpMcpServer(t *testing.T) {
158158
//nolint:tparallel,paralleltest
159159
func TestExpMcpConfigureClaudeCode(t *testing.T) {
160160
t.Run("NoReportTaskWhenNoAgentToken", func(t *testing.T) {
161+
t.Setenv("CODER_AGENT_TOKEN", "")
161162
ctx := testutil.Context(t, testutil.WaitShort)
162163
cancelCtx, cancel := context.WithCancel(ctx)
163164
t.Cleanup(cancel)

cli/restart_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ func TestRestartWithParameters(t *testing.T) {
359359
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
360360
owner := coderdtest.CreateFirstUser(t, client)
361361
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
362-
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, mutableParamsResponse)
362+
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, mutableParamsResponse())
363363
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
364364
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
365365
workspace := coderdtest.CreateWorkspace(t, member, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {

cli/ssh.go

Lines changed: 123 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"io"
1010
"log"
11+
"net"
1112
"net/http"
1213
"net/url"
1314
"os"
@@ -66,6 +67,7 @@ func (r *RootCmd) ssh() *serpent.Command {
6667
stdio bool
6768
hostPrefix string
6869
hostnameSuffix string
70+
forceNewTunnel bool
6971
forwardAgent bool
7072
forwardGPG bool
7173
identityAgent string
@@ -85,6 +87,7 @@ func (r *RootCmd) ssh() *serpent.Command {
8587
containerUser string
8688
)
8789
client := new(codersdk.Client)
90+
wsClient := workspacesdk.New(client)
8891
cmd := &serpent.Command{
8992
Annotations: workspaceCommand,
9093
Use: "ssh <workspace>",
@@ -203,14 +206,14 @@ func (r *RootCmd) ssh() *serpent.Command {
203206
parsedEnv = append(parsedEnv, [2]string{k, v})
204207
}
205208

206-
deploymentSSHConfig := codersdk.SSHConfigResponse{
209+
cliConfig := codersdk.SSHConfigResponse{
207210
HostnamePrefix: hostPrefix,
208211
HostnameSuffix: hostnameSuffix,
209212
}
210213

211214
workspace, workspaceAgent, err := findWorkspaceAndAgentByHostname(
212215
ctx, inv, client,
213-
inv.Args[0], deploymentSSHConfig, disableAutostart)
216+
inv.Args[0], cliConfig, disableAutostart)
214217
if err != nil {
215218
return err
216219
}
@@ -275,10 +278,44 @@ func (r *RootCmd) ssh() *serpent.Command {
275278
return err
276279
}
277280

281+
// If we're in stdio mode, check to see if we can use Coder Connect.
282+
// We don't support Coder Connect over non-stdio coder ssh yet.
283+
if stdio && !forceNewTunnel {
284+
connInfo, err := wsClient.AgentConnectionInfoGeneric(ctx)
285+
if err != nil {
286+
return xerrors.Errorf("get agent connection info: %w", err)
287+
}
288+
coderConnectHost := fmt.Sprintf("%s.%s.%s.%s",
289+
workspaceAgent.Name, workspace.Name, workspace.OwnerName, connInfo.HostnameSuffix)
290+
exists, _ := workspacesdk.ExistsViaCoderConnect(ctx, coderConnectHost)
291+
if exists {
292+
defer cancel()
293+
294+
if networkInfoDir != "" {
295+
if err := writeCoderConnectNetInfo(ctx, networkInfoDir); err != nil {
296+
logger.Error(ctx, "failed to write coder connect net info file", slog.Error(err))
297+
}
298+
}
299+
300+
stopPolling := tryPollWorkspaceAutostop(ctx, client, workspace)
301+
defer stopPolling()
302+
303+
usageAppName := getUsageAppName(usageApp)
304+
if usageAppName != "" {
305+
closeUsage := client.UpdateWorkspaceUsageWithBodyContext(ctx, workspace.ID, codersdk.PostWorkspaceUsageRequest{
306+
AgentID: workspaceAgent.ID,
307+
AppName: usageAppName,
308+
})
309+
defer closeUsage()
310+
}
311+
return runCoderConnectStdio(ctx, fmt.Sprintf("%s:22", coderConnectHost), stdioReader, stdioWriter, stack)
312+
}
313+
}
314+
278315
if r.disableDirect {
279316
_, _ = fmt.Fprintln(inv.Stderr, "Direct connections disabled.")
280317
}
281-
conn, err := workspacesdk.New(client).
318+
conn, err := wsClient.
282319
DialAgent(ctx, workspaceAgent.ID, &workspacesdk.DialAgentOptions{
283320
Logger: logger,
284321
BlockEndpoints: r.disableDirect,
@@ -660,6 +697,12 @@ func (r *RootCmd) ssh() *serpent.Command {
660697
Value: serpent.StringOf(&containerUser),
661698
Hidden: true, // Hidden until this features is at least in beta.
662699
},
700+
{
701+
Flag: "force-new-tunnel",
702+
Description: "Force the creation of a new tunnel to the workspace, even if the Coder Connect tunnel is available.",
703+
Value: serpent.BoolOf(&forceNewTunnel),
704+
Hidden: true,
705+
},
663706
sshDisableAutostartOption(serpent.BoolOf(&disableAutostart)),
664707
}
665708
return cmd
@@ -1372,12 +1415,13 @@ func setStatsCallback(
13721415
}
13731416

13741417
type sshNetworkStats struct {
1375-
P2P bool `json:"p2p"`
1376-
Latency float64 `json:"latency"`
1377-
PreferredDERP string `json:"preferred_derp"`
1378-
DERPLatency map[string]float64 `json:"derp_latency"`
1379-
UploadBytesSec int64 `json:"upload_bytes_sec"`
1380-
DownloadBytesSec int64 `json:"download_bytes_sec"`
1418+
P2P bool `json:"p2p"`
1419+
Latency float64 `json:"latency"`
1420+
PreferredDERP string `json:"preferred_derp"`
1421+
DERPLatency map[string]float64 `json:"derp_latency"`
1422+
UploadBytesSec int64 `json:"upload_bytes_sec"`
1423+
DownloadBytesSec int64 `json:"download_bytes_sec"`
1424+
UsingCoderConnect bool `json:"using_coder_connect"`
13811425
}
13821426

13831427
func collectNetworkStats(ctx context.Context, agentConn *workspacesdk.AgentConn, start, end time.Time, counts map[netlogtype.Connection]netlogtype.Counts) (*sshNetworkStats, error) {
@@ -1448,6 +1492,76 @@ func collectNetworkStats(ctx context.Context, agentConn *workspacesdk.AgentConn,
14481492
}, nil
14491493
}
14501494

1495+
type coderConnectDialerContextKey struct{}
1496+
1497+
type coderConnectDialer interface {
1498+
DialContext(ctx context.Context, network, addr string) (net.Conn, error)
1499+
}
1500+
1501+
func WithTestOnlyCoderConnectDialer(ctx context.Context, dialer coderConnectDialer) context.Context {
1502+
return context.WithValue(ctx, coderConnectDialerContextKey{}, dialer)
1503+
}
1504+
1505+
func testOrDefaultDialer(ctx context.Context) coderConnectDialer {
1506+
dialer, ok := ctx.Value(coderConnectDialerContextKey{}).(coderConnectDialer)
1507+
if !ok || dialer == nil {
1508+
return &net.Dialer{}
1509+
}
1510+
return dialer
1511+
}
1512+
1513+
func runCoderConnectStdio(ctx context.Context, addr string, stdin io.Reader, stdout io.Writer, stack *closerStack) error {
1514+
dialer := testOrDefaultDialer(ctx)
1515+
conn, err := dialer.DialContext(ctx, "tcp", addr)
1516+
if err != nil {
1517+
return xerrors.Errorf("dial coder connect host: %w", err)
1518+
}
1519+
if err := stack.push("tcp conn", conn); err != nil {
1520+
return err
1521+
}
1522+
1523+
agentssh.Bicopy(ctx, conn, &StdioRwc{
1524+
Reader: stdin,
1525+
Writer: stdout,
1526+
})
1527+
1528+
return nil
1529+
}
1530+
1531+
type StdioRwc struct {
1532+
io.Reader
1533+
io.Writer
1534+
}
1535+
1536+
func (*StdioRwc) Close() error {
1537+
return nil
1538+
}
1539+
1540+
func writeCoderConnectNetInfo(ctx context.Context, networkInfoDir string) error {
1541+
fs, ok := ctx.Value("fs").(afero.Fs)
1542+
if !ok {
1543+
fs = afero.NewOsFs()
1544+
}
1545+
// The VS Code extension obtains the PID of the SSH process to
1546+
// find the log file associated with a SSH session.
1547+
//
1548+
// We get the parent PID because it's assumed `ssh` is calling this
1549+
// command via the ProxyCommand SSH option.
1550+
networkInfoFilePath := filepath.Join(networkInfoDir, fmt.Sprintf("%d.json", os.Getppid()))
1551+
stats := &sshNetworkStats{
1552+
UsingCoderConnect: true,
1553+
}
1554+
rawStats, err := json.Marshal(stats)
1555+
if err != nil {
1556+
return xerrors.Errorf("marshal network stats: %w", err)
1557+
}
1558+
err = afero.WriteFile(fs, networkInfoFilePath, rawStats, 0o600)
1559+
if err != nil {
1560+
return xerrors.Errorf("write network stats: %w", err)
1561+
}
1562+
return nil
1563+
}
1564+
14511565
// Converts workspace name input to owner/workspace.agent format
14521566
// Possible valid input formats:
14531567
// workspace

cli/ssh_internal_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@ package cli
33
import (
44
"context"
55
"fmt"
6+
"io"
7+
"net"
68
"net/url"
79
"sync"
810
"testing"
911
"time"
1012

13+
gliderssh "github.com/gliderlabs/ssh"
1114
"github.com/stretchr/testify/assert"
1215
"github.com/stretchr/testify/require"
16+
"golang.org/x/crypto/ssh"
1317
"golang.org/x/xerrors"
1418

1519
"cdr.dev/slog"
@@ -220,6 +224,87 @@ func TestCloserStack_Timeout(t *testing.T) {
220224
testutil.TryReceive(ctx, t, closed)
221225
}
222226

227+
func TestCoderConnectStdio(t *testing.T) {
228+
t.Parallel()
229+
230+
ctx := testutil.Context(t, testutil.WaitShort)
231+
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
232+
stack := newCloserStack(ctx, logger, quartz.NewMock(t))
233+
234+
clientOutput, clientInput := io.Pipe()
235+
serverOutput, serverInput := io.Pipe()
236+
defer func() {
237+
for _, c := range []io.Closer{clientOutput, clientInput, serverOutput, serverInput} {
238+
_ = c.Close()
239+
}
240+
}()
241+
242+
server := newSSHServer("127.0.0.1:0")
243+
ln, err := net.Listen("tcp", server.server.Addr)
244+
require.NoError(t, err)
245+
246+
go func() {
247+
_ = server.Serve(ln)
248+
}()
249+
t.Cleanup(func() {
250+
_ = server.Close()
251+
})
252+
253+
stdioDone := make(chan struct{})
254+
go func() {
255+
err = runCoderConnectStdio(ctx, ln.Addr().String(), clientOutput, serverInput, stack)
256+
assert.NoError(t, err)
257+
close(stdioDone)
258+
}()
259+
260+
conn, channels, requests, err := ssh.NewClientConn(&testutil.ReaderWriterConn{
261+
Reader: serverOutput,
262+
Writer: clientInput,
263+
}, "", &ssh.ClientConfig{
264+
// #nosec
265+
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
266+
})
267+
require.NoError(t, err)
268+
defer conn.Close()
269+
270+
sshClient := ssh.NewClient(conn, channels, requests)
271+
session, err := sshClient.NewSession()
272+
require.NoError(t, err)
273+
defer session.Close()
274+
275+
// We're not connected to a real shell
276+
err = session.Run("")
277+
require.NoError(t, err)
278+
err = sshClient.Close()
279+
require.NoError(t, err)
280+
_ = clientOutput.Close()
281+
282+
<-stdioDone
283+
}
284+
285+
type sshServer struct {
286+
server *gliderssh.Server
287+
}
288+
289+
func newSSHServer(addr string) *sshServer {
290+
return &sshServer{
291+
server: &gliderssh.Server{
292+
Addr: addr,
293+
Handler: func(s gliderssh.Session) {
294+
_, _ = io.WriteString(s.Stderr(), "Connected!")
295+
},
296+
},
297+
}
298+
}
299+
300+
func (s *sshServer) Serve(ln net.Listener) error {
301+
return s.server.Serve(ln)
302+
}
303+
304+
func (s *sshServer) Close() error {
305+
return s.server.Close()
306+
}
307+
223308
type fakeCloser struct {
224309
closes *[]*fakeCloser
225310
err error

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy