mirror of
https://github.com/coder/coder.git
synced 2025-07-09 11:45:56 +00:00
This change will improve over CLI performance and "snappiness" as well as substantially reduce our test times. Preliminary benchmarks show `coder server --help` times cut from 300ms to 120ms on my dogfood instance. The inefficiency of lipgloss disproportionately impacts our system, as all help text for every command is generated whenever any command is invoked. The `pretty` API could clean up a lot of the code (e.g., by replacing complex string concatenations with Printf), but this commit is too expansive as is so that work will be done in a follow up.
166 lines
6.4 KiB
Go
166 lines
6.4 KiB
Go
package cliui
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
"strconv"
|
|
|
|
"github.com/jedib0t/go-pretty/v6/table"
|
|
"golang.org/x/mod/semver"
|
|
|
|
"github.com/coder/coder/v2/coderd/database/dbtime"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/pretty"
|
|
)
|
|
|
|
type WorkspaceResourcesOptions struct {
|
|
WorkspaceName string
|
|
HideAgentState bool
|
|
HideAccess bool
|
|
Title string
|
|
ServerVersion string
|
|
}
|
|
|
|
// WorkspaceResources displays the connection status and tree-view of provided resources.
|
|
// ┌────────────────────────────────────────────────────────────────────────────┐
|
|
// │ RESOURCE STATUS ACCESS │
|
|
// ├────────────────────────────────────────────────────────────────────────────┤
|
|
// │ google_compute_disk.root │
|
|
// ├────────────────────────────────────────────────────────────────────────────┤
|
|
// │ google_compute_instance.dev │
|
|
// │ └─ dev (linux, amd64) ⦾ connecting [10s] coder ssh dev.dev │
|
|
// ├────────────────────────────────────────────────────────────────────────────┤
|
|
// │ kubernetes_pod.dev │
|
|
// │ ├─ go (linux, amd64) ⦿ connected coder ssh dev.go │
|
|
// │ └─ postgres (linux, amd64) ⦾ disconnected [4s] coder ssh dev.postgres │
|
|
// └────────────────────────────────────────────────────────────────────────────┘
|
|
func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource, options WorkspaceResourcesOptions) error {
|
|
// Sort resources by type for consistent output.
|
|
sort.Slice(resources, func(i, j int) bool {
|
|
return resources[i].Type < resources[j].Type
|
|
})
|
|
|
|
tableWriter := table.NewWriter()
|
|
if options.Title != "" {
|
|
tableWriter.SetTitle(options.Title)
|
|
}
|
|
tableWriter.SetStyle(table.StyleLight)
|
|
tableWriter.Style().Options.SeparateColumns = false
|
|
row := table.Row{"Resource"}
|
|
if !options.HideAgentState {
|
|
row = append(row, "Status")
|
|
row = append(row, "Health")
|
|
row = append(row, "Version")
|
|
}
|
|
if !options.HideAccess {
|
|
row = append(row, "Access")
|
|
}
|
|
tableWriter.AppendHeader(row)
|
|
|
|
totalAgents := 0
|
|
for _, resource := range resources {
|
|
totalAgents += len(resource.Agents)
|
|
}
|
|
|
|
for _, resource := range resources {
|
|
if resource.Type == "random_string" {
|
|
// Hide resources that aren't substantial to a user!
|
|
// This is an unfortunate case, and we should allow
|
|
// callers to hide resources eventually.
|
|
continue
|
|
}
|
|
resourceAddress := resource.Type + "." + resource.Name
|
|
|
|
// Sort agents by name for consistent output.
|
|
sort.Slice(resource.Agents, func(i, j int) bool {
|
|
return resource.Agents[i].Name < resource.Agents[j].Name
|
|
})
|
|
|
|
// Display a line for the resource.
|
|
tableWriter.AppendRow(table.Row{
|
|
Bold(resourceAddress),
|
|
"",
|
|
"",
|
|
"",
|
|
})
|
|
// 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
|
|
}
|
|
sshCommand = pretty.Sprint(DefaultStyles.Code, sshCommand)
|
|
row = append(row, sshCommand)
|
|
}
|
|
tableWriter.AppendRow(row)
|
|
}
|
|
tableWriter.AppendSeparator()
|
|
}
|
|
_, err := fmt.Fprintln(writer, tableWriter.Render())
|
|
return err
|
|
}
|
|
|
|
func renderAgentStatus(agent codersdk.WorkspaceAgent) string {
|
|
switch agent.Status {
|
|
case codersdk.WorkspaceAgentConnecting:
|
|
since := dbtime.Now().Sub(agent.CreatedAt)
|
|
return pretty.Sprint(DefaultStyles.Warn, "⦾ connecting") + " " +
|
|
pretty.Sprint(DefaultStyles.Placeholder, "["+strconv.Itoa(int(since.Seconds()))+"s]")
|
|
case codersdk.WorkspaceAgentDisconnected:
|
|
since := dbtime.Now().Sub(*agent.DisconnectedAt)
|
|
return pretty.Sprint(DefaultStyles.Error, "⦾ disconnected") + " " +
|
|
pretty.Sprint(DefaultStyles.Placeholder, "["+strconv.Itoa(int(since.Seconds()))+"s]")
|
|
case codersdk.WorkspaceAgentTimeout:
|
|
since := dbtime.Now().Sub(agent.CreatedAt)
|
|
return fmt.Sprintf(
|
|
"%s %s",
|
|
pretty.Sprint(DefaultStyles.Warn, "⦾ timeout"),
|
|
pretty.Sprint(DefaultStyles.Placeholder, "["+strconv.Itoa(int(since.Seconds()))+"s]"),
|
|
)
|
|
case codersdk.WorkspaceAgentConnected:
|
|
return pretty.Sprint(DefaultStyles.Keyword, "⦿ connected")
|
|
default:
|
|
return pretty.Sprint(DefaultStyles.Warn, "○ unknown")
|
|
}
|
|
}
|
|
|
|
func renderAgentHealth(agent codersdk.WorkspaceAgent) string {
|
|
if agent.Health.Healthy {
|
|
return pretty.Sprint(DefaultStyles.Keyword, "✔ healthy")
|
|
}
|
|
return pretty.Sprint(DefaultStyles.Error, "✘ "+agent.Health.Reason)
|
|
}
|
|
|
|
func renderAgentVersion(agentVersion, serverVersion string) string {
|
|
if agentVersion == "" {
|
|
agentVersion = "(unknown)"
|
|
}
|
|
if !semver.IsValid(serverVersion) || !semver.IsValid(agentVersion) {
|
|
return pretty.Sprint(DefaultStyles.Placeholder, agentVersion)
|
|
}
|
|
outdated := semver.Compare(agentVersion, serverVersion) < 0
|
|
if outdated {
|
|
return pretty.Sprint(DefaultStyles.Warn, agentVersion+" (outdated)")
|
|
}
|
|
return pretty.Sprint(DefaultStyles.Keyword, agentVersion)
|
|
}
|