diff --git a/cli/cliui/resources.go b/cli/cliui/resources.go index be112ea177200..36ce4194d72c8 100644 --- a/cli/cliui/resources.go +++ b/cli/cliui/resources.go @@ -12,6 +12,7 @@ import ( "golang.org/x/mod/semver" "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/coder/v2/coderd/util/slice" "github.com/coder/coder/v2/codersdk" "github.com/coder/pretty" ) @@ -29,6 +30,7 @@ type WorkspaceResourcesOptions struct { ServerVersion string ListeningPorts map[uuid.UUID]codersdk.WorkspaceAgentListeningPortsResponse Devcontainers map[uuid.UUID]codersdk.WorkspaceAgentListContainersResponse + ShowDetails bool } // WorkspaceResources displays the connection status and tree-view of provided resources. @@ -69,7 +71,11 @@ func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource totalAgents := 0 for _, resource := range resources { - totalAgents += len(resource.Agents) + for _, agent := range resource.Agents { + if !agent.ParentID.Valid { + totalAgents++ + } + } } for _, resource := range resources { @@ -94,12 +100,15 @@ func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource "", }) // Display all agents associated with the resource. - for index, agent := range resource.Agents { + agents := slice.Filter(resource.Agents, func(agent codersdk.WorkspaceAgent) bool { + return !agent.ParentID.Valid + }) + for index, agent := range agents { tableWriter.AppendRow(renderAgentRow(agent, index, totalAgents, options)) for _, row := range renderListeningPorts(options, agent.ID, index, totalAgents) { tableWriter.AppendRow(row) } - for _, row := range renderDevcontainers(options, agent.ID, index, totalAgents) { + for _, row := range renderDevcontainers(resources, options, agent.ID, index, totalAgents) { tableWriter.AppendRow(row) } } @@ -125,7 +134,7 @@ func renderAgentRow(agent codersdk.WorkspaceAgent, index, totalAgents int, optio } if !options.HideAccess { sshCommand := "coder ssh " + options.WorkspaceName - if totalAgents > 1 { + if totalAgents > 1 || len(options.Devcontainers) > 0 { sshCommand += "." + agent.Name } sshCommand = pretty.Sprint(DefaultStyles.Code, sshCommand) @@ -164,45 +173,129 @@ func renderPortRow(port codersdk.WorkspaceAgentListeningPort, idx, total int) ta return table.Row{sb.String()} } -func renderDevcontainers(wro WorkspaceResourcesOptions, agentID uuid.UUID, index, totalAgents int) []table.Row { +func renderDevcontainers(resources []codersdk.WorkspaceResource, wro WorkspaceResourcesOptions, agentID uuid.UUID, index, totalAgents int) []table.Row { var rows []table.Row if wro.Devcontainers == nil { return []table.Row{} } dc, ok := wro.Devcontainers[agentID] - if !ok || len(dc.Containers) == 0 { + if !ok || len(dc.Devcontainers) == 0 { return []table.Row{} } rows = append(rows, table.Row{ fmt.Sprintf(" %s─ %s", renderPipe(index, totalAgents), "Devcontainers"), }) - for idx, container := range dc.Containers { - rows = append(rows, renderDevcontainerRow(container, idx, len(dc.Containers))) + for idx, devcontainer := range dc.Devcontainers { + rows = append(rows, renderDevcontainerRow(resources, devcontainer, idx, len(dc.Devcontainers), wro)...) } return rows } -func renderDevcontainerRow(container codersdk.WorkspaceAgentContainer, index, total int) table.Row { - var row table.Row - var sb strings.Builder - _, _ = sb.WriteString(" ") - _, _ = sb.WriteString(renderPipe(index, total)) - _, _ = sb.WriteString("─ ") - _, _ = sb.WriteString(pretty.Sprintf(DefaultStyles.Code, "%s", container.FriendlyName)) - row = append(row, sb.String()) - sb.Reset() - if container.Running { - _, _ = sb.WriteString(pretty.Sprintf(DefaultStyles.Keyword, "(%s)", container.Status)) - } else { - _, _ = sb.WriteString(pretty.Sprintf(DefaultStyles.Error, "(%s)", container.Status)) +func renderDevcontainerRow(resources []codersdk.WorkspaceResource, devcontainer codersdk.WorkspaceAgentDevcontainer, index, total int, wro WorkspaceResourcesOptions) []table.Row { + var rows []table.Row + + // If the devcontainer is running and has an associated agent, we want to + // display the agent's details. Otherwise, we just display the devcontainer + // name and status. + var subAgent *codersdk.WorkspaceAgent + displayName := devcontainer.Name + if devcontainer.Agent != nil && devcontainer.Status == codersdk.WorkspaceAgentDevcontainerStatusRunning { + for _, resource := range resources { + if agent, found := slice.Find(resource.Agents, func(agent codersdk.WorkspaceAgent) bool { + return agent.ID == devcontainer.Agent.ID + }); found { + subAgent = &agent + break + } + } + if subAgent != nil { + displayName = subAgent.Name + displayName += fmt.Sprintf(" (%s, %s)", subAgent.OperatingSystem, subAgent.Architecture) + } + } + + if devcontainer.Container != nil { + displayName += " " + pretty.Sprint(DefaultStyles.Keyword, "["+devcontainer.Container.FriendlyName+"]") + } + + // Build the main row. + row := table.Row{ + fmt.Sprintf(" %s─ %s", renderPipe(index, total), displayName), + } + + // Add status, health, and version columns. + if !wro.HideAgentState { + if subAgent != nil { + row = append(row, renderAgentStatus(*subAgent)) + row = append(row, renderAgentHealth(*subAgent)) + row = append(row, renderAgentVersion(subAgent.Version, wro.ServerVersion)) + } else { + row = append(row, renderDevcontainerStatus(devcontainer.Status)) + row = append(row, "") // No health for devcontainer without agent. + row = append(row, "") // No version for devcontainer without agent. + } + } + + // Add access column. + if !wro.HideAccess { + if subAgent != nil { + accessString := fmt.Sprintf("coder ssh %s.%s", wro.WorkspaceName, subAgent.Name) + row = append(row, pretty.Sprint(DefaultStyles.Code, accessString)) + } else { + row = append(row, "") // No access for devcontainers without agent. + } + } + + rows = append(rows, row) + + // Add error message if present. + if errorMessage := devcontainer.Error; errorMessage != "" { + // Cap error message length for display. + if !wro.ShowDetails && len(errorMessage) > 80 { + errorMessage = errorMessage[:79] + "…" + } + errorRow := table.Row{ + " × " + pretty.Sprint(DefaultStyles.Error, errorMessage), + "", + "", + "", + } + if !wro.HideAccess { + errorRow = append(errorRow, "") + } + rows = append(rows, errorRow) + } + + // Add listening ports for the devcontainer agent. + if subAgent != nil { + portRows := renderListeningPorts(wro, subAgent.ID, index, total) + for _, portRow := range portRows { + // Adjust indentation for ports under devcontainer agent. + if len(portRow) > 0 { + if str, ok := portRow[0].(string); ok { + portRow[0] = " " + str // Add extra indentation. + } + } + rows = append(rows, portRow) + } + } + + return rows +} + +func renderDevcontainerStatus(status codersdk.WorkspaceAgentDevcontainerStatus) string { + switch status { + case codersdk.WorkspaceAgentDevcontainerStatusRunning: + return pretty.Sprint(DefaultStyles.Keyword, "▶ running") + case codersdk.WorkspaceAgentDevcontainerStatusStopped: + return pretty.Sprint(DefaultStyles.Placeholder, "⏹ stopped") + case codersdk.WorkspaceAgentDevcontainerStatusStarting: + return pretty.Sprint(DefaultStyles.Warn, "⧗ starting") + case codersdk.WorkspaceAgentDevcontainerStatusError: + return pretty.Sprint(DefaultStyles.Error, "✘ error") + default: + return pretty.Sprint(DefaultStyles.Placeholder, "○ "+string(status)) } - row = append(row, sb.String()) - sb.Reset() - // "health" is not applicable here. - row = append(row, sb.String()) - _, _ = sb.WriteString(container.Image) - row = append(row, sb.String()) - return row } func renderAgentStatus(agent codersdk.WorkspaceAgent) string { diff --git a/cli/show.go b/cli/show.go index f2d3df3ecc3c5..284e8581f5dda 100644 --- a/cli/show.go +++ b/cli/show.go @@ -8,6 +8,7 @@ import ( "github.com/google/uuid" + "github.com/coder/coder/v2/agent/agentcontainers" "github.com/coder/coder/v2/cli/cliui" "github.com/coder/coder/v2/codersdk" "github.com/coder/serpent" @@ -15,9 +16,18 @@ import ( func (r *RootCmd) show() *serpent.Command { client := new(codersdk.Client) + var details bool return &serpent.Command{ Use: "show ", Short: "Display details of a workspace's resources and agents", + Options: serpent.OptionSet{ + { + Flag: "details", + Description: "Show full error messages and additional details.", + Default: "false", + Value: serpent.BoolOf(&details), + }, + }, Middleware: serpent.Chain( serpent.RequireNArgs(1), r.InitClient(client), @@ -35,6 +45,7 @@ func (r *RootCmd) show() *serpent.Command { options := cliui.WorkspaceResourcesOptions{ WorkspaceName: workspace.Name, ServerVersion: buildInfo.Version, + ShowDetails: details, } if workspace.LatestBuild.Status == codersdk.WorkspaceStatusRunning { // Get listening ports for each agent. @@ -42,6 +53,7 @@ func (r *RootCmd) show() *serpent.Command { options.ListeningPorts = ports options.Devcontainers = devcontainers } + return cliui.WorkspaceResources(inv.Stdout, workspace.LatestBuild.Resources, options) }, } @@ -68,13 +80,17 @@ func fetchRuntimeResources(inv *serpent.Invocation, client *codersdk.Client, res ports[agent.ID] = lp mu.Unlock() }() + + if agent.ParentID.Valid { + continue + } wg.Add(1) go func() { defer wg.Done() dc, err := client.WorkspaceAgentListContainers(inv.Context(), agent.ID, map[string]string{ // Labels set by VSCode Remote Containers and @devcontainers/cli. - "devcontainer.config_file": "", - "devcontainer.local_folder": "", + agentcontainers.DevcontainerConfigFileLabel: "", + agentcontainers.DevcontainerLocalFolderLabel: "", }) if err != nil { cliui.Warnf(inv.Stderr, "Failed to get devcontainers for agent %s: %v", agent.Name, err) diff --git a/cli/show_test.go b/cli/show_test.go index 7191898f8c0ec..36a5824174fc4 100644 --- a/cli/show_test.go +++ b/cli/show_test.go @@ -1,12 +1,19 @@ package cli_test import ( + "bytes" "testing" + "time" + "github.com/google/uuid" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/coder/coder/v2/agent/agentcontainers" "github.com/coder/coder/v2/cli/clitest" + "github.com/coder/coder/v2/cli/cliui" "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/pty/ptytest" ) @@ -53,3 +60,354 @@ func TestShow(t *testing.T) { <-doneChan }) } + +func TestShowDevcontainers_Golden(t *testing.T) { + t.Parallel() + + mainAgentID := uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") + agentID := mainAgentID + + testCases := []struct { + name string + showDetails bool + devcontainers []codersdk.WorkspaceAgentDevcontainer + listeningPorts map[uuid.UUID]codersdk.WorkspaceAgentListeningPortsResponse + }{ + { + name: "running_devcontainer_with_agent", + devcontainers: []codersdk.WorkspaceAgentDevcontainer{ + { + ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"), + Name: "web-dev", + WorkspaceFolder: "/workspaces/web-dev", + ConfigPath: "/workspaces/web-dev/.devcontainer/devcontainer.json", + Status: codersdk.WorkspaceAgentDevcontainerStatusRunning, + Dirty: false, + Container: &codersdk.WorkspaceAgentContainer{ + ID: "container-web-dev", + FriendlyName: "quirky_lovelace", + Image: "mcr.microsoft.com/devcontainers/typescript-node:1.0.0", + Running: true, + Status: "running", + CreatedAt: time.Now().Add(-1 * time.Hour), + Labels: map[string]string{ + agentcontainers.DevcontainerConfigFileLabel: "/workspaces/web-dev/.devcontainer/devcontainer.json", + agentcontainers.DevcontainerLocalFolderLabel: "/workspaces/web-dev", + }, + }, + Agent: &codersdk.WorkspaceAgentDevcontainerAgent{ + ID: uuid.MustParse("22222222-2222-2222-2222-222222222222"), + Name: "web-dev", + Directory: "/workspaces/web-dev", + }, + }, + }, + listeningPorts: map[uuid.UUID]codersdk.WorkspaceAgentListeningPortsResponse{ + uuid.MustParse("22222222-2222-2222-2222-222222222222"): { + Ports: []codersdk.WorkspaceAgentListeningPort{ + { + ProcessName: "node", + Network: "tcp", + Port: 3000, + }, + { + ProcessName: "webpack-dev-server", + Network: "tcp", + Port: 8080, + }, + }, + }, + }, + }, + { + name: "running_devcontainer_without_agent", + devcontainers: []codersdk.WorkspaceAgentDevcontainer{ + { + ID: uuid.MustParse("33333333-3333-3333-3333-333333333333"), + Name: "web-server", + WorkspaceFolder: "/workspaces/web-server", + ConfigPath: "/workspaces/web-server/.devcontainer/devcontainer.json", + Status: codersdk.WorkspaceAgentDevcontainerStatusRunning, + Dirty: false, + Container: &codersdk.WorkspaceAgentContainer{ + ID: "container-web-server", + FriendlyName: "amazing_turing", + Image: "nginx:latest", + Running: true, + Status: "running", + CreatedAt: time.Now().Add(-30 * time.Minute), + Labels: map[string]string{ + agentcontainers.DevcontainerConfigFileLabel: "/workspaces/web-server/.devcontainer/devcontainer.json", + agentcontainers.DevcontainerLocalFolderLabel: "/workspaces/web-server", + }, + }, + Agent: nil, // No agent for this running container. + }, + }, + }, + { + name: "stopped_devcontainer", + devcontainers: []codersdk.WorkspaceAgentDevcontainer{ + { + ID: uuid.MustParse("44444444-4444-4444-4444-444444444444"), + Name: "api-dev", + WorkspaceFolder: "/workspaces/api-dev", + ConfigPath: "/workspaces/api-dev/.devcontainer/devcontainer.json", + Status: codersdk.WorkspaceAgentDevcontainerStatusStopped, + Dirty: false, + Container: &codersdk.WorkspaceAgentContainer{ + ID: "container-api-dev", + FriendlyName: "clever_darwin", + Image: "mcr.microsoft.com/devcontainers/go:1.0.0", + Running: false, + Status: "exited", + CreatedAt: time.Now().Add(-2 * time.Hour), + Labels: map[string]string{ + agentcontainers.DevcontainerConfigFileLabel: "/workspaces/api-dev/.devcontainer/devcontainer.json", + agentcontainers.DevcontainerLocalFolderLabel: "/workspaces/api-dev", + }, + }, + Agent: nil, // No agent for stopped container. + }, + }, + }, + { + name: "starting_devcontainer", + devcontainers: []codersdk.WorkspaceAgentDevcontainer{ + { + ID: uuid.MustParse("55555555-5555-5555-5555-555555555555"), + Name: "database-dev", + WorkspaceFolder: "/workspaces/database-dev", + ConfigPath: "/workspaces/database-dev/.devcontainer/devcontainer.json", + Status: codersdk.WorkspaceAgentDevcontainerStatusStarting, + Dirty: false, + Container: &codersdk.WorkspaceAgentContainer{ + ID: "container-database-dev", + FriendlyName: "nostalgic_hawking", + Image: "mcr.microsoft.com/devcontainers/postgres:1.0.0", + Running: false, + Status: "created", + CreatedAt: time.Now().Add(-5 * time.Minute), + Labels: map[string]string{ + agentcontainers.DevcontainerConfigFileLabel: "/workspaces/database-dev/.devcontainer/devcontainer.json", + agentcontainers.DevcontainerLocalFolderLabel: "/workspaces/database-dev", + }, + }, + Agent: nil, // No agent yet while starting. + }, + }, + }, + { + name: "error_devcontainer", + devcontainers: []codersdk.WorkspaceAgentDevcontainer{ + { + ID: uuid.MustParse("66666666-6666-6666-6666-666666666666"), + Name: "failed-dev", + WorkspaceFolder: "/workspaces/failed-dev", + ConfigPath: "/workspaces/failed-dev/.devcontainer/devcontainer.json", + Status: codersdk.WorkspaceAgentDevcontainerStatusError, + Dirty: false, + Error: "Failed to pull image mcr.microsoft.com/devcontainers/go:latest: timeout after 5m0s", + Container: nil, // No container due to error. + Agent: nil, // No agent due to error. + }, + }, + }, + + { + name: "mixed_devcontainer_states", + devcontainers: []codersdk.WorkspaceAgentDevcontainer{ + { + ID: uuid.MustParse("88888888-8888-8888-8888-888888888888"), + Name: "frontend", + WorkspaceFolder: "/workspaces/frontend", + Status: codersdk.WorkspaceAgentDevcontainerStatusRunning, + Container: &codersdk.WorkspaceAgentContainer{ + ID: "container-frontend", + FriendlyName: "vibrant_tesla", + Image: "node:18", + Running: true, + Status: "running", + CreatedAt: time.Now().Add(-30 * time.Minute), + }, + Agent: &codersdk.WorkspaceAgentDevcontainerAgent{ + ID: uuid.MustParse("99999999-9999-9999-9999-999999999999"), + Name: "frontend", + Directory: "/workspaces/frontend", + }, + }, + { + ID: uuid.MustParse("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"), + Name: "backend", + WorkspaceFolder: "/workspaces/backend", + Status: codersdk.WorkspaceAgentDevcontainerStatusStopped, + Container: &codersdk.WorkspaceAgentContainer{ + ID: "container-backend", + FriendlyName: "peaceful_curie", + Image: "python:3.11", + Running: false, + Status: "exited", + CreatedAt: time.Now().Add(-1 * time.Hour), + }, + Agent: nil, + }, + { + ID: uuid.MustParse("bbbbbbbb-cccc-dddd-eeee-ffffffffffff"), + Name: "error-container", + WorkspaceFolder: "/workspaces/error-container", + Status: codersdk.WorkspaceAgentDevcontainerStatusError, + Error: "Container build failed: dockerfile syntax error on line 15", + Container: nil, + Agent: nil, + }, + }, + listeningPorts: map[uuid.UUID]codersdk.WorkspaceAgentListeningPortsResponse{ + uuid.MustParse("99999999-9999-9999-9999-999999999999"): { + Ports: []codersdk.WorkspaceAgentListeningPort{ + { + ProcessName: "vite", + Network: "tcp", + Port: 5173, + }, + }, + }, + }, + }, + { + name: "running_devcontainer_with_agent_and_error", + devcontainers: []codersdk.WorkspaceAgentDevcontainer{ + { + ID: uuid.MustParse("cccccccc-dddd-eeee-ffff-000000000000"), + Name: "problematic-dev", + WorkspaceFolder: "/workspaces/problematic-dev", + ConfigPath: "/workspaces/problematic-dev/.devcontainer/devcontainer.json", + Status: codersdk.WorkspaceAgentDevcontainerStatusRunning, + Dirty: false, + Error: "Warning: Container started but healthcheck failed", + Container: &codersdk.WorkspaceAgentContainer{ + ID: "container-problematic", + FriendlyName: "cranky_mendel", + Image: "mcr.microsoft.com/devcontainers/python:1.0.0", + Running: true, + Status: "running", + CreatedAt: time.Now().Add(-15 * time.Minute), + Labels: map[string]string{ + agentcontainers.DevcontainerConfigFileLabel: "/workspaces/problematic-dev/.devcontainer/devcontainer.json", + agentcontainers.DevcontainerLocalFolderLabel: "/workspaces/problematic-dev", + }, + }, + Agent: &codersdk.WorkspaceAgentDevcontainerAgent{ + ID: uuid.MustParse("dddddddd-eeee-ffff-aaaa-111111111111"), + Name: "problematic-dev", + Directory: "/workspaces/problematic-dev", + }, + }, + }, + listeningPorts: map[uuid.UUID]codersdk.WorkspaceAgentListeningPortsResponse{ + uuid.MustParse("dddddddd-eeee-ffff-aaaa-111111111111"): { + Ports: []codersdk.WorkspaceAgentListeningPort{ + { + ProcessName: "python", + Network: "tcp", + Port: 8000, + }, + }, + }, + }, + }, + { + name: "long_error_message", + devcontainers: []codersdk.WorkspaceAgentDevcontainer{ + { + ID: uuid.MustParse("eeeeeeee-ffff-0000-1111-222222222222"), + Name: "long-error-dev", + WorkspaceFolder: "/workspaces/long-error-dev", + ConfigPath: "/workspaces/long-error-dev/.devcontainer/devcontainer.json", + Status: codersdk.WorkspaceAgentDevcontainerStatusError, + Dirty: false, + Error: "Failed to build devcontainer: dockerfile parse error at line 25: unknown instruction 'INSTALL', did you mean 'RUN apt-get install'? This is a very long error message that should be truncated when detail flag is not used", + Container: nil, + Agent: nil, + }, + }, + }, + { + name: "long_error_message_with_detail", + showDetails: true, + devcontainers: []codersdk.WorkspaceAgentDevcontainer{ + { + ID: uuid.MustParse("eeeeeeee-ffff-0000-1111-222222222222"), + Name: "long-error-dev", + WorkspaceFolder: "/workspaces/long-error-dev", + ConfigPath: "/workspaces/long-error-dev/.devcontainer/devcontainer.json", + Status: codersdk.WorkspaceAgentDevcontainerStatusError, + Dirty: false, + Error: "Failed to build devcontainer: dockerfile parse error at line 25: unknown instruction 'INSTALL', did you mean 'RUN apt-get install'? This is a very long error message that should be truncated when detail flag is not used", + Container: nil, + Agent: nil, + }, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + var allAgents []codersdk.WorkspaceAgent + mainAgent := codersdk.WorkspaceAgent{ + ID: mainAgentID, + Name: "main", + OperatingSystem: "linux", + Architecture: "amd64", + Status: codersdk.WorkspaceAgentConnected, + Health: codersdk.WorkspaceAgentHealth{Healthy: true}, + Version: "v2.15.0", + } + allAgents = append(allAgents, mainAgent) + + for _, dc := range tc.devcontainers { + if dc.Agent != nil { + devcontainerAgent := codersdk.WorkspaceAgent{ + ID: dc.Agent.ID, + ParentID: uuid.NullUUID{UUID: mainAgentID, Valid: true}, + Name: dc.Agent.Name, + OperatingSystem: "linux", + Architecture: "amd64", + Status: codersdk.WorkspaceAgentConnected, + Health: codersdk.WorkspaceAgentHealth{Healthy: true}, + Version: "v2.15.0", + } + allAgents = append(allAgents, devcontainerAgent) + } + } + + resources := []codersdk.WorkspaceResource{ + { + Type: "compute", + Name: "main", + Agents: allAgents, + }, + } + options := cliui.WorkspaceResourcesOptions{ + WorkspaceName: "test-workspace", + ServerVersion: "v2.15.0", + ShowDetails: tc.showDetails, + Devcontainers: map[uuid.UUID]codersdk.WorkspaceAgentListContainersResponse{ + agentID: { + Devcontainers: tc.devcontainers, + }, + }, + ListeningPorts: tc.listeningPorts, + } + + var buf bytes.Buffer + err := cliui.WorkspaceResources(&buf, resources, options) + require.NoError(t, err) + + replacements := map[string]string{} + clitest.TestGoldenFile(t, "TestShowDevcontainers_Golden/"+tc.name, buf.Bytes(), replacements) + }) + } +} diff --git a/cli/testdata/TestShowDevcontainers_Golden/error_devcontainer.golden b/cli/testdata/TestShowDevcontainers_Golden/error_devcontainer.golden new file mode 100644 index 0000000000000..03a19f16df4e1 --- /dev/null +++ b/cli/testdata/TestShowDevcontainers_Golden/error_devcontainer.golden @@ -0,0 +1,9 @@ +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ RESOURCE STATUS HEALTH VERSION ACCESS │ +├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ compute.main │ +│ └─ main (linux, amd64) ⦿ connected ✔ healthy v2.15.0 coder ssh test-workspace.main │ +│ └─ Devcontainers │ +│ └─ failed-dev ✘ error │ +│ × Failed to pull image mcr.microsoft.com/devcontainers/go:latest: timeout after 5… │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/cli/testdata/TestShowDevcontainers_Golden/long_error_message.golden b/cli/testdata/TestShowDevcontainers_Golden/long_error_message.golden new file mode 100644 index 0000000000000..1e80d338a74a8 --- /dev/null +++ b/cli/testdata/TestShowDevcontainers_Golden/long_error_message.golden @@ -0,0 +1,9 @@ +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ RESOURCE STATUS HEALTH VERSION ACCESS │ +├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ compute.main │ +│ └─ main (linux, amd64) ⦿ connected ✔ healthy v2.15.0 coder ssh test-workspace.main │ +│ └─ Devcontainers │ +│ └─ long-error-dev ✘ error │ +│ × Failed to build devcontainer: dockerfile parse error at line 25: unknown instru… │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/cli/testdata/TestShowDevcontainers_Golden/long_error_message_with_detail.golden b/cli/testdata/TestShowDevcontainers_Golden/long_error_message_with_detail.golden new file mode 100644 index 0000000000000..9310f7f19a350 --- /dev/null +++ b/cli/testdata/TestShowDevcontainers_Golden/long_error_message_with_detail.golden @@ -0,0 +1,9 @@ +┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ RESOURCE STATUS HEALTH VERSION ACCESS │ +├────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ compute.main │ +│ └─ main (linux, amd64) ⦿ connected ✔ healthy v2.15.0 coder ssh test-workspace.main │ +│ └─ Devcontainers │ +│ └─ long-error-dev ✘ error │ +│ × Failed to build devcontainer: dockerfile parse error at line 25: unknown instruction 'INSTALL', did you mean 'RUN apt-get install'? This is a very long error message that should be truncated when detail flag is not used │ +└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/cli/testdata/TestShowDevcontainers_Golden/mixed_devcontainer_states.golden b/cli/testdata/TestShowDevcontainers_Golden/mixed_devcontainer_states.golden new file mode 100644 index 0000000000000..dfbd677cc3dbe --- /dev/null +++ b/cli/testdata/TestShowDevcontainers_Golden/mixed_devcontainer_states.golden @@ -0,0 +1,13 @@ +┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ RESOURCE STATUS HEALTH VERSION ACCESS │ +├───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ compute.main │ +│ └─ main (linux, amd64) ⦿ connected ✔ healthy v2.15.0 coder ssh test-workspace.main │ +│ └─ Devcontainers │ +│ ├─ frontend (linux, amd64) [vibrant_tesla] ⦿ connected ✔ healthy v2.15.0 coder ssh test-workspace.frontend │ +│ ├─ Open Ports │ +│ └─ 5173/tcp [vite] │ +│ ├─ backend [peaceful_curie] ⏹ stopped │ +│ └─ error-container ✘ error │ +│ × Container build failed: dockerfile syntax error on line 15 │ +└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/cli/testdata/TestShowDevcontainers_Golden/running_devcontainer_with_agent.golden b/cli/testdata/TestShowDevcontainers_Golden/running_devcontainer_with_agent.golden new file mode 100644 index 0000000000000..ab5d2a2085227 --- /dev/null +++ b/cli/testdata/TestShowDevcontainers_Golden/running_devcontainer_with_agent.golden @@ -0,0 +1,11 @@ +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ RESOURCE STATUS HEALTH VERSION ACCESS │ +├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ compute.main │ +│ └─ main (linux, amd64) ⦿ connected ✔ healthy v2.15.0 coder ssh test-workspace.main │ +│ └─ Devcontainers │ +│ └─ web-dev (linux, amd64) [quirky_lovelace] ⦿ connected ✔ healthy v2.15.0 coder ssh test-workspace.web-dev │ +│ └─ Open Ports │ +│ ├─ 3000/tcp [node] │ +│ └─ 8080/tcp [webpack-dev-server] │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/cli/testdata/TestShowDevcontainers_Golden/running_devcontainer_with_agent_and_error.golden b/cli/testdata/TestShowDevcontainers_Golden/running_devcontainer_with_agent_and_error.golden new file mode 100644 index 0000000000000..6b73f7175bac8 --- /dev/null +++ b/cli/testdata/TestShowDevcontainers_Golden/running_devcontainer_with_agent_and_error.golden @@ -0,0 +1,11 @@ +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ RESOURCE STATUS HEALTH VERSION ACCESS │ +├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ compute.main │ +│ └─ main (linux, amd64) ⦿ connected ✔ healthy v2.15.0 coder ssh test-workspace.main │ +│ └─ Devcontainers │ +│ └─ problematic-dev (linux, amd64) [cranky_mendel] ⦿ connected ✔ healthy v2.15.0 coder ssh test-workspace.problematic-dev │ +│ × Warning: Container started but healthcheck failed │ +│ └─ Open Ports │ +│ └─ 8000/tcp [python] │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/cli/testdata/TestShowDevcontainers_Golden/running_devcontainer_without_agent.golden b/cli/testdata/TestShowDevcontainers_Golden/running_devcontainer_without_agent.golden new file mode 100644 index 0000000000000..70c3874acc774 --- /dev/null +++ b/cli/testdata/TestShowDevcontainers_Golden/running_devcontainer_without_agent.golden @@ -0,0 +1,8 @@ +┌──────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ RESOURCE STATUS HEALTH VERSION ACCESS │ +├──────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ compute.main │ +│ └─ main (linux, amd64) ⦿ connected ✔ healthy v2.15.0 coder ssh test-workspace.main │ +│ └─ Devcontainers │ +│ └─ web-server [amazing_turing] ▶ running │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/cli/testdata/TestShowDevcontainers_Golden/starting_devcontainer.golden b/cli/testdata/TestShowDevcontainers_Golden/starting_devcontainer.golden new file mode 100644 index 0000000000000..472201ecc7818 --- /dev/null +++ b/cli/testdata/TestShowDevcontainers_Golden/starting_devcontainer.golden @@ -0,0 +1,8 @@ +┌───────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ RESOURCE STATUS HEALTH VERSION ACCESS │ +├───────────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ compute.main │ +│ └─ main (linux, amd64) ⦿ connected ✔ healthy v2.15.0 coder ssh test-workspace.main │ +│ └─ Devcontainers │ +│ └─ database-dev [nostalgic_hawking] ⧗ starting │ +└───────────────────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/cli/testdata/TestShowDevcontainers_Golden/stopped_devcontainer.golden b/cli/testdata/TestShowDevcontainers_Golden/stopped_devcontainer.golden new file mode 100644 index 0000000000000..41313b235acc7 --- /dev/null +++ b/cli/testdata/TestShowDevcontainers_Golden/stopped_devcontainer.golden @@ -0,0 +1,8 @@ +┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ RESOURCE STATUS HEALTH VERSION ACCESS │ +├──────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ compute.main │ +│ └─ main (linux, amd64) ⦿ connected ✔ healthy v2.15.0 coder ssh test-workspace.main │ +│ └─ Devcontainers │ +│ └─ api-dev [clever_darwin] ⏹ stopped │ +└──────────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/cli/testdata/coder_show_--help.golden b/cli/testdata/coder_show_--help.golden index fc048aa067ea6..76555221e4602 100644 --- a/cli/testdata/coder_show_--help.golden +++ b/cli/testdata/coder_show_--help.golden @@ -1,9 +1,13 @@ coder v0.0.0-devel USAGE: - coder show + coder show [flags] Display details of a workspace's resources and agents +OPTIONS: + --details bool (default: false) + Show full error messages and additional details. + ——— Run `coder --help` for a list of global options. diff --git a/docs/reference/cli/show.md b/docs/reference/cli/show.md index 87c527ed939f9..c6fb9a2c81f64 100644 --- a/docs/reference/cli/show.md +++ b/docs/reference/cli/show.md @@ -6,5 +6,16 @@ Display details of a workspace's resources and agents ## Usage ```console -coder show +coder show [flags] ``` + +## Options + +### --details + +| | | +|---------|--------------------| +| Type | bool | +| Default | false | + +Show full error messages and additional details.