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

[v13] Fix Kubernetes selected cluster #32087

Merged
merged 9 commits into from Sep 19, 2023
56 changes: 46 additions & 10 deletions lib/kube/kubeconfig/kubeconfig.go
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
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
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
3 changes: 2 additions & 1 deletion tool/tsh/access_request.go
Expand Up @@ -33,6 +33,7 @@ import (
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/asciitable"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/kube/kubeconfig"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/tool/common"
Expand Down Expand Up @@ -385,7 +386,7 @@ func onRequestSearch(cf *CLIConf) error {

// If KubeCluster not provided try to read it from kubeconfig.
if cf.KubernetesCluster == "" {
cf.KubernetesCluster = selectedKubeCluster(tc.SiteName, getKubeConfigPath(cf, ""))
cf.KubernetesCluster, _ = kubeconfig.SelectedKubeCluster(getKubeConfigPath(cf, ""), tc.SiteName)
}
if cf.ResourceKind == types.KindKubePod && cf.KubernetesCluster == "" {
return trace.BadParameter("when searching for Pods, --kube-cluster cannot be empty")
Expand Down
14 changes: 3 additions & 11 deletions tool/tsh/kube.go
Expand Up @@ -974,7 +974,9 @@ func (c *kubeLSCommand) run(cf *CLIConf) error {
return trace.Wrap(err)
}

selectedCluster := selectedKubeCluster(currentTeleportCluster, getKubeConfigPath(cf, ""))
// Ignore errors from fetching the current cluster, since it's not
// mandatory to have a cluster selected or even to have a kubeconfig file.
selectedCluster, _ := kubeconfig.SelectedKubeCluster(getKubeConfigPath(cf, ""), currentTeleportCluster)
err = c.showKubeClusters(cf.Stdout(), kubeClusters, selectedCluster)
return trace.Wrap(err)
}
Expand Down Expand Up @@ -1158,16 +1160,6 @@ func serializeKubeListings(kubeListings []kubeListing, format string) (string, e
return string(out), trace.Wrap(err)
}

// selectedKubeCluster determines which kube cluster, if any, is selected.
func selectedKubeCluster(currentTeleportCluster string, kubeconfgPath string) string {
kc, err := kubeconfig.Load(kubeconfgPath)
if err != nil {
log.WithError(err).Warning("Failed parsing existing kubeconfig")
return ""
}
return kubeconfig.KubeClusterFromContext(kc.CurrentContext, currentTeleportCluster)
}

type kubeLoginCommand struct {
*kingpin.CmdClause
kubeCluster string
Expand Down
7 changes: 5 additions & 2 deletions tool/tsh/kubectl.go
Expand Up @@ -379,7 +379,11 @@ func getKubeClusterName(args []string, teleportClusterName string) (string, erro
kubeName, err := kubeconfig.SelectedKubeCluster(kubeconfigLocation, teleportClusterName)
return kubeName, trace.Wrap(err)
}
kubeName := kubeconfig.KubeClusterFromContext(selectedContext, teleportClusterName)
kc, err := kubeconfig.Load(kubeconfigLocation)
if err != nil {
return "", trace.Wrap(err)
}
kubeName := kubeconfig.KubeClusterFromContext(selectedContext, kc.Contexts[selectedContext], teleportClusterName)
if kubeName == "" {
return "", trace.BadParameter("selected context %q does not belong to Teleport cluster %q", selectedContext, teleportClusterName)
}
Expand Down Expand Up @@ -497,7 +501,6 @@ func shouldUseKubeLocalProxy(cf *CLIConf, kubectlArgs []string) (*clientcmdapi.C
return nil, nil, false
}
return defaultConfig, kubeconfig.LocalProxyClusters{kubeCluster}, true

}

func isKubectlConfigCommand(kubectlCommand *cobra.Command, args []string) bool {
Expand Down
7 changes: 4 additions & 3 deletions tool/tsh/tsh.go
Expand Up @@ -4173,6 +4173,7 @@ func makeProfileInfo(p *client.ProfileStatus, env map[string]string, isActive bo
}
}

selectedKubeCluster, _ := kubeconfig.SelectedKubeCluster("", p.Cluster)
out := &profileInfo{
ProxyURL: p.ProxyURL.String(),
Username: p.Username,
Expand All @@ -4182,7 +4183,7 @@ func makeProfileInfo(p *client.ProfileStatus, env map[string]string, isActive bo
Traits: p.Traits,
Logins: logins,
KubernetesEnabled: p.KubeEnabled,
KubernetesCluster: selectedKubeCluster(p.Cluster, ""),
KubernetesCluster: selectedKubeCluster,
KubernetesUsers: p.KubeUsers,
KubernetesGroups: p.KubeGroups,
Databases: p.DatabaseServices(),
Expand Down Expand Up @@ -4649,7 +4650,7 @@ func onEnvironment(cf *CLIConf) error {
fmt.Printf("unset %v\n", kubeClusterEnvVar)
fmt.Printf("unset %v\n", teleport.EnvKubeConfig)
case !cf.unsetEnvironment:
kubeName := selectedKubeCluster(profile.Cluster, "")
kubeName, _ := kubeconfig.SelectedKubeCluster("", profile.Cluster)
fmt.Printf("export %v=%v\n", proxyEnvVar, profile.ProxyURL.Host)
fmt.Printf("export %v=%v\n", clusterEnvVar, profile.Cluster)
if kubeName != "" {
Expand All @@ -4674,7 +4675,7 @@ func serializeEnvironment(profile *client.ProfileStatus, format string) (string,
proxyEnvVar: profile.ProxyURL.Host,
clusterEnvVar: profile.Cluster,
}
kubeName := selectedKubeCluster(profile.Cluster, "")
kubeName, _ := kubeconfig.SelectedKubeCluster("", profile.Cluster)
if kubeName != "" {
env[kubeClusterEnvVar] = kubeName
env[teleport.EnvKubeConfig] = profile.KubeConfigPath(kubeName)
Expand Down