Skip to content

Commit 695d552

Browse files
authored
feat(cli): add display of open ports in coder show (#16464)
Relates to #16418 -- devcontainers will be shown in a similar manner. Without ports (status quo): ![Screenshot 2025-02-10 at 12 50 46](https://github.com/user-attachments/assets/c25fd532-2e35-469c-bb28-26e59ded3eb4) With ports: ![Screenshot 2025-02-10 at 12 50 06](https://github.com/user-attachments/assets/a4671349-5866-4e1e-848e-a6e819479793)
1 parent e9b3561 commit 695d552

File tree

2 files changed

+100
-26
lines changed

2 files changed

+100
-26
lines changed

cli/cliui/resources.go

+61-24
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import (
55
"io"
66
"sort"
77
"strconv"
8+
"strings"
89

10+
"github.com/google/uuid"
911
"github.com/jedib0t/go-pretty/v6/table"
1012
"golang.org/x/mod/semver"
1113

@@ -14,12 +16,18 @@ import (
1416
"github.com/coder/pretty"
1517
)
1618

19+
var (
20+
pipeMid = "├"
21+
pipeEnd = "└"
22+
)
23+
1724
type WorkspaceResourcesOptions struct {
1825
WorkspaceName string
1926
HideAgentState bool
2027
HideAccess bool
2128
Title string
2229
ServerVersion string
30+
ListeningPorts map[uuid.UUID]codersdk.WorkspaceAgentListeningPortsResponse
2331
}
2432

2533
// WorkspaceResources displays the connection status and tree-view of provided resources.
@@ -86,39 +94,61 @@ func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource
8694
})
8795
// Display all agents associated with the resource.
8896
for index, agent := range resource.Agents {
89-
pipe := "├"
90-
if index == len(resource.Agents)-1 {
91-
pipe = "└"
92-
}
93-
row := table.Row{
94-
// These tree from a resource!
95-
fmt.Sprintf("%s─ %s (%s, %s)", pipe, agent.Name, agent.OperatingSystem, agent.Architecture),
96-
}
97-
if !options.HideAgentState {
98-
var agentStatus, agentHealth, agentVersion string
99-
if !options.HideAgentState {
100-
agentStatus = renderAgentStatus(agent)
101-
agentHealth = renderAgentHealth(agent)
102-
agentVersion = renderAgentVersion(agent.Version, options.ServerVersion)
103-
}
104-
row = append(row, agentStatus, agentHealth, agentVersion)
105-
}
106-
if !options.HideAccess {
107-
sshCommand := "coder ssh " + options.WorkspaceName
108-
if totalAgents > 1 {
109-
sshCommand += "." + agent.Name
97+
tableWriter.AppendRow(renderAgentRow(agent, index, totalAgents, options))
98+
if options.ListeningPorts != nil {
99+
if lp, ok := options.ListeningPorts[agent.ID]; ok && len(lp.Ports) > 0 {
100+
tableWriter.AppendRow(table.Row{
101+
fmt.Sprintf(" %s─ %s", renderPipe(index, totalAgents), "Open Ports"),
102+
})
103+
for _, port := range lp.Ports {
104+
tableWriter.AppendRow(renderPortRow(port, index, totalAgents))
105+
}
110106
}
111-
sshCommand = pretty.Sprint(DefaultStyles.Code, sshCommand)
112-
row = append(row, sshCommand)
113107
}
114-
tableWriter.AppendRow(row)
115108
}
116109
tableWriter.AppendSeparator()
117110
}
118111
_, err := fmt.Fprintln(writer, tableWriter.Render())
119112
return err
120113
}
121114

115+
func renderAgentRow(agent codersdk.WorkspaceAgent, index, totalAgents int, options WorkspaceResourcesOptions) table.Row {
116+
row := table.Row{
117+
// These tree from a resource!
118+
fmt.Sprintf("%s─ %s (%s, %s)", renderPipe(index, totalAgents), agent.Name, agent.OperatingSystem, agent.Architecture),
119+
}
120+
if !options.HideAgentState {
121+
var agentStatus, agentHealth, agentVersion string
122+
if !options.HideAgentState {
123+
agentStatus = renderAgentStatus(agent)
124+
agentHealth = renderAgentHealth(agent)
125+
agentVersion = renderAgentVersion(agent.Version, options.ServerVersion)
126+
}
127+
row = append(row, agentStatus, agentHealth, agentVersion)
128+
}
129+
if !options.HideAccess {
130+
sshCommand := "coder ssh " + options.WorkspaceName
131+
if totalAgents > 1 {
132+
sshCommand += "." + agent.Name
133+
}
134+
sshCommand = pretty.Sprint(DefaultStyles.Code, sshCommand)
135+
row = append(row, sshCommand)
136+
}
137+
return row
138+
}
139+
140+
func renderPortRow(port codersdk.WorkspaceAgentListeningPort, index, totalPorts int) table.Row {
141+
var sb strings.Builder
142+
_, _ = sb.WriteString(" ")
143+
_, _ = sb.WriteString(renderPipe(index, totalPorts))
144+
_, _ = sb.WriteString("─ ")
145+
_, _ = sb.WriteString(pretty.Sprintf(DefaultStyles.Code, "%5d/%s", port.Port, port.Network))
146+
if port.ProcessName != "" {
147+
_, _ = sb.WriteString(pretty.Sprintf(DefaultStyles.Keyword, " [%s]", port.ProcessName))
148+
}
149+
return table.Row{sb.String()}
150+
}
151+
122152
func renderAgentStatus(agent codersdk.WorkspaceAgent) string {
123153
switch agent.Status {
124154
case codersdk.WorkspaceAgentConnecting:
@@ -163,3 +193,10 @@ func renderAgentVersion(agentVersion, serverVersion string) string {
163193
}
164194
return pretty.Sprint(DefaultStyles.Keyword, agentVersion)
165195
}
196+
197+
func renderPipe(idx, total int) string {
198+
if idx == total-1 {
199+
return pipeEnd
200+
}
201+
return pipeMid
202+
}

cli/show.go

+39-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
package cli
22

33
import (
4+
"sort"
5+
"sync"
6+
47
"golang.org/x/xerrors"
58

9+
"github.com/google/uuid"
10+
611
"github.com/coder/coder/v2/cli/cliui"
712
"github.com/coder/coder/v2/codersdk"
813
"github.com/coder/serpent"
@@ -26,10 +31,42 @@ func (r *RootCmd) show() *serpent.Command {
2631
if err != nil {
2732
return xerrors.Errorf("get workspace: %w", err)
2833
}
29-
return cliui.WorkspaceResources(inv.Stdout, workspace.LatestBuild.Resources, cliui.WorkspaceResourcesOptions{
34+
35+
options := cliui.WorkspaceResourcesOptions{
3036
WorkspaceName: workspace.Name,
3137
ServerVersion: buildInfo.Version,
32-
})
38+
}
39+
if workspace.LatestBuild.Status == codersdk.WorkspaceStatusRunning {
40+
// Get listening ports for each agent.
41+
options.ListeningPorts = fetchListeningPorts(inv, client, workspace.LatestBuild.Resources...)
42+
}
43+
return cliui.WorkspaceResources(inv.Stdout, workspace.LatestBuild.Resources, options)
3344
},
3445
}
3546
}
47+
48+
func fetchListeningPorts(inv *serpent.Invocation, client *codersdk.Client, resources ...codersdk.WorkspaceResource) map[uuid.UUID]codersdk.WorkspaceAgentListeningPortsResponse {
49+
ports := make(map[uuid.UUID]codersdk.WorkspaceAgentListeningPortsResponse)
50+
var wg sync.WaitGroup
51+
var mu sync.Mutex
52+
for _, res := range resources {
53+
for _, agent := range res.Agents {
54+
wg.Add(1)
55+
go func() {
56+
defer wg.Done()
57+
lp, err := client.WorkspaceAgentListeningPorts(inv.Context(), agent.ID)
58+
if err != nil {
59+
cliui.Warnf(inv.Stderr, "Failed to get listening ports for agent %s: %v", agent.Name, err)
60+
}
61+
sort.Slice(lp.Ports, func(i, j int) bool {
62+
return lp.Ports[i].Port < lp.Ports[j].Port
63+
})
64+
mu.Lock()
65+
ports[agent.ID] = lp
66+
mu.Unlock()
67+
}()
68+
}
69+
}
70+
wg.Wait()
71+
return ports
72+
}

0 commit comments

Comments
 (0)