Skip to content

Commit

Permalink
[v13] show discovered name in tsh kube ls
Browse files Browse the repository at this point in the history
backports #30149 to branch/v13.

* update tsh kube ls tests
* fix SiteName godoc typo
  • Loading branch information
GavinFrazar committed Sep 16, 2023
1 parent b2c7f53 commit 46a54d4
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 80 deletions.
139 changes: 90 additions & 49 deletions tool/tsh/kube.go
Original file line number Diff line number Diff line change
Expand Up @@ -979,51 +979,75 @@ func (c *kubeLSCommand) run(cf *CLIConf) error {
}

selectedCluster := selectedKubeCluster(currentTeleportCluster, getKubeConfigPath(cf, ""))
err = c.showKubeClusters(cf.Stdout(), kubeClusters, selectedCluster)
return trace.Wrap(err)
}

func (c *kubeLSCommand) showKubeClusters(w io.Writer, kubeClusters types.KubeClusters, selectedCluster string) error {
format := strings.ToLower(c.format)
switch format {
case teleport.Text, "":
var (
t asciitable.Table
columns = []string{"Kube Cluster Name", "Labels", "Selected"}
rows [][]string
)

for _, cluster := range kubeClusters {
var selectedMark string
if cluster.GetName() == selectedCluster {
selectedMark = "*"
}
rows = append(rows, []string{
cluster.GetName(),
common.FormatLabels(cluster.GetAllLabels(), c.verbose),
selectedMark,
})
}

if c.quiet {
t = asciitable.MakeHeadlessTable(2)
for _, row := range rows {
t.AddRow(row[:2])
}
} else if c.verbose {
t = asciitable.MakeTable(columns, rows...)
} else {
t = asciitable.MakeTableWithTruncatedColumn(columns, rows, "Labels")
}
fmt.Fprintln(cf.Stdout(), t.AsBuffer().String())
out := formatKubeClustersAsText(kubeClusters, selectedCluster, c.quiet, c.verbose)
fmt.Fprintln(w, out)
case teleport.JSON, teleport.YAML:
sort.Sort(kubeClusters)
out, err := serializeKubeClusters(kubeClusters, selectedCluster, format)
if err != nil {
return trace.Wrap(err)
}
fmt.Fprintln(cf.Stdout(), out)
fmt.Fprintln(w, out)
default:
return trace.BadParameter("unsupported format %q", cf.Format)
return trace.BadParameter("unsupported format %q", c.format)
}

return nil
}

func getKubeClusterTextRow(kc types.KubeCluster, selectedCluster string, verbose bool) []string {
var selectedMark string
var row []string
if selectedCluster != "" && kc.GetName() == selectedCluster {
selectedMark = "*"
}
printName := kc.GetName()
if d, ok := getDiscoveredName(kc); ok && !verbose {
// print the (shorter) discovered name in non-verbose mode.
printName = d
}
labels := common.FormatLabels(kc.GetAllLabels(), verbose)
row = append(row, printName, labels, selectedMark)
return row
}

func formatKubeClustersAsText(kubeClusters types.KubeClusters, selectedCluster string, quiet, verbose bool) string {
var (
columns = []string{"Kube Cluster Name", "Labels", "Selected"}
t asciitable.Table
rows [][]string
)

for _, cluster := range kubeClusters {
r := getKubeClusterTextRow(cluster, selectedCluster, verbose)
rows = append(rows, r)
}

switch {
case quiet:
// no column headers and only include the cluster name and labels.
t = asciitable.MakeHeadlessTable(2)
for _, row := range rows {
t.AddRow(row)
}
case verbose:
t = asciitable.MakeTable(columns, rows...)
default:
t = asciitable.MakeTableWithTruncatedColumn(columns, rows, "Labels")
}

// stable sort by kube cluster name.
t.SortRowsBy([]int{0}, true)
return t.AsBuffer().String()
}

