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

Add support for a profile specific kubeconfig file. #7840

Merged
merged 25 commits into from
Aug 24, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d95ae82
Add support for a profile specific kubeconfig file.
Joerger Aug 6, 2021
8fc3c80
Fix issue in keystore where any file found in some directories were t…
Joerger Aug 9, 2021
6a6de4e
Cleanup.
Joerger Aug 9, 2021
acb3ad9
Merge branch 'master' into joerger/handle-multiple-kubeconfigs
Joerger Aug 9, 2021
1b263e4
Cleanup.
Joerger Aug 9, 2021
b8ac838
Merge branch 'joerger/handle-multiple-kubeconfigs' of github.com:grav…
Joerger Aug 10, 2021
3038fff
Fix linting error.
Joerger Aug 10, 2021
724613a
Tweak user error message.
Joerger Aug 10, 2021
cdb3775
Merge branch 'master' into joerger/handle-multiple-kubeconfigs
Joerger Aug 10, 2021
925db85
Merge branch 'master' into joerger/handle-multiple-kubeconfigs
Joerger Aug 10, 2021
15c4e6a
Fix bug.
Joerger Aug 11, 2021
f3a1bb0
Merge branch 'master' into joerger/handle-multiple-kubeconfigs
Joerger Aug 12, 2021
46b0b47
Merge branch 'master' into joerger/handle-multiple-kubeconfigs
Joerger Aug 12, 2021
0797e31
Merge branch 'master' into joerger/handle-multiple-kubeconfigs
Joerger Aug 12, 2021
e574f10
Add small keypaths test.
Joerger Aug 12, 2021
9fc957c
Resolve comments.
Joerger Aug 13, 2021
faf7af4
Merge branch 'master' into joerger/handle-multiple-kubeconfigs
Joerger Aug 13, 2021
22633c1
Merge branch 'master' into joerger/handle-multiple-kubeconfigs
Joerger Aug 16, 2021
2bdadf0
Merge branch 'master' into joerger/handle-multiple-kubeconfigs
Joerger Aug 18, 2021
afd1dac
Small adjustments.
Joerger Aug 18, 2021
24805d9
Merge branch 'master' into joerger/handle-multiple-kubeconfigs
Joerger Aug 19, 2021
390b139
Merge branch 'master' into joerger/handle-multiple-kubeconfigs
Joerger Aug 19, 2021
d8c2430
Merge branch 'master' into joerger/handle-multiple-kubeconfigs
Joerger Aug 19, 2021
c8e50be
Merge branch 'master' into joerger/handle-multiple-kubeconfigs
Joerger Aug 20, 2021
4c55e37
Merge branch 'master' into joerger/handle-multiple-kubeconfigs
Joerger Aug 24, 2021
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
28 changes: 23 additions & 5 deletions api/utils/keypaths/keypaths.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ const (
dbDirSuffix = "-db"
// kubeDirSuffix is the suffix of a sub-directory where kube TLS certs are stored.
kubeDirSuffix = "-kube"
// kubeConfigSuffix is the suffix of a kubeconfig file stored under the keys directory.
kubeConfigSuffix = "-kubeconfig"
)

// Here's the file layout of all these keypaths.
Joerger marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -226,16 +228,32 @@ func KubeCertPath(baseDir, proxy, username, cluster, kubename string) string {
return filepath.Join(KubeCertDir(baseDir, proxy, username, cluster), kubename+fileExtTLSCert)
Joerger marked this conversation as resolved.
Show resolved Hide resolved
}

// KubeConfigPath returns the path to the user's standalone kubeconfig
// for the given proxy, cluster, and kube cluster.
//
// <baseDir>/keys/<proxy>/<username>-kube/<cluster>/<kubename>-kubeconfig
func KubeConfigPath(baseDir, proxy, username, cluster, kubename string) string {
return filepath.Join(KubeCertDir(baseDir, proxy, username, cluster), kubename+kubeConfigSuffix)
}

