mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-03 07:40:28 +08:00
While loading a key from storage, Teleport client transverses the storage directory to read the secrets and reads each file one by one. This has huge impacts on `tsh kube credentials` because when calling `kubectl` in parallel, the disk operations become overloaded and the operation time increases exponentially. This PR removes the transverse when reading credentials for Kubernetes clusters and adds a quick read operation that reads the following files: - `$TSH_HOME/current_profile` - `$TSH_HOME/$profile.yaml` - `$TSH_HOME/keys/$PROXY/$USER-kube/$TELEPORT_CLUSTER/$KUBE_CLUSTER-x509.pem` - `$TSH_HOME/keys/$PROXY/$USER` | - | P=1 | P=10 | P=40 | P=80 | P=120 | | --- | --- | --- | --- | --- | --- | | old direct | 9.90s user 3.14s system 64% cpu 20.284 total | 11.08s user 3.60s system 496% cpu 2.955 total | 12.46s user 4.36s system 820% cpu 2.050 total | 12.81s user 4.76s system 811% cpu 2.164 total | 13.02s user 5.07s system 861% cpu 2.100 total| | old teleport | 18.45s user 7.80s system 51% cpu 51.085 total | 21.58s user 10.03s system 434% cpu 7.270 total | 22.33s user 10.52s system 178% cpu 18.402 total | 22.04s user 10.83s system 96% cpu 33.977 total | 22.57s user 11.45s system 55% cpu 1:01.34 total| | new direct | 9.51s user 2.90s system 146% cpu 8.460 total | 13.75s user 4.95s system 718% cpu 2.603 total | 12.21s user 4.43s system 832% cpu 1.999 total | 11.72s user 4.50s system 839% cpu 1.933 total | 12.06s user 4.62s system 830% cpu 2.009 total | | new teleport | 16.10s user 5.97s system 42% cpu 52.104 total | 21.62s user 9.01s system 381% cpu 8.023 total | 23.22s user 10.11s system 540% cpu 6.162 total | 21.64s user 9.81s system 603% cpu 5.208 total | 20.70s user 9.13s system 602% cpu 4.948 total | --- | - | P=1 | P=10 | P=40 | P=80 | P=120 | | --- | --- | --- | --- | --- | --- | | old no tsh kube credential | 8.61s user 2.99s system 31% cpu 36.336 total | 9.57s user 3.39s system 283% cpu 4.565 total | 10.22s user 4.03s system 553% cpu 2.574 total | 9.32s user 3.82s system 559% cpu 2.351 total | 11.49s user 4.85s system 624% cpu 2.617 total | new no tsh kube credential | 8.27s user 2.94s system 24% cpu 45.041 total | 10.85s user 4.31s system 195% cpu 7.767 total | 11.32s user 4.64s system 469% cpu 3.401 total | 10.83s user 4.51s system 553% cpu 2.773 total | 10.90s user 4.84s system 537% cpu 2.930 total | --- | instances | old total | old avg | **new total** | **total avg** | | --- | --- | --- | --- | --- | | 1 | 0.145 | 0.145 | 0.074 | 0.074 | | 10 | 0.430 | 0.043 | 0.146 | 0.0146 | | 20 | 0.740 | 0.037 | 0.284 | 0.0142 | | 30 | 1.108 | 0.037 | 0.417 | 0.0139 | | 40 | 1.483 | 0.037 | 0.580 | 0.0145 | | 60 | 13.604 | 0.228 | 0.825 | 0.0137 | | 80 | 30.236 | 0.378 | 1.143 | 0.0142 | | 106 | 46.681 | 0.440 | 1.413 | 0.0133 | Fixes #19313 Co-authored-by: Brian Joerger <bjoerger@goteleport.com>
1255 lines
40 KiB
Go
1255 lines
40 KiB
Go
/*
|
|
Copyright 2020-2021 Gravitational, Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/ghodss/yaml"
|
|
"github.com/gravitational/kingpin"
|
|
"github.com/gravitational/trace"
|
|
dockerterm "github.com/moby/term"
|
|
"golang.org/x/exp/slices"
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
|
"k8s.io/cli-runtime/pkg/resource"
|
|
coreclient "k8s.io/client-go/kubernetes/typed/core/v1"
|
|
"k8s.io/client-go/pkg/apis/clientauthentication"
|
|
clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
|
restclient "k8s.io/client-go/rest"
|
|
"k8s.io/client-go/tools/remotecommand"
|
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
|
"k8s.io/kubectl/pkg/cmd/util/podcmd"
|
|
"k8s.io/kubectl/pkg/polymorphichelpers"
|
|
"k8s.io/kubectl/pkg/scheme"
|
|
"k8s.io/kubectl/pkg/util/term"
|
|
|
|
"github.com/gravitational/teleport"
|
|
"github.com/gravitational/teleport/api/client/proto"
|
|
"github.com/gravitational/teleport/api/profile"
|
|
"github.com/gravitational/teleport/api/types"
|
|
"github.com/gravitational/teleport/api/utils/keypaths"
|
|
"github.com/gravitational/teleport/lib/asciitable"
|
|
"github.com/gravitational/teleport/lib/client"
|
|
"github.com/gravitational/teleport/lib/defaults"
|
|
"github.com/gravitational/teleport/lib/kube/kubeconfig"
|
|
kubeutils "github.com/gravitational/teleport/lib/kube/utils"
|
|
"github.com/gravitational/teleport/lib/tlsca"
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
)
|
|
|
|
type kubeCommands struct {
|
|
credentials *kubeCredentialsCommand
|
|
ls *kubeLSCommand
|
|
login *kubeLoginCommand
|
|
sessions *kubeSessionsCommand
|
|
exec *kubeExecCommand
|
|
join *kubeJoinCommand
|
|
}
|
|
|
|
func newKubeCommand(app *kingpin.Application) kubeCommands {
|
|
kube := app.Command("kube", "Manage available Kubernetes clusters")
|
|
cmds := kubeCommands{
|
|
credentials: newKubeCredentialsCommand(kube),
|
|
ls: newKubeLSCommand(kube),
|
|
login: newKubeLoginCommand(kube),
|
|
sessions: newKubeSessionsCommand(kube),
|
|
exec: newKubeExecCommand(kube),
|
|
join: newKubeJoinCommand(kube),
|
|
}
|
|
return cmds
|
|
}
|
|
|
|
type kubeJoinCommand struct {
|
|
*kingpin.CmdClause
|
|
session string
|
|
mode string
|
|
siteName string
|
|
}
|
|
|
|
func newKubeJoinCommand(parent *kingpin.CmdClause) *kubeJoinCommand {
|
|
c := &kubeJoinCommand{
|
|
CmdClause: parent.Command("join", "Join an active Kubernetes session."),
|
|
}
|
|
|
|
c.Flag("mode", "Mode of joining the session, valid modes are observer and moderator").Short('m').Default("moderator").StringVar(&c.mode)
|
|
c.Flag("cluster", clusterHelp).Short('c').StringVar(&c.siteName)
|
|
c.Arg("session", "The ID of the target session.").Required().StringVar(&c.session)
|
|
return c
|
|
}
|
|
|
|
func (c *kubeJoinCommand) getSessionMeta(ctx context.Context, tc *client.TeleportClient) (types.SessionTracker, error) {
|
|
proxy, err := tc.ConnectToProxy(ctx)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
site := proxy.CurrentCluster()
|
|
|
|
return site.GetSessionTracker(ctx, c.session)
|
|
}
|
|
|
|
func (c *kubeJoinCommand) run(cf *CLIConf) error {
|
|
if err := validateParticipantMode(types.SessionParticipantMode(c.mode)); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
cf.SiteName = c.siteName
|
|
tc, err := makeClient(cf, true)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
meta, err := c.getSessionMeta(cf.Context, tc)
|
|
if trace.IsNotFound(err) {
|
|
return trace.NotFound("Failed to find session %q. The ID may be incorrect.", c.session)
|
|
} else if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
cluster := meta.GetClusterName()
|
|
kubeCluster := meta.GetKubeCluster()
|
|
var k *client.Key
|
|
|
|
// Try loading existing keys.
|
|
k, err = tc.LocalAgent().GetKey(cluster, client.WithKubeCerts{})
|
|
if err != nil && !trace.IsNotFound(err) {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
// Loaded existing credentials and have a cert for this cluster? Return it
|
|
// right away.
|
|
if err == nil {
|
|
crt, err := k.KubeTLSCertificate(kubeCluster)
|
|
if err != nil && !trace.IsNotFound(err) {
|
|
return trace.Wrap(err)
|
|
}
|
|
if crt != nil && time.Until(crt.NotAfter) > time.Minute {
|
|
log.Debugf("Re-using existing TLS cert for kubernetes cluster %q", kubeCluster)
|
|
} else {
|
|
err = client.RetryWithRelogin(cf.Context, tc, func() error {
|
|
var err error
|
|
k, err = tc.IssueUserCertsWithMFA(cf.Context, client.ReissueParams{
|
|
RouteToCluster: cluster,
|
|
KubernetesCluster: kubeCluster,
|
|
}, nil /*applyOpts*/)
|
|
|
|
return trace.Wrap(err)
|
|
})
|
|
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
// Cache the new cert on disk for reuse.
|
|
if err := tc.LocalAgent().AddKey(k); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
// Otherwise, cert for this k8s cluster is missing or expired. Request
|
|
// a new one.
|
|
}
|
|
|
|
if _, err := tc.Ping(cf.Context); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
if tc.KubeProxyAddr == "" {
|
|
// Kubernetes support disabled, don't touch kubeconfig.
|
|
return trace.AccessDenied("this cluster does not support Kubernetes")
|
|
}
|
|
|
|
kubeStatus, err := fetchKubeStatus(cf.Context, tc)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
ciphers := utils.DefaultCipherSuites()
|
|
tlsConfig, err := k.KubeClientTLSConfig(ciphers, kubeCluster)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
tlsConfig.InsecureSkipVerify = cf.InsecureSkipVerify
|
|
session, err := client.NewKubeSession(cf.Context, tc, meta, tc.KubeProxyAddr, kubeStatus.tlsServerName, types.SessionParticipantMode(c.mode), tlsConfig)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
session.Wait()
|
|
return trace.Wrap(session.Detach())
|
|
}
|
|
|
|
// RemoteExecutor defines the interface accepted by the Exec command - provided for test stubbing
|
|
type RemoteExecutor interface {
|
|
Execute(ctx context.Context, method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error
|
|
}
|
|
|
|
// DefaultRemoteExecutor is the standard implementation of remote command execution
|
|
type DefaultRemoteExecutor struct{}
|
|
|
|
func (*DefaultRemoteExecutor) Execute(ctx context.Context, method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error {
|
|
exec, err := remotecommand.NewSPDYExecutor(config, method, url)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return exec.StreamWithContext(ctx, remotecommand.StreamOptions{
|
|
Stdin: stdin,
|
|
Stdout: stdout,
|
|
Stderr: stderr,
|
|
Tty: tty,
|
|
TerminalSizeQueue: terminalSizeQueue,
|
|
})
|
|
}
|
|
|
|
type StreamOptions struct {
|
|
Namespace string
|
|
PodName string
|
|
ContainerName string
|
|
Stdin bool
|
|
TTY bool
|
|
// minimize unnecessary output
|
|
Quiet bool
|
|
|
|
genericclioptions.IOStreams
|
|
|
|
overrideStreams func() (io.ReadCloser, io.Writer, io.Writer)
|
|
isTerminalIn func(t term.TTY) bool
|
|
}
|
|
|
|
func (o *StreamOptions) SetupTTY() term.TTY {
|
|
t := term.TTY{
|
|
Out: o.Out,
|
|
}
|
|
|
|
if !o.Stdin {
|
|
// need to nil out o.In to make sure we don't create a stream for stdin
|
|
o.In = nil
|
|
o.TTY = false
|
|
return t
|
|
}
|
|
|
|
t.In = o.In
|
|
if !o.TTY {
|
|
return t
|
|
}
|
|
|
|
if o.isTerminalIn == nil {
|
|
o.isTerminalIn = func(tty term.TTY) bool {
|
|
return tty.IsTerminalIn()
|
|
}
|
|
}
|
|
if !o.isTerminalIn(t) {
|
|
o.TTY = false
|
|
|
|
if !o.Quiet && o.ErrOut != nil {
|
|
fmt.Fprintln(o.ErrOut, "Unable to use a TTY - input is not a terminal or the right kind of file")
|
|
}
|
|
|
|
return t
|
|
}
|
|
|
|
// if we get to here, the user wants to attach stdin, wants a TTY, and o.In is a terminal, so we
|
|
// can safely set t.Raw to true
|
|
t.Raw = true
|
|
|
|
if o.overrideStreams == nil {
|
|
// use dockerterm.StdStreams() to get the right I/O handles on Windows
|
|
o.overrideStreams = dockerterm.StdStreams
|
|
}
|
|
|
|
stdin, stdout, _ := o.overrideStreams()
|
|
o.In = stdin
|
|
t.In = stdin
|
|
if o.Out != nil {
|
|
o.Out = stdout
|
|
t.Out = stdout
|
|
}
|
|
|
|
return t
|
|
}
|
|
|
|
type ExecOptions struct {
|
|
StreamOptions
|
|
resource.FilenameOptions
|
|
|
|
ResourceName string
|
|
Command []string
|
|
EnforceNamespace bool
|
|
|
|
Builder func() *resource.Builder
|
|
ExecutablePodFn polymorphichelpers.AttachablePodForObjectFunc
|
|
restClientGetter genericclioptions.RESTClientGetter
|
|
|
|
Pod *corev1.Pod
|
|
Executor RemoteExecutor
|
|
PodClient coreclient.PodsGetter
|
|
GetPodTimeout time.Duration
|
|
Config *restclient.Config
|
|
displayParticipantRequirements bool
|
|
}
|
|
|
|
// Run executes a validated remote execution against a pod.
|
|
func (p *ExecOptions) Run(ctx context.Context) error {
|
|
var err error
|
|
if len(p.PodName) != 0 {
|
|
p.Pod, err = p.PodClient.Pods(p.Namespace).Get(ctx, p.PodName, metav1.GetOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
builder := p.Builder().
|
|
WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
|
|
FilenameParam(p.EnforceNamespace, &p.FilenameOptions).
|
|
NamespaceParam(p.Namespace).DefaultNamespace()
|
|
if len(p.ResourceName) > 0 {
|
|
builder = builder.ResourceNames("pods", p.ResourceName)
|
|
}
|
|
|
|
obj, err := builder.Do().Object()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
p.Pod, err = p.ExecutablePodFn(p.restClientGetter, obj, p.GetPodTimeout)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
pod := p.Pod
|
|
|
|
if pod.Status.Phase == corev1.PodSucceeded || pod.Status.Phase == corev1.PodFailed {
|
|
return fmt.Errorf("cannot exec into a container in a completed pod; current phase is %s", pod.Status.Phase)
|
|
}
|
|
|
|
containerName := p.ContainerName
|
|
if len(containerName) == 0 {
|
|
container, err := podcmd.FindOrDefaultContainerByName(pod, containerName, p.Quiet, p.ErrOut)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
containerName = container.Name
|
|
}
|
|
|
|
// ensure we can recover the terminal while attached
|
|
t := p.SetupTTY()
|
|
|
|
var sizeQueue remotecommand.TerminalSizeQueue
|
|
if t.Raw {
|
|
// this call spawns a goroutine to monitor/update the terminal size
|
|
sizeQueue = t.MonitorSize(t.GetSize())
|
|
|
|
// unset p.Err if it was previously set because both stdout and stderr go over p.Out when tty is
|
|
// true
|
|
p.ErrOut = nil
|
|
}
|
|
|
|
fn := func() error {
|
|
restClient, err := restclient.RESTClientFor(p.Config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req := restClient.Post().
|
|
Resource("pods").
|
|
Name(pod.Name).
|
|
Namespace(pod.Namespace).
|
|
SubResource("exec").
|
|
Param("displayParticipantRequirements", strconv.FormatBool(p.displayParticipantRequirements))
|
|
req.VersionedParams(&corev1.PodExecOptions{
|
|
Container: containerName,
|
|
Command: p.Command,
|
|
Stdin: p.Stdin,
|
|
Stdout: p.Out != nil,
|
|
Stderr: p.ErrOut != nil,
|
|
TTY: t.Raw,
|
|
}, scheme.ParameterCodec)
|
|
|
|
return p.Executor.Execute(ctx, "POST", req.URL(), p.Config, p.In, p.Out, p.ErrOut, t.Raw, sizeQueue)
|
|
}
|
|
|
|
return trace.Wrap(t.Safe(fn))
|
|
}
|
|
|
|
type kubeExecCommand struct {
|
|
*kingpin.CmdClause
|
|
target string
|
|
container string
|
|
filename string
|
|
quiet bool
|
|
stdin bool
|
|
tty bool
|
|
reason string
|
|
invited string
|
|
command []string
|
|
displayParticipantRequirements bool
|
|
}
|
|
|
|
func newKubeExecCommand(parent *kingpin.CmdClause) *kubeExecCommand {
|
|
c := &kubeExecCommand{
|
|
CmdClause: parent.Command("exec", "Execute a command in a Kubernetes pod"),
|
|
}
|
|
|
|
c.Flag("container", "Container name. If omitted, use the kubectl.kubernetes.io/default-container annotation for selecting the container to be attached or the first container in the pod will be chosen").Short('c').StringVar(&c.container)
|
|
c.Flag("filename", "to use to exec into the resource").Short('f').StringVar(&c.filename)
|
|
c.Flag("quiet", "Only print output from the remote session").Short('q').BoolVar(&c.quiet)
|
|
c.Flag("stdin", "Pass stdin to the container").Short('s').BoolVar(&c.stdin)
|
|
c.Flag("tty", "Stdin is a TTY").Short('t').BoolVar(&c.tty)
|
|
c.Flag("reason", "The purpose of the session.").StringVar(&c.reason)
|
|
c.Flag("invite", "A comma separated list of people to mark as invited for the session.").StringVar(&c.invited)
|
|
c.Flag("participant-req", "Displays a verbose list of required participants in a moderated session.").BoolVar(&c.displayParticipantRequirements)
|
|
c.Arg("target", "Pod or deployment name").Required().StringVar(&c.target)
|
|
c.Arg("command", "Command to execute in the container").Required().StringsVar(&c.command)
|
|
return c
|
|
}
|
|
|
|
func (c *kubeExecCommand) run(cf *CLIConf) error {
|
|
var p ExecOptions
|
|
var err error
|
|
|
|
p.IOStreams = genericclioptions.IOStreams{
|
|
In: os.Stdin,
|
|
Out: os.Stdout,
|
|
ErrOut: os.Stderr,
|
|
}
|
|
kubeConfigFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag()
|
|
matchVersionKubeConfigFlags := cmdutil.NewMatchVersionFlags(kubeConfigFlags)
|
|
f := cmdutil.NewFactory(matchVersionKubeConfigFlags)
|
|
p.ResourceName = c.target
|
|
p.ContainerName = c.container
|
|
p.Quiet = c.quiet
|
|
p.Stdin = c.stdin
|
|
p.TTY = c.tty
|
|
p.Command = c.command
|
|
p.ExecutablePodFn = polymorphichelpers.AttachablePodForObjectFn
|
|
p.GetPodTimeout = time.Second * 5
|
|
p.Builder = f.NewBuilder
|
|
p.restClientGetter = f
|
|
p.Executor = &DefaultRemoteExecutor{}
|
|
p.displayParticipantRequirements = c.displayParticipantRequirements
|
|
p.Namespace, p.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
p.Config, err = f.ToRESTConfig()
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
clientset, err := f.KubernetesClientSet()
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
p.PodClient = clientset.CoreV1()
|
|
return trace.Wrap(p.Run(cf.Context))
|
|
}
|
|
|
|
type kubeSessionsCommand struct {
|
|
*kingpin.CmdClause
|
|
format string
|
|
}
|
|
|
|
func newKubeSessionsCommand(parent *kingpin.CmdClause) *kubeSessionsCommand {
|
|
c := &kubeSessionsCommand{
|
|
CmdClause: parent.Command("sessions", "Get a list of active Kubernetes sessions."),
|
|
}
|
|
c.Flag("format", defaults.FormatFlagDescription(defaults.DefaultFormats...)).Short('f').Default(teleport.Text).EnumVar(&c.format, defaults.DefaultFormats...)
|
|
|
|
return c
|
|
}
|
|
|
|
func (c *kubeSessionsCommand) run(cf *CLIConf) error {
|
|
tc, err := makeClient(cf, true)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
proxy, err := tc.ConnectToProxy(cf.Context)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
site := proxy.CurrentCluster()
|
|
sessions, err := site.GetActiveSessionTrackers(cf.Context)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
filteredSessions := make([]types.SessionTracker, 0)
|
|
for _, session := range sessions {
|
|
if session.GetSessionKind() == types.KubernetesSessionKind {
|
|
filteredSessions = append(filteredSessions, session)
|
|
}
|
|
}
|
|
|
|
sort.Slice(filteredSessions, func(i, j int) bool {
|
|
return filteredSessions[i].GetCreated().Before(filteredSessions[j].GetCreated())
|
|
})
|
|
|
|
format := strings.ToLower(c.format)
|
|
switch format {
|
|
case teleport.Text, "":
|
|
printSessions(cf.Stdout(), filteredSessions)
|
|
case teleport.JSON, teleport.YAML:
|
|
out, err := serializeKubeSessions(sessions, format)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
fmt.Fprintln(cf.Stdout(), out)
|
|
default:
|
|
return trace.BadParameter("unsupported format %q", c.format)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func serializeKubeSessions(sessions []types.SessionTracker, format string) (string, error) {
|
|
var out []byte
|
|
var err error
|
|
if format == teleport.JSON {
|
|
out, err = utils.FastMarshalIndent(sessions, "", " ")
|
|
} else {
|
|
out, err = yaml.Marshal(sessions)
|
|
}
|
|
return string(out), trace.Wrap(err)
|
|
}
|
|
|
|
func printSessions(output io.Writer, sessions []types.SessionTracker) {
|
|
table := asciitable.MakeTable([]string{"ID", "State", "Created", "Hostname", "Address", "Login", "Reason"})
|
|
for _, s := range sessions {
|
|
table.AddRow([]string{s.GetSessionID(), s.GetState().String(), s.GetCreated().Format(time.RFC3339), s.GetHostname(), s.GetAddress(), s.GetLogin(), s.GetReason()})
|
|
}
|
|
|
|
tableOutput := table.AsBuffer().String()
|
|
fmt.Fprintln(output, tableOutput)
|
|
}
|
|
|
|
type kubeCredentialsCommand struct {
|
|
*kingpin.CmdClause
|
|
kubeCluster string
|
|
teleportCluster string
|
|
}
|
|
|
|
func newKubeCredentialsCommand(parent *kingpin.CmdClause) *kubeCredentialsCommand {
|
|
c := &kubeCredentialsCommand{
|
|
// This command is always hidden. It's called from the kubeconfig that
|
|
// tsh generates and never by users directly.
|
|
CmdClause: parent.Command("credentials", "Get credentials for kubectl access").Hidden(),
|
|
}
|
|
c.Flag("teleport-cluster", "Name of the teleport cluster to get credentials for.").Required().StringVar(&c.teleportCluster)
|
|
c.Flag("kube-cluster", "Name of the Kubernetes cluster to get credentials for.").Required().StringVar(&c.kubeCluster)
|
|
return c
|
|
}
|
|
|
|
func (c *kubeCredentialsCommand) run(cf *CLIConf) error {
|
|
// client.LoadKeysToKubeFromStore function is used to speed up the credentials
|
|
// loading process since Teleport Store transverses the entire store to find the keys.
|
|
// This operation takes a long time when the store has a lot of keys and when
|
|
// we call the function multiple times in parallel.
|
|
// Although client.LoadKeysToKubeFromStore function speeds up the process
|
|
// since it removes all transversals, it still has to read 4 different files:
|
|
// - $TSH_HOME/current_profile
|
|
// - $TSH_HOME/$profile.yaml
|
|
// - $TSH_HOME/keys/$PROXY/$USER-kube/$TELEPORT_CLUSTER/$KUBE_CLUSTER-x509.pem
|
|
// - $TSH_HOME/keys/$PROXY/$USER
|
|
if kubeCert, privKey, err := client.LoadKeysToKubeFromStore(
|
|
cf.HomePath,
|
|
c.teleportCluster,
|
|
c.kubeCluster,
|
|
); err == nil {
|
|
crt, _ := tlsca.ParseCertificatePEM(kubeCert)
|
|
if crt != nil && time.Until(crt.NotAfter) > time.Minute {
|
|
log.Debugf("Re-using existing TLS cert for Kubernetes cluster %q", c.kubeCluster)
|
|
return c.writeByteResponse(cf.Stdout(), kubeCert, privKey, crt.NotAfter)
|
|
}
|
|
}
|
|
|
|
tc, err := makeClient(cf, true)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
_, span := tc.Tracer.Start(cf.Context, "tsh.kubeCredentials/GetKey")
|
|
// Try loading existing keys.
|
|
k, err := tc.LocalAgent().GetKey(c.teleportCluster, client.WithKubeCerts{})
|
|
span.End()
|
|
|
|
if err != nil && !trace.IsNotFound(err) {
|
|
return trace.Wrap(err)
|
|
}
|
|
// Loaded existing credentials and have a cert for this cluster? Return it
|
|
// right away.
|
|
if err == nil {
|
|
_, span := tc.Tracer.Start(cf.Context, "tsh.kubeCredentials/KubeTLSCertificate")
|
|
crt, err := k.KubeTLSCertificate(c.kubeCluster)
|
|
span.End()
|
|
if err != nil && !trace.IsNotFound(err) {
|
|
return trace.Wrap(err)
|
|
}
|
|
if crt != nil && time.Until(crt.NotAfter) > time.Minute {
|
|
log.Debugf("Re-using existing TLS cert for Kubernetes cluster %q", c.kubeCluster)
|
|
return c.writeKeyResponse(cf.Stdout(), k, c.kubeCluster)
|
|
}
|
|
// Otherwise, cert for this k8s cluster is missing or expired. Request
|
|
// a new one.
|
|
}
|
|
|
|
log.Debugf("Requesting TLS cert for Kubernetes cluster %q", c.kubeCluster)
|
|
|
|
ctx, span := tc.Tracer.Start(cf.Context, "tsh.kubeCredentials/RetryWithRelogin")
|
|
err = client.RetryWithRelogin(ctx, tc, func() error {
|
|
var err error
|
|
k, err = tc.IssueUserCertsWithMFA(ctx, client.ReissueParams{
|
|
RouteToCluster: c.teleportCluster,
|
|
KubernetesCluster: c.kubeCluster,
|
|
}, nil /*applyOpts*/)
|
|
return err
|
|
})
|
|
span.End()
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
// Make sure the cert is allowed to access the cluster.
|
|
// At this point we already know that the user has access to the cluster
|
|
// via the RBAC rules, but we also need to make sure that the user has
|
|
// access to the cluster with at least one kubernetes_user or kubernetes_group
|
|
// defined.
|
|
if err := checkIfCertsAreAllowedToAccessCluster(k, c.kubeCluster); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
// Cache the new cert on disk for reuse.
|
|
if err := tc.LocalAgent().AddKey(k); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
return c.writeKeyResponse(cf.Stdout(), k, c.kubeCluster)
|
|
}
|
|
|
|
// checkIfCertsAreAllowedToAccessCluster evaluates if the new cert created by the user
|
|
// to access kubeCluster has at least one kubernetes_user or kubernetes_group
|
|
// defined. If not, it returns an error.
|
|
// This is a safety check in order to print a better message to the user even
|
|
// before hitting Teleport Kubernetes Proxy.
|
|
func checkIfCertsAreAllowedToAccessCluster(k *client.Key, kubeCluster string) error {
|
|
for k8sCluster, cert := range k.KubeTLSCerts {
|
|
if k8sCluster != kubeCluster {
|
|
continue
|
|
}
|
|
log.Debugf("Got TLS cert for Kubernetes cluster %q", k8sCluster)
|
|
exist, err := checkIfCertHasKubeGroupsAndUsers(cert)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
} else if exist {
|
|
return nil
|
|
}
|
|
}
|
|
errMsg := "Your user's Teleport role does not allow Kubernetes access." +
|
|
" Please ask cluster administrator to ensure your role has appropriate kubernetes_groups and kubernetes_users set."
|
|
return trace.AccessDenied(errMsg)
|
|
}
|
|
|
|
// checkIfCertHasKubeGroupsAndUsers checks if the certificate has Kubernetes groups or users
|
|
// in the Subject Name. If it does, it returns true, otherwise false.
|
|
// Having no Kubernetes groups or users in the certificate means that the user
|
|
// is not allowed to access the Kubernetes cluster since Kubernetes Access enforces
|
|
// the presence of at least one of Kubernetes groups or users in the certificate.
|
|
// If the certificate does not have any Kubernetes groups or users, the
|
|
func checkIfCertHasKubeGroupsAndUsers(certB []byte) (bool, error) {
|
|
cert, err := tlsca.ParseCertificatePEM(certB)
|
|
if err != nil {
|
|
return false, trace.Wrap(err)
|
|
}
|
|
for _, name := range cert.Subject.Names {
|
|
if name.Type.Equal(tlsca.KubeGroupsASN1ExtensionOID) || name.Type.Equal(tlsca.KubeUsersASN1ExtensionOID) {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func (c *kubeCredentialsCommand) writeKeyResponse(output io.Writer, key *client.Key, kubeClusterName string) error {
|
|
crt, err := key.KubeTLSCertificate(kubeClusterName)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
expiry := crt.NotAfter
|
|
// Indicate slightly earlier expiration to avoid the cert expiring
|
|
// mid-request, if possible.
|
|
if time.Until(expiry) > time.Minute {
|
|
expiry = expiry.Add(-1 * time.Minute)
|
|
}
|
|
|
|
// TODO (Joerger): Create a custom k8s Auth Provider or Exec Provider to use non-rsa
|
|
// private keys for kube credentials (if possible)
|
|
rsaKeyPEM, err := key.PrivateKey.RSAPrivateKeyPEM()
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
return trace.Wrap(c.writeResponse(output, key.KubeTLSCerts[kubeClusterName], rsaKeyPEM, expiry))
|
|
}
|
|
|
|
// writeByteResponse writes the exec credential response to the output stream.
|
|
func (c *kubeCredentialsCommand) writeByteResponse(output io.Writer, kubeTLSCert, rsaKeyPEM []byte, expiry time.Time) error {
|
|
// Indicate slightly earlier expiration to avoid the cert expiring
|
|
// mid-request, if possible.
|
|
if time.Until(expiry) > time.Minute {
|
|
expiry = expiry.Add(-1 * time.Minute)
|
|
}
|
|
|
|
return trace.Wrap(c.writeResponse(output, kubeTLSCert, rsaKeyPEM, expiry))
|
|
}
|
|
|
|
// writeResponse writes the exec credential response to the output stream.
|
|
func (c *kubeCredentialsCommand) writeResponse(output io.Writer, kubeTLSCert, rsaKeyPEM []byte, expiry time.Time) error {
|
|
resp := &clientauthentication.ExecCredential{
|
|
Status: &clientauthentication.ExecCredentialStatus{
|
|
ExpirationTimestamp: &metav1.Time{Time: expiry},
|
|
ClientCertificateData: string(kubeTLSCert),
|
|
ClientKeyData: string(rsaKeyPEM),
|
|
},
|
|
}
|
|
data, err := runtime.Encode(kubeCodecs.LegacyCodec(kubeGroupVersion), resp)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
fmt.Fprintln(output, string(data))
|
|
return nil
|
|
}
|
|
|
|
type kubeLSCommand struct {
|
|
*kingpin.CmdClause
|
|
labels string
|
|
predicateExpr string
|
|
searchKeywords string
|
|
format string
|
|
listAll bool
|
|
siteName string
|
|
verbose bool
|
|
quiet bool
|
|
}
|
|
|
|
func newKubeLSCommand(parent *kingpin.CmdClause) *kubeLSCommand {
|
|
c := &kubeLSCommand{
|
|
CmdClause: parent.Command("ls", "Get a list of Kubernetes clusters"),
|
|
}
|
|
c.Flag("cluster", clusterHelp).Short('c').StringVar(&c.siteName)
|
|
c.Flag("search", searchHelp).StringVar(&c.searchKeywords)
|
|
c.Flag("query", queryHelp).StringVar(&c.predicateExpr)
|
|
c.Flag("format", defaults.FormatFlagDescription(defaults.DefaultFormats...)).Short('f').Default(teleport.Text).EnumVar(&c.format, defaults.DefaultFormats...)
|
|
c.Flag("all", "List Kubernetes clusters from all clusters and proxies.").Short('R').BoolVar(&c.listAll)
|
|
c.Arg("labels", labelHelp).StringVar(&c.labels)
|
|
c.Flag("verbose", "Show an untruncated list of labels.").Short('v').BoolVar(&c.verbose)
|
|
c.Flag("quiet", "Quiet mode.").Short('q').BoolVar(&c.quiet)
|
|
return c
|
|
}
|
|
|
|
type kubeListing struct {
|
|
Proxy string `json:"proxy"`
|
|
Cluster string `json:"cluster"`
|
|
KubeCluster types.KubeCluster `json:"kube_cluster"`
|
|
}
|
|
|
|
type kubeListings []kubeListing
|
|
|
|
func (l kubeListings) Len() int {
|
|
return len(l)
|
|
}
|
|
|
|
func (l kubeListings) Less(i, j int) bool {
|
|
if l[i].Proxy != l[j].Proxy {
|
|
return l[i].Proxy < l[j].Proxy
|
|
}
|
|
if l[i].Cluster != l[j].Cluster {
|
|
return l[i].Cluster < l[j].Cluster
|
|
}
|
|
return l[i].KubeCluster.GetName() < l[j].KubeCluster.GetName()
|
|
}
|
|
|
|
func (l kubeListings) Swap(i, j int) {
|
|
l[i], l[j] = l[j], l[i]
|
|
}
|
|
|
|
func formatKubeLabels(cluster types.KubeCluster) string {
|
|
labels := make([]string, 0, len(cluster.GetStaticLabels())+len(cluster.GetDynamicLabels()))
|
|
for key, value := range cluster.GetStaticLabels() {
|
|
labels = append(labels, fmt.Sprintf("%s=%s", key, value))
|
|
}
|
|
for key, value := range cluster.GetDynamicLabels() {
|
|
labels = append(labels, fmt.Sprintf("%s=%s", key, value.GetResult()))
|
|
}
|
|
sort.Strings(labels)
|
|
return strings.Join(labels, " ")
|
|
}
|
|
|
|
func (c *kubeLSCommand) run(cf *CLIConf) error {
|
|
cf.SearchKeywords = c.searchKeywords
|
|
cf.UserHost = c.labels
|
|
cf.PredicateExpression = c.predicateExpr
|
|
cf.SiteName = c.siteName
|
|
|
|
if c.listAll {
|
|
return trace.Wrap(c.runAllClusters(cf))
|
|
}
|
|
|
|
tc, err := makeClient(cf, true)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
currentTeleportCluster, kubeClusters, err := fetchKubeClusters(cf.Context, tc)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
selectedCluster := selectedKubeCluster(currentTeleportCluster)
|
|
format := strings.ToLower(c.format)
|
|
switch format {
|
|
case teleport.Text, "":
|
|
var (
|
|
t asciitable.Table
|
|
columns = []string{"Kube Cluster Name", "Labels", "Selected"}
|
|
rows [][]string
|
|
)
|
|
|
|
for _, cluster := range kubeClusters {
|
|
var selectedMark string
|
|
if cluster.GetName() == selectedCluster {
|
|
selectedMark = "*"
|
|
}
|
|
rows = append(rows, []string{cluster.GetName(), formatKubeLabels(cluster), selectedMark})
|
|
}
|
|
|
|
if c.quiet {
|
|
t = asciitable.MakeHeadlessTable(2)
|
|
for _, row := range rows {
|
|
t.AddRow(row[:2])
|
|
}
|
|
} else if c.verbose {
|
|
t = asciitable.MakeTable(columns, rows...)
|
|
} else {
|
|
t = asciitable.MakeTableWithTruncatedColumn(columns, rows, "Labels")
|
|
}
|
|
fmt.Fprintln(cf.Stdout(), t.AsBuffer().String())
|
|
case teleport.JSON, teleport.YAML:
|
|
out, err := serializeKubeClusters(kubeClusters, selectedCluster, format)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
fmt.Fprintln(cf.Stdout(), out)
|
|
default:
|
|
return trace.BadParameter("unsupported format %q", cf.Format)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func serializeKubeClusters(kubeClusters []types.KubeCluster, selectedCluster, format string) (string, error) {
|
|
type cluster struct {
|
|
KubeClusterName string `json:"kube_cluster_name"`
|
|
Labels map[string]string `json:"labels"`
|
|
Selected bool `json:"selected"`
|
|
}
|
|
clusterInfo := make([]cluster, 0, len(kubeClusters))
|
|
for _, cl := range kubeClusters {
|
|
labels := cl.GetStaticLabels()
|
|
for key, value := range cl.GetDynamicLabels() {
|
|
labels[key] = value.GetResult()
|
|
}
|
|
|
|
clusterInfo = append(clusterInfo, cluster{
|
|
KubeClusterName: cl.GetName(),
|
|
Labels: labels,
|
|
Selected: cl.GetName() == selectedCluster,
|
|
})
|
|
}
|
|
var out []byte
|
|
var err error
|
|
if format == teleport.JSON {
|
|
out, err = utils.FastMarshalIndent(clusterInfo, "", " ")
|
|
} else {
|
|
out, err = yaml.Marshal(clusterInfo)
|
|
}
|
|
return string(out), trace.Wrap(err)
|
|
}
|
|
|
|
func (c *kubeLSCommand) runAllClusters(cf *CLIConf) error {
|
|
var listings kubeListings
|
|
|
|
err := forEachProfile(cf, func(tc *client.TeleportClient, profile *client.ProfileStatus) error {
|
|
req := proto.ListResourcesRequest{
|
|
SearchKeywords: tc.SearchKeywords,
|
|
PredicateExpression: tc.PredicateExpression,
|
|
Labels: tc.Labels,
|
|
}
|
|
|
|
kubeClusters, err := tc.ListKubernetesClustersWithFiltersAllClusters(cf.Context, req)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
for clusterName, kubeClusters := range kubeClusters {
|
|
for _, kc := range kubeClusters {
|
|
listings = append(listings, kubeListing{
|
|
Proxy: profile.ProxyURL.Host,
|
|
Cluster: clusterName,
|
|
KubeCluster: kc,
|
|
})
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
sort.Sort(listings)
|
|
|
|
format := strings.ToLower(c.format)
|
|
switch format {
|
|
case teleport.Text, "":
|
|
var t asciitable.Table
|
|
if cf.Quiet {
|
|
t = asciitable.MakeHeadlessTable(3)
|
|
} else {
|
|
t = asciitable.MakeTable([]string{"Proxy", "Cluster", "Kube Cluster Name", "Labels"})
|
|
}
|
|
for _, listing := range listings {
|
|
t.AddRow([]string{listing.Proxy, listing.Cluster, listing.KubeCluster.GetName(), formatKubeLabels(listing.KubeCluster)})
|
|
}
|
|
fmt.Fprintln(cf.Stdout(), t.AsBuffer().String())
|
|
case teleport.JSON, teleport.YAML:
|
|
out, err := serializeKubeListings(listings, format)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
fmt.Fprintln(cf.Stdout(), out)
|
|
default:
|
|
return trace.BadParameter("Unrecognized format %q", c.format)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func serializeKubeListings(kubeListings []kubeListing, format string) (string, error) {
|
|
var out []byte
|
|
var err error
|
|
if format == teleport.JSON {
|
|
out, err = utils.FastMarshalIndent(kubeListings, "", " ")
|
|
} else {
|
|
out, err = yaml.Marshal(kubeListings)
|
|
}
|
|
return string(out), trace.Wrap(err)
|
|
}
|
|
|
|
func selectedKubeCluster(currentTeleportCluster string) string {
|
|
kc, err := kubeconfig.Load("")
|
|
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
|
|
siteName string
|
|
impersonateUser string
|
|
impersonateGroups []string
|
|
namespace string
|
|
all bool
|
|
}
|
|
|
|
func newKubeLoginCommand(parent *kingpin.CmdClause) *kubeLoginCommand {
|
|
c := &kubeLoginCommand{
|
|
CmdClause: parent.Command("login", "Login to a Kubernetes cluster"),
|
|
}
|
|
c.Flag("cluster", clusterHelp).Short('c').StringVar(&c.siteName)
|
|
c.Arg("kube-cluster", "Name of the Kubernetes cluster to login to. Check 'tsh kube ls' for a list of available clusters.").StringVar(&c.kubeCluster)
|
|
c.Flag("as", "Configure custom Kubernetes user impersonation.").StringVar(&c.impersonateUser)
|
|
c.Flag("as-groups", "Configure custom Kubernetes group impersonation.").StringsVar(&c.impersonateGroups)
|
|
// 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)
|
|
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.")
|
|
}
|
|
// Set CLIConf.KubernetesCluster so that the kube cluster's context is automatically selected.
|
|
cf.KubernetesCluster = c.kubeCluster
|
|
cf.SiteName = c.siteName
|
|
cf.kubernetesImpersonationConfig = impersonationConfig{
|
|
kubernetesUser: c.impersonateUser,
|
|
kubernetesGroups: c.impersonateGroups,
|
|
}
|
|
cf.kubeNamespace = c.namespace
|
|
cf.ListAll = c.all
|
|
|
|
tc, err := makeClient(cf, true)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
// Check that this kube cluster exists.
|
|
currentTeleportCluster, kubeClusters, err := fetchKubeClusters(cf.Context, tc)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
clusterNames := kubeClustersToStrings(kubeClusters)
|
|
// If the user is trying to login to a specific cluster, check that it exists.
|
|
if c.kubeCluster != "" && !slices.Contains(clusterNames, c.kubeCluster) {
|
|
return trace.NotFound("kubernetes cluster %q not found, check 'tsh kube ls' for a list of known clusters", c.kubeCluster)
|
|
}
|
|
|
|
// 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 {
|
|
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)
|
|
}
|
|
if c.kubeCluster != "" {
|
|
fmt.Printf("Logged into Kubernetes cluster %q. Try 'kubectl version' to test the connection.\n", c.kubeCluster)
|
|
} else {
|
|
fmt.Printf("Created kubeconfig with every Kubernetes cluster available. Select a context and try 'kubectl version' to test the connection.\n")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func fetchKubeClusters(ctx context.Context, tc *client.TeleportClient) (teleportCluster string, kubeClusters []types.KubeCluster, err error) {
|
|
err = client.RetryWithRelogin(ctx, tc, func() error {
|
|
pc, err := tc.ConnectToProxy(ctx)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
defer pc.Close()
|
|
|
|
ac := pc.CurrentCluster()
|
|
defer ac.Close()
|
|
|
|
teleportCluster = pc.ClusterName()
|
|
kubeClusters, err = kubeutils.ListKubeClustersWithFilters(ctx, ac, proto.ListResourcesRequest{
|
|
SearchKeywords: tc.SearchKeywords,
|
|
PredicateExpression: tc.PredicateExpression,
|
|
Labels: tc.Labels,
|
|
})
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return "", nil, trace.Wrap(err)
|
|
}
|
|
return teleportCluster, kubeClusters, nil
|
|
}
|
|
|
|
func kubeClustersToStrings(kubeClusters []types.KubeCluster) []string {
|
|
names := make([]string, len(kubeClusters))
|
|
for i, cluster := range kubeClusters {
|
|
names[i] = cluster.GetName()
|
|
}
|
|
|
|
return names
|
|
}
|
|
|
|
// kubernetesStatus holds teleport client information necessary to populate the user's kubeconfig.
|
|
type kubernetesStatus struct {
|
|
clusterAddr string
|
|
teleportClusterName string
|
|
kubeClusters []types.KubeCluster
|
|
credentials *client.Key
|
|
tlsServerName string
|
|
}
|
|
|
|
// fetchKubeStatus returns a kubernetesStatus populated from the given TeleportClient.
|
|
func fetchKubeStatus(ctx context.Context, tc *client.TeleportClient) (*kubernetesStatus, error) {
|
|
var err error
|
|
kubeStatus := &kubernetesStatus{
|
|
clusterAddr: tc.KubeClusterAddr(),
|
|
}
|
|
kubeStatus.credentials, err = tc.LocalAgent().GetCoreKey()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
kubeStatus.teleportClusterName, kubeStatus.kubeClusters, err = fetchKubeClusters(ctx, tc)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
if tc.TLSRoutingEnabled {
|
|
k8host, _ := tc.KubeProxyHostPort()
|
|
kubeStatus.tlsServerName = client.GetKubeTLSServerName(k8host)
|
|
}
|
|
|
|
return kubeStatus, nil
|
|
}
|
|
|
|
// 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) {
|
|
v := &kubeconfig.Values{
|
|
ClusterAddr: kubeStatus.clusterAddr,
|
|
TeleportClusterName: kubeStatus.teleportClusterName,
|
|
Credentials: kubeStatus.credentials,
|
|
ProxyAddr: cf.Proxy,
|
|
TLSServerName: kubeStatus.tlsServerName,
|
|
Impersonate: cf.kubernetesImpersonationConfig.kubernetesUser,
|
|
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,
|
|
}
|
|
|
|
if cf.executablePath == "" {
|
|
// Don't know tsh path.
|
|
// Fall back to the old kubeconfig, with static credentials from v.Credentials.
|
|
return v, nil
|
|
}
|
|
|
|
if len(kubeStatus.kubeClusters) == 0 {
|
|
// If there are no registered k8s clusters, we may have an older teleport cluster.
|
|
// Fall back to the old kubeconfig, with static credentials from v.Credentials.
|
|
log.Debug("Disabling exec plugin mode for kubeconfig because this Teleport cluster has no Kubernetes clusters.")
|
|
return v, nil
|
|
}
|
|
|
|
clusterNames := kubeClustersToStrings(kubeStatus.kubeClusters)
|
|
|
|
// Validate if cf.KubernetesCluster is part of the returned list of clusters
|
|
if cf.KubernetesCluster != "" && !slices.Contains(clusterNames, cf.KubernetesCluster) {
|
|
return nil, trace.NotFound("Kubernetes cluster %q is not registered in this Teleport cluster; you can list registered Kubernetes clusters using 'tsh kube ls'.", cf.KubernetesCluster)
|
|
}
|
|
// If ListAll is not enabled, update only cf.KubernetesCluster cluster.
|
|
if cf.KubernetesCluster != "" && !cf.ListAll {
|
|
clusterNames = []string{cf.KubernetesCluster}
|
|
}
|
|
|
|
v.KubeClusters = clusterNames
|
|
v.Exec = &kubeconfig.ExecValues{
|
|
TshBinaryPath: cf.executablePath,
|
|
TshBinaryInsecure: cf.InsecureSkipVerify,
|
|
Env: make(map[string]string),
|
|
}
|
|
|
|
if cf.HomePath != "" {
|
|
v.Exec.Env[types.HomeEnvVar] = cf.HomePath
|
|
}
|
|
|
|
return v, nil
|
|
}
|
|
|
|
// impersonationConfig allows to configure custom kubernetes impersonation values.
|
|
type impersonationConfig struct {
|
|
// kubernetesUser specifies the kubernetes user to impersonate request as.
|
|
kubernetesUser string
|
|
// kubernetesGroups specifies the kubernetes groups to impersonate request as.
|
|
kubernetesGroups []string
|
|
}
|
|
|
|
// 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 {
|
|
// Fetch proxy's advertised ports to check for k8s support.
|
|
if _, err := tc.Ping(cf.Context); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if tc.KubeProxyAddr == "" {
|
|
// Kubernetes support disabled, don't touch kubeconfig.
|
|
return nil
|
|
}
|
|
|
|
kubeStatus, err := fetchKubeStatus(cf.Context, tc)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
values, err := buildKubeConfigUpdate(cf, kubeStatus)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
path = getKubeConfigPath(cf, path)
|
|
|
|
// If this is a profile specific kubeconfig, we only need
|
|
// to put the selected kube cluster into the kubeconfig.
|
|
isKubeConfig, err := keypaths.IsProfileKubeConfigPath(path)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if isKubeConfig {
|
|
if !strings.Contains(path, cf.KubernetesCluster) {
|
|
return trace.BadParameter("profile specific kubeconfig is in use, run 'eval $(tsh env --unset)' to switch contexts to another kube cluster")
|
|
}
|
|
values.KubeClusters = []string{cf.KubernetesCluster}
|
|
}
|
|
|
|
return trace.Wrap(kubeconfig.Update(path, *values, tc.LoadAllCAs))
|
|
}
|
|
|
|
func getKubeConfigPath(cf *CLIConf, path string) string {
|
|
// cf.kubeConfigPath is used in tests to allow Teleport to run tsh login commands
|
|
// in parallel. If defined, it should take precedence over kubeconfig.PathFromEnv().
|
|
if path == "" && cf.kubeConfigPath != "" {
|
|
path = cf.kubeConfigPath
|
|
} else if path == "" {
|
|
path = kubeconfig.PathFromEnv()
|
|
}
|
|
return path
|
|
}
|
|
|
|
// Required magic boilerplate to use the k8s encoder.
|
|
|
|
var (
|
|
kubeScheme = runtime.NewScheme()
|
|
kubeCodecs = serializer.NewCodecFactory(kubeScheme)
|
|
kubeGroupVersion = schema.GroupVersion{
|
|
Group: "client.authentication.k8s.io",
|
|
Version: "v1beta1",
|
|
}
|
|
)
|
|
|
|
func init() {
|
|
metav1.AddToGroupVersion(kubeScheme, schema.GroupVersion{Version: "v1"})
|
|
clientauthv1beta1.AddToScheme(kubeScheme)
|
|
clientauthentication.AddToScheme(kubeScheme)
|
|
}
|