Skip to content

feat(cli): display devcontainers in show command #16515

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 67 additions & 11 deletions cli/cliui/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type WorkspaceResourcesOptions struct {
Title string
ServerVersion string
ListeningPorts map[uuid.UUID]codersdk.WorkspaceAgentListeningPortsResponse
Devcontainers map[uuid.UUID]codersdk.WorkspaceAgentListContainersResponse
}

// WorkspaceResources displays the connection status and tree-view of provided resources.
Expand Down Expand Up @@ -95,15 +96,11 @@ func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource
// Display all agents associated with the resource.
for index, agent := range resource.Agents {
tableWriter.AppendRow(renderAgentRow(agent, index, totalAgents, options))
if options.ListeningPorts != nil {
if lp, ok := options.ListeningPorts[agent.ID]; ok && len(lp.Ports) > 0 {
tableWriter.AppendRow(table.Row{
fmt.Sprintf(" %s─ %s", renderPipe(index, totalAgents), "Open Ports"),
})
for _, port := range lp.Ports {
tableWriter.AppendRow(renderPortRow(port, index, totalAgents))
}
}
for _, row := range renderListeningPorts(options, agent.ID, index, totalAgents) {
tableWriter.AppendRow(row)
}
for _, row := range renderDevcontainers(options, agent.ID, index, totalAgents) {
tableWriter.AppendRow(row)
}
}
tableWriter.AppendSeparator()
Expand Down Expand Up @@ -137,10 +134,28 @@ func renderAgentRow(agent codersdk.WorkspaceAgent, index, totalAgents int, optio
return row
}

func renderPortRow(port codersdk.WorkspaceAgentListeningPort, index, totalPorts int) table.Row {
func renderListeningPorts(wro WorkspaceResourcesOptions, agentID uuid.UUID, idx, total int) []table.Row {
var rows []table.Row
if wro.ListeningPorts == nil {
return []table.Row{}
}
lp, ok := wro.ListeningPorts[agentID]
if !ok || len(lp.Ports) == 0 {
return []table.Row{}
}
rows = append(rows, table.Row{
fmt.Sprintf(" %s─ Open Ports", renderPipe(idx, total)),
})
for idx, port := range lp.Ports {
rows = append(rows, renderPortRow(port, idx, len(lp.Ports)))
}
return rows
}

func renderPortRow(port codersdk.WorkspaceAgentListeningPort, idx, total int) table.Row {
var sb strings.Builder
_, _ = sb.WriteString(" ")
_, _ = sb.WriteString(renderPipe(index, totalPorts))
_, _ = sb.WriteString(renderPipe(idx, total))
_, _ = sb.WriteString("─ ")
_, _ = sb.WriteString(pretty.Sprintf(DefaultStyles.Code, "%5d/%s", port.Port, port.Network))
if port.ProcessName != "" {
Expand All @@ -149,6 +164,47 @@ func renderPortRow(port codersdk.WorkspaceAgentListeningPort, index, totalPorts
return table.Row{sb.String()}
}

func renderDevcontainers(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 {
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)))
}
return rows
}

func renderDevcontainerRow(container codersdk.WorkspaceAgentDevcontainer, 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))
}
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 {
switch agent.Status {
case codersdk.WorkspaceAgentConnecting:
Expand Down
24 changes: 21 additions & 3 deletions cli/show.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,18 @@ func (r *RootCmd) show() *serpent.Command {
}
if workspace.LatestBuild.Status == codersdk.WorkspaceStatusRunning {
// Get listening ports for each agent.
options.ListeningPorts = fetchListeningPorts(inv, client, workspace.LatestBuild.Resources...)
ports, devcontainers := fetchRuntimeResources(inv, client, workspace.LatestBuild.Resources...)
options.ListeningPorts = ports
options.Devcontainers = devcontainers
}
return cliui.WorkspaceResources(inv.Stdout, workspace.LatestBuild.Resources, options)
},
}
}