func serializeKubeClusters(kubeClusters []types.KubeCluster, selectedCluster, format string) (string, error) {
type cluster struct {
KubeClusterName string `json:"kube_cluster_name"`
Expand Down Expand Up @@ -1081,27 +1105,13 @@ func (c *kubeLSCommand) runAllClusters(cf *CLIConf) error {
return trace.Wrap(err)
}

sort.Sort(listings)

format := strings.ToLower(c.format)
switch format {
case teleport.Text, "":
var t asciitable.Table
if cf.Quiet {
t = asciitable.MakeHeadlessTable(3)
} else {
t = asciitable.MakeTable([]string{"Proxy", "Cluster", "Kube Cluster Name", "Labels"})
}
for _, listing := range listings {
t.AddRow([]string{
listing.Proxy,
listing.Cluster,
listing.KubeCluster.GetName(),
common.FormatLabels(listing.KubeCluster.GetAllLabels(), c.verbose),
})
}
fmt.Fprintln(cf.Stdout(), t.AsBuffer().String())
out := formatKubeListingsAsText(listings, c.quiet, c.verbose)
fmt.Fprintln(cf.Stdout(), out)
case teleport.JSON, teleport.YAML:
sort.Sort(listings)
out, err := serializeKubeListings(listings, format)
if err != nil {
return trace.Wrap(err)
Expand All @@ -1114,6 +1124,37 @@ func (c *kubeLSCommand) runAllClusters(cf *CLIConf) error {
return nil
}

func formatKubeListingsAsText(listings kubeListings, quiet, verbose bool) string {
var (
columns = []string{"Proxy", "Cluster", "Kube Cluster Name", "Labels"}
t asciitable.Table
rows [][]string
)
for _, listing := range listings {
r := append([]string{
listing.Proxy,
listing.Cluster,
}, getKubeClusterTextRow(listing.KubeCluster, "", verbose)...)
rows = append(rows, r)
}

switch {
case quiet:
// quiet, so no column headers.
t = asciitable.MakeHeadlessTable(4)
for _, row := range rows {
t.AddRow(row)
}
case verbose:
t = asciitable.MakeTable(columns, rows...)
default:
t = asciitable.MakeTableWithTruncatedColumn(columns, rows, "Labels")
}
// stable sort by proxy, then cluster, then kube cluster name.
t.SortRowsBy([]int{0, 1, 2}, true)
return t.AsBuffer().String()
}

func serializeKubeListings(kubeListings []kubeListing, format string) (string, error) {
var out []byte
var err error
Expand Down
97 changes: 67 additions & 30 deletions tool/tsh/kube_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,13 @@ package main
import (
"bytes"
"context"
"fmt"
"path/filepath"
"reflect"
"sort"
"strings"
"testing"
"time"

"github.com/google/go-cmp/cmp"
"github.com/gravitational/trace"
"github.com/stretchr/testify/require"
"golang.org/x/sync/errgroup"
Expand All @@ -40,6 +39,7 @@ import (
"github.com/gravitational/teleport/lib/modules"
"github.com/gravitational/teleport/lib/service/servicecfg"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/tool/common"
)

func TestKube(t *testing.T) {
Expand All @@ -59,8 +59,6 @@ type kubeTestPack struct {
rootKubeCluster1 string
rootKubeCluster2 string
leafKubeCluster string
serviceLabels map[string]string
formatedLabels string
}

func setupKubeTestPack(t *testing.T) *kubeTestPack {
Expand All @@ -69,20 +67,26 @@ func setupKubeTestPack(t *testing.T) *kubeTestPack {
ctx := context.Background()
rootKubeCluster1 := "root-cluster"
rootKubeCluster2 := "first-cluster"
leafKubeCluster := "leaf-cluster"
serviceLabels := map[string]string{
// mock a discovered kube cluster name in the leaf Teleport cluster.
leafKubeCluster := "leaf-cluster-some-suffix-added-by-discovery-service"
rootLabels := map[string]string{
"label1": "val1",
"ultra_long_label_for_teleport_kubernetes_service_list_kube_clusters_method": "ultra_long_label_value_for_teleport_kubernetes_service_list_kube_clusters_method",
}
formatedLabels := formatServiceLabels(serviceLabels)
leafLabels := map[string]string{
"label1": "val1",
"ultra_long_label_for_teleport_kubernetes_service_list_kube_clusters_method": "ultra_long_label_value_for_teleport_kubernetes_service_list_kube_clusters_method",
// mock a discovered kube cluster in the leaf Teleport cluster.
types.DiscoveredNameLabel: "leaf-cluster",
}

s := newTestSuite(t,
withRootConfigFunc(func(cfg *servicecfg.Config) {
cfg.Auth.NetworkingConfig.SetProxyListenerMode(types.ProxyListenerMode_Multiplex)
cfg.Kube.Enabled = true
cfg.Kube.ListenAddr = utils.MustParseAddr(localListenerAddr())
cfg.Kube.KubeconfigPath = newKubeConfigFile(t, rootKubeCluster1, rootKubeCluster2)
cfg.Kube.StaticLabels = serviceLabels
cfg.Kube.StaticLabels = rootLabels
}),
withLeafCluster(),
withLeafConfigFunc(
Expand All @@ -91,6 +95,7 @@ func setupKubeTestPack(t *testing.T) *kubeTestPack {
cfg.Kube.Enabled = true
cfg.Kube.ListenAddr = utils.MustParseAddr(localListenerAddr())
cfg.Kube.KubeconfigPath = newKubeConfigFile(t, leafKubeCluster)
cfg.Kube.StaticLabels = leafLabels
},
),
withValidationFunc(func(s *suite) bool {
Expand All @@ -110,12 +115,18 @@ func setupKubeTestPack(t *testing.T) *kubeTestPack {
rootKubeCluster1: rootKubeCluster1,
rootKubeCluster2: rootKubeCluster2,
leafKubeCluster: leafKubeCluster,
serviceLabels: serviceLabels,
formatedLabels: formatedLabels,
}
}

func (p *kubeTestPack) testListKube(t *testing.T) {
staticRootLabels := p.suite.root.Config.Kube.StaticLabels
formattedRootLabels := common.FormatLabels(staticRootLabels, false)
formattedRootLabelsVerbose := common.FormatLabels(staticRootLabels, true)

staticLeafLabels := p.suite.leaf.Config.Kube.StaticLabels
formattedLeafLabels := common.FormatLabels(staticLeafLabels, false)
formattedLeafLabelsVerbose := common.FormatLabels(staticLeafLabels, true)

tests := []struct {
name string
args []string
Expand All @@ -129,7 +140,10 @@ func (p *kubeTestPack) testListKube(t *testing.T) {
// p.rootKubeCluster1 ("root-cluster") after sorting.
table := asciitable.MakeTableWithTruncatedColumn(
[]string{"Kube Cluster Name", "Labels", "Selected"},
[][]string{{p.rootKubeCluster2, p.formatedLabels, ""}, {p.rootKubeCluster1, p.formatedLabels, ""}},
[][]string{
{p.rootKubeCluster2, formattedRootLabels, ""},
{p.rootKubeCluster1, formattedRootLabels, ""},
},
"Labels")
return table.AsBuffer().String()
},
Expand All @@ -140,8 +154,8 @@ func (p *kubeTestPack) testListKube(t *testing.T) {
wantTable: func() string {
table := asciitable.MakeTable(
[]string{"Kube Cluster Name", "Labels", "Selected"},
[]string{p.rootKubeCluster2, p.formatedLabels, ""},
[]string{p.rootKubeCluster1, p.formatedLabels, ""})
[]string{p.rootKubeCluster2, formattedRootLabelsVerbose, ""},
[]string{p.rootKubeCluster1, formattedRootLabelsVerbose, ""})
return table.AsBuffer().String()
},
},
Expand All @@ -150,26 +164,56 @@ func (p *kubeTestPack) testListKube(t *testing.T) {
args: []string{"--quiet"},
wantTable: func() string {
table := asciitable.MakeHeadlessTable(2)
table.AddRow([]string{p.rootKubeCluster2, p.formatedLabels, ""})
table.AddRow([]string{p.rootKubeCluster1, p.formatedLabels, ""})
table.AddRow([]string{p.rootKubeCluster2, formattedRootLabels, ""})
table.AddRow([]string{p.rootKubeCluster1, formattedRootLabels, ""})

return table.AsBuffer().String()
},
},
{
name: "list all clusters including leaf clusters",
args: []string{"--all"},
wantTable: func() string {
table := asciitable.MakeTableWithTruncatedColumn(
[]string{"Proxy", "Cluster", "Kube Cluster Name", "Labels"},
[][]string{
// "leaf-cluster" should be displayed instead of the
// full leaf cluster name, since it is mocked as a
// discovered resource and the discovered resource name
// is displayed in non-verbose mode.
{p.root.Config.Proxy.WebAddr.String(), "leaf1", "leaf-cluster", formattedLeafLabels},
{p.root.Config.Proxy.WebAddr.String(), "root", p.rootKubeCluster2, formattedRootLabels},
{p.root.Config.Proxy.WebAddr.String(), "root", p.rootKubeCluster1, formattedRootLabels},
},
"Labels",
)
return table.AsBuffer().String()
},
},
{
name: "list all clusters including leaf clusters with complete list of labels",
args: []string{"--all", "--verbose"},
wantTable: func() string {
table := asciitable.MakeTable(
[]string{"Proxy", "Cluster", "Kube Cluster Name", "Labels"},

[]string{p.root.Config.Proxy.WebAddr.String(), "leaf1", p.leafKubeCluster, ""},
[]string{p.root.Config.Proxy.WebAddr.String(), "root", p.rootKubeCluster2, p.formatedLabels},
[]string{p.root.Config.Proxy.WebAddr.String(), "root", p.rootKubeCluster1, p.formatedLabels},
[]string{p.root.Config.Proxy.WebAddr.String(), "leaf1", p.leafKubeCluster, formattedLeafLabelsVerbose},
[]string{p.root.Config.Proxy.WebAddr.String(), "root", p.rootKubeCluster2, formattedRootLabelsVerbose},
[]string{p.root.Config.Proxy.WebAddr.String(), "root", p.rootKubeCluster1, formattedRootLabelsVerbose},
)
return table.AsBuffer().String()
},
},
{
name: "list all clusters including leaf clusters in headless table",
args: []string{"--all", "--quiet"},
wantTable: func() string {
table := asciitable.MakeHeadlessTable(4)
table.AddRow([]string{p.root.Config.Proxy.WebAddr.String(), "leaf1", "leaf-cluster", formattedLeafLabels})
table.AddRow([]string{p.root.Config.Proxy.WebAddr.String(), "root", p.rootKubeCluster2, formattedRootLabels})
table.AddRow([]string{p.root.Config.Proxy.WebAddr.String(), "root", p.rootKubeCluster1, formattedRootLabels})
return table.AsBuffer().String()
},
},
}

for _, tc := range tests {
Expand All @@ -195,7 +239,10 @@ func (p *kubeTestPack) testListKube(t *testing.T) {
setKubeConfigPath(filepath.Join(t.TempDir(), "kubeconfig")),
)
require.NoError(t, err)
require.Contains(t, captureStdout.String(), tc.wantTable())
got := strings.TrimSpace(captureStdout.String())
want := strings.TrimSpace(tc.wantTable())
diff := cmp.Diff(want, got)
require.Empty(t, diff)
})
}
}
Expand Down Expand Up @@ -334,16 +381,6 @@ func newKubeConfigFile(t *testing.T, clusterNames ...string) string {
return kubeConfigLocation
}

func formatServiceLabels(labels map[string]string) string {
labelSlice := make([]string, 0, len(labels))
for key, value := range labels {
labelSlice = append(labelSlice, fmt.Sprintf("%s=%s", key, value))
}

sort.Strings(labelSlice)
return strings.Join(labelSlice, ",")
}

func newKubeSelfSubjectServer(t *testing.T) string {
srv, err := kubeserver.NewKubeAPIMock()
require.NoError(t, err)
Expand Down
2 changes: 1 addition & 1 deletion tool/tsh/tsh.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ type CLIConf struct {
ProxyJump string
// --local flag for ssh
LocalExec bool
// SiteName specifies remote site go login to
// SiteName specifies remote site to login to.
SiteName string
// KubernetesCluster specifies the kubernetes cluster to login to.
KubernetesCluster string
Expand Down

0 comments on commit 46a54d4

Please sign in to comment.