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

Commit9e2eb4b

Browse files
committed
feat: Add config-ssh command
Closes#254 and#499.
1 parent1bab7e8 commit9e2eb4b

File tree

14 files changed

+314
-48
lines changed

14 files changed

+314
-48
lines changed

‎.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"cSpell.words": [
3+
"cliflag",
34
"cliui",
45
"coderd",
56
"coderdtest",

‎agent/agent.go

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ func (s *agent) init(ctx context.Context) {
148148
Handler:func(session ssh.Session) {
149149
err:=s.handleSSHSession(session)
150150
iferr!=nil {
151-
s.options.Logger.Debug(ctx,"ssh session failed",slog.Error(err))
151+
s.options.Logger.Warn(ctx,"ssh session failed",slog.Error(err))
152152
_=session.Exit(1)
153153
return
154154
}
@@ -177,12 +177,6 @@ func (s *agent) init(ctx context.Context) {
177177
},
178178
ServerConfigCallback:func(ctx ssh.Context)*gossh.ServerConfig {
179179
return&gossh.ServerConfig{
180-
Config: gossh.Config{
181-
// "arcfour" is the fastest SSH cipher. We prioritize throughput
182-
// over encryption here, because the WebRTC connection is already
183-
// encrypted. If possible, we'd disable encryption entirely here.
184-
Ciphers: []string{"arcfour"},
185-
},
186180
NoClientAuth:true,
187181
}
188182
},
@@ -198,14 +192,11 @@ func (*agent) handleSSHSession(session ssh.Session) error {
198192
errerror
199193
)
200194

201-
username:=session.User()
202-
ifusername=="" {
203-
currentUser,err:=user.Current()
204-
iferr!=nil {
205-
returnxerrors.Errorf("get current user: %w",err)
206-
}
207-
username=currentUser.Username
195+
currentUser,err:=user.Current()
196+
iferr!=nil {
197+
returnxerrors.Errorf("get current user: %w",err)
208198
}
199+
username:=currentUser.Username
209200

210201
// gliderlabs/ssh returns a command slice of zero
211202
// when a shell is requested.
@@ -249,10 +240,7 @@ func (*agent) handleSSHSession(session ssh.Session) error {
249240
}
250241
gofunc() {
251242
forwin:=rangewindowSize {
252-
err:=ptty.Resize(uint16(win.Width),uint16(win.Height))
253-
iferr!=nil {
254-
panic(err)
255-
}
243+
_=ptty.Resize(uint16(win.Width),uint16(win.Height))
256244
}
257245
}()
258246
gofunc() {

‎agent/conn.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,6 @@ func (c *Conn) SSHClient() (*ssh.Client, error) {
3939
returnnil,xerrors.Errorf("ssh: %w",err)
4040
}
4141
sshConn,channels,requests,err:=ssh.NewClientConn(netConn,"localhost:22",&ssh.ClientConfig{
42-
Config: ssh.Config{
43-
Ciphers: []string{"arcfour"},
44-
},
4542
// SSH host validation isn't helpful, because obtaining a peer
4643
// connection already signifies user-intent to dial a workspace.
4744
// #nosec

‎agent/usershell/usershell_other.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,5 @@ func Get(username string) (string, error) {
2727
}
2828
returnparts[6],nil
2929
}
30-
return"",xerrors.New("user not found in /etc/passwd and $SHELL not set")
30+
return"",xerrors.Errorf("user%qnot found in /etc/passwd",username)
3131
}

‎cli/cliui/agent.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ package cliui
33
import (
44
"context"
55
"fmt"
6+
"io"
67
"sync"
78
"time"
89

910
"github.com/briandowns/spinner"
10-
"github.com/spf13/cobra"
1111
"golang.org/x/xerrors"
1212

1313
"github.com/coder/coder/codersdk"
@@ -21,15 +21,15 @@ type AgentOptions struct {
2121
}
2222

2323
// Agent displays a spinning indicator that waits for a workspace agent to connect.
24-
funcAgent(cmd*cobra.Command,optsAgentOptions)error {
24+
funcAgent(ctx context.Context,writer io.Writer,optsAgentOptions)error {
2525
ifopts.FetchInterval==0 {
2626
opts.FetchInterval=500*time.Millisecond
2727
}
2828
ifopts.WarnInterval==0 {
2929
opts.WarnInterval=30*time.Second
3030
}
3131
varresourceMutex sync.Mutex
32-
resource,err:=opts.Fetch(cmd.Context())
32+
resource,err:=opts.Fetch(ctx)
3333
iferr!=nil {
3434
returnxerrors.Errorf("fetch: %w",err)
3535
}
@@ -40,7 +40,8 @@ func Agent(cmd *cobra.Command, opts AgentOptions) error {
4040
opts.WarnInterval=0
4141
}
4242
spin:=spinner.New(spinner.CharSets[78],100*time.Millisecond,spinner.WithColor("fgHiGreen"))
43-
spin.Writer=cmd.OutOrStdout()
43+
spin.Writer=writer
44+
spin.ForceOutput=true
4445
spin.Suffix=" Waiting for connection from "+Styles.Field.Render(resource.Type+"."+resource.Name)+"..."
4546
spin.Start()
4647
deferspin.Stop()
@@ -51,7 +52,7 @@ func Agent(cmd *cobra.Command, opts AgentOptions) error {
5152
defertimer.Stop()
5253
gofunc() {
5354
select {
54-
case<-cmd.Context().Done():
55+
case<-ctx.Done():
5556
return
5657
case<-timer.C:
5758
}
@@ -63,17 +64,17 @@ func Agent(cmd *cobra.Command, opts AgentOptions) error {
6364
}
6465
// This saves the cursor position, then defers clearing from the cursor
6566
// position to the end of the screen.
66-
_,_=fmt.Fprintf(cmd.OutOrStdout(),"\033[s\r\033[2K%s\n\n",Styles.Paragraph.Render(Styles.Prompt.String()+message))
67-
deferfmt.Fprintf(cmd.OutOrStdout(),"\033[u\033[J")
67+
_,_=fmt.Fprintf(writer,"\033[s\r\033[2K%s\n\n",Styles.Paragraph.Render(Styles.Prompt.String()+message))
68+
deferfmt.Fprintf(writer,"\033[u\033[J")
6869
}()
6970
for {
7071
select {
71-
case<-cmd.Context().Done():
72-
returncmd.Context().Err()
72+
case<-ctx.Done():
73+
returnctx.Err()
7374
case<-ticker.C:
7475
}
7576
resourceMutex.Lock()
76-
resource,err=opts.Fetch(cmd.Context())
77+
resource,err=opts.Fetch(ctx)
7778
iferr!=nil {
7879
returnxerrors.Errorf("fetch: %w",err)
7980
}

‎cli/cliui/agent_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ func TestAgent(t *testing.T) {
2020
ptty:=ptytest.New(t)
2121
cmd:=&cobra.Command{
2222
RunE:func(cmd*cobra.Command,args []string)error {
23-
err:=cliui.Agent(cmd, cliui.AgentOptions{
23+
err:=cliui.Agent(cmd.Context(),cmd.OutOrStdout(), cliui.AgentOptions{
2424
WorkspaceName:"example",
2525
Fetch:func(ctx context.Context) (codersdk.WorkspaceResource,error) {
2626
resource:= codersdk.WorkspaceResource{

‎cli/cliui/log.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package cliui
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"strings"
7+
8+
"github.com/charmbracelet/lipgloss"
9+
)
10+
11+
// cliMessage provides a human-readable message for CLI errors and messages.
12+
typecliMessagestruct {
13+
Levelstring
14+
Style lipgloss.Style
15+
Headerstring
16+
Lines []string
17+
}
18+
19+
// String formats the CLI message for consumption by a human.
20+
func (mcliMessage)String()string {
21+
varstr strings.Builder
22+
_,_=fmt.Fprintf(&str,"%s\r\n",
23+
Styles.Bold.Render(m.Header))
24+
for_,line:=rangem.Lines {
25+
_,_=fmt.Fprintf(&str," %s %s\r\n",m.Style.Render("|"),line)
26+
}
27+
returnstr.String()
28+
}
29+
30+
// Warn writes a log to the writer provided.
31+
funcWarn(wtr io.Writer,headerstring,lines...string) {
32+
_,_=fmt.Fprint(wtr,cliMessage{
33+
Level:"warning",
34+
Style:Styles.Warn,
35+
Header:header,
36+
Lines:lines,
37+
}.String())
38+
}

‎cli/configssh.go

Lines changed: 142 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,159 @@
11
package cli
22

33
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"runtime"
8+
"strings"
9+
"sync"
10+
11+
"github.com/cli/safeexec"
412
"github.com/spf13/cobra"
13+
"golang.org/x/sync/errgroup"
14+
"golang.org/x/xerrors"
15+
16+
"github.com/coder/coder/cli/cliflag"
17+
"github.com/coder/coder/cli/cliui"
18+
"github.com/coder/coder/codersdk"
519
)
620

7-
//const sshStartToken = "# ------------START-CODER-----------"
8-
//const sshStartMessage = `# This was generated by "coder config-ssh".
9-
//#
10-
//# To remove this blob, run:
11-
//#
12-
//# coder config-ssh --remove
13-
//#
14-
//# You should not hand-edit this section, unless you are deleting it.`
15-
//const sshEndToken = "# ------------END-CODER------------"
21+
constsshStartToken="# ------------START-CODER-----------"
22+
constsshStartMessage=`# This was generated by "coder config-ssh".
23+
#
24+
# To remove this blob, run:
25+
#
26+
# coder config-ssh --remove
27+
#
28+
# You should not hand-edit this section, unless you are deleting it.`
29+
constsshEndToken="# ------------END-CODER------------"
1630

1731
funcconfigSSH()*cobra.Command {
32+
var (
33+
sshConfigFilestring
34+
)
1835
cmd:=&cobra.Command{
1936
Use:"config-ssh",
2037
RunE:func(cmd*cobra.Command,args []string)error {
38+
client,err:=createClient(cmd)
39+
iferr!=nil {
40+
returnerr
41+
}
42+
ifstrings.HasPrefix(sshConfigFile,"~/") {
43+
dirname,_:=os.UserHomeDir()
44+
sshConfigFile=filepath.Join(dirname,sshConfigFile[2:])
45+
}
46+
// Doesn't matter if this fails, because we write the file anyways.
47+
sshConfigContentRaw,_:=os.ReadFile(sshConfigFile)
48+
sshConfigContent:=string(sshConfigContentRaw)
49+
startIndex:=strings.Index(sshConfigContent,sshStartToken)
50+
endIndex:=strings.Index(sshConfigContent,sshEndToken)
51+
ifstartIndex!=-1&&endIndex!=-1 {
52+
sshConfigContent=sshConfigContent[:startIndex-1]+sshConfigContent[endIndex+len(sshEndToken):]
53+
}
54+
55+
workspaces,err:=client.WorkspacesByUser(cmd.Context(),"")
56+
iferr!=nil {
57+
returnerr
58+
}
59+
binPath,err:=currentBinPath(cmd)
60+
iferr!=nil {
61+
returnerr
62+
}
63+
64+
sshConfigContent+="\n"+sshStartToken+"\n"+sshStartMessage+"\n\n"
65+
sshConfigContentMutex:= sync.Mutex{}
66+
varerrGroup errgroup.Group
67+
for_,workspace:=rangeworkspaces {
68+
workspace:=workspace
69+
errGroup.Go(func()error {
70+
resources,err:=client.WorkspaceResourcesByBuild(cmd.Context(),workspace.LatestBuild.ID)
71+
iferr!=nil {
72+
returnerr
73+
}
74+
resourcesWithAgents:=make([]codersdk.WorkspaceResource,0)
75+
for_,resource:=rangeresources {
76+
ifresource.Agent==nil {
77+
continue
78+
}
79+
resourcesWithAgents=append(resourcesWithAgents,resource)
80+
}
81+
sshConfigContentMutex.Lock()
82+
defersshConfigContentMutex.Unlock()
83+
iflen(resourcesWithAgents)==1 {
84+
sshConfigContent+=strings.Join([]string{
85+
"Host coder."+workspace.Name,
86+
"\tHostName coder."+workspace.Name,
87+
fmt.Sprintf("\tProxyCommand %q ssh --stdio %s",binPath,workspace.Name),
88+
"\tConnectTimeout=0",
89+
"\tStrictHostKeyChecking=no",
90+
},"\n")+"\n"
91+
}
92+
93+
returnnil
94+
})
95+
}
96+
err=errGroup.Wait()
97+
iferr!=nil {
98+
returnerr
99+
}
100+
sshConfigContent+="\n"+sshEndToken
101+
err=os.MkdirAll(filepath.Dir(sshConfigFile),os.ModePerm)
102+
iferr!=nil {
103+
returnerr
104+
}
105+
err=os.WriteFile(sshConfigFile, []byte(sshConfigContent),os.ModePerm)
106+
iferr!=nil {
107+
returnerr
108+
}
109+
_,_=fmt.Printf("An auto-generated ssh config was written to\"%s\"\n",sshConfigFile)
110+
_,_=fmt.Println("You should now be able to ssh into your workspace")
111+
_,_=fmt.Printf("For example, try running\n\n\t$ ssh coder.%s\n\n",workspaces[0].Name)
21112
returnnil
22113
},
23114
}
115+
cliflag.StringVarP(cmd.Flags(),&sshConfigFile,"ssh-config-file","","CODER_SSH_CONFIG_FILE","~/.ssh/config","Specifies the path to an SSH config.")
24116

25117
returncmd
26118
}
119+
120+
// currentBinPath returns the path to the coder binary suitable for use in ssh
121+
// ProxyCommand.
122+
funccurrentBinPath(cmd*cobra.Command) (string,error) {
123+
exePath,err:=os.Executable()
124+
iferr!=nil {
125+
return"",xerrors.Errorf("get executable path: %w",err)
126+
}
127+
128+
binName:=filepath.Base(exePath)
129+
// We use safeexec instead of os/exec because os/exec returns paths in
130+
// the current working directory, which we will run into very often when
131+
// looking for our own path.
132+
pathPath,err:=safeexec.LookPath(binName)
133+
// On Windows, the coder-cli executable must be in $PATH for both Msys2/Git
134+
// Bash and OpenSSH for Windows (used by Powershell and VS Code) to function
135+
// correctly. Check if the current executable is in $PATH, and warn the user
136+
// if it isn't.
137+
iferr!=nil&&runtime.GOOS=="windows" {
138+
cliui.Warn(cmd.OutOrStdout(),
139+
"The current executable is not in $PATH.",
140+
"This may lead to problems connecting to your workspace via SSH.",
141+
fmt.Sprintf("Please move %q to a location in your $PATH (such as System32) and run `%s config-ssh` again.",binName,binName),
142+
)
143+
// Return the exePath so SSH at least works outside of Msys2.
144+
returnexePath,nil
145+
}
146+
147+
// Warn the user if the current executable is not the same as the one in
148+
// $PATH.
149+
iffilepath.Clean(pathPath)!=filepath.Clean(exePath) {
150+
cliui.Warn(cmd.OutOrStdout(),
151+
"The current executable path does not match the executable path found in $PATH.",
152+
"This may cause issues connecting to your workspace via SSH.",
153+
fmt.Sprintf("\tCurrent executable path: %q",exePath),
154+
fmt.Sprintf("\tExecutable path in $PATH: %q",pathPath),
155+
)
156+
}
157+
158+
returnbinName,nil
159+
}

‎cli/configssh_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package cli_test
2+
3+
import"testing"
4+
5+
funcTestConfigSSH(t*testing.T) {
6+
t.Parallel()
7+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp