Skip to content

Commit 4867cbe

Browse files
authored
feat(cli): display devcontainers in show command (coder#16515)
Displays running devcontainers into the `coder show` CLI command.
1 parent 72f6257 commit 4867cbe

File tree

5 files changed

+121
-19
lines changed

5 files changed

+121
-19
lines changed

cli/cliui/resources.go

Lines changed: 67 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type WorkspaceResourcesOptions struct {
2828
Title string
2929
ServerVersion string
3030
ListeningPorts map[uuid.UUID]codersdk.WorkspaceAgentListeningPortsResponse
31+
Devcontainers map[uuid.UUID]codersdk.WorkspaceAgentListContainersResponse
3132
}
3233

3334
// WorkspaceResources displays the connection status and tree-view of provided resources.
@@ -95,15 +96,11 @@ func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource
9596
// Display all agents associated with the resource.
9697
for index, agent := range resource.Agents {
9798
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-
}
106-
}
99+
for _, row := range renderListeningPorts(options, agent.ID, index, totalAgents) {
100+
tableWriter.AppendRow(row)
101+
}
102+
for _, row := range renderDevcontainers(options, agent.ID, index, totalAgents) {
103+
tableWriter.AppendRow(row)
107104
}
108105
}
109106
tableWriter.AppendSeparator()
@@ -137,10 +134,28 @@ func renderAgentRow(agent codersdk.WorkspaceAgent, index, totalAgents int, optio
137134
return row
138135
}
139136

140-
func renderPortRow(port codersdk.WorkspaceAgentListeningPort, index, totalPorts int) table.Row {
137+
func renderListeningPorts(wro WorkspaceResourcesOptions, agentID uuid.UUID, idx, total int) []table.Row {
138+
var rows []table.Row
139+
if wro.ListeningPorts == nil {
140+
return []table.Row{}
141+
}
142+
lp, ok := wro.ListeningPorts[agentID]
143+
if !ok || len(lp.Ports) == 0 {
144+
return []table.Row{}
145+
}
146+
rows = append(rows, table.Row{
147+
fmt.Sprintf(" %s─ Open Ports", renderPipe(idx, total)),
148+
})
149+
for idx, port := range lp.Ports {
150+
rows = append(rows, renderPortRow(port, idx, len(lp.Ports)))
151+
}
152+
return rows
153+
}
154+
155+
func renderPortRow(port codersdk.WorkspaceAgentListeningPort, idx, total int) table.Row {
141156
var sb strings.Builder
142157
_, _ = sb.WriteString(" ")
143-
_, _ = sb.WriteString(renderPipe(index, totalPorts))
158+
_, _ = sb.WriteString(renderPipe(idx, total))
144159
_, _ = sb.WriteString("─ ")
145160
_, _ = sb.WriteString(pretty.Sprintf(DefaultStyles.Code, "%5d/%s", port.Port, port.Network))
146161
if port.ProcessName != "" {
@@ -149,6 +164,47 @@ func renderPortRow(port codersdk.WorkspaceAgentListeningPort, index, totalPorts
149164
return table.Row{sb.String()}
150165
}
151166

167+
func renderDevcontainers(wro WorkspaceResourcesOptions, agentID uuid.UUID, index, totalAgents int) []table.Row {
168+
var rows []table.Row
169+
if wro.Devcontainers == nil {
170+
return []table.Row{}
171+
}
172+
dc, ok := wro.Devcontainers[agentID]
173+
if !ok || len(dc.Containers) == 0 {
174+
return []table.Row{}
175+
}
176+
rows = append(rows, table.Row{
177+
fmt.Sprintf(" %s─ %s", renderPipe(index, totalAgents), "Devcontainers"),
178+
})
179+
for idx, container := range dc.Containers {
180+
rows = append(rows, renderDevcontainerRow(container, idx, len(dc.Containers)))
181+
}
182+
return rows
183+
}
184+
185+
func renderDevcontainerRow(container codersdk.WorkspaceAgentDevcontainer, index, total int) table.Row {
186+
var row table.Row
187+
var sb strings.Builder
188+
_, _ = sb.WriteString(" ")
189+
_, _ = sb.WriteString(renderPipe(index, total))
190+
_, _ = sb.WriteString("─ ")
191+
_, _ = sb.WriteString(pretty.Sprintf(DefaultStyles.Code, "%s", container.FriendlyName))
192+
row = append(row, sb.String())
193+
sb.Reset()
194+
if container.Running {
195+
_, _ = sb.WriteString(pretty.Sprintf(DefaultStyles.Keyword, "(%s)", container.Status))
196+
} else {
197+
_, _ = sb.WriteString(pretty.Sprintf(DefaultStyles.Error, "(%s)", container.Status))
198+
}
199+
row = append(row, sb.String())
200+
sb.Reset()
201+
// "health" is not applicable here.
202+
row = append(row, sb.String())
203+
_, _ = sb.WriteString(container.Image)
204+
row = append(row, sb.String())
205+
return row
206+
}
207+
152208
func renderAgentStatus(agent codersdk.WorkspaceAgent) string {
153209
switch agent.Status {
154210
case codersdk.WorkspaceAgentConnecting:

cli/show.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,18 @@ func (r *RootCmd) show() *serpent.Command {
3838
}
3939
if workspace.LatestBuild.Status == codersdk.WorkspaceStatusRunning {
4040
// Get listening ports for each agent.
41-
options.ListeningPorts = fetchListeningPorts(inv, client, workspace.LatestBuild.Resources...)
41+
ports, devcontainers := fetchRuntimeResources(inv, client, workspace.LatestBuild.Resources...)
42+
options.ListeningPorts = ports
43+
options.Devcontainers = devcontainers
4244
}
4345
return cliui.WorkspaceResources(inv.Stdout, workspace.LatestBuild.Resources, options)
4446
},
4547
}
4648
}
4749

48-
func fetchListeningPorts(inv *serpent.Invocation, client *codersdk.Client, resources ...codersdk.WorkspaceResource) map[uuid.UUID]codersdk.WorkspaceAgentListeningPortsResponse {
50+
func fetchRuntimeResources(inv *serpent.Invocation, client *codersdk.Client, resources ...codersdk.WorkspaceResource) (map[uuid.UUID]codersdk.WorkspaceAgentListeningPortsResponse, map[uuid.UUID]codersdk.WorkspaceAgentListContainersResponse) {
4951
ports := make(map[uuid.UUID]codersdk.WorkspaceAgentListeningPortsResponse)
52+
devcontainers := make(map[uuid.UUID]codersdk.WorkspaceAgentListContainersResponse)
5053
var wg sync.WaitGroup
5154
var mu sync.Mutex
5255
for _, res := range resources {
@@ -65,8 +68,23 @@ func fetchListeningPorts(inv *serpent.Invocation, client *codersdk.Client, resou
6568
ports[agent.ID] = lp
6669
mu.Unlock()
6770
}()
71+
wg.Add(1)
72+
go func() {
73+
defer wg.Done()
74+
dc, err := client.WorkspaceAgentListContainers(inv.Context(), agent.ID, map[string]string{
75+
// Labels set by VSCode Remote Containers and @devcontainers/cli.
76+
"devcontainer.config_file": "",
77+
"devcontainer.local_folder": "",
78+
})
79+
if err != nil {
80+
cliui.Warnf(inv.Stderr, "Failed to get devcontainers for agent %s: %v", agent.Name, err)
81+
}
82+
mu.Lock()
83+
devcontainers[agent.ID] = dc
84+
mu.Unlock()
85+
}()
6886
}
6987
}
7088
wg.Wait()
71-
return ports
89+
return ports, devcontainers
7290
}

coderd/util/maps/maps.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,14 @@ import (
88

99
// Subset returns true if all the keys of a are present
1010
// in b and have the same values.
11+
// If the corresponding value of a[k] is the zero value in
12+
// b, Subset will skip comparing that value.
13+
// This allows checking for the presence of map keys.
1114
func Subset[T, U comparable](a, b map[T]U) bool {
15+
var uz U
1216
for ka, va := range a {
13-
if vb, ok := b[ka]; !ok || va != vb {
17+
ignoreZeroValue := va == uz
18+
if vb, ok := b[ka]; !ok || (!ignoreZeroValue && va != vb) {
1419
return false
1520
}
1621
}

coderd/util/maps/maps_test.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ func TestSubset(t *testing.T) {
1111
t.Parallel()
1212

1313
for idx, tc := range []struct {
14-
a map[string]string
15-
b map[string]string
14+
a map[string]string
15+
b map[string]string
16+
// expected value from Subset
1617
expected bool
1718
}{
1819
{
@@ -50,6 +51,24 @@ func TestSubset(t *testing.T) {
5051
b: map[string]string{"a": "1", "b": "3"},
5152
expected: false,
5253
},
54+
// Zero value
55+
{
56+
a: map[string]string{"a": "1", "b": ""},
57+
b: map[string]string{"a": "1", "b": "3"},
58+
expected: true,
59+
},
60+
// Zero value, but the other way round
61+
{
62+
a: map[string]string{"a": "1", "b": "3"},
63+
b: map[string]string{"a": "1", "b": ""},
64+
expected: false,
65+
},
66+
// Both zero values
67+
{
68+
a: map[string]string{"a": "1", "b": ""},
69+
b: map[string]string{"a": "1", "b": ""},
70+
expected: true,
71+
},
5372
} {
5473
tc := tc
5574
t.Run("#"+strconv.Itoa(idx), func(t *testing.T) {

coderd/workspaceagents_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1076,7 +1076,8 @@ func TestWorkspaceAgentContainers(t *testing.T) {
10761076
pool, err := dockertest.NewPool("")
10771077
require.NoError(t, err, "Could not connect to docker")
10781078
testLabels := map[string]string{
1079-
"com.coder.test": uuid.New().String(),
1079+
"com.coder.test": uuid.New().String(),
1080+
"com.coder.empty": "",
10801081
}
10811082
ct, err := pool.RunWithOptions(&dockertest.RunOptions{
10821083
Repository: "busybox",
@@ -1097,7 +1098,10 @@ func TestWorkspaceAgentContainers(t *testing.T) {
10971098
Repository: "busybox",
10981099
Tag: "latest",
10991100
Cmd: []string{"sleep", "infinity"},
1100-
Labels: map[string]string{"com.coder.test": "ignoreme"},
1101+
Labels: map[string]string{
1102+
"com.coder.test": "ignoreme",
1103+
"com.coder.empty": "",
1104+
},
11011105
}, func(config *docker.HostConfig) {
11021106
config.AutoRemove = true
11031107
config.RestartPolicy = docker.RestartPolicy{Name: "no"}

0 commit comments

Comments
 (0)