Ctrl K
ComunidadGuías…

cli/cliui/resources.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package cliui

import (
	"fmt"
	"io"
	"sort"
	"strconv"
	"strings"

	"github.com/google/uuid"
	"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/coderd/util/slice"
	"github.com/coder/coder/v2/codersdk"
	"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
	Devcontainers  map[uuid.UUID]codersdk.WorkspaceAgentListContainersResponse
	ShowDetails    bool
}

// 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 {
		for _, agent := range resource.Agents {
			if !agent.ParentID.Valid {
				totalAgents++
			}
		}
	}

	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.
		agents := slice.Filter(resource.Agents, func(agent codersdk.WorkspaceAgent) bool {
			return !agent.ParentID.Valid
		})
		for index, agent := range agents {
			tableWriter.AppendRow(renderAgentRow(agent, index, totalAgents, options))
			for _, row := range renderListeningPorts(options, agent.ID, index, totalAgents) {
				tableWriter.AppendRow(row)
			}
			for _, row := range renderDevcontainers(resources, options, agent.ID, index, totalAgents) {
				tableWriter.AppendRow(row)
			}
		}
		tableWriter.AppendSeparator()
	}
	_, err := fmt.Fprintln(writer, tableWriter.Render())
	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 || len(options.Devcontainers) > 0 {
			sshCommand += "." + agent.Name
		}
		sshCommand = pretty.Sprint(DefaultStyles.Code, sshCommand)
		row = append(row, sshCommand)
	}
	return 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(idx, total))
	_, _ = 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 renderDevcontainers(resources []codersdk.WorkspaceResource, 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.Devcontainers) == 0 {
		return []table.Row{}
	}
	rows = append(rows, table.Row{
		fmt.Sprintf("   %s─ %s", renderPipe(index, totalAgents), "Devcontainers"),
	})
	for idx, devcontainer := range dc.Devcontainers {
		rows = append(rows, renderDevcontainerRow(resources, devcontainer, idx, len(dc.Devcontainers), wro)...)
	}
	return rows
}

func renderDevcontainerRow(resources []codersdk.WorkspaceResource, devcontainer codersdk.WorkspaceAgentDevcontainer, index, total int, wro WorkspaceResourcesOptions) []table.Row {
	var rows []table.Row

	// If the devcontainer is running and has an associated agent, we want to
	// display the agent's details. Otherwise, we just display the devcontainer
	// name and status.
	var subAgent *codersdk.WorkspaceAgent
	displayName := devcontainer.Name
	if devcontainer.Agent != nil && devcontainer.Status == codersdk.WorkspaceAgentDevcontainerStatusRunning {
		for _, resource := range resources {
			if agent, found := slice.Find(resource.Agents, func(agent codersdk.WorkspaceAgent) bool {
				return agent.ID == devcontainer.Agent.ID
			}); found {
				subAgent = &agent
				break
			}
		}
		if subAgent != nil {
			displayName = subAgent.Name
			displayName += fmt.Sprintf(" (%s, %s)", subAgent.OperatingSystem, subAgent.Architecture)
		}
	}

	if devcontainer.Container != nil {
		displayName += " " + pretty.Sprint(DefaultStyles.Keyword, "["+devcontainer.Container.FriendlyName+"]")
	}

	// Build the main row.
	row := table.Row{
		fmt.Sprintf("      %s─ %s", renderPipe(index, total), displayName),
	}

	// Add status, health, and version columns.
	if !wro.HideAgentState {
		if subAgent != nil {
			row = append(row, renderAgentStatus(*subAgent))
			row = append(row, renderAgentHealth(*subAgent))
			row = append(row, renderAgentVersion(subAgent.Version, wro.ServerVersion))
		} else {
			row = append(row, renderDevcontainerStatus(devcontainer.Status))
			row = append(row, "") // No health for devcontainer without agent.
			row = append(row, "") // No version for devcontainer without agent.
		}
	}

	// Add access column.
	if !wro.HideAccess {
		if subAgent != nil {
			accessString := fmt.Sprintf("coder ssh %s.%s", wro.WorkspaceName, subAgent.Name)
			row = append(row, pretty.Sprint(DefaultStyles.Code, accessString))
		} else {
			row = append(row, "") // No access for devcontainers without agent.
		}
	}

	rows = append(rows, row)

	// Add error message if present.
	if errorMessage := devcontainer.Error; errorMessage != "" {
		// Cap error message length for display.
		if !wro.ShowDetails && len(errorMessage) > 80 {
			errorMessage = errorMessage[:79] + "…"
		}
		errorRow := table.Row{
			"         × " + pretty.Sprint(DefaultStyles.Error, errorMessage),
			"",
			"",
			"",
		}
		if !wro.HideAccess {
			errorRow = append(errorRow, "")
		}
		rows = append(rows, errorRow)
	}

	// Add listening ports for the devcontainer agent.
	if subAgent != nil {
		portRows := renderListeningPorts(wro, subAgent.ID, index, total)
		for _, portRow := range portRows {
			// Adjust indentation for ports under devcontainer agent.
			if len(portRow) > 0 {
				if str, ok := portRow[0].(string); ok {
					portRow[0] = "      " + str // Add extra indentation.
				}
			}
			rows = append(rows, portRow)
		}
	}

	return rows
}

func renderDevcontainerStatus(status codersdk.WorkspaceAgentDevcontainerStatus) string {
	switch status {
	case codersdk.WorkspaceAgentDevcontainerStatusRunning:
		return pretty.Sprint(DefaultStyles.Keyword, "▶ running")
	case codersdk.WorkspaceAgentDevcontainerStatusStopped:
		return pretty.Sprint(DefaultStyles.Placeholder, "⏹ stopped")
	case codersdk.WorkspaceAgentDevcontainerStatusStarting:
		return pretty.Sprint(DefaultStyles.Warn, "⧗ starting")
	case codersdk.WorkspaceAgentDevcontainerStatusError:
		return pretty.Sprint(DefaultStyles.Error, "✘ error")
	default:
		return pretty.Sprint(DefaultStyles.Placeholder, "○ "+string(status))
	}
}

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)
}

func renderPipe(idx, total int) string {
	if idx == total-1 {
		return pipeEnd
	}
	return pipeMid
}