diff --git a/cli/cliui/resources.go b/cli/cliui/resources.go index a9204c968c10a..8921033ddc9da 100644 --- a/cli/cliui/resources.go +++ b/cli/cliui/resources.go @@ -5,7 +5,9 @@ import ( "io" "sort" "strconv" + "strings" + "github.com/google/uuid" "github.com/jedib0t/go-pretty/v6/table" "golang.org/x/mod/semver" @@ -14,12 +16,18 @@ import ( "github.com/coder/pretty" ) +var ( + pipeMid = "├" + pipeEnd = "└" +) + type WorkspaceResourcesOptions struct { WorkspaceName string HideAgentState bool HideAccess bool Title string ServerVersion string + ListeningPorts map[uuid.UUID]codersdk.WorkspaceAgentListeningPortsResponse } // WorkspaceResources displays the connection status and tree-view of provided resources. @@ -86,32 +94,17 @@ func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource }) // Display all agents associated with the resource. for index, agent := range resource.Agents { - pipe := "├" - if index == len(resource.Agents)-1 { - pipe = "└" - } - row := table.Row{ - // These tree from a resource! - fmt.Sprintf("%s─ %s (%s, %s)", pipe, agent.Name, agent.OperatingSystem, agent.Architecture), - } - if !options.HideAgentState { - var agentStatus, agentHealth, agentVersion string - if !options.HideAgentState { - agentStatus = renderAgentStatus(agent) - agentHealth = renderAgentHealth(agent) - agentVersion = renderAgentVersion(agent.Version, options.ServerVersion) - } - row = append(row, agentStatus, agentHealth, agentVersion) - } - if !options.HideAccess { - sshCommand := "coder ssh " + options.WorkspaceName - if totalAgents > 1 { - sshCommand += "." + agent.Name + 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)) + } } - sshCommand = pretty.Sprint(DefaultStyles.Code, sshCommand) - row = append(row, sshCommand) } - tableWriter.AppendRow(row) } tableWriter.AppendSeparator() } @@ -119,6 +112,43 @@ func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource return err } +func renderAgentRow(agent codersdk.WorkspaceAgent, index, totalAgents int, options WorkspaceResourcesOptions) table.Row { + row := table.Row{ + // These tree from a resource! + fmt.Sprintf("%s─ %s (%s, %s)", renderPipe(index, totalAgents), agent.Name, agent.OperatingSystem, agent.Architecture), + } + if !options.HideAgentState { + var agentStatus, agentHealth, agentVersion string + if !options.HideAgentState { + agentStatus = renderAgentStatus(agent) + agentHealth = renderAgentHealth(agent) + agentVersion = renderAgentVersion(agent.Version, options.ServerVersion) + } + row = append(row, agentStatus, agentHealth, agentVersion) + } + if !options.HideAccess { + sshCommand := "coder ssh " + options.WorkspaceName + if totalAgents > 1 { + sshCommand += "." + agent.Name + } + sshCommand = pretty.Sprint(DefaultStyles.Code, sshCommand) + row = append(row, sshCommand) + } + return row +} + +func renderPortRow(port codersdk.WorkspaceAgentListeningPort, index, totalPorts int) table.Row { + var sb strings.Builder + _, _ = sb.WriteString(" ") + _, _ = sb.WriteString(renderPipe(index, totalPorts)) + _, _ = sb.WriteString("─ ") + _, _ = sb.WriteString(pretty.Sprintf(DefaultStyles.Code, "%5d/%s", port.Port, port.Network)) + if port.ProcessName != "" { + _, _ = sb.WriteString(pretty.Sprintf(DefaultStyles.Keyword, " [%s]", port.ProcessName)) + } + return table.Row{sb.String()} +} + func renderAgentStatus(agent codersdk.WorkspaceAgent) string { switch agent.Status { case codersdk.WorkspaceAgentConnecting: @@ -163,3 +193,10 @@ func renderAgentVersion(agentVersion, serverVersion string) string { } return pretty.Sprint(DefaultStyles.Keyword, agentVersion) } + +func renderPipe(idx, total int) string { + if idx == total-1 { + return pipeEnd + } + return pipeMid +} diff --git a/cli/show.go b/cli/show.go index 00c50292d69c1..7da747d6fffa1 100644 --- a/cli/show.go +++ b/cli/show.go @@ -1,8 +1,13 @@ package cli import ( + "sort" + "sync" + "golang.org/x/xerrors" + "github.com/google/uuid" + "github.com/coder/coder/v2/cli/cliui" "github.com/coder/coder/v2/codersdk" "github.com/coder/serpent" @@ -26,10 +31,42 @@ func (r *RootCmd) show() *serpent.Command { if err != nil { return xerrors.Errorf("get workspace: %w", err) } - return cliui.WorkspaceResources(inv.Stdout, workspace.LatestBuild.Resources, cliui.WorkspaceResourcesOptions{ + + options := cliui.WorkspaceResourcesOptions{ WorkspaceName: workspace.Name, ServerVersion: buildInfo.Version, - }) + } + if workspace.LatestBuild.Status == codersdk.WorkspaceStatusRunning { + // Get listening ports for each agent. + options.ListeningPorts = fetchListeningPorts(inv, client, workspace.LatestBuild.Resources...) + } + 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 { + ports := make(map[uuid.UUID]codersdk.WorkspaceAgentListeningPortsResponse) + var wg sync.WaitGroup + var mu sync.Mutex + for _, res := range resources { + for _, agent := range res.Agents { + wg.Add(1) + go func() { + defer wg.Done() + lp, err := client.WorkspaceAgentListeningPorts(inv.Context(), agent.ID) + if err != nil { + cliui.Warnf(inv.Stderr, "Failed to get listening ports for agent %s: %v", agent.Name, err) + } + sort.Slice(lp.Ports, func(i, j int) bool { + return lp.Ports[i].Port < lp.Ports[j].Port + }) + mu.Lock() + ports[agent.ID] = lp + mu.Unlock() + }() + } + } + wg.Wait() + return ports +}