Skip to content

Commit 5f50dcc

Browse files
authored
feat(cli): improve devcontainer support for coder show (#18793)
Fixes coder/internal#747
1 parent 2f50b3b commit 5f50dcc

14 files changed

+600
-32
lines changed

cli/cliui/resources.go

Lines changed: 121 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,129 @@ 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+
if agent, found := slice.Find(resource.Agents, func(agent codersdk.WorkspaceAgent) bool {
205+
return agent.ID == devcontainer.Agent.ID
206+
}); found {
207+
subAgent = &agent
208+
break
209+
}
210+
}
211+
if subAgent != nil {
212+
displayName = subAgent.Name
213+
displayName += fmt.Sprintf(" (%s, %s)", subAgent.OperatingSystem, subAgent.Architecture)
214+
}
215+
}
216+
217+
if devcontainer.Container != nil {
218+
displayName += " " + pretty.Sprint(DefaultStyles.Keyword, "["+devcontainer.Container.FriendlyName+"]")
219+
}
220+
221+
// Build the main row.
222+
row := table.Row{
223+
fmt.Sprintf(" %s─ %s", renderPipe(index, total), displayName),
224+
}
225+
226+
// Add status, health, and version columns.
227+
if !wro.HideAgentState {
228+
if subAgent != nil {
229+
row = append(row, renderAgentStatus(*subAgent))
230+
row = append(row, renderAgentHealth(*subAgent))
231+
row = append(row, renderAgentVersion(subAgent.Version, wro.ServerVersion))
232+
} else {
233+
row = append(row, renderDevcontainerStatus(devcontainer.Status))
234+
row = append(row, "") // No health for devcontainer without agent.
235+
row = append(row, "") // No version for devcontainer without agent.
236+
}
237+
}
238+
239+
// Add access column.
240+
if !wro.HideAccess {
241+
if subAgent != nil {
242+
accessString := fmt.Sprintf("coder ssh %s.%s", wro.WorkspaceName, subAgent.Name)
243+
row = append(row, pretty.Sprint(DefaultStyles.Code, accessString))
244+
} else {
245+
row = append(row, "") // No access for devcontainers without agent.
246+
}
247+
}
248+
249+
rows = append(rows, row)
250+
251+
// Add error message if present.
252+
if errorMessage := devcontainer.Error; errorMessage != "" {
253+
// Cap error message length for display.
254+
if !wro.ShowDetails && len(errorMessage) > 80 {
255+
errorMessage = errorMessage[:79] + "…"
256+
}
257+
errorRow := table.Row{
258+
" × " + pretty.Sprint(DefaultStyles.Error, errorMessage),
259+
"",
260+
"",
261+
"",
262+
}
263+
if !wro.HideAccess {
264+
errorRow = append(errorRow, "")
265+
}
266+
rows = append(rows, errorRow)
267+
}
268+
269+
// Add listening ports for the devcontainer agent.
270+
if subAgent != nil {
271+
portRows := renderListeningPorts(wro, subAgent.ID, index, total)
272+
for _, portRow := range portRows {
273+
// Adjust indentation for ports under devcontainer agent.
274+
if len(portRow) > 0 {
275+
if str, ok := portRow[0].(string); ok {
276+
portRow[0] = " " + str // Add extra indentation.
277+
}
278+
}
279+
rows = append(rows, portRow)
280+
}
281+
}
282+
283+
return rows
284+
}
285+
286+
func renderDevcontainerStatus(status codersdk.WorkspaceAgentDevcontainerStatus) string {
287+
switch status {
288+
case codersdk.WorkspaceAgentDevcontainerStatusRunning:
289+
return pretty.Sprint(DefaultStyles.Keyword, "▶ running")
290+
case codersdk.WorkspaceAgentDevcontainerStatusStopped:
291+
return pretty.Sprint(DefaultStyles.Placeholder, "⏹ stopped")
292+
case codersdk.WorkspaceAgentDevcontainerStatusStarting:
293+
return pretty.Sprint(DefaultStyles.Warn, "⧗ starting")
294+
case codersdk.WorkspaceAgentDevcontainerStatusError:
295+
return pretty.Sprint(DefaultStyles.Error, "✘ error")
296+
default:
297+
return pretty.Sprint(DefaultStyles.Placeholder, "○ "+string(status))
198298
}
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
206299
}
207300

208301
func renderAgentStatus(agent codersdk.WorkspaceAgent) string {

cli/show.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,26 @@ import (
88

99
"github.com/google/uuid"
1010

11+
"github.com/coder/coder/v2/agent/agentcontainers"
1112
"github.com/coder/coder/v2/cli/cliui"
1213
"github.com/coder/coder/v2/codersdk"
1314
"github.com/coder/serpent"
1415
)
1516

1617
func (r *RootCmd) show() *serpent.Command {
1718
client := new(codersdk.Client)
19+
var details bool
1820
return &serpent.Command{
1921
Use: "show <workspace>",
2022
Short: "Display details of a workspace's resources and agents",
23+
Options: serpent.OptionSet{
24+
{
25+
Flag: "details",
26+
Description: "Show full error messages and additional details.",
27+
Default: "false",
28+
Value: serpent.BoolOf(&details),
29+
},
30+
},
2131
Middleware: serpent.Chain(
2232
serpent.RequireNArgs(1),
2333
r.InitClient(client),
@@ -35,13 +45,15 @@ func (r *RootCmd) show() *serpent.Command {
3545
options := cliui.WorkspaceResourcesOptions{
3646
WorkspaceName: workspace.Name,
3747
ServerVersion: buildInfo.Version,
48+
ShowDetails: details,
3849
}
3950
if workspace.LatestBuild.Status == codersdk.WorkspaceStatusRunning {
4051
// Get listening ports for each agent.
4152
ports, devcontainers := fetchRuntimeResources(inv, client, workspace.LatestBuild.Resources...)
4253
options.ListeningPorts = ports
4354
options.Devcontainers = devcontainers
4455
}
56+
4557
return cliui.WorkspaceResources(inv.Stdout, workspace.LatestBuild.Resources, options)
4658
},
4759
}
@@ -68,13 +80,17 @@ func fetchRuntimeResources(inv *serpent.Invocation, client *codersdk.Client, res
6880
ports[agent.ID] = lp
6981
mu.Unlock()
7082
}()
83+
84+
if agent.ParentID.Valid {
85+
continue
86+
}
7187
wg.Add(1)
7288
go func() {
7389
defer wg.Done()
7490
dc, err := client.WorkspaceAgentListContainers(inv.Context(), agent.ID, map[string]string{
7591
// Labels set by VSCode Remote Containers and @devcontainers/cli.
76-
"devcontainer.config_file": "",
77-
"devcontainer.local_folder": "",
92+
agentcontainers.DevcontainerConfigFileLabel: "",
93+
agentcontainers.DevcontainerLocalFolderLabel: "",
7894
})
7995
if err != nil {
8096
cliui.Warnf(inv.Stderr, "Failed to get devcontainers for agent %s: %v", agent.Name, err)

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