Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commita862472

Browse files
committed
feat(cli): use coder connect incoder ssh, if available
1 parent02b2de9 commita862472

File tree

7 files changed

+411
-77
lines changed

7 files changed

+411
-77
lines changed

‎cli/cliutil/stdioconn.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package cliutil
2+
3+
import (
4+
"io"
5+
"net"
6+
"time"
7+
)
8+
9+
typeStdioConnstruct {
10+
io.Reader
11+
io.Writer
12+
}
13+
14+
func (*StdioConn)Close() (errerror) {
15+
returnnil
16+
}
17+
18+
func (*StdioConn)LocalAddr() net.Addr {
19+
returnnil
20+
}
21+
22+
func (*StdioConn)RemoteAddr() net.Addr {
23+
returnnil
24+
}
25+
26+
func (*StdioConn)SetDeadline(_ time.Time)error {
27+
returnnil
28+
}
29+
30+
func (*StdioConn)SetReadDeadline(_ time.Time)error {
31+
returnnil
32+
}
33+
34+
func (*StdioConn)SetWriteDeadline(_ time.Time)error {
35+
returnnil
36+
}

‎cli/ssh.go

Lines changed: 195 additions & 37 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
stdiobool
6768
hostPrefixstring
6869
hostnameSuffixstring
70+
forceTunnelbool
6971
forwardAgentbool
7072
forwardGPGbool
7173
identityAgentstring
@@ -85,6 +87,7 @@ func (r *RootCmd) ssh() *serpent.Command {
8587
containerUserstring
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
iferr!=nil {
215218
returnerr
216219
}
@@ -275,10 +278,34 @@ func (r *RootCmd) ssh() *serpent.Command {
275278
returnerr
276279
}
277280

281+
// See if we can use the Coder Connect tunnel
282+
if!forceTunnel {
283+
connInfo,err:=wsClient.AgentConnectionInfoGeneric(ctx)
284+
iferr!=nil {
285+
returnxerrors.Errorf("get agent connection info: %w",err)
286+
}
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+
ifexists {
292+
_,_=fmt.Fprintln(inv.Stderr,"Connecting to workspace via Coder Connect...")
293+
defercancel()
294+
addr:=fmt.Sprintf("%s:22",coderConnectHost)
295+
ifstdio {
296+
iferr:=writeCoderConnectNetInfo(ctx,networkInfoDir);err!=nil {
297+
logger.Error(ctx,"failed to write coder connect net info file",slog.Error(err))
298+
}
299+
returnrunCoderConnectStdio(ctx,addr,stdioReader,stdioWriter,stack)
300+
}
301+
returnrunCoderConnectPTY(ctx,addr,inv.Stdin,inv.Stdout,inv.Stderr,stack)
302+
}
303+
}
304+
278305
ifr.disableDirect {
279306
_,_=fmt.Fprintln(inv.Stderr,"Direct connections disabled.")
280307
}
281-
conn,err:=workspacesdk.New(client).
308+
conn,err:=wsClient.
282309
DialAgent(ctx,workspaceAgent.ID,&workspacesdk.DialAgentOptions{
283310
Logger:logger,
284311
BlockEndpoints:r.disableDirect,
@@ -454,36 +481,11 @@ func (r *RootCmd) ssh() *serpent.Command {
454481
stdinFile,validIn:=inv.Stdin.(*os.File)
455482
stdoutFile,validOut:=inv.Stdout.(*os.File)
456483
ifvalidIn&&validOut&&isatty.IsTerminal(stdinFile.Fd())&&isatty.IsTerminal(stdoutFile.Fd()) {
457-
inState,err:=pty.MakeInputRaw(stdinFile.Fd())
458-
iferr!=nil {
459-
returnerr
460-
}
461-
deferfunc() {
462-
_=pty.RestoreTerminal(stdinFile.Fd(),inState)
463-
}()
464-
outState,err:=pty.MakeOutputRaw(stdoutFile.Fd())
484+
restorePtyFn,err:=configurePTY(ctx,stdinFile,stdoutFile,sshSession)
485+
deferrestorePtyFn()
465486
iferr!=nil {
466-
returnerr
487+
returnxerrors.Errorf("configure pty: %w",err)
467488
}
468-
deferfunc() {
469-
_=pty.RestoreTerminal(stdoutFile.Fd(),outState)
470-
}()
471-
472-
windowChange:=listenWindowSize(ctx)
473-
gofunc() {
474-
for {
475-
select {
476-
case<-ctx.Done():
477-
return
478-
case<-windowChange:
479-
}
480-
width,height,err:=term.GetSize(int(stdoutFile.Fd()))
481-
iferr!=nil {
482-
continue
483-
}
484-
_=sshSession.WindowChange(height,width)
485-
}
486-
}()
487489
}
488490

489491
for_,kv:=rangeparsedEnv {
@@ -662,11 +664,51 @@ func (r *RootCmd) ssh() *serpent.Command {
662664
Value:serpent.StringOf(&containerUser),
663665
Hidden:true,// Hidden until this features is at least in beta.
664666
},
667+
{
668+
Flag:"force-tunnel",
669+
Description:"Force the use of a new tunnel to the workspace, even if the Coder Connect tunnel is available.",
670+
Value:serpent.BoolOf(&forceTunnel),
671+
},
665672
sshDisableAutostartOption(serpent.BoolOf(&disableAutostart)),
666673
}
667674
returncmd
668675
}
669676

677+
funcconfigurePTY(ctx context.Context,stdinFile*os.File,stdoutFile*os.File,sshSession*gossh.Session) (restoreFnfunc(),errerror) {
678+
inState,err:=pty.MakeInputRaw(stdinFile.Fd())
679+
iferr!=nil {
680+
returnrestoreFn,err
681+
}
682+
restoreFn=func() {
683+
_=pty.RestoreTerminal(stdinFile.Fd(),inState)
684+
}
685+
outState,err:=pty.MakeOutputRaw(stdoutFile.Fd())
686+
iferr!=nil {
687+
returnrestoreFn,err
688+
}
689+
restoreFn=func() {
690+
_=pty.RestoreTerminal(stdinFile.Fd(),inState)
691+
_=pty.RestoreTerminal(stdoutFile.Fd(),outState)
692+
}
693+
694+
windowChange:=listenWindowSize(ctx)
695+
gofunc() {
696+
for {
697+
select {
698+
case<-ctx.Done():
699+
return
700+
case<-windowChange:
701+
}
702+
width,height,err:=term.GetSize(int(stdoutFile.Fd()))
703+
iferr!=nil {
704+
continue
705+
}
706+
_=sshSession.WindowChange(height,width)
707+
}
708+
}()
709+
returnrestoreFn,nil
710+
}
711+
670712
// findWorkspaceAndAgentByHostname parses the hostname from the commandline and finds the workspace and agent it
671713
// corresponds to, taking into account any name prefixes or suffixes configured (e.g. myworkspace.coder, or
672714
// vscode-coder--myusername--myworkspace).
@@ -1374,12 +1416,13 @@ func setStatsCallback(
13741416
}
13751417

13761418
typesshNetworkStatsstruct {
1377-
P2Pbool`json:"p2p"`
1378-
Latencyfloat64`json:"latency"`
1379-
PreferredDERPstring`json:"preferred_derp"`
1380-
DERPLatencymap[string]float64`json:"derp_latency"`
1381-
UploadBytesSecint64`json:"upload_bytes_sec"`
1382-
DownloadBytesSecint64`json:"download_bytes_sec"`
1419+
P2Pbool`json:"p2p"`
1420+
Latencyfloat64`json:"latency"`
1421+
PreferredDERPstring`json:"preferred_derp"`
1422+
DERPLatencymap[string]float64`json:"derp_latency"`
1423+
UploadBytesSecint64`json:"upload_bytes_sec"`
1424+
DownloadBytesSecint64`json:"download_bytes_sec"`
1425+
UsingCoderConnectbool`json:"using_coder_connect"`
13831426
}
13841427

13851428
funccollectNetworkStats(ctx context.Context,agentConn*workspacesdk.AgentConn,start,end time.Time,countsmap[netlogtype.Connection]netlogtype.Counts) (*sshNetworkStats,error) {
@@ -1450,6 +1493,121 @@ func collectNetworkStats(ctx context.Context, agentConn *workspacesdk.AgentConn,
14501493
},nil
14511494
}
14521495

1496+
funcrunCoderConnectStdio(ctx context.Context,addrstring,stdin io.Reader,stdout io.Writer,stack*closerStack)error {
1497+
conn,err:=net.Dial("tcp",addr)
1498+
iferr!=nil {
1499+
returnxerrors.Errorf("dial coder connect host: %w",err)
1500+
}
1501+
iferr:=stack.push("tcp conn",conn);err!=nil {
1502+
returnerr
1503+
}
1504+
1505+
agentssh.Bicopy(ctx,conn,&cliutil.StdioConn{
1506+
Reader:stdin,
1507+
Writer:stdout,
1508+
})
1509+
1510+
returnnil
1511+
}
1512+
1513+
funcrunCoderConnectPTY(ctx context.Context,addrstring,stdin io.Reader,stdout io.Writer,stderr io.Writer,stack*closerStack)error {
1514+
client,err:=gossh.Dial("tcp",addr,&gossh.ClientConfig{
1515+
// We've already checked the agent's address
1516+
// is within the Coder service prefix.
1517+
// #nosec
1518+
HostKeyCallback:gossh.InsecureIgnoreHostKey(),
1519+
})
1520+
iferr!=nil {
1521+
returnxerrors.Errorf("dial coder connect host: %w",err)
1522+
}
1523+
iferr:=stack.push("ssh client",client);err!=nil {
1524+
returnerr
1525+
}
1526+
1527+
session,err:=client.NewSession()
1528+
iferr!=nil {
1529+
returnxerrors.Errorf("create ssh session: %w",err)
1530+
}
1531+
iferr:=stack.push("ssh session",session);err!=nil {
1532+
returnerr
1533+
}
1534+
1535+
stdinFile,validIn:=stdin.(*os.File)
1536+
stdoutFile,validOut:=stdout.(*os.File)
1537+
ifvalidIn&&validOut&&isatty.IsTerminal(stdinFile.Fd())&&isatty.IsTerminal(stdoutFile.Fd()) {
1538+
restorePtyFn,err:=configurePTY(ctx,stdinFile,stdoutFile,session)
1539+
deferrestorePtyFn()
1540+
iferr!=nil {
1541+
returnxerrors.Errorf("configure pty: %w",err)
1542+
}
1543+
}
1544+
1545+
session.Stdin=stdin
1546+
session.Stdout=stdout
1547+
session.Stderr=stderr
1548+
1549+
err=session.RequestPty("xterm-256color",80,24, gossh.TerminalModes{})
1550+
iferr!=nil {
1551+
returnxerrors.Errorf("request pty: %w",err)
1552+
}
1553+
1554+
err=session.Shell()
1555+
iferr!=nil {
1556+
returnxerrors.Errorf("start shell: %w",err)
1557+
}
1558+
1559+
ifvalidOut {
1560+
// Set initial window size.
1561+
width,height,err:=term.GetSize(int(stdoutFile.Fd()))
1562+
iferr==nil {
1563+
_=session.WindowChange(height,width)
1564+
}
1565+
}
1566+
1567+
err=session.Wait()
1568+
iferr!=nil {
1569+
ifexitErr:= (&gossh.ExitError{});errors.As(err,&exitErr) {
1570+
// Clear the error since it's not useful beyond
1571+
// reporting status.
1572+
returnExitError(exitErr.ExitStatus(),nil)
1573+
}
1574+
// If the connection drops unexpectedly, we get an
1575+
// ExitMissingError but no other error details, so try to at
1576+
// least give the user a better message
1577+
iferrors.Is(err,&gossh.ExitMissingError{}) {
1578+
returnExitError(255,xerrors.New("SSH connection ended unexpectedly"))
1579+
}
1580+
returnxerrors.Errorf("session ended: %w",err)
1581+
}
1582+
1583+
returnnil
1584+
}
1585+
1586+
funcwriteCoderConnectNetInfo(ctx context.Context,networkInfoDirstring)error {
1587+
fs,ok:=ctx.Value("fs").(afero.Fs)
1588+
if!ok {
1589+
fs=afero.NewOsFs()
1590+
}
1591+
// The VS Code extension obtains the PID of the SSH process to
1592+
// find the log file associated with a SSH session.
1593+
//
1594+
// We get the parent PID because it's assumed `ssh` is calling this
1595+
// command via the ProxyCommand SSH option.
1596+
networkInfoFilePath:=filepath.Join(networkInfoDir,fmt.Sprintf("%d.json",os.Getppid()))
1597+
stats:=&sshNetworkStats{
1598+
UsingCoderConnect:true,
1599+
}
1600+
rawStats,err:=json.Marshal(stats)
1601+
iferr!=nil {
1602+
returnxerrors.Errorf("marshal network stats: %w",err)
1603+
}
1604+
err=afero.WriteFile(fs,networkInfoFilePath,rawStats,0o600)
1605+
iferr!=nil {
1606+
returnxerrors.Errorf("write network stats: %w",err)
1607+
}
1608+
returnnil
1609+
}
1610+
14531611
// Converts workspace name input to owner/workspace.agent format
14541612
// Possible valid input formats:
14551613
// workspace

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp