Skip to content

Commit 28e09c5

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

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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy