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 +}
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: