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

[v12] Add option to override kube context on tsh kube login #25253

Merged
merged 1 commit into from Apr 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 14 additions & 0 deletions lib/kube/kubeconfig/kubeconfig.go
Expand Up @@ -76,6 +76,10 @@ type Values struct {
// SelectCluster is the name of the kubernetes cluster to set in
// current-context.
SelectCluster string
// OverrideContext is the name of the context to set when adding a new cluster.
// If empty, the context name will be generated from the {teleport-cluster}-{kube-cluster}.
// It can only be used when adding a single cluster.
OverrideContext string
}

// ExecValues contain values for configuring tsh as an exec auth plugin in
Expand All @@ -96,6 +100,10 @@ type ExecValues struct {
// If `path` is empty, Update will try to guess it based on the environment or
// known defaults.
func Update(path string, v Values, storeAllCAs bool) error {
if v.OverrideContext != "" && len(v.KubeClusters) > 1 {
return trace.BadParameter("cannot override context when adding multiple clusters")
}

config, err := Load(path)
if err != nil {
return trace.Wrap(err)
Expand Down Expand Up @@ -134,6 +142,9 @@ func Update(path string, v Values, storeAllCAs bool) error {
for _, c := range v.KubeClusters {
contextName := ContextName(v.TeleportClusterName, c)
authName := contextName
if v.OverrideContext != "" {
contextName = v.OverrideContext
}
execArgs := []string{
"kube", "credentials",
fmt.Sprintf("--kube-cluster=%s", c),
Expand Down Expand Up @@ -163,6 +174,9 @@ func Update(path string, v Values, storeAllCAs bool) error {
}
if v.SelectCluster != "" {
contextName := ContextName(v.TeleportClusterName, v.SelectCluster)
if v.OverrideContext != "" {
contextName = v.OverrideContext
}
if _, ok := config.Contexts[contextName]; !ok {
return trace.BadParameter("can't switch kubeconfig context to cluster %q, run 'tsh kube ls' to see available clusters", v.SelectCluster)
}
Expand Down
26 changes: 19 additions & 7 deletions lib/kube/kubeconfig/kubeconfig_test.go
Expand Up @@ -225,10 +225,11 @@ func TestUpdateWithExec(t *testing.T) {
require.NoError(t, err)

tests := []struct {
name string
namespace string
impersonatedUser string
impersonatedGroups []string
name string
namespace string
impersonatedUser string
impersonatedGroups []string
overrideContextName string
}{
{
name: "config with namespace selection",
Expand Down Expand Up @@ -256,6 +257,13 @@ func TestUpdateWithExec(t *testing.T) {
impersonatedUser: "user",
impersonatedGroups: []string{"group1", "group2"},
},
{
name: "config with custom context name",
impersonatedUser: "",
impersonatedGroups: nil,
namespace: namespace,
overrideContextName: "custom-context-name",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand All @@ -274,18 +282,23 @@ func TestUpdateWithExec(t *testing.T) {
homeEnvVar: home,
},
},
OverrideContext: tt.overrideContextName,
}, false)
require.NoError(t, err)

wantConfig := initialConfig.DeepCopy()
contextName := ContextName(clusterName, kubeCluster)
authInfoName := contextName
if tt.overrideContextName != "" {
contextName = tt.overrideContextName
}
wantConfig.Clusters[clusterName] = &clientcmdapi.Cluster{
Server: clusterAddr,
CertificateAuthorityData: caCertPEM,
LocationOfOrigin: kubeconfigPath,
Extensions: map[string]runtime.Object{},
}
wantConfig.AuthInfos[contextName] = &clientcmdapi.AuthInfo{
wantConfig.AuthInfos[authInfoName] = &clientcmdapi.AuthInfo{
LocationOfOrigin: kubeconfigPath,
Extensions: map[string]runtime.Object{},
Impersonate: tt.impersonatedUser,
Expand All @@ -304,12 +317,11 @@ func TestUpdateWithExec(t *testing.T) {
}
wantConfig.Contexts[contextName] = &clientcmdapi.Context{
Cluster: clusterName,
AuthInfo: contextName,
AuthInfo: authInfoName,
LocationOfOrigin: kubeconfigPath,
Extensions: map[string]runtime.Object{},
Namespace: tt.namespace,
}

config, err := Load(kubeconfigPath)
require.NoError(t, err)
require.Equal(t, wantConfig, config)
Expand Down
31 changes: 19 additions & 12 deletions tool/tsh/kube.go
Expand Up @@ -983,12 +983,13 @@ func selectedKubeCluster(currentTeleportCluster string) string {

type kubeLoginCommand struct {
*kingpin.CmdClause
kubeCluster string
siteName string
impersonateUser string
impersonateGroups []string
namespace string
all bool
kubeCluster string
siteName string
impersonateUser string
impersonateGroups []string
namespace string
all bool
overrideContextName string
}

func newKubeLoginCommand(parent *kingpin.CmdClause) *kubeLoginCommand {
Expand All @@ -1002,13 +1003,18 @@ func newKubeLoginCommand(parent *kingpin.CmdClause) *kubeLoginCommand {
// TODO (tigrato): move this back to namespace once teleport drops the namespace flag.
c.Flag("kube-namespace", "Configure the default Kubernetes namespace.").Short('n').StringVar(&c.namespace)
c.Flag("all", "Generate a kubeconfig with every cluster the user has access to.").BoolVar(&c.all)
c.Flag("set-context-name", "Define a custom context name.").StringVar(&c.overrideContextName)
return c
}

func (c *kubeLoginCommand) run(cf *CLIConf) error {
if c.kubeCluster == "" && !c.all {
return trace.BadParameter("kube-cluster name is required. Check 'tsh kube ls' for a list of available clusters.")
}
if c.all && c.overrideContextName != "" {
return trace.BadParameter("cannot use --set-context-name with --all")
}

// Set CLIConf.KubernetesCluster so that the kube cluster's context is automatically selected.
cf.KubernetesCluster = c.kubeCluster
cf.SiteName = c.siteName
Expand Down Expand Up @@ -1036,7 +1042,7 @@ func (c *kubeLoginCommand) run(cf *CLIConf) error {

// Update default kubeconfig file located at ~/.kube/config or the value of
// KUBECONFIG env var even if the context exists.
if err := updateKubeConfig(cf, tc, ""); err != nil {
if err := updateKubeConfig(cf, tc, "", c.overrideContextName); err != nil {
return trace.Wrap(err)
}

Expand All @@ -1045,7 +1051,7 @@ func (c *kubeLoginCommand) run(cf *CLIConf) error {
profileKubeconfigPath := keypaths.KubeConfigPath(
profile.FullProfilePath(cf.HomePath), tc.WebProxyHost(), tc.Username, currentTeleportCluster, c.kubeCluster,
)
if err := updateKubeConfig(cf, tc, profileKubeconfigPath); err != nil {
if err := updateKubeConfig(cf, tc, profileKubeconfigPath, c.overrideContextName); err != nil {
return trace.Wrap(err)
}
if c.kubeCluster != "" {
Expand Down Expand Up @@ -1128,7 +1134,7 @@ func fetchKubeStatus(ctx context.Context, tc *client.TeleportClient) (*kubernete

// buildKubeConfigUpdate returns a kubeconfig.Values suitable for updating the user's kubeconfig
// based on the CLI parameters and the given kubernetesStatus.
func buildKubeConfigUpdate(cf *CLIConf, kubeStatus *kubernetesStatus) (*kubeconfig.Values, error) {
func buildKubeConfigUpdate(cf *CLIConf, kubeStatus *kubernetesStatus, overrideContextName string) (*kubeconfig.Values, error) {
v := &kubeconfig.Values{
ClusterAddr: kubeStatus.clusterAddr,
TeleportClusterName: kubeStatus.teleportClusterName,
Expand All @@ -1139,7 +1145,8 @@ func buildKubeConfigUpdate(cf *CLIConf, kubeStatus *kubernetesStatus) (*kubeconf
ImpersonateGroups: cf.kubernetesImpersonationConfig.kubernetesGroups,
Namespace: cf.kubeNamespace,
// Only switch the current context if kube-cluster is explicitly set on the command line.
SelectCluster: cf.KubernetesCluster,
SelectCluster: cf.KubernetesCluster,
OverrideContext: overrideContextName,
}

if cf.executablePath == "" {
Expand Down Expand Up @@ -1191,7 +1198,7 @@ type impersonationConfig struct {
// updateKubeConfig adds Teleport configuration to the users's kubeconfig based on the CLI
// parameters and the kubernetes services in the current Teleport cluster. If no path for
// the kubeconfig is given, it will use environment values or known defaults to get a path.
func updateKubeConfig(cf *CLIConf, tc *client.TeleportClient, path string) error {
func updateKubeConfig(cf *CLIConf, tc *client.TeleportClient, path string, overrideContext string) error {
// Fetch proxy's advertised ports to check for k8s support.
if _, err := tc.Ping(cf.Context); err != nil {
return trace.Wrap(err)
Expand All @@ -1210,7 +1217,7 @@ func updateKubeConfig(cf *CLIConf, tc *client.TeleportClient, path string) error
cf.Proxy = tc.WebProxyAddr
}

values, err := buildKubeConfigUpdate(cf, kubeStatus)
values, err := buildKubeConfigUpdate(cf, kubeStatus, overrideContext)
if err != nil {
return trace.Wrap(err)
}
Expand Down
2 changes: 1 addition & 1 deletion tool/tsh/tsh.go
Expand Up @@ -4584,7 +4584,7 @@ func updateKubeConfigOnLogin(cf *CLIConf, tc *client.TeleportClient, path string
if len(cf.KubernetesCluster) == 0 {
return nil
}
err := updateKubeConfig(cf, tc, "")
err := updateKubeConfig(cf, tc, "" /* update the default kubeconfig */, "" /* do not override the context name */)
return trace.Wrap(err)
}

Expand Down
2 changes: 1 addition & 1 deletion tool/tsh/tsh_test.go
Expand Up @@ -2302,7 +2302,7 @@ func TestKubeConfigUpdate(t *testing.T) {
}
for _, testcase := range tests {
t.Run(testcase.desc, func(t *testing.T) {
values, err := buildKubeConfigUpdate(testcase.cf, testcase.kubeStatus)
values, err := buildKubeConfigUpdate(testcase.cf, testcase.kubeStatus, "")
testcase.errorAssertion(t, err)
require.Equal(t, testcase.expectedValues, values)
})
Expand Down