Forward TELEPORT_HOME to kubeconfig (#9546)

This change fixes `tsh kube login` to respect TELEPORT_HOME when it's set.
This commit is contained in:
Andrew Burke 2021-12-29 17:20:07 -08:00 committed by GitHub
parent de893cf65c
commit 61a4f35af0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 145 additions and 72 deletions

View file

@ -73,6 +73,8 @@ type ExecValues struct {
// exec plugin arguments. This is used when the proxy doesn't have a
// trusted TLS cert during login.
TshBinaryInsecure bool
// Env is a map of environment variables to forward.
Env map[string]string
}
// Update adds Teleport configuration to kubeconfig.
@ -98,6 +100,13 @@ func Update(path string, v Values) error {
if v.Exec != nil {
// Called from tsh, use the exec plugin model.
clusterName := v.TeleportClusterName
envVars := make([]clientcmdapi.ExecEnvVar, 0, len(v.Exec.Env))
if v.Exec.Env != nil {
for name, value := range v.Exec.Env {
envVars = append(envVars, clientcmdapi.ExecEnvVar{Name: name, Value: value})
}
}
for _, c := range v.Exec.KubeClusters {
contextName := ContextName(v.TeleportClusterName, c)
authName := contextName
@ -114,6 +123,9 @@ func Update(path string, v Values) error {
if v.Exec.TshBinaryInsecure {
authInfo.Exec.Args = append(authInfo.Exec.Args, "--insecure")
}
if len(envVars) > 0 {
authInfo.Exec.Env = envVars
}
config.AuthInfos[authName] = authInfo
setContext(config.Contexts, contextName, clusterName, authName)

View file

@ -16,6 +16,7 @@ package kubeconfig
import (
"crypto/x509/pkix"
"fmt"
"io/ioutil"
"os"
"testing"
@ -31,34 +32,28 @@ import (
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"gopkg.in/check.v1"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
)
func TestKubeconfig(t *testing.T) { check.TestingT(t) }
type KubeconfigSuite struct {
kubeconfigPath string
initialConfig clientcmdapi.Config
}
var _ = check.Suite(&KubeconfigSuite{})
func (s *KubeconfigSuite) SetUpTest(c *check.C) {
func setup(t *testing.T) (string, clientcmdapi.Config) {
f, err := ioutil.TempFile("", "kubeconfig")
if err != nil {
c.Fatalf("failed to create temp kubeconfig file: %v", err)
t.Fatalf("failed to create temp kubeconfig file: %v", err)
}
defer f.Close()
// Note: LocationOfOrigin and Extensions would be automatically added on
// clientcmd.Write below. Set them explicitly so we can compare
// s.initialConfig against loaded config.
// initialConfig against loaded config.
//
// TODO: use a comparison library that can ignore individual fields.
s.initialConfig = clientcmdapi.Config{
kubeconfigPath := f.Name()
t.Cleanup(func() { os.Remove(kubeconfigPath) })
initialConfig := clientcmdapi.Config{
CurrentContext: "dev",
Clusters: map[string]*clientcmdapi.Cluster{
"cluster-1": {
@ -116,40 +111,38 @@ func (s *KubeconfigSuite) SetUpTest(c *check.C) {
Extensions: map[string]runtime.Object{},
}
initialContent, err := clientcmd.Write(s.initialConfig)
c.Assert(err, check.IsNil)
initialContent, err := clientcmd.Write(initialConfig)
require.NoError(t, err)
if _, err := f.Write(initialContent); err != nil {
c.Fatalf("failed to write kubeconfig: %v", err)
t.Fatalf("failed to write kubeconfig: %v", err)
}
s.kubeconfigPath = f.Name()
return kubeconfigPath, initialConfig
}
func (s *KubeconfigSuite) TearDownTest(c *check.C) {
os.Remove(s.kubeconfigPath)
func TestLoad(t *testing.T) {
kubeconfigPath, initialConfig := setup(t)
config, err := Load(kubeconfigPath)
require.NoError(t, err)
require.Equal(t, initialConfig, *config)
}
func (s *KubeconfigSuite) TestLoad(c *check.C) {
config, err := Load(s.kubeconfigPath)
c.Assert(err, check.IsNil)
c.Assert(*config, check.DeepEquals, s.initialConfig)
}
func (s *KubeconfigSuite) TestSave(c *check.C) {
func TestSave(t *testing.T) {
kubeconfigPath, _ := setup(t)
cfg := clientcmdapi.Config{
CurrentContext: "a",
Clusters: map[string]*clientcmdapi.Cluster{
"cluster": {
CertificateAuthority: "fake-ca-file",
Server: "https://1.2.3.4",
LocationOfOrigin: s.kubeconfigPath,
LocationOfOrigin: kubeconfigPath,
Extensions: map[string]runtime.Object{},
},
},
AuthInfos: map[string]*clientcmdapi.AuthInfo{
"user": {
LocationOfOrigin: s.kubeconfigPath,
LocationOfOrigin: kubeconfigPath,
Extensions: map[string]runtime.Object{},
},
},
@ -157,7 +150,7 @@ func (s *KubeconfigSuite) TestSave(c *check.C) {
"a": {
Cluster: "cluster",
AuthInfo: "user",
LocationOfOrigin: s.kubeconfigPath,
LocationOfOrigin: kubeconfigPath,
Extensions: map[string]runtime.Object{},
},
},
@ -167,115 +160,176 @@ func (s *KubeconfigSuite) TestSave(c *check.C) {
Extensions: map[string]runtime.Object{},
}
err := Save(s.kubeconfigPath, cfg)
c.Assert(err, check.IsNil)
err := Save(kubeconfigPath, cfg)
require.NoError(t, err)
config, err := Load(s.kubeconfigPath)
c.Assert(err, check.IsNil)
c.Assert(*config, check.DeepEquals, cfg)
config, err := Load(kubeconfigPath)
require.NoError(t, err)
require.Equal(t, cfg, *config)
}
func (s *KubeconfigSuite) TestUpdate(c *check.C) {
func TestUpdate(t *testing.T) {
const (
clusterName = "teleport-cluster"
clusterAddr = "https://1.2.3.6:3080"
)
creds, caCertPEM, err := s.genUserKey()
c.Assert(err, check.IsNil)
err = Update(s.kubeconfigPath, Values{
kubeconfigPath, initialConfig := setup(t)
creds, caCertPEM, err := genUserKey()
require.NoError(t, err)
err = Update(kubeconfigPath, Values{
TeleportClusterName: clusterName,
ClusterAddr: clusterAddr,
Credentials: creds,
})
c.Assert(err, check.IsNil)
require.NoError(t, err)
wantConfig := s.initialConfig.DeepCopy()
wantConfig := initialConfig.DeepCopy()
wantConfig.Clusters[clusterName] = &clientcmdapi.Cluster{
Server: clusterAddr,
CertificateAuthorityData: caCertPEM,
LocationOfOrigin: s.kubeconfigPath,
LocationOfOrigin: kubeconfigPath,
Extensions: map[string]runtime.Object{},
}
wantConfig.AuthInfos[clusterName] = &clientcmdapi.AuthInfo{
ClientCertificateData: creds.TLSCert,
ClientKeyData: creds.Priv,
LocationOfOrigin: s.kubeconfigPath,
LocationOfOrigin: kubeconfigPath,
Extensions: map[string]runtime.Object{},
}
wantConfig.Contexts[clusterName] = &clientcmdapi.Context{
Cluster: clusterName,
AuthInfo: clusterName,
LocationOfOrigin: s.kubeconfigPath,
LocationOfOrigin: kubeconfigPath,
Extensions: map[string]runtime.Object{},
}
wantConfig.CurrentContext = clusterName
config, err := Load(s.kubeconfigPath)
c.Assert(err, check.IsNil)
c.Assert(config, check.DeepEquals, wantConfig)
config, err := Load(kubeconfigPath)
require.NoError(t, err)
require.Equal(t, wantConfig, config)
}
func (s *KubeconfigSuite) TestRemove(c *check.C) {
func TestUpdateWithExec(t *testing.T) {
const (
clusterName = "teleport-cluster"
clusterAddr = "https://1.2.3.6:3080"
tshPath = "/path/to/tsh"
kubeCluster = "my-cluster"
homeEnvVar = "TELEPORT_HOME"
home = "/alt/home"
)
kubeconfigPath, initialConfig := setup(t)
creds, caCertPEM, err := genUserKey()
require.NoError(t, err)
err = Update(kubeconfigPath, Values{
TeleportClusterName: clusterName,
ClusterAddr: clusterAddr,
Credentials: creds,
Exec: &ExecValues{
TshBinaryPath: tshPath,
KubeClusters: []string{kubeCluster},
Env: map[string]string{
homeEnvVar: home,
},
},
})
require.NoError(t, err)
wantConfig := initialConfig.DeepCopy()
contextName := ContextName(clusterName, kubeCluster)
wantConfig.Clusters[clusterName] = &clientcmdapi.Cluster{
Server: clusterAddr,
CertificateAuthorityData: caCertPEM,
LocationOfOrigin: kubeconfigPath,
Extensions: map[string]runtime.Object{},
}
wantConfig.AuthInfos[contextName] = &clientcmdapi.AuthInfo{
LocationOfOrigin: kubeconfigPath,
Extensions: map[string]runtime.Object{},
Exec: &clientcmdapi.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1beta1",
Command: tshPath,
Args: []string{"kube", "credentials",
fmt.Sprintf("--kube-cluster=%s", kubeCluster),
fmt.Sprintf("--teleport-cluster=%s", clusterName),
},
Env: []clientcmdapi.ExecEnvVar{{Name: homeEnvVar, Value: home}},
},
}
wantConfig.Contexts[contextName] = &clientcmdapi.Context{
Cluster: clusterName,
AuthInfo: contextName,
LocationOfOrigin: kubeconfigPath,
Extensions: map[string]runtime.Object{},
}
config, err := Load(kubeconfigPath)
require.NoError(t, err)
require.Equal(t, wantConfig, config)
}
func TestRemove(t *testing.T) {
const (
clusterName = "teleport-cluster"
clusterAddr = "https://1.2.3.6:3080"
)
creds, _, err := s.genUserKey()
c.Assert(err, check.IsNil)
kubeconfigPath, initialConfig := setup(t)
creds, _, err := genUserKey()
require.NoError(t, err)
// Add teleport-generated entries to kubeconfig.
err = Update(s.kubeconfigPath, Values{
err = Update(kubeconfigPath, Values{
TeleportClusterName: clusterName,
ClusterAddr: clusterAddr,
Credentials: creds,
})
c.Assert(err, check.IsNil)
require.NoError(t, err)
// Remove those generated entries from kubeconfig.
err = Remove(s.kubeconfigPath, clusterName)
c.Assert(err, check.IsNil)
err = Remove(kubeconfigPath, clusterName)
require.NoError(t, err)
// Verify that kubeconfig changed back to the initial state.
wantConfig := s.initialConfig.DeepCopy()
config, err := Load(s.kubeconfigPath)
c.Assert(err, check.IsNil)
wantConfig := initialConfig.DeepCopy()
config, err := Load(kubeconfigPath)
require.NoError(t, err)
// CurrentContext can end up as either of the remaining contexts, as long
// as it's not the one we just removed.
c.Assert(config.CurrentContext, check.Not(check.Equals), clusterName)
require.NotEqual(t, clusterName, config.CurrentContext)
wantConfig.CurrentContext = config.CurrentContext
c.Assert(config, check.DeepEquals, wantConfig)
require.Equal(t, wantConfig, config)
// Add teleport-generated entries to kubeconfig again.
err = Update(s.kubeconfigPath, Values{
err = Update(kubeconfigPath, Values{
TeleportClusterName: clusterName,
ClusterAddr: clusterAddr,
Credentials: creds,
})
c.Assert(err, check.IsNil)
require.NoError(t, err)
// This time, explicitly switch CurrentContext to "prod".
// Remove should preserve this CurrentContext!
config, err = Load(s.kubeconfigPath)
c.Assert(err, check.IsNil)
config, err = Load(kubeconfigPath)
require.NoError(t, err)
config.CurrentContext = "prod"
err = Save(s.kubeconfigPath, *config)
c.Assert(err, check.IsNil)
err = Save(kubeconfigPath, *config)
require.NoError(t, err)
// Remove teleport-generated entries from kubeconfig.
err = Remove(s.kubeconfigPath, clusterName)
c.Assert(err, check.IsNil)
err = Remove(kubeconfigPath, clusterName)
require.NoError(t, err)
wantConfig = s.initialConfig.DeepCopy()
wantConfig = initialConfig.DeepCopy()
// CurrentContext should always end up as "prod" because we explicitly set
// it above and Remove shouldn't touch it unless it matches the cluster
// being removed.
wantConfig.CurrentContext = "prod"
config, err = Load(s.kubeconfigPath)
c.Assert(err, check.IsNil)
c.Assert(config, check.DeepEquals, wantConfig)
config, err = Load(kubeconfigPath)
require.NoError(t, err)
require.Equal(t, wantConfig, config)
}
func (s *KubeconfigSuite) genUserKey() (*client.Key, []byte, error) {
func genUserKey() (*client.Key, []byte, error) {
caKey, caCert, err := tlsca.GenerateSelfSignedCA(pkix.Name{
CommonName: "localhost",
Organization: []string{"localhost"},

View file

@ -366,6 +366,11 @@ func buildKubeConfigUpdate(cf *CLIConf, kubeStatus *kubernetesStatus) (*kubeconf
TshBinaryPath: cf.executablePath,
TshBinaryInsecure: cf.InsecureSkipVerify,
KubeClusters: kubeStatus.kubeClusters,
Env: make(map[string]string),
}
if cf.HomePath != "" {
v.Exec.Env[homeEnvVar] = cf.HomePath
}
// Only switch the current context if kube-cluster is explicitly set on the command line.

View file

@ -994,6 +994,7 @@ func TestKubeConfigUpdate(t *testing.T) {
TshBinaryPath: "/bin/tsh",
KubeClusters: []string{"dev", "prod"},
SelectCluster: "dev",
Env: make(map[string]string),
},
},
},
@ -1018,6 +1019,7 @@ func TestKubeConfigUpdate(t *testing.T) {
TshBinaryPath: "/bin/tsh",
KubeClusters: []string{"dev", "prod"},
SelectCluster: "",
Env: make(map[string]string),
},
},
},