Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make tctl <resource> ls command outputs consistent #9519

Merged
merged 10 commits into from
Mar 15, 2022
6 changes: 5 additions & 1 deletion api/types/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,9 +297,13 @@ const (
)

const (
// TeleportNamespace is used as the namespace prefix for any
// labels defined by teleport
TeleportNamespace = "teleport.dev"

// OriginLabel is a resource metadata label name used to identify a source
// that the resource originates from.
OriginLabel = "teleport.dev/origin"
OriginLabel = TeleportNamespace + "/origin"

// OriginConfigFile is an origin value indicating that the resource was
// constructed as a default value.
Expand Down
7 changes: 7 additions & 0 deletions api/types/desktop.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ type WindowsDesktopService interface {
GetAddr() string
// GetVersion returns the teleport binary version of this service.
GetTeleportVersion() string
// GetHostname returns the hostname of this service
GetHostname() string
}

var _ WindowsDesktopService = &WindowsDesktopServiceV3{}
Expand Down Expand Up @@ -97,6 +99,11 @@ func (s *WindowsDesktopServiceV3) GetAllLabels() map[string]string {
return s.Metadata.Labels
}

// GetHostname returns the windows hostname of this service.
func (s *WindowsDesktopServiceV3) GetHostname() string {
return s.Spec.Hostname
}

// MatchSearch goes through select field values and tries to
// match against the list of search values.
func (s *WindowsDesktopServiceV3) MatchSearch(values []string) bool {
Expand Down
1,496 changes: 771 additions & 725 deletions api/types/types.pb.go

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions api/types/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -2776,6 +2776,8 @@ message WindowsDesktopServiceSpecV3 {
string Addr = 1 [ (gogoproto.jsontag) = "addr" ];
// TeleportVersion is teleport binary version running this service.
string TeleportVersion = 2 [ (gogoproto.jsontag) = "teleport_version" ];
// Hostname is the desktop service hostname.
string Hostname = 3 [ (gogoproto.jsontag) = "hostname" ];
}

// WindowsDesktopFilter are filters to apply when searching for windows desktops.
Expand Down
62 changes: 60 additions & 2 deletions lib/asciitable/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ package asciitable
import (
"bytes"
"fmt"
"os"
"strings"
"text/tabwriter"

"golang.org/x/term"
)

// Column represents a column in the table.
Expand Down Expand Up @@ -50,13 +53,68 @@ func MakeHeadlessTable(columnCount int) Table {
}
}

// MakeTable creates a new instance of the table with given column names.
func MakeTable(headers []string) Table {
// MakeTable creates a new instance of the table with given column
// names. Optionally rows to be added to the table may be included.
func MakeTable(headers []string, rows ...[]string) Table {
t := MakeHeadlessTable(len(headers))
for i := range t.columns {
t.columns[i].Title = headers[i]
t.columns[i].width = len(headers[i])
}
for _, row := range rows {
t.AddRow(row)
}
return t
}

// MakeTableWithTruncatedColumn creates a table where the column
// matching truncatedColumn will be shortened to account for terminal
// width.
func MakeTableWithTruncatedColumn(columnOrder []string, rows [][]string, truncatedColumn string) Table {
lxea marked this conversation as resolved.
Show resolved Hide resolved
width, _, err := term.GetSize(int(os.Stdin.Fd()))
if err != nil {
width = 80
}
truncatedColMinSize := 16
maxColWidth := (width - truncatedColMinSize) / (len(columnOrder) - 1)
t := MakeTable([]string{})
totalLen := 0
columns := []Column{}

for collIndex, colName := range columnOrder {
column := Column{
Title: colName,
MaxCellLength: len(colName),
}
if colName == truncatedColumn { // truncated column is handled separately in next loop
columns = append(columns, column)
continue
}
for _, row := range rows {
cellLen := row[collIndex]
if len(cellLen) > column.MaxCellLength {
column.MaxCellLength = len(cellLen)
}
}
if column.MaxCellLength > maxColWidth {
column.MaxCellLength = maxColWidth
totalLen += column.MaxCellLength + 4 // "...<space>"
} else {
totalLen += column.MaxCellLength + 1 // +1 for column separator
}
columns = append(columns, column)
}

for _, column := range columns {
if column.Title == truncatedColumn {
column.MaxCellLength = width - totalLen - len("... ")
}
t.AddColumn(column)
}

for _, row := range rows {
t.AddRow(row)
}
return t
}

Expand Down
52 changes: 52 additions & 0 deletions lib/asciitable/table_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,55 @@ func TestTruncatedTable(t *testing.T) {

require.Equal(t, truncatedTable, table.AsBuffer().String())
}

func TestMakeTableWithTruncatedColumn(t *testing.T) {
// os.Stdin.Fd() fails during go test, so width is defaulted to 80
columns := []string{"column1", "column2", "column3"}
rows := [][]string{{strings.Repeat("cell1", 6), strings.Repeat("cell2", 6), strings.Repeat("cell3", 6)}}

testCases := []struct {
truncatedColumn string
expectedWidth int
expectedOutput []string
}{
{
truncatedColumn: "column2",
expectedWidth: 80,
expectedOutput: []string{
"column1 column2 column3 ",
"------------------------------ ----------------- ------------------------------ ",
"cell1cell1cell1cell1cell1cell1 cell2cell2cell... cell3cell3cell3cell3cell3cell3 ",
"",
},
},
{
truncatedColumn: "column3",
expectedWidth: 80,
expectedOutput: []string{
"column1 column2 column3 ",
"------------------------------ ------------------------------ ----------------- ",
"cell1cell1cell1cell1cell1cell1 cell2cell2cell2cell2cell2cell2 cell3cell3cell... ",
"",
},
},
{
truncatedColumn: "no column match",
expectedWidth: 93,
expectedOutput: []string{
"column1 column2 column3 ",
"------------------------------ ------------------------------ ------------------------------ ",
"cell1cell1cell1cell1cell1cell1 cell2cell2cell2cell2cell2cell2 cell3cell3cell3cell3cell3cell3 ",
"",
},
},
}
for _, testCase := range testCases {
t.Run(testCase.truncatedColumn, func(t *testing.T) {
table := MakeTableWithTruncatedColumn(columns, rows, testCase.truncatedColumn)
rows := strings.Split(table.AsBuffer().String(), "\n")
require.Len(t, rows, 4)
require.Len(t, rows[2], testCase.expectedWidth)
require.Equal(t, testCase.expectedOutput, rows)
})
}
}
1 change: 1 addition & 0 deletions lib/service/desktop.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ func (process *TeleportProcess) initWindowsDesktopServiceRegistered(log *logrus.
LDAPConfig: desktop.LDAPConfig(cfg.WindowsDesktop.LDAP),
DiscoveryBaseDN: cfg.WindowsDesktop.Discovery.BaseDN,
DiscoveryLDAPFilters: cfg.WindowsDesktop.Discovery.Filters,
Hostname: cfg.Hostname,
})
if err != nil {
return trace.Wrap(err)
Expand Down
12 changes: 6 additions & 6 deletions lib/srv/desktop/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,14 +177,14 @@ func (s *WindowsService) deleteDesktop(ctx context.Context, r types.ResourceWith
}

func applyLabelsFromLDAP(entry *ldap.Entry, labels map[string]string) {
labels["teleport.dev/dns_host_name"] = entry.GetAttributeValue(attrDNSHostName)
labels["teleport.dev/computer_name"] = entry.GetAttributeValue(attrName)
labels["teleport.dev/os"] = entry.GetAttributeValue(attrOS)
labels["teleport.dev/os_version"] = entry.GetAttributeValue(attrOSVersion)
labels[types.TeleportNamespace+"/dns_host_name"] = entry.GetAttributeValue(attrDNSHostName)
labels[types.TeleportNamespace+"/computer_name"] = entry.GetAttributeValue(attrName)
labels[types.TeleportNamespace+"/os"] = entry.GetAttributeValue(attrOS)
labels[types.TeleportNamespace+"/os_version"] = entry.GetAttributeValue(attrOSVersion)
labels[types.OriginLabel] = types.OriginDynamic
switch entry.GetAttributeValue(attrPrimaryGroupID) {
case writableDomainControllerGroupID, readOnlyDomainControllerGroupID:
labels["teleport.dev/is_domain_controller"] = "true"
labels[types.TeleportNamespace+"/is_domain_controller"] = "true"
}
}

Expand All @@ -193,7 +193,7 @@ func applyLabelsFromLDAP(entry *ldap.Entry, labels map[string]string) {
func (s *WindowsService) ldapEntryToWindowsDesktop(ctx context.Context, entry *ldap.Entry, getHostLabels func(string) map[string]string) (types.ResourceWithLabels, error) {
hostname := entry.GetAttributeValue(attrDNSHostName)
labels := getHostLabels(hostname)
labels["teleport.dev/windows_domain"] = s.cfg.Domain
labels[types.TeleportNamespace+"/windows_domain"] = s.cfg.Domain
applyLabelsFromLDAP(entry, labels)

addrs, err := s.dnsResolver.LookupHost(ctx, hostname)
Expand Down
10 changes: 5 additions & 5 deletions lib/srv/desktop/discovery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,10 @@ func TestAppliesLDAPLabels(t *testing.T) {
applyLabelsFromLDAP(entry, l)

require.Equal(t, l[types.OriginLabel], types.OriginDynamic)
require.Equal(t, l["teleport.dev/dns_host_name"], "foo.example.com")
require.Equal(t, l["teleport.dev/computer_name"], "foo")
require.Equal(t, l["teleport.dev/os"], "Windows Server")
require.Equal(t, l["teleport.dev/os_version"], "6.1")
require.Equal(t, l[types.TeleportNamespace+"/dns_host_name"], "foo.example.com")
require.Equal(t, l[types.TeleportNamespace+"/computer_name"], "foo")
require.Equal(t, l[types.TeleportNamespace+"/os"], "Windows Server")
require.Equal(t, l[types.TeleportNamespace+"/os_version"], "6.1")
}

func TestLabelsDomainControllers(t *testing.T) {
Expand Down Expand Up @@ -109,7 +109,7 @@ func TestLabelsDomainControllers(t *testing.T) {
l := make(map[string]string)
applyLabelsFromLDAP(test.entry, l)

b, _ := strconv.ParseBool(l["teleport.dev/is_domain_controller"])
b, _ := strconv.ParseBool(l[types.TeleportNamespace+"/is_domain_controller"])
test.assert(t, b)
})
}
Expand Down
3 changes: 3 additions & 0 deletions lib/srv/desktop/windows_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ type WindowsServiceConfig struct {
// Windows Desktops. If multiple filters are specified, they are ANDed
// together into a single search.
DiscoveryLDAPFilters []string
// Hostname of the windows desktop service
Hostname string
}

// LDAPConfig contains parameters for connecting to an LDAP server.
Expand Down Expand Up @@ -941,6 +943,7 @@ func (s *WindowsService) getServiceHeartbeatInfo() (types.Resource, error) {
types.WindowsDesktopServiceSpecV3{
Addr: s.cfg.Heartbeat.PublicAddr,
TeleportVersion: teleport.Version,
Hostname: s.cfg.Hostname,
})
if err != nil {
return nil, trace.Wrap(err)
Expand Down
5 changes: 3 additions & 2 deletions lib/utils/jsontools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"bytes"
"testing"

"github.com/gravitational/teleport/api/types"
"github.com/stretchr/testify/require"
)

Expand All @@ -28,8 +29,8 @@ import (
// operations that depend on the byte ordering fail (e.g. CompareAndSwap).
func TestMarshalMapConsistency(t *testing.T) {
value := map[string]string{
"teleport.dev/foo": "1234",
"teleport.dev/bar": "5678",
types.TeleportNamespace + "/foo": "1234",
types.TeleportNamespace + "/bar": "5678",
}

compareTo, err := FastMarshal(value)
Expand Down
18 changes: 9 additions & 9 deletions tool/tctl/common/app_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ type AppsCommand struct {
predicateExpr string
labels string

// verbose sets whether full table output should be shown for labels
verbose bool

// appsList implements the "tctl apps ls" subcommand.
appsList *kingpin.CmdClause
}
Expand All @@ -55,10 +58,11 @@ func (c *AppsCommand) Initialize(app *kingpin.Application, config *service.Confi

apps := app.Command("apps", "Operate on applications registered with the cluster.")
c.appsList = apps.Command("ls", "List all applications registered with the cluster.")
c.appsList.Flag("format", "Output format, 'text', 'json', or 'yaml'").Default("text").StringVar(&c.format)
c.appsList.Flag("format", "Output format, 'text', 'json', or 'yaml'").Default(teleport.Text).StringVar(&c.format)
c.appsList.Arg("labels", labelHelp).StringVar(&c.labels)
c.appsList.Flag("search", searchHelp).StringVar(&c.searchKeywords)
c.appsList.Flag("query", queryHelp).StringVar(&c.predicateExpr)
c.appsList.Flag("verbose", "Verbose table output, shows full label output").Short('v').BoolVar(&c.verbose)
}

// TryRun attempts to run subcommands like "apps ls".
Expand Down Expand Up @@ -108,22 +112,18 @@ func (c *AppsCommand) ListApps(clt auth.ClientI) error {
}
}

coll := &appServerCollection{servers: servers}
coll := &appServerCollection{servers: servers, verbose: c.verbose}

switch c.format {
case teleport.Text:
err = coll.writeText(os.Stdout)
return trace.Wrap(coll.writeText(os.Stdout))
case teleport.JSON:
err = coll.writeJSON(os.Stdout)
return trace.Wrap(coll.writeJSON(os.Stdout))
case teleport.YAML:
err = coll.writeYAML(os.Stdout)
return trace.Wrap(coll.writeYAML(os.Stdout))
default:
return trace.BadParameter("unknown format %q", c.format)
}
if err != nil {
return trace.Wrap(err)
}
return nil
}

var appMessageTemplate = template.Must(template.New("app").Parse(`The invite token: {{.token}}.
Expand Down
Loading