mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat(cli): improve devcontainer support for coder show (#18793)
Fixes coder/internal#747
This commit is contained in:
committed by
GitHub
parent
2f50b3b7bb
commit
5f50dcce5a
+121
-28
@@ -12,6 +12,7 @@ import (
|
|||||||
"golang.org/x/mod/semver"
|
"golang.org/x/mod/semver"
|
||||||
|
|
||||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
"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/coder/v2/codersdk"
|
||||||
"github.com/coder/pretty"
|
"github.com/coder/pretty"
|
||||||
)
|
)
|
||||||
@@ -29,6 +30,7 @@ type WorkspaceResourcesOptions struct {
|
|||||||
ServerVersion string
|
ServerVersion string
|
||||||
ListeningPorts map[uuid.UUID]codersdk.WorkspaceAgentListeningPortsResponse
|
ListeningPorts map[uuid.UUID]codersdk.WorkspaceAgentListeningPortsResponse
|
||||||
Devcontainers map[uuid.UUID]codersdk.WorkspaceAgentListContainersResponse
|
Devcontainers map[uuid.UUID]codersdk.WorkspaceAgentListContainersResponse
|
||||||
|
ShowDetails bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// WorkspaceResources displays the connection status and tree-view of provided resources.
|
// 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
|
totalAgents := 0
|
||||||
for _, resource := range resources {
|
for _, resource := range resources {
|
||||||
totalAgents += len(resource.Agents)
|
for _, agent := range resource.Agents {
|
||||||
|
if !agent.ParentID.Valid {
|
||||||
|
totalAgents++
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, resource := range resources {
|
for _, resource := range resources {
|
||||||
@@ -94,12 +100,15 @@ func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource
|
|||||||
"",
|
"",
|
||||||
})
|
})
|
||||||
// Display all agents associated with the resource.
|
// 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))
|
tableWriter.AppendRow(renderAgentRow(agent, index, totalAgents, options))
|
||||||
for _, row := range renderListeningPorts(options, agent.ID, index, totalAgents) {
|
for _, row := range renderListeningPorts(options, agent.ID, index, totalAgents) {
|
||||||
tableWriter.AppendRow(row)
|
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)
|
tableWriter.AppendRow(row)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,7 +134,7 @@ func renderAgentRow(agent codersdk.WorkspaceAgent, index, totalAgents int, optio
|
|||||||
}
|
}
|
||||||
if !options.HideAccess {
|
if !options.HideAccess {
|
||||||
sshCommand := "coder ssh " + options.WorkspaceName
|
sshCommand := "coder ssh " + options.WorkspaceName
|
||||||
if totalAgents > 1 {
|
if totalAgents > 1 || len(options.Devcontainers) > 0 {
|
||||||
sshCommand += "." + agent.Name
|
sshCommand += "." + agent.Name
|
||||||
}
|
}
|
||||||
sshCommand = pretty.Sprint(DefaultStyles.Code, sshCommand)
|
sshCommand = pretty.Sprint(DefaultStyles.Code, sshCommand)
|
||||||
@@ -164,45 +173,129 @@ func renderPortRow(port codersdk.WorkspaceAgentListeningPort, idx, total int) ta
|
|||||||
return table.Row{sb.String()}
|
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
|
var rows []table.Row
|
||||||
if wro.Devcontainers == nil {
|
if wro.Devcontainers == nil {
|
||||||
return []table.Row{}
|
return []table.Row{}
|
||||||
}
|
}
|
||||||
dc, ok := wro.Devcontainers[agentID]
|
dc, ok := wro.Devcontainers[agentID]
|
||||||
if !ok || len(dc.Containers) == 0 {
|
if !ok || len(dc.Devcontainers) == 0 {
|
||||||
return []table.Row{}
|
return []table.Row{}
|
||||||
}
|
}
|
||||||
rows = append(rows, table.Row{
|
rows = append(rows, table.Row{
|
||||||
fmt.Sprintf(" %s─ %s", renderPipe(index, totalAgents), "Devcontainers"),
|
fmt.Sprintf(" %s─ %s", renderPipe(index, totalAgents), "Devcontainers"),
|
||||||
})
|
})
|
||||||
for idx, container := range dc.Containers {
|
for idx, devcontainer := range dc.Devcontainers {
|
||||||
rows = append(rows, renderDevcontainerRow(container, idx, len(dc.Containers)))
|
rows = append(rows, renderDevcontainerRow(resources, devcontainer, idx, len(dc.Devcontainers), wro)...)
|
||||||
}
|
}
|
||||||
return rows
|
return rows
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderDevcontainerRow(container codersdk.WorkspaceAgentContainer, index, total int) table.Row {
|
func renderDevcontainerRow(resources []codersdk.WorkspaceResource, devcontainer codersdk.WorkspaceAgentDevcontainer, index, total int, wro WorkspaceResourcesOptions) []table.Row {
|
||||||
var row table.Row
|
var rows []table.Row
|
||||||
var sb strings.Builder
|
|
||||||
_, _ = sb.WriteString(" ")
|
// If the devcontainer is running and has an associated agent, we want to
|
||||||
_, _ = sb.WriteString(renderPipe(index, total))
|
// display the agent's details. Otherwise, we just display the devcontainer
|
||||||
_, _ = sb.WriteString("─ ")
|
// name and status.
|
||||||
_, _ = sb.WriteString(pretty.Sprintf(DefaultStyles.Code, "%s", container.FriendlyName))
|
var subAgent *codersdk.WorkspaceAgent
|
||||||
row = append(row, sb.String())
|
displayName := devcontainer.Name
|
||||||
sb.Reset()
|
if devcontainer.Agent != nil && devcontainer.Status == codersdk.WorkspaceAgentDevcontainerStatusRunning {
|
||||||
if container.Running {
|
for _, resource := range resources {
|
||||||
_, _ = sb.WriteString(pretty.Sprintf(DefaultStyles.Keyword, "(%s)", container.Status))
|
if agent, found := slice.Find(resource.Agents, func(agent codersdk.WorkspaceAgent) bool {
|
||||||
} else {
|
return agent.ID == devcontainer.Agent.ID
|
||||||
_, _ = sb.WriteString(pretty.Sprintf(DefaultStyles.Error, "(%s)", container.Status))
|
}); 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 {
|
func renderAgentStatus(agent codersdk.WorkspaceAgent) string {
|
||||||
|
|||||||
+18
-2
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
|
||||||
|
"github.com/coder/coder/v2/agent/agentcontainers"
|
||||||
"github.com/coder/coder/v2/cli/cliui"
|
"github.com/coder/coder/v2/cli/cliui"
|
||||||
"github.com/coder/coder/v2/codersdk"
|
"github.com/coder/coder/v2/codersdk"
|
||||||
"github.com/coder/serpent"
|
"github.com/coder/serpent"
|
||||||
@@ -15,9 +16,18 @@ import (
|
|||||||
|
|
||||||
func (r *RootCmd) show() *serpent.Command {
|
func (r *RootCmd) show() *serpent.Command {
|
||||||
client := new(codersdk.Client)
|
client := new(codersdk.Client)
|
||||||
|
var details bool
|
||||||
return &serpent.Command{
|
return &serpent.Command{
|
||||||
Use: "show <workspace>",
|
Use: "show <workspace>",
|
||||||
Short: "Display details of a workspace's resources and agents",
|
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(
|
Middleware: serpent.Chain(
|
||||||
serpent.RequireNArgs(1),
|
serpent.RequireNArgs(1),
|
||||||
r.InitClient(client),
|
r.InitClient(client),
|
||||||
@@ -35,6 +45,7 @@ func (r *RootCmd) show() *serpent.Command {
|
|||||||
options := cliui.WorkspaceResourcesOptions{
|
options := cliui.WorkspaceResourcesOptions{
|
||||||
WorkspaceName: workspace.Name,
|
WorkspaceName: workspace.Name,
|
||||||
ServerVersion: buildInfo.Version,
|
ServerVersion: buildInfo.Version,
|
||||||
|
ShowDetails: details,
|
||||||
}
|
}
|
||||||
if workspace.LatestBuild.Status == codersdk.WorkspaceStatusRunning {
|
if workspace.LatestBuild.Status == codersdk.WorkspaceStatusRunning {
|
||||||
// Get listening ports for each agent.
|
// Get listening ports for each agent.
|
||||||
@@ -42,6 +53,7 @@ func (r *RootCmd) show() *serpent.Command {
|
|||||||
options.ListeningPorts = ports
|
options.ListeningPorts = ports
|
||||||
options.Devcontainers = devcontainers
|
options.Devcontainers = devcontainers
|
||||||
}
|
}
|
||||||
|
|
||||||
return cliui.WorkspaceResources(inv.Stdout, workspace.LatestBuild.Resources, options)
|
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
|
ports[agent.ID] = lp
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
if agent.ParentID.Valid {
|
||||||
|
continue
|
||||||
|
}
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
dc, err := client.WorkspaceAgentListContainers(inv.Context(), agent.ID, map[string]string{
|
dc, err := client.WorkspaceAgentListContainers(inv.Context(), agent.ID, map[string]string{
|
||||||
// Labels set by VSCode Remote Containers and @devcontainers/cli.
|
// Labels set by VSCode Remote Containers and @devcontainers/cli.
|
||||||
"devcontainer.config_file": "",
|
agentcontainers.DevcontainerConfigFileLabel: "",
|
||||||
"devcontainer.local_folder": "",
|
agentcontainers.DevcontainerLocalFolderLabel: "",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cliui.Warnf(inv.Stderr, "Failed to get devcontainers for agent %s: %v", agent.Name, err)
|
cliui.Warnf(inv.Stderr, "Failed to get devcontainers for agent %s: %v", agent.Name, err)
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
package cli_test
|
package cli_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/assert"
|
"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/clitest"
|
||||||
|
"github.com/coder/coder/v2/cli/cliui"
|
||||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||||
|
"github.com/coder/coder/v2/codersdk"
|
||||||
"github.com/coder/coder/v2/pty/ptytest"
|
"github.com/coder/coder/v2/pty/ptytest"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -53,3 +60,354 @@ func TestShow(t *testing.T) {
|
|||||||
<-doneChan
|
<-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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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… │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||||
@@ -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… │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||||
+9
@@ -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 │
|
||||||
|
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||||
@@ -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 │
|
||||||
|
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||||
+11
@@ -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] │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||||
+11
@@ -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] │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||||
+8
@@ -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 │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||||
@@ -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 │
|
||||||
|
└───────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||||
@@ -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 │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||||
+5
-1
@@ -1,9 +1,13 @@
|
|||||||
coder v0.0.0-devel
|
coder v0.0.0-devel
|
||||||
|
|
||||||
USAGE:
|
USAGE:
|
||||||
coder show <workspace>
|
coder show [flags] <workspace>
|
||||||
|
|
||||||
Display details of a workspace's resources and agents
|
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.
|
Run `coder --help` for a list of global options.
|
||||||
|
|||||||
Generated
+12
-1
@@ -6,5 +6,16 @@ Display details of a workspace's resources and agents
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```console
|
```console
|
||||||
coder show <workspace>
|
coder show [flags] <workspace>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
### --details
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---------|--------------------|
|
||||||
|
| Type | <code>bool</code> |
|
||||||
|
| Default | <code>false</code> |
|
||||||
|
|
||||||
|
Show full error messages and additional details.
|
||||||
|
|||||||
Reference in New Issue
Block a user