Skip to content

Commit a586bab

Browse files
committed
feat: Add config-ssh command
Closes #254 and #499.
1 parent 1bab7e8 commit a586bab

22 files changed

+442
-69
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
if err != 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
err error
199193
)
200194

201-
username := session.User()
202-
if username == "" {
203-
currentUser, err := user.Current()
204-
if err != nil {
205-
return xerrors.Errorf("get current user: %w", err)
206-
}
207-
username = currentUser.Username
195+
currentUser, err := user.Current()
196+
if err != nil {
197+
return xerrors.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
go func() {
251242
for win := range windowSize {
252-
err := ptty.Resize(uint16(win.Width), uint16(win.Height))
253-
if err != nil {
254-
panic(err)
255-
}
243+
_ = ptty.Resize(uint16(win.Width), uint16(win.Height))
256244
}
257245
}()
258246
go func() {

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
return nil, 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
return parts[6], nil
2929
}
30-
return "", xerrors.New("user not found in /etc/passwd and $SHELL not set")
30+
return "", xerrors.Errorf("user %q not 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-
func Agent(cmd *cobra.Command, opts AgentOptions) error {
24+
func Agent(ctx context.Context, writer io.Writer, opts AgentOptions) error {
2525
if opts.FetchInterval == 0 {
2626
opts.FetchInterval = 500 * time.Millisecond
2727
}
2828
if opts.WarnInterval == 0 {
2929
opts.WarnInterval = 30 * time.Second
3030
}
3131
var resourceMutex sync.Mutex
32-
resource, err := opts.Fetch(cmd.Context())
32+
resource, err := opts.Fetch(ctx)
3333
if err != nil {
3434
return xerrors.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
defer spin.Stop()
@@ -51,7 +52,7 @@ func Agent(cmd *cobra.Command, opts AgentOptions) error {
5152
defer timer.Stop()
5253
go func() {
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-
defer fmt.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+
defer fmt.Fprintf(writer, "\033[u\033[J")
6869
}()
6970
for {
7071
select {
71-
case <-cmd.Context().Done():
72-
return cmd.Context().Err()
72+
case <-ctx.Done():
73+
return ctx.Err()
7374
case <-ticker.C:
7475
}
7576
resourceMutex.Lock()
76-
resource, err = opts.Fetch(cmd.Context())
77+
resource, err = opts.Fetch(ctx)
7778
if err != nil {
7879
return xerrors.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+
type cliMessage struct {
13+
Level string
14+
Style lipgloss.Style
15+
Header string
16+
Lines []string
17+
}
18+
19+
// String formats the CLI message for consumption by a human.
20+
func (m cliMessage) String() string {
21+
var str strings.Builder
22+
_, _ = fmt.Fprintf(&str, "%s\r\n",
23+
Styles.Bold.Render(m.Header))
24+
for _, line := range m.Lines {
25+
_, _ = fmt.Fprintf(&str, " %s %s\r\n", m.Style.Render("|"), line)
26+
}
27+
return str.String()
28+
}
29+
30+
// Warn writes a log to the writer provided.
31+
func Warn(wtr io.Writer, header string, lines ...string) {
32+
_, _ = fmt.Fprint(wtr, cliMessage{
33+
Level: "warning",
34+
Style: Styles.Warn,
35+
Header: header,
36+
Lines: lines,
37+
}.String())
38+
}

cli/cliui/provisionerjob.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,27 @@ package cliui
33
import (
44
"context"
55
"fmt"
6+
"io"
67
"os"
78
"os/signal"
89
"sync"
910
"time"
1011

1112
"github.com/google/uuid"
12-
"github.com/spf13/cobra"
1313
"golang.org/x/xerrors"
1414

1515
"github.com/coder/coder/coderd/database"
1616
"github.com/coder/coder/codersdk"
1717
)
1818

19-
func WorkspaceBuild(cmd *cobra.Command, client *codersdk.Client, build uuid.UUID, before time.Time) error {
20-
return ProvisionerJob(cmd, ProvisionerJobOptions{
19+
func WorkspaceBuild(ctx context.Context, writer io.Writer, client *codersdk.Client, build uuid.UUID, before time.Time) error {
20+
return ProvisionerJob(ctx, writer, ProvisionerJobOptions{
2121
Fetch: func() (codersdk.ProvisionerJob, error) {
22-
build, err := client.WorkspaceBuild(cmd.Context(), build)
22+
build, err := client.WorkspaceBuild(ctx, build)
2323
return build.Job, err
2424
},
2525
Logs: func() (<-chan codersdk.ProvisionerJobLog, error) {
26-
return client.WorkspaceBuildLogsAfter(cmd.Context(), build, before)
26+
return client.WorkspaceBuildLogsAfter(ctx, build, before)
2727
},
2828
})
2929
}
@@ -39,25 +39,25 @@ type ProvisionerJobOptions struct {
3939
}
4040

4141
// ProvisionerJob renders a provisioner job with interactive cancellation.
42-
func ProvisionerJob(cmd *cobra.Command, opts ProvisionerJobOptions) error {
42+
func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOptions) error {
4343
if opts.FetchInterval == 0 {
4444
opts.FetchInterval = time.Second
4545
}
46+
ctx, cancelFunc := context.WithCancel(ctx)
47+
defer cancelFunc()
4648

4749
var (
4850
currentStage = "Queued"
4951
currentStageStartedAt = time.Now().UTC()
5052
didLogBetweenStage = false
51-
ctx, cancelFunc = context.WithCancel(cmd.Context())
5253

5354
errChan = make(chan error, 1)
5455
job codersdk.ProvisionerJob
5556
jobMutex sync.Mutex
5657
)
57-
defer cancelFunc()
5858

5959
printStage := func() {
60-
_, _ = fmt.Fprintf(cmd.OutOrStdout(), Styles.Prompt.Render("⧗")+"%s\n", Styles.Field.Render(currentStage))
60+
_, _ = fmt.Fprintf(writer, Styles.Prompt.Render("⧗")+"%s\n", Styles.Field.Render(currentStage))
6161
}
6262

6363
updateStage := func(stage string, startedAt time.Time) {
@@ -70,7 +70,7 @@ func ProvisionerJob(cmd *cobra.Command, opts ProvisionerJobOptions) error {
7070
if job.CompletedAt != nil && job.Status != codersdk.ProvisionerJobSucceeded {
7171
mark = Styles.Crossmark
7272
}
73-
_, _ = fmt.Fprintf(cmd.OutOrStdout(), prefix+mark.String()+Styles.Placeholder.Render(" %s [%dms]")+"\n", currentStage, startedAt.Sub(currentStageStartedAt).Milliseconds())
73+
_, _ = fmt.Fprintf(writer, prefix+mark.String()+Styles.Placeholder.Render(" %s [%dms]")+"\n", currentStage, startedAt.Sub(currentStageStartedAt).Milliseconds())
7474
}
7575
if stage == "" {
7676
return
@@ -116,7 +116,7 @@ func ProvisionerJob(cmd *cobra.Command, opts ProvisionerJobOptions) error {
116116
return
117117
}
118118
}
119-
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\033[2K\r\n"+Styles.FocusedPrompt.String()+Styles.Bold.Render("Gracefully canceling...")+"\n\n")
119+
_, _ = fmt.Fprintf(writer, "\033[2K\r\n"+Styles.FocusedPrompt.String()+Styles.Bold.Render("Gracefully canceling...")+"\n\n")
120120
err := opts.Cancel()
121121
if err != nil {
122122
errChan <- xerrors.Errorf("cancel: %w", err)
@@ -183,7 +183,7 @@ func ProvisionerJob(cmd *cobra.Command, opts ProvisionerJobOptions) error {
183183
jobMutex.Unlock()
184184
continue
185185
}
186-
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s %s\n", Styles.Placeholder.Render(" "), output)
186+
_, _ = fmt.Fprintf(writer, "%s %s\n", Styles.Placeholder.Render(" "), output)
187187
didLogBetweenStage = true
188188
jobMutex.Unlock()
189189
}

cli/cliui/provisionerjob_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ func newProvisionerJob(t *testing.T) provisionerJobTest {
126126
logs := make(chan codersdk.ProvisionerJobLog, 1)
127127
cmd := &cobra.Command{
128128
RunE: func(cmd *cobra.Command, args []string) error {
129-
return cliui.ProvisionerJob(cmd, cliui.ProvisionerJobOptions{
129+
return cliui.ProvisionerJob(cmd.Context(), cmd.OutOrStdout(), cliui.ProvisionerJobOptions{
130130
FetchInterval: time.Millisecond,
131131
Fetch: func() (codersdk.ProvisionerJob, error) {
132132
jobLock.Lock()

0 commit comments

Comments
 (0)