// IsProfileKubeConfigPath makes a best effort attempt to check if the given
// path is a profile specific kubeconfig path generated by this package.
func IsProfileKubeConfigPath(path string) bool {
return strings.Contains(path, "/"+sessionKeyDir+"/") &&
strings.Contains(path, kubeDirSuffix+"/") &&
strings.Contains(path, kubeConfigSuffix)
Joerger marked this conversation as resolved.
Show resolved Hide resolved
}

// IdentitySSHCertPath returns the path to the identity file's SSH certificate.
//
// <identity-file-dir>/<path>-cert.pub
func IdentitySSHCertPath(path string) string {
return path + fileExtSSHCert
}

// TrimPathSuffix trims the suffix/extension off of the given cert path.
func TrimCertPathSuffix(path string) string {
path = strings.TrimSuffix(path, fileExtTLSCert)
path = strings.TrimSuffix(path, fileExtSSHCert)
return path
// TrimCertPathSuffix returns the given path with any cert suffix/extension trimmed off.
func TrimCertPathSuffix(path string) (trimmedPath string, isCertPath bool) {
trimmedPath = strings.TrimSuffix(path, fileExtTLSCert)
trimmedPath = strings.TrimSuffix(trimmedPath, fileExtSSHCert)
return trimmedPath, path == trimmedPath
}
15 changes: 11 additions & 4 deletions lib/client/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -403,35 +403,42 @@ func (p *ProfileStatus) IsExpired(clock clockwork.Clock) bool {

// CACertPath returns path to the CA certificate for this profile.
//
// It's stored in ~/.tsh/keys/<proxy>/certs.pem by default.
// It's stored in <profile-dir>/keys/<proxy>/certs.pem by default.
func (p *ProfileStatus) CACertPath() string {
return keypaths.TLSCAsPath(p.Dir, p.Name)
}

// KeyPath returns path to the private key for this profile.
//
// It's kept in ~/.tsh/keys/<proxy>/<user>.
// It's kept in <profile-dir>/keys/<proxy>/<user>.
func (p *ProfileStatus) KeyPath() string {
return keypaths.UserKeyPath(p.Dir, p.Name, p.Username)
}

// DatabaseCertPath returns path to the specified database access certificate
// for this profile.
//
// It's kept in ~/.tsh/keys/<proxy>/<user>-db/<cluster>/<name>-x509.pem
// It's kept in <profile-dir>/keys/<proxy>/<user>-db/<cluster>/<name>-x509.pem
func (p *ProfileStatus) DatabaseCertPath(name string) string {
return keypaths.DatabaseCertPath(p.Dir, p.Name, p.Username, p.Cluster, name)
}

// AppCertPath returns path to the specified app access certificate
// for this profile.
//
// It's kept in ~/.tsh/keys/<proxy>/<user>-app/<cluster>/<name>-x509.pem
// It's kept in <profile-dir>/keys/<proxy>/<user>-app/<cluster>/<name>-x509.pem
func (p *ProfileStatus) AppCertPath(name string) string {
return keypaths.AppCertPath(p.Dir, p.Name, p.Username, p.Cluster, name)

}

// KubeConfigPath returns path to the specified kubeconfig for this profile.
//
// It's kept in <profile-dir>/keys/<proxy>/<user>-kube/<cluster>/<name>-kubeconfig
func (p *ProfileStatus) KubeConfigPath(name string) string {
return keypaths.KubeConfigPath(p.Dir, p.Name, p.Username, p.Cluster, name)
}

// DatabaseServices returns a list of database service names for this profile.
func (p *ProfileStatus) DatabaseServices() (result []string) {
for _, db := range p.Databases {
Expand Down
12 changes: 7 additions & 5 deletions lib/client/keystore.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,12 +309,14 @@ func (fs *FSLocalKeyStore) updateKeyWithCerts(o CertOption, key *Key) error {
return trace.ConvertSystemError(err)
}
for _, certFile := range certFiles {
data, err := ioutil.ReadFile(filepath.Join(certPath, certFile.Name()))
if err != nil {
return trace.ConvertSystemError(err)
name, isCert := keypaths.TrimCertPathSuffix(certFile.Name())
if isCert {
data, err := ioutil.ReadFile(filepath.Join(certPath, certFile.Name()))
if err != nil {
return trace.ConvertSystemError(err)
}
certDataMap[name] = data
}
name := keypaths.TrimCertPathSuffix(certFile.Name())
certDataMap[name] = data
}
return o.updateKeyWithMap(key, certDataMap)
}
Expand Down
6 changes: 3 additions & 3 deletions lib/kube/kubeconfig/kubeconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ func Save(path string, config clientcmdapi.Config) error {
// missing.
func finalPath(customPath string) (string, error) {
if customPath == "" {
customPath = pathFromEnv()
customPath = PathFromEnv()
}
finalPath, err := utils.EnsureLocalPath(customPath, teleport.KubeConfigDir, teleport.KubeConfigFile)
if err != nil {
Expand All @@ -225,8 +225,8 @@ func finalPath(customPath string) (string, error) {
return finalPath, nil
}

// pathFromEnv extracts location of kubeconfig from the environment.
func pathFromEnv() string {
// PathFromEnv extracts location of kubeconfig from the environment.
func PathFromEnv() string {
kubeconfig := os.Getenv(teleport.EnvKubeConfig)

// The KUBECONFIG environment variable is a list. On Windows it's
Expand Down
40 changes: 33 additions & 7 deletions tool/tsh/kube.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,15 @@ package main
import (
"context"
"fmt"
"strings"
"time"

"github.com/gravitational/kingpin"
"github.com/gravitational/trace"

"github.com/gravitational/teleport/api/profile"
apiutils "github.com/gravitational/teleport/api/utils"
"github.com/gravitational/teleport/api/utils/keypaths"
"github.com/gravitational/teleport/lib/asciitable"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/kube/kubeconfig"
Expand Down Expand Up @@ -208,6 +211,9 @@ func newKubeLoginCommand(parent *kingpin.CmdClause) *kubeLoginCommand {
}

func (c *kubeLoginCommand) run(cf *CLIConf) error {
// Set CLIConf.KubernetesCluster so that the kube cluster's context is automatically selected.
cf.KubernetesCluster = c.kubeCluster

tc, err := makeClient(cf, true)
if err != nil {
return trace.Wrap(err)
Expand All @@ -232,14 +238,20 @@ func (c *kubeLoginCommand) run(cf *CLIConf) error {
//
// Re-generate kubeconfig contexts and try selecting this kube cluster
// again.
if err := updateKubeConfig(cf, tc); err != nil {
return trace.Wrap(err)
}
if err := kubeconfig.SelectContext(currentTeleportCluster, c.kubeCluster); err != nil {
if err := updateKubeConfig(cf, tc, ""); err != nil {
return trace.Wrap(err)
}
}

// Generate a profile specific kubeconfig which can be used
// by setting the kubeconfig environment variable (with `tsh env`)
profileKubeconfigPath := keypaths.KubeConfigPath(
profile.FullProfilePath(cf.HomePath), tc.WebProxyHost(), tc.Username, currentTeleportCluster, c.kubeCluster,
)
if err := updateKubeConfig(cf, tc, profileKubeconfigPath); err != nil {
return trace.Wrap(err)
}

fmt.Printf("Logged into kubernetes cluster %q\n", c.kubeCluster)
return nil
}
Expand Down Expand Up @@ -339,8 +351,9 @@ func buildKubeConfigUpdate(cf *CLIConf, kubeStatus *kubernetesStatus) (*kubeconf
}

// updateKubeConfig adds Teleport configuration to the users's kubeconfig based on the CLI
// parameters and the kubernetes services in the current Teleport cluster.
func updateKubeConfig(cf *CLIConf, tc *client.TeleportClient) error {
// 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 {
// Fetch proxy's advertised ports to check for k8s support.
if _, err := tc.Ping(cf.Context); err != nil {
return trace.Wrap(err)
Expand All @@ -360,7 +373,20 @@ func updateKubeConfig(cf *CLIConf, tc *client.TeleportClient) error {
return trace.Wrap(err)
}

return trace.Wrap(kubeconfig.Update("", *values))
if path == "" {
path = kubeconfig.PathFromEnv()
}

// If this is a profile specific kubeconfig, we only need
// to put the selected kube cluster into the kubeconfig.
if keypaths.IsProfileKubeConfigPath(path) {
if !strings.Contains(path, cf.KubernetesCluster) {
return trace.BadParameter("profile specific kubeconfig is in use, unset $KUBECONFIG to switch contexts to another kube cluster")
Joerger marked this conversation as resolved.
Show resolved Hide resolved
}
values.Exec.KubeClusters = []string{cf.KubernetesCluster}
}

return trace.Wrap(kubeconfig.Update(path, *values))
}

// Required magic boilerplate to use the k8s encoder.
Expand Down
17 changes: 11 additions & 6 deletions tool/tsh/tsh.go
Original file line number Diff line number Diff line change
Expand Up @@ -742,15 +742,15 @@ func onLogin(cf *CLIConf) error {
// in case if nothing is specified, re-fetch kube clusters and print
// current status
case cf.Proxy == "" && cf.SiteName == "" && cf.DesiredRoles == "" && cf.IdentityFileOut == "":
if err := updateKubeConfig(cf, tc); err != nil {
if err := updateKubeConfig(cf, tc, ""); err != nil {
return trace.Wrap(err)
}
printProfiles(cf.Debug, profile, profiles)
return nil
// in case if parameters match, re-fetch kube clusters and print
// current status
case host(cf.Proxy) == host(profile.ProxyURL.Host) && cf.SiteName == profile.Cluster && cf.DesiredRoles == "":
if err := updateKubeConfig(cf, tc); err != nil {
if err := updateKubeConfig(cf, tc, ""); err != nil {
return trace.Wrap(err)
}
printProfiles(cf.Debug, profile, profiles)
Expand All @@ -770,7 +770,7 @@ func onLogin(cf *CLIConf) error {
if err := tc.SaveProfile(cf.HomePath, true); err != nil {
return trace.Wrap(err)
}
if err := updateKubeConfig(cf, tc); err != nil {
if err := updateKubeConfig(cf, tc, ""); err != nil {
return trace.Wrap(err)
}
return trace.Wrap(onStatus(cf))
Expand All @@ -781,7 +781,7 @@ func onLogin(cf *CLIConf) error {
if err := executeAccessRequest(cf, tc); err != nil {
return trace.Wrap(err)
}
if err := updateKubeConfig(cf, tc); err != nil {
if err := updateKubeConfig(cf, tc, ""); err != nil {
return trace.Wrap(err)
}
return trace.Wrap(onStatus(cf))
Expand Down Expand Up @@ -843,7 +843,7 @@ func onLogin(cf *CLIConf) error {

// If the proxy is advertising that it supports Kubernetes, update kubeconfig.
if tc.KubeProxyAddr != "" {
if err := updateKubeConfig(cf, tc); err != nil {
if err := updateKubeConfig(cf, tc, ""); err != nil {
return trace.Wrap(err)
}
}
Expand Down Expand Up @@ -2142,7 +2142,7 @@ func reissueWithRequests(cf *CLIConf, tc *client.TeleportClient, reqIDs ...strin
if err := tc.SaveProfile("", true); err != nil {
return trace.Wrap(err)
}
if err := updateKubeConfig(cf, tc); err != nil {
if err := updateKubeConfig(cf, tc, ""); err != nil {
return trace.Wrap(err)
}
return nil
Expand Down Expand Up @@ -2191,9 +2191,14 @@ func onEnvironment(cf *CLIConf) error {
case cf.unsetEnvironment:
fmt.Printf("unset %v\n", proxyEnvVar)
fmt.Printf("unset %v\n", clusterEnvVar)
fmt.Printf("unset %v\n", teleport.EnvKubeConfig)
case !cf.unsetEnvironment:
fmt.Printf("export %v=%v\n", proxyEnvVar, profile.ProxyURL.Host)
fmt.Printf("export %v=%v\n", clusterEnvVar, profile.Cluster)
if kubeName := selectedKubeCluster(profile.Cluster); kubeName != "" {
fmt.Printf("# set %v to a standalone kubeconfig for the selected kube cluster\n", teleport.EnvKubeConfig)
fmt.Printf("export %v=%v\n", teleport.EnvKubeConfig, profile.KubeConfigPath(kubeName))
}
}

return nil
Expand Down