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

Commitfb9dc4f

Browse files
feat: Improve resource preview and first-time experience (#946)
* Improve CLI documentation* feat: Allow workspace resources to attach multiple agentsThis enables a "kubernetes_pod" to attach multiple agents thatcould be for multiple services. Each agent is required to havea unique name, so SSH syntax is:`coder ssh <workspace>.<agent>`A resource can have zero agents too, they aren't required.* Add tree view* Improve table UI* feat: Allow workspace resources to attach multiple agentsThis enables a "kubernetes_pod" to attach multiple agents thatcould be for multiple services. Each agent is required to havea unique name, so SSH syntax is:`coder ssh <workspace>.<agent>`A resource can have zero agents too, they aren't required.* Rename `tunnel` to `skip-tunnel`This command was `true` by default, which causesa confusing user experience.* Add disclaimer about editing templates* Add help to template create* Improve workspace create flow* Add end-to-end test for config-ssh* Improve testing of config-ssh* Fix workspace list* Fix config ssh tests* Update cli/configssh.goCo-authored-by: Cian Johnston <public@cianjohnston.ie>* Fix requested changes* Remove socat requirement* Fix resources not reading in TTYCo-authored-by: Cian Johnston <public@cianjohnston.ie>
1 parent19b4323 commitfb9dc4f

Some content is hidden

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

41 files changed

+978
-316
lines changed

‎agent/agent.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ func (a *agent) run(ctx context.Context) {
101101

102102
func (a*agent)handlePeerConn(ctx context.Context,conn*peer.Conn) {
103103
gofunc() {
104+
select {
105+
case<-a.closed:
106+
_=conn.Close()
107+
case<-conn.Closed():
108+
}
104109
<-conn.Closed()
105110
a.connCloseWait.Done()
106111
}()

‎agent/agent_test.go

Lines changed: 80 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ package agent_test
22

33
import (
44
"context"
5+
"fmt"
6+
"io"
7+
"net"
8+
"os/exec"
59
"runtime"
10+
"strconv"
611
"strings"
712
"testing"
813

@@ -29,7 +34,8 @@ func TestAgent(t *testing.T) {
2934
t.Parallel()
3035
t.Run("SessionExec",func(t*testing.T) {
3136
t.Parallel()
32-
session:=setupSSH(t)
37+
session:=setupSSHSession(t)
38+
3339
command:="echo test"
3440
ifruntime.GOOS=="windows" {
3541
command="cmd.exe /c echo test"
@@ -41,7 +47,7 @@ func TestAgent(t *testing.T) {
4147

4248
t.Run("GitSSH",func(t*testing.T) {
4349
t.Parallel()
44-
session:=setupSSH(t)
50+
session:=setupSSHSession(t)
4551
command:="sh -c 'echo $GIT_SSH_COMMAND'"
4652
ifruntime.GOOS=="windows" {
4753
command="cmd.exe /c echo %GIT_SSH_COMMAND%"
@@ -53,7 +59,7 @@ func TestAgent(t *testing.T) {
5359

5460
t.Run("SessionTTY",func(t*testing.T) {
5561
t.Parallel()
56-
session:=setupSSH(t)
62+
session:=setupSSHSession(t)
5763
prompt:="$"
5864
command:="bash"
5965
ifruntime.GOOS=="windows" {
@@ -76,9 +82,77 @@ func TestAgent(t *testing.T) {
7682
err=session.Wait()
7783
require.NoError(t,err)
7884
})
85+
86+
t.Run("LocalForwarding",func(t*testing.T) {
87+
t.Parallel()
88+
random,err:=net.Listen("tcp","127.0.0.1:0")
89+
require.NoError(t,err)
90+
_=random.Close()
91+
tcpAddr,valid:=random.Addr().(*net.TCPAddr)
92+
require.True(t,valid)
93+
randomPort:=tcpAddr.Port
94+
95+
local,err:=net.Listen("tcp","127.0.0.1:0")
96+
require.NoError(t,err)
97+
tcpAddr,valid=local.Addr().(*net.TCPAddr)
98+
require.True(t,valid)
99+
localPort:=tcpAddr.Port
100+
done:=make(chanstruct{})
101+
gofunc() {
102+
conn,err:=local.Accept()
103+
require.NoError(t,err)
104+
_=conn.Close()
105+
close(done)
106+
}()
107+
108+
err=setupSSHCommand(t, []string{"-L",fmt.Sprintf("%d:127.0.0.1:%d",randomPort,localPort)}, []string{"echo","test"}).Start()
109+
require.NoError(t,err)
110+
111+
conn,err:=net.Dial("tcp","127.0.0.1:"+strconv.Itoa(localPort))
112+
require.NoError(t,err)
113+
conn.Close()
114+
<-done
115+
})
79116
}
80117

81-
funcsetupSSH(t*testing.T)*ssh.Session {
118+
funcsetupSSHCommand(t*testing.T,beforeArgs []string,afterArgs []string)*exec.Cmd {
119+
agentConn:=setupAgent(t)
120+
listener,err:=net.Listen("tcp","127.0.0.1:0")
121+
require.NoError(t,err)
122+
gofunc() {
123+
for {
124+
conn,err:=listener.Accept()
125+
iferr!=nil {
126+
return
127+
}
128+
ssh,err:=agentConn.SSH()
129+
require.NoError(t,err)
130+
goio.Copy(conn,ssh)
131+
goio.Copy(ssh,conn)
132+
}
133+
}()
134+
t.Cleanup(func() {
135+
_=listener.Close()
136+
})
137+
tcpAddr,valid:=listener.Addr().(*net.TCPAddr)
138+
require.True(t,valid)
139+
args:=append(beforeArgs,
140+
"-o","HostName "+tcpAddr.IP.String(),
141+
"-o","Port "+strconv.Itoa(tcpAddr.Port),
142+
"-o","StrictHostKeyChecking=no","host")
143+
args=append(args,afterArgs...)
144+
returnexec.Command("ssh",args...)
145+
}
146+
147+
funcsetupSSHSession(t*testing.T)*ssh.Session {
148+
sshClient,err:=setupAgent(t).SSHClient()
149+
require.NoError(t,err)
150+
session,err:=sshClient.NewSession()
151+
require.NoError(t,err)
152+
returnsession
153+
}
154+
155+
funcsetupAgent(t*testing.T)*agent.Conn {
82156
client,server:=provisionersdk.TransportPipe()
83157
closer:=agent.New(func(ctx context.Context,opts*peer.ConnOptions) (*peerbroker.Listener,error) {
84158
returnpeerbroker.Listen(server,nil,opts)
@@ -100,14 +174,9 @@ func setupSSH(t *testing.T) *ssh.Session {
100174
t.Cleanup(func() {
101175
_=conn.Close()
102176
})
103-
agentClient:=&agent.Conn{
177+
178+
return&agent.Conn{
104179
Negotiator:api,
105180
Conn:conn,
106181
}
107-
sshClient,err:=agentClient.SSHClient()
108-
require.NoError(t,err)
109-
session,err:=sshClient.NewSession()
110-
require.NoError(t,err)
111-
112-
returnsession
113182
}

‎cli/cliui/cliui.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ var Styles = struct {
2626
Checkmark,
2727
Code,
2828
Crossmark,
29+
Error,
2930
Field,
3031
Keyword,
3132
Paragraph,
@@ -41,6 +42,7 @@ var Styles = struct {
4142
Checkmark:defaultStyles.Checkmark,
4243
Code:defaultStyles.Code,
4344
Crossmark:defaultStyles.Error.Copy().SetString("✘"),
45+
Error:defaultStyles.Error,
4446
Field:defaultStyles.Code.Copy().Foreground(lipgloss.AdaptiveColor{Light:"#000000",Dark:"#FFFFFF"}),
4547
Keyword:defaultStyles.Keyword,
4648
Paragraph:defaultStyles.Paragraph,

‎cli/cliui/prompt.go

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"bytes"
66
"encoding/json"
77
"fmt"
8-
"io"
98
"os"
109
"os/signal"
1110
"runtime"
@@ -45,11 +44,11 @@ func Prompt(cmd *cobra.Command, opts PromptOptions) (string, error) {
4544
varlinestring
4645
varerrerror
4746

48-
inFile,valid:=cmd.InOrStdin().(*os.File)
49-
ifopts.Secret&&valid&&isatty.IsTerminal(inFile.Fd()) {
47+
inFile,isInputFile:=cmd.InOrStdin().(*os.File)
48+
ifopts.Secret&&isInputFile&&isatty.IsTerminal(inFile.Fd()) {
5049
line,err=speakeasy.Ask("")
5150
}else {
52-
if!opts.IsConfirm&&runtime.GOOS=="darwin"&&valid {
51+
if!opts.IsConfirm&&runtime.GOOS=="darwin"&&isInputFile {
5352
varrestorefunc()
5453
restore,err=removeLineLengthLimit(int(inFile.Fd()))
5554
iferr!=nil {
@@ -66,22 +65,7 @@ func Prompt(cmd *cobra.Command, opts PromptOptions) (string, error) {
6665
// This enables multiline JSON to be pasted into an input, and have
6766
// it parse properly.
6867
iferr==nil&& (strings.HasPrefix(line,"{")||strings.HasPrefix(line,"[")) {
69-
pipeReader,pipeWriter:=io.Pipe()
70-
deferpipeWriter.Close()
71-
deferpipeReader.Close()
72-
gofunc() {
73-
_,_=pipeWriter.Write([]byte(line))
74-
_,_=reader.WriteTo(pipeWriter)
75-
}()
76-
varrawMessage json.RawMessage
77-
err:=json.NewDecoder(pipeReader).Decode(&rawMessage)
78-
iferr==nil {
79-
varbuf bytes.Buffer
80-
err=json.Compact(&buf,rawMessage)
81-
iferr==nil {
82-
line=buf.String()
83-
}
84-
}
68+
line,err=promptJSON(reader,line)
8569
}
8670
}
8771
iferr!=nil {
@@ -118,3 +102,39 @@ func Prompt(cmd *cobra.Command, opts PromptOptions) (string, error) {
118102
return"",Canceled
119103
}
120104
}
105+
106+
funcpromptJSON(reader*bufio.Reader,linestring) (string,error) {
107+
vardata bytes.Buffer
108+
for {
109+
_,_=data.WriteString(line)
110+
varrawMessage json.RawMessage
111+
err:=json.Unmarshal(data.Bytes(),&rawMessage)
112+
iferr!=nil {
113+
iferr.Error()!="unexpected end of JSON input" {
114+
// If a real syntax error occurs in JSON,
115+
// we want to return that partial line to the user.
116+
err=nil
117+
line=data.String()
118+
break
119+
}
120+
121+
// Read line-by-line. We can't use a JSON decoder
122+
// here because it doesn't work by newline, so
123+
// reads will block.
124+
line,err=reader.ReadString('\n')
125+
iferr!=nil {
126+
break
127+
}
128+
continue
129+
}
130+
// Compacting the JSON makes it easier for parsing and testing.
131+
rawJSON:=data.Bytes()
132+
data.Reset()
133+
err=json.Compact(&data,rawJSON)
134+
iferr!=nil {
135+
returnline,xerrors.Errorf("compact json: %w",err)
136+
}
137+
returndata.String(),nil
138+
}
139+
returnline,nil
140+
}

‎cli/cliui/resources.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package cliui
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"sort"
7+
"strconv"
8+
9+
"github.com/jedib0t/go-pretty/v6/table"
10+
11+
"github.com/coder/coder/coderd/database"
12+
"github.com/coder/coder/codersdk"
13+
)
14+
15+
typeWorkspaceResourcesOptionsstruct {
16+
WorkspaceNamestring
17+
HideAgentStatebool
18+
HideAccessbool
19+
Titlestring
20+
}
21+
22+
// WorkspaceResources displays the connection status and tree-view of provided resources.
23+
// ┌────────────────────────────────────────────────────────────────────────────┐
24+
// │ RESOURCE STATUS ACCESS │
25+
// ├────────────────────────────────────────────────────────────────────────────┤
26+
// │ google_compute_disk.root persistent │
27+
// ├────────────────────────────────────────────────────────────────────────────┤
28+
// │ google_compute_instance.dev ephemeral │
29+
// │ └─ dev (linux, amd64) ⦾ connecting [10s] coder ssh dev.dev │
30+
// ├────────────────────────────────────────────────────────────────────────────┤
31+
// │ kubernetes_pod.dev ephemeral │
32+
// │ ├─ go (linux, amd64) ⦿ connected coder ssh dev.go │
33+
// │ └─ postgres (linux, amd64) ⦾ disconnected [4s] coder ssh dev.postgres │
34+
// └────────────────────────────────────────────────────────────────────────────┘
35+
funcWorkspaceResources(writer io.Writer,resources []codersdk.WorkspaceResource,optionsWorkspaceResourcesOptions)error {
36+
// Sort resources by type for consistent output.
37+
sort.Slice(resources,func(i,jint)bool {
38+
returnresources[i].Type<resources[j].Type
39+
})
40+
41+
// Address on stop indexes whether a resource still exists when in the stopped transition.
42+
addressOnStop:=map[string]codersdk.WorkspaceResource{}
43+
for_,resource:=rangeresources {
44+
ifresource.Transition!=database.WorkspaceTransitionStop {
45+
continue
46+
}
47+
addressOnStop[resource.Address]=resource
48+
}
49+
// Displayed stores whether a resource has already been shown.
50+
// Resources can be stored with numerous states, which we
51+
// process prior to display.
52+
displayed:=map[string]struct{}{}
53+
54+
tableWriter:=table.NewWriter()
55+
ifoptions.Title!="" {
56+
tableWriter.SetTitle(options.Title)
57+
}
58+
tableWriter.SetStyle(table.StyleLight)
59+
tableWriter.Style().Options.SeparateColumns=false
60+
row:= table.Row{"Resource","Status"}
61+
if!options.HideAccess {
62+
row=append(row,"Access")
63+
}
64+
tableWriter.AppendHeader(row)
65+
66+
totalAgents:=0
67+
for_,resource:=rangeresources {
68+
totalAgents+=len(resource.Agents)
69+
}
70+
71+
for_,resource:=rangeresources {
72+
ifresource.Type=="random_string" {
73+
// Hide resources that aren't substantial to a user!
74+
// This is an unfortunate case, and we should allow
75+
// callers to hide resources eventually.
76+
continue
77+
}
78+
if_,shown:=displayed[resource.Address];shown {
79+
// The same resource can have multiple transitions.
80+
continue
81+
}
82+
displayed[resource.Address]=struct{}{}
83+
84+
// Sort agents by name for consistent output.
85+
sort.Slice(resource.Agents,func(i,jint)bool {
86+
returnresource.Agents[i].Name<resource.Agents[j].Name
87+
})
88+
_,existsOnStop:=addressOnStop[resource.Address]
89+
resourceState:="ephemeral"
90+
ifexistsOnStop {
91+
resourceState="persistent"
92+
}
93+
// Display a line for the resource.
94+
tableWriter.AppendRow(table.Row{
95+
Styles.Bold.Render(resource.Type+"."+resource.Name),
96+
Styles.Placeholder.Render(resourceState),
97+
"",
98+
})
99+
// Display all agents associated with the resource.
100+
forindex,agent:=rangeresource.Agents {
101+
sshCommand:="coder ssh "+options.WorkspaceName
102+
iftotalAgents>1 {
103+
sshCommand+="."+agent.Name
104+
}
105+
sshCommand=Styles.Code.Render(sshCommand)
106+
varagentStatusstring
107+
if!options.HideAgentState {
108+
switchagent.Status {
109+
casecodersdk.WorkspaceAgentConnecting:
110+
since:=database.Now().Sub(agent.CreatedAt)
111+
agentStatus=Styles.Warn.Render("⦾ connecting")+" "+
112+
Styles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]")
113+
casecodersdk.WorkspaceAgentDisconnected:
114+
since:=database.Now().Sub(*agent.DisconnectedAt)
115+
agentStatus=Styles.Error.Render("⦾ disconnected")+" "+
116+
Styles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]")
117+
casecodersdk.WorkspaceAgentConnected:
118+
agentStatus=Styles.Keyword.Render("⦿ connected")
119+
}
120+
}
121+
122+
pipe:="├"
123+
ifindex==len(resource.Agents)-1 {
124+
pipe="└"
125+
}
126+
row:= table.Row{
127+
// These tree from a resource!
128+
fmt.Sprintf("%s─ %s (%s, %s)",pipe,agent.Name,agent.OperatingSystem,agent.Architecture),
129+
agentStatus,
130+
}
131+
if!options.HideAccess {
132+
row=append(row,sshCommand)
133+
}
134+
tableWriter.AppendRow(row)
135+
}
136+
tableWriter.AppendSeparator()
137+
}
138+
_,err:=fmt.Fprintln(writer,tableWriter.Render())
139+
returnerr
140+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp