Skip to content

Commit 762257b

Browse files
committed
feat(cli): improve devcontainer support for coder show
Fixes coder/internal#747
1 parent 211393a commit 762257b

13 files changed

+586
-29
lines changed

cli/cliui/resources.go

Lines changed: 127 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"golang.org/x/mod/semver"
1313

1414
"github.com/coder/coder/v2/coderd/database/dbtime"
15+
"github.com/coder/coder/v2/coderd/util/slice"
1516
"github.com/coder/coder/v2/codersdk"
1617
"github.com/coder/pretty"
1718
)
@@ -29,6 +30,7 @@ type WorkspaceResourcesOptions struct {
2930
ServerVersion string
3031
ListeningPorts map[uuid.UUID]codersdk.WorkspaceAgentListeningPortsResponse
3132
Devcontainers map[uuid.UUID]codersdk.WorkspaceAgentListContainersResponse
33+
ShowDetails bool
3234
}
3335

3436
// WorkspaceResources displays the connection status and tree-view of provided resources.
@@ -69,7 +71,11 @@ func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource
6971

7072
totalAgents := 0
7173
for _, resource := range resources {
72-
totalAgents += len(resource.Agents)
74+
for _, agent := range resource.Agents {
75+
if !agent.ParentID.Valid {
76+
totalAgents++
77+
}
78+
}
7379
}
7480

