Skip to content
25 changes: 25 additions & 0 deletions lib/asciitable/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"strings"
"text/tabwriter"

"golang.org/x/exp/slices"
"golang.org/x/term"
)

Expand Down Expand Up @@ -208,6 +209,30 @@ func (t *Table) IsHeadless() bool {
return true
}

// SortRowsBy sorts the table rows with the given column indices as the sorting
// key, optionally performing a stable sort. Column indices out of range are
// ignored - it is the caller's responsibility to ensure the indices are in
// range.
func (t *Table) SortRowsBy(colIdxKey []int, stable bool) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we validate the the ColIdx is not greater than len(t.columns) ?

lessFn := func(a, b []string) bool {
for _, col := range colIdxKey {
limit := min(len(a), len(b))
if col >= limit {
continue
}
if a[col] != b[col] {
return a[col] < b[col]
}
}
return false
}
if stable {
slices.SortStableFunc(t.rows, lessFn)
} else {
slices.SortFunc(t.rows, lessFn)
}
}

func min(a, b int) int {
if a < b {
return a
Expand Down
36 changes: 31 additions & 5 deletions tool/tctl/common/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -687,7 +687,7 @@ func (c *databaseServerCollection) writeText(w io.Writer, verbose bool) error {
labels := stripInternalTeleportLabels(verbose, server.GetDatabase().GetAllLabels())
rows = append(rows, []string{
server.GetHostname(),
server.GetDatabase().GetName(),
nameOrDiscoveredName(server.GetDatabase(), verbose),
server.GetDatabase().GetProtocol(),
server.GetDatabase().GetURI(),
labels,
Expand All @@ -701,6 +701,8 @@ func (c *databaseServerCollection) writeText(w io.Writer, verbose bool) error {
} else {
t = asciitable.MakeTableWithTruncatedColumn(headers, rows, "Labels")
}
// stable sort by hostname then by name.
t.SortRowsBy([]int{0, 1}, true)
_, err := t.AsBuffer().WriteTo(w)
return trace.Wrap(err)
}
Expand Down Expand Up @@ -729,7 +731,10 @@ func (c *databaseCollection) writeText(w io.Writer, verbose bool) error {
for _, database := range c.databases {
labels := stripInternalTeleportLabels(verbose, database.GetAllLabels())
rows = append(rows, []string{
database.GetName(), database.GetProtocol(), database.GetURI(), labels,
nameOrDiscoveredName(database, verbose),
database.GetProtocol(),
database.GetURI(),
labels,
})
}
headers := []string{"Name", "Protocol", "URI", "Labels"}
Expand All @@ -739,6 +744,8 @@ func (c *databaseCollection) writeText(w io.Writer, verbose bool) error {
} else {
t = asciitable.MakeTableWithTruncatedColumn(headers, rows, "Labels")
}
// stable sort by name.
t.SortRowsBy([]int{0}, true)
_, err := t.AsBuffer().WriteTo(w)
return trace.Wrap(err)
}
Expand Down Expand Up @@ -882,7 +889,7 @@ func (c *kubeServerCollection) writeText(w io.Writer, verbose bool) error {
labels := stripInternalTeleportLabels(verbose,
types.CombineLabels(kube.GetStaticLabels(), types.LabelsToV2(kube.GetDynamicLabels())))
rows = append(rows, []string{
kube.GetName(),
nameOrDiscoveredName(kube, verbose),
labels,
server.GetTeleportVersion(),
})
Expand All @@ -895,6 +902,8 @@ func (c *kubeServerCollection) writeText(w io.Writer, verbose bool) error {
} else {
t = asciitable.MakeTableWithTruncatedColumn(headers, rows, "Labels")
}
// stable sort by cluster name.
t.SortRowsBy([]int{0}, true)

_, err := t.AsBuffer().WriteTo(w)
return trace.Wrap(err)
Expand Down Expand Up @@ -928,12 +937,12 @@ func (c *kubeClusterCollection) resources() (r []types.Resource) {
// cluster4 owner=cluster4,region=southcentralus,resource-group=cluster4,subscription-id=subID
// If verbose is disabled, labels column can be truncated to fit into the console.
func (c *kubeClusterCollection) writeText(w io.Writer, verbose bool) error {
sort.Sort(types.KubeClusters(c.clusters))
var rows [][]string
for _, cluster := range c.clusters {
labels := stripInternalTeleportLabels(verbose, cluster.GetAllLabels())
rows = append(rows, []string{
cluster.GetName(), labels,
nameOrDiscoveredName(cluster, verbose),
labels,
})
}
headers := []string{"Name", "Labels"}
Expand All @@ -943,6 +952,8 @@ func (c *kubeClusterCollection) writeText(w io.Writer, verbose bool) error {
} else {
t = asciitable.MakeTableWithTruncatedColumn(headers, rows, "Labels")
}
// stable sort by name.
t.SortRowsBy([]int{0}, true)
_, err := t.AsBuffer().WriteTo(w)
return trace.Wrap(err)
}
Expand Down Expand Up @@ -1193,3 +1204,18 @@ func (c *userGroupCollection) writeText(w io.Writer, verbose bool) error {
_, err := t.AsBuffer().WriteTo(w)
return trace.Wrap(err)
}

// nameOrDiscoveredName returns the resource's name or its name as originally
// discovered in the cloud by the Teleport Discovery Service.
// In verbose mode, it always returns the resource name.
// In non-verbose mode, if the resource came from discovery and has the
// discovered name label, it returns the discovered name.
func nameOrDiscoveredName(r types.ResourceWithLabels, verbose bool) string {
if !verbose {
originalName, ok := r.GetAllLabels()[types.DiscoveredNameLabel]
if ok && originalName != "" {
return originalName
}
}
return r.GetName()
}
Loading