Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
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
56 changes: 46 additions & 10 deletions lib/kube/kubeconfig/kubeconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/gravitational/trace"
"github.com/sirupsen/logrus"
"golang.org/x/exp/maps"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"

Expand All @@ -37,6 +38,12 @@ var log = logrus.WithFields(logrus.Fields{
trace.Component: teleport.ComponentKubeClient,
})

const (
// teleportKubeClusterNameExtension is the name of the extension that
// contains the Teleport Kube cluster name.
teleportKubeClusterNameExtension = "teleport.kube.name"
)

// Values are Teleport user data needed to generate kubeconfig entries.
type Values struct {
// TeleportClusterName is used to name kubeconfig sections ("context", "cluster" and
Expand Down Expand Up @@ -173,7 +180,7 @@ func Update(path string, v Values, storeAllCAs bool) error {
}
config.AuthInfos[authName] = authInfo

setContext(config.Contexts, contextName, clusterName, authName, v.Namespace)
setContext(config.Contexts, contextName, clusterName, authName, c, v.Namespace)
}
if v.SelectCluster != "" {
contextName := ContextName(v.TeleportClusterName, v.SelectCluster)
Expand All @@ -199,9 +206,9 @@ func Update(path string, v Values, storeAllCAs bool) error {

clusterName := v.TeleportClusterName
contextName := clusterName

var kubeClusterName string
if len(v.KubeClusters) == 1 {
kubeClusterName := v.KubeClusters[0]
kubeClusterName = v.KubeClusters[0]
contextName = ContextName(clusterName, kubeClusterName)
}

Expand All @@ -222,7 +229,7 @@ func Update(path string, v Values, storeAllCAs bool) error {
ClientCertificateData: v.Credentials.TLSCert,
ClientKeyData: rsaKeyPEM,
}
setContext(config.Contexts, contextName, clusterName, contextName, v.Namespace)
setContext(config.Contexts, contextName, clusterName, contextName, kubeClusterName, v.Namespace)
setSelectedExtension(config.Contexts, config.CurrentContext, clusterName)
config.CurrentContext = contextName
} else if !trace.IsBadParameter(err) {
Expand All @@ -234,7 +241,7 @@ func Update(path string, v Values, storeAllCAs bool) error {
return Save(path, *config)
}

func setContext(contexts map[string]*clientcmdapi.Context, name, cluster, auth string, namespace string) {
func setContext(contexts map[string]*clientcmdapi.Context, name, cluster, auth, kubeName, namespace string) {
lastContext := contexts[name]
newContext := &clientcmdapi.Context{
Cluster: cluster,
Expand All @@ -245,6 +252,16 @@ func setContext(contexts map[string]*clientcmdapi.Context, name, cluster, auth s
newContext.Extensions = lastContext.Extensions
}

if newContext.Extensions == nil {
newContext.Extensions = make(map[string]runtime.Object)
}
if kubeName != "" {
newContext.Extensions[teleportKubeClusterNameExtension] = &runtime.Unknown{
// We need to wrap the kubeName in quotes to make sure it is parsed as a string.
Raw: []byte(fmt.Sprintf("%q", kubeName)),
}
}

// If a user specifies the default namespace we should override it.
// Otherwise we should carry the namespace previously defined for the context.
if len(namespace) > 0 {
Expand Down Expand Up @@ -395,13 +412,29 @@ func ContextName(teleportCluster, kubeCluster string) string {

// KubeClusterFromContext extracts the kubernetes cluster name from context
// name generated by this package.
func KubeClusterFromContext(contextName, teleportCluster string) string {
// If context name doesn't start with teleport cluster name, it was not
func KubeClusterFromContext(contextName string, ctx *clientcmdapi.Context, teleportCluster string) string {
switch {
// If the context name starts with teleport cluster name, it was
// generated by tsh.
if !strings.HasPrefix(contextName, teleportCluster+"-") {
case strings.HasPrefix(contextName, teleportCluster+"-"):
return strings.TrimPrefix(contextName, teleportCluster+"-")
// If the context cluster matches teleport cluster, it was generated by
// tsh using --set-context-override flag.
case ctx != nil && ctx.Cluster == teleportCluster:
if v, ok := ctx.Extensions[teleportKubeClusterNameExtension]; ok {
if raw, ok := v.(*runtime.Unknown); ok && trimQuotes(string(raw.Raw)) != "" {
// The value is a JSON string, so we need to trim the quotes.
return trimQuotes(string(raw.Raw))
}
}
return contextName
default:
return ""
}
return strings.TrimPrefix(contextName, teleportCluster+"-")
}

func trimQuotes(s string) string {
return strings.TrimSuffix(strings.TrimPrefix(s, "\""), "\"")
}

// SelectContext switches the active kubeconfig context to point to the
Expand Down Expand Up @@ -475,7 +508,10 @@ func SelectedKubeCluster(path, teleportCluster string) (string, error) {
return "", trace.Wrap(err)
}

if kubeCluster := KubeClusterFromContext(kubeconfig.CurrentContext, teleportCluster); kubeCluster != "" {
if kubeCluster := KubeClusterFromContext(
kubeconfig.CurrentContext,
kubeconfig.Contexts[kubeconfig.CurrentContext],
teleportCluster); kubeCluster != "" {
return kubeCluster, nil
}
return "", trace.NotFound("default context does not belong to Teleport")
Expand Down
88 changes: 85 additions & 3 deletions lib/kube/kubeconfig/kubeconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,8 +319,13 @@ func TestUpdateWithExec(t *testing.T) {
Cluster: clusterName,
AuthInfo: authInfoName,
LocationOfOrigin: kubeconfigPath,
Extensions: map[string]runtime.Object{},
Namespace: tt.namespace,
Extensions: map[string]runtime.Object{
teleportKubeClusterNameExtension: &runtime.Unknown{
Raw: []byte(fmt.Sprintf("%q", kubeCluster)),
ContentType: "application/json",
},
},
Namespace: tt.namespace,
}
config, err := Load(kubeconfigPath)
require.NoError(t, err)
Expand Down Expand Up @@ -386,7 +391,12 @@ func TestUpdateWithExecAndProxy(t *testing.T) {
Cluster: clusterName,
AuthInfo: contextName,
LocationOfOrigin: kubeconfigPath,
Extensions: map[string]runtime.Object{},
Extensions: map[string]runtime.Object{
teleportKubeClusterNameExtension: &runtime.Unknown{
Raw: []byte(fmt.Sprintf("%q", kubeCluster)),
ContentType: "application/json",
},
},
}

config, err := Load(kubeconfigPath)
Expand Down Expand Up @@ -577,3 +587,75 @@ func genUserKey(hostname string) (*client.Key, []byte, error) {
}},
}, caCert, nil
}

func TestKubeClusterFromContext(t *testing.T) {
type args struct {
contextName string
ctx *clientcmdapi.Context
teleportCluster string
}
tests := []struct {
name string
args args
want string
}{
{
name: "context name is cluster name",
args: args{
contextName: "cluster1",
ctx: &clientcmdapi.Context{Cluster: "cluster1"},
teleportCluster: "cluster1",
},
want: "cluster1",
},
{
name: "context name is {teleport-cluster}-cluster name",
args: args{
contextName: "telecluster-cluster1",
ctx: &clientcmdapi.Context{Cluster: "cluster1"},
teleportCluster: "telecluster",
},
want: "cluster1",
},
{
name: "context name is {kube-cluster} name",
args: args{
contextName: "cluster1",
ctx: &clientcmdapi.Context{Cluster: "telecluster"},
teleportCluster: "telecluster",
},
want: "cluster1",
},
{
name: "kube cluster name is set in extension",
args: args{
contextName: "cluster1",
ctx: &clientcmdapi.Context{
Cluster: "telecluster",
Extensions: map[string]runtime.Object{
teleportKubeClusterNameExtension: &runtime.Unknown{
Raw: []byte("\"another\""),
},
},
},
teleportCluster: "telecluster",
},
want: "another",
},
{
name: "context isn't from teleport",
args: args{
contextName: "cluster1",
ctx: &clientcmdapi.Context{Cluster: "someothercluster"},
teleportCluster: "telecluster",
},
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := KubeClusterFromContext(tt.args.contextName, tt.args.ctx, tt.args.teleportCluster)
require.Equal(t, tt.want, got)
})
}
}
10 changes: 5 additions & 5 deletions lib/kube/kubeconfig/localproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ func LocalProxyClustersFromDefaultConfig(defaultConfig *clientcmdapi.Config, clu
continue
}

for contextName, context := range defaultConfig.Contexts {
if context.Cluster != teleportClusterName {
for contextName, ctx := range defaultConfig.Contexts {
if ctx.Cluster != teleportClusterName {
continue
}
auth, found := defaultConfig.AuthInfos[contextName]
Expand All @@ -142,8 +142,8 @@ func LocalProxyClustersFromDefaultConfig(defaultConfig *clientcmdapi.Config, clu

clusters = append(clusters, LocalProxyCluster{
TeleportCluster: teleportClusterName,
KubeCluster: KubeClusterFromContext(contextName, teleportClusterName),
Namespace: context.Namespace,
KubeCluster: KubeClusterFromContext(contextName, ctx, teleportClusterName),
Namespace: ctx.Namespace,
Impersonate: auth.Impersonate,
ImpersonateGroups: auth.ImpersonateGroups,
})
Expand Down Expand Up @@ -178,7 +178,7 @@ func FindTeleportClusterForLocalProxy(defaultConfig *clientcmdapi.Config, cluste

return LocalProxyCluster{
TeleportCluster: context.Cluster,
KubeCluster: KubeClusterFromContext(contextName, context.Cluster),
KubeCluster: KubeClusterFromContext(contextName, context, context.Cluster),
Namespace: context.Namespace,
Impersonate: auth.Impersonate,
ImpersonateGroups: auth.ImpersonateGroups,
Expand Down
16 changes: 16 additions & 0 deletions tool/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,19 @@ func FormatLabels(labels map[string]string, verbose bool) string {
namespaced = append(namespaced, teleportNamespaced...)
return strings.Join(append(result, namespaced...), ",")
}

// FormatResourceName 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 FormatResourceName(r types.ResourceWithLabels, verbose bool) string {
if !verbose {
// return the (shorter) discovered name in non-verbose mode.
discoveredName, ok := r.GetAllLabels()[types.DiscoveredNameLabel]
if ok && discoveredName != "" {
return discoveredName
}
}
return r.GetName()
}
21 changes: 16 additions & 5 deletions tool/tctl/common/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,7 @@ func (c *databaseServerCollection) writeText(w io.Writer, verbose bool) error {
labels := common.FormatLabels(server.GetDatabase().GetAllLabels(), verbose)
rows = append(rows, []string{
server.GetHostname(),
server.GetDatabase().GetName(),
common.FormatResourceName(server.GetDatabase(), verbose),
server.GetDatabase().GetProtocol(),
server.GetDatabase().GetURI(),
labels,
Expand All @@ -702,6 +702,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 @@ -730,7 +732,10 @@ func (c *databaseCollection) writeText(w io.Writer, verbose bool) error {
for _, database := range c.databases {
labels := common.FormatLabels(database.GetAllLabels(), verbose)
rows = append(rows, []string{
database.GetName(), database.GetProtocol(), database.GetURI(), labels,
common.FormatResourceName(database, verbose),
database.GetProtocol(),
database.GetURI(),
labels,
})
}
headers := []string{"Name", "Protocol", "URI", "Labels"}
Expand All @@ -740,6 +745,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 @@ -870,7 +877,7 @@ func (c *kubeServerCollection) writeText(w io.Writer, verbose bool) error {
}
labels := common.FormatLabels(kube.GetAllLabels(), verbose)
rows = append(rows, []string{
kube.GetName(),
common.FormatResourceName(kube, verbose),
labels,
server.GetTeleportVersion(),
})
Expand All @@ -883,6 +890,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 @@ -916,12 +925,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 := common.FormatLabels(cluster.GetAllLabels(), verbose)
rows = append(rows, []string{
cluster.GetName(), labels,
common.FormatResourceName(cluster, verbose),
labels,
})
}
headers := []string{"Name", "Labels"}
Expand All @@ -931,6 +940,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
Loading