7581
for _, resource := range resources {
@@ -94,12 +100,15 @@ func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource
94100
"",
95101
})
96102
// Display all agents associated with the resource.
97-
for index, agent := range resource.Agents {
103+
agents := slice.Filter(resource.Agents, func(agent codersdk.WorkspaceAgent) bool {
104+
return !agent.ParentID.Valid
105+
})
106+
for index, agent := range agents {
98107
tableWriter.AppendRow(renderAgentRow(agent, index, totalAgents, options))
99108
for _, row := range renderListeningPorts(options, agent.ID, index, totalAgents) {
100109
tableWriter.AppendRow(row)
101110
}
102-
for _, row := range renderDevcontainers(options, agent.ID, index, totalAgents) {
111+
for _, row := range renderDevcontainers(resources, options, agent.ID, index, totalAgents) {
103112
tableWriter.AppendRow(row)
104113
}
105114
}
@@ -125,7 +134,7 @@ func renderAgentRow(agent codersdk.WorkspaceAgent, index, totalAgents int, optio
125134
}
126135
if !options.HideAccess {
127136
sshCommand := "coder ssh " + options.WorkspaceName
128-
if totalAgents > 1 {
137+
if totalAgents > 1 || len(options.Devcontainers) > 0 {
129138
sshCommand += "." + agent.Name
130139
}
131140
sshCommand = pretty.Sprint(DefaultStyles.Code, sshCommand)
@@ -164,45 +173,135 @@ func renderPortRow(port codersdk.WorkspaceAgentListeningPort, idx, total int) ta
164173
return table.Row{sb.String()}
165174
}
166175

167-
func renderDevcontainers(wro WorkspaceResourcesOptions, agentID uuid.UUID, index, totalAgents int) []table.Row {
176+
func renderDevcontainers(resources []codersdk.WorkspaceResource, wro WorkspaceResourcesOptions, agentID uuid.UUID, index, totalAgents int) []table.Row {
168177
var rows []table.Row
169178
if wro.Devcontainers == nil {
170179
return []table.Row{}
171180
}
172181
dc, ok := wro.Devcontainers[agentID]
173-
if !ok || len(dc.Containers) == 0 {
182+
if !ok || len(dc.Devcontainers) == 0 {
174183
return []table.Row{}
175184
}
176185
rows = append(rows, table.Row{
177186
fmt.Sprintf(" %s─ %s", renderPipe(index, totalAgents), "Devcontainers"),
178187
})
179-
for idx, container := range dc.Containers {
180-
rows = append(rows, renderDevcontainerRow(container, idx, len(dc.Containers)))
188+
for idx, devcontainer := range dc.Devcontainers {
189+
rows = append(rows, renderDevcontainerRow(resources, devcontainer, idx, len(dc.Devcontainers), wro)...)
181190
}
182191
return rows
183192
}
184193

185-
func renderDevcontainerRow(container codersdk.WorkspaceAgentContainer, 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))
194+
func renderDevcontainerRow(resources []codersdk.WorkspaceResource, devcontainer codersdk.WorkspaceAgentDevcontainer, index, total int, wro WorkspaceResourcesOptions) []table.Row {
195+
var rows []table.Row
196+
197+
// If the devcontainer is running and has an associated agent, we want to
198+
// display the agent's details. Otherwise, we just display the devcontainer
199+
// name and status.
200+
var subAgent *codersdk.WorkspaceAgent
201+
displayName := devcontainer.Name
202+
if devcontainer.Agent != nil && devcontainer.Status == codersdk.WorkspaceAgentDevcontainerStatusRunning {
203+
for _, resource := range resources {
204+
for _, agent := range resource.Agents {
205+
if agent.ID == devcontainer.Agent.ID {
206+
subAgent = &agent
207+
break
208+
}
209+
}
210+
if subAgent != nil {
211+
break
212+
}
213+
}
214+
215+
displayName = devcontainer.Agent.Name
216+
if subAgent != nil {
217+
displayName += fmt.Sprintf(" (%s, %s)", subAgent.OperatingSystem, subAgent.Architecture)
218+
} else {
219+
displayName += " (linux, amd64)"
220+
}
221+
}
222+
223+
if devcontainer.Container != nil {
224+
displayName += " " + pretty.Sprint(DefaultStyles.Keyword, "["+devcontainer.Container.FriendlyName+"]")
225+
}
226+
227+
// Build the main row.
228+
row := table.Row{
229+
fmt.Sprintf(" %s─ %s", renderPipe(index, total), displayName),
230+
}
231+
232+
// Add status, health, and version columns.
233+
if !wro.HideAgentState {
234+
if subAgent != nil {
235+
row = append(row, renderAgentStatus(*subAgent))
236+
row = append(row, renderAgentHealth(*subAgent))
237+
row = append(row, renderAgentVersion(subAgent.Version, wro.ServerVersion))
238+
} else {
239+
row = append(row, renderDevcontainerStatus(devcontainer.Status))
240+
row = append(row, "") // No health for devcontainer without agent.
241+
row = append(row, "") // No version for devcontainer without agent.
242+
}
243+
}
244+
245+
// Add access column.
246+
if !wro.HideAccess {
247+
if subAgent != nil {
248+
accessString := fmt.Sprintf("coder ssh %s.%s", wro.WorkspaceName, subAgent.Name)
249+
row = append(row, pretty.Sprint(DefaultStyles.Code, accessString))
250+
} else {
251+
row = append(row, "") // No access for devcontainers without agent.
252+
}
253+
}
254+
255+
rows = append(rows, row)
256+
257+
// Add error message if present.
258+
if errorMessage := devcontainer.Error; errorMessage != "" {
259+
// Cap error message length for display.
260+
if !wro.ShowDetails && len(errorMessage) > 80 {
261+
errorMessage = errorMessage[:77] + "..."
262+
}
263+
errorRow := table.Row{
264+
" × " + pretty.Sprint(DefaultStyles.Error, errorMessage),
265+
"",
266+
"",
267+
"",
268+
}
269+
if !wro.HideAccess {
270+
errorRow = append(errorRow, "")
271+
}
272+
rows = append(rows, errorRow)
273+
}
274+
275+
// Add listening ports for the devcontainer agent.
276+
if subAgent != nil {
277+
portRows := renderListeningPorts(wro, subAgent.ID, index, total)
278+
for _, portRow := range portRows {
279+
// Adjust indentation for ports under devcontainer agent.
280+
if len(portRow) > 0 {
281+
if str, ok := portRow[0].(string); ok {
282+
portRow[0] = " " + str[3:] // Add extra indentation.
283+
}
284+
}
285+
rows = append(rows, portRow)
286+
}
287+
}
288+
289+
return rows
290+
}
291+
292+
func renderDevcontainerStatus(status codersdk.WorkspaceAgentDevcontainerStatus) string {
293+
switch status {
294+
case codersdk.WorkspaceAgentDevcontainerStatusRunning:
295+
return pretty.Sprint(DefaultStyles.Keyword, "▶ running")
296+
case codersdk.WorkspaceAgentDevcontainerStatusStopped:
297+
return pretty.Sprint(DefaultStyles.Placeholder, "⏹ stopped")
298+
case codersdk.WorkspaceAgentDevcontainerStatusStarting:
299+
return pretty.Sprint(DefaultStyles.Warn, "⧗ starting")
300+
case codersdk.WorkspaceAgentDevcontainerStatusError:
301+
return pretty.Sprint(DefaultStyles.Error, "✘ error")
302+
default:
303+
return pretty.Sprint(DefaultStyles.Placeholder, "○ "+string(status))
198304
}
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
206305
}
207306

208307
func renderAgentStatus(agent codersdk.WorkspaceAgent) string {

cli/show.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,17 @@ import (
1515

1616
func (r *RootCmd) show() *serpent.Command {
1717
client := new(codersdk.Client)
18+
var details bool
1819
return &serpent.Command{
1920
Use: "show <workspace>",
2021
Short: "Display details of a workspace's resources and agents",
22+
Options: serpent.OptionSet{
23+
{
24+
Flag: "details",
25+
Description: "Show full error messages and additional details.",
26+
Value: serpent.BoolOf(&details),
27+
},
28+
},
2129
Middleware: serpent.Chain(
2230
serpent.RequireNArgs(1),
2331
r.InitClient(client),
@@ -35,13 +43,15 @@ func (r *RootCmd) show() *serpent.Command {
3543
options := cliui.WorkspaceResourcesOptions{
3644
WorkspaceName: workspace.Name,
3745
ServerVersion: buildInfo.Version,
46+
ShowDetails: details,
3847
}
3948
if workspace.LatestBuild.Status == codersdk.WorkspaceStatusRunning {
4049
// Get listening ports for each agent.
4150
ports, devcontainers := fetchRuntimeResources(inv, client, workspace.LatestBuild.Resources...)
4251
options.ListeningPorts = ports
4352
options.Devcontainers = devcontainers
4453
}
54+
4555
return cliui.WorkspaceResources(inv.Stdout, workspace.LatestBuild.Resources, options)
4656
},
4757
}

0 commit comments

Comments
 (0)