mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
feat(cli): display devcontainers in show command (#16515)
Displays running devcontainers into the `coder show` CLI command.
This commit is contained in:
+67
-11
@@ -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.
|
||||
@@ -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()
|
||||
@@ -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 != "" {
|
||||
@@ -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:
|
||||
|
||||
+21
-3
@@ -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 {
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}{
|
||||
{
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
@@ -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"}
|
||||
|
||||
Reference in New Issue
Block a user