func fetchListeningPorts(inv *serpent.Invocation, client *codersdk.Client, resources ...codersdk.WorkspaceResource) map[uuid.UUID]codersdk.WorkspaceAgentListeningPortsResponse {
func fetchRuntimeResources(inv *serpent.Invocation, client *codersdk.Client, resources ...codersdk.WorkspaceResource) (map[uuid.UUID]codersdk.WorkspaceAgentListeningPortsResponse, map[uuid.UUID]codersdk.WorkspaceAgentListContainersResponse) {
ports := make(map[uuid.UUID]codersdk.WorkspaceAgentListeningPortsResponse)
devcontainers := make(map[uuid.UUID]codersdk.WorkspaceAgentListContainersResponse)
var wg sync.WaitGroup
var mu sync.Mutex
for _, res := range resources {
Expand All @@ -65,8 +68,23 @@ func fetchListeningPorts(inv *serpent.Invocation, client *codersdk.Client, resou
ports[agent.ID] = lp
mu.Unlock()
}()
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": "",
})
if err != nil {
cliui.Warnf(inv.Stderr, "Failed to get devcontainers for agent %s: %v", agent.Name, err)
}
mu.Lock()
devcontainers[agent.ID] = dc
mu.Unlock()
}()
}
}
wg.Wait()
return ports
return ports, devcontainers
}
7 changes: 6 additions & 1 deletion coderd/util/maps/maps.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ import (

// Subset returns true if all the keys of a are present
// in b and have the same values.
// If the corresponding value of a[k] is the zero value in
// b, Subset will skip comparing that value.
// This allows checking for the presence of map keys.
func Subset[T, U comparable](a, b map[T]U) bool {
var uz U
for ka, va := range a {
if vb, ok := b[ka]; !ok || va != vb {
ignoreZeroValue := va == uz
if vb, ok := b[ka]; !ok || (!ignoreZeroValue && va != vb) {
return false
}
}
Expand Down
23 changes: 21 additions & 2 deletions coderd/util/maps/maps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ func TestSubset(t *testing.T) {
t.Parallel()

for idx, tc := range []struct {
a map[string]string
b map[string]string
a map[string]string
b map[string]string
// expected value from Subset
expected bool
}{
{
Expand Down Expand Up @@ -50,6 +51,24 @@ func TestSubset(t *testing.T) {
b: map[string]string{"a": "1", "b": "3"},
expected: false,
},
// Zero value
{
a: map[string]string{"a": "1", "b": ""},
b: map[string]string{"a": "1", "b": "3"},
expected: true,
},
// Zero value, but the other way round
{
a: map[string]string{"a": "1", "b": "3"},
b: map[string]string{"a": "1", "b": ""},
expected: false,
},
// Both zero values
{
a: map[string]string{"a": "1", "b": ""},
b: map[string]string{"a": "1", "b": ""},
expected: true,
},
} {
tc := tc
t.Run("#"+strconv.Itoa(idx), func(t *testing.T) {
Expand Down
8 changes: 6 additions & 2 deletions coderd/workspaceagents_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1076,7 +1076,8 @@ func TestWorkspaceAgentContainers(t *testing.T) {
pool, err := dockertest.NewPool("")
require.NoError(t, err, "Could not connect to docker")
testLabels := map[string]string{
"com.coder.test": uuid.New().String(),
"com.coder.test": uuid.New().String(),
"com.coder.empty": "",
}
ct, err := pool.RunWithOptions(&dockertest.RunOptions{
Repository: "busybox",
Expand All @@ -1097,7 +1098,10 @@ func TestWorkspaceAgentContainers(t *testing.T) {
Repository: "busybox",
Tag: "latest",
Cmd: []string{"sleep", "infinity"},
Labels: map[string]string{"com.coder.test": "ignoreme"},
Labels: map[string]string{
"com.coder.test": "ignoreme",
"com.coder.empty": "",
},
}, func(config *docker.HostConfig) {
config.AutoRemove = true
config.RestartPolicy = docker.RestartPolicy{Name: "no"}
Expand Down
Loading