mirror of
https://github.com/gravitational/teleport
synced 2024-10-21 17:53:28 +00:00
Update tsh login to select clusters.
The following changes have been introduced to tsh login behavior: 1. tsh login now accepts cluster name as an optional positional argument: $ tsh login clustername 2. If tsh login is called without arguments and the current credentials are valid, tsh login now prints status, previous behavior always forced login: $ tsh login ... print status if logged in... 2. If tsh login is called with the proxy equal to current, tsh login selects cluster, otherwise it will re-login to another proxy: $ tsh login one ... selected cluster one $ tsh login two ... selected cluster two $ tsh login --proxy=example.com three ... selected cluster three because proxy is the same $ tsh login --proxy=acme.example.com four ...will switch to proxy acme.example.com and cluster four
This commit is contained in:
parent
f8b8de0a0a
commit
e570b24eeb
|
@ -889,7 +889,7 @@ func (i *TeleInstance) NewUnauthenticatedClient(cfg ClientConfig) (tc *client.Te
|
|||
SiteName: cfg.Cluster,
|
||||
ForwardAgent: cfg.ForwardAgent,
|
||||
}
|
||||
cconf.SetProxy(proxyHost, proxyWebPort, proxySSHPort)
|
||||
cconf.SetProxy(proxyHost, proxyWebPort, proxySSHPort, 0)
|
||||
|
||||
return client.NewClient(cconf)
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@ import (
|
|||
"github.com/gravitational/teleport/lib/utils"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/jonboulle/clockwork"
|
||||
"github.com/moby/moby/pkg/term"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
@ -233,6 +234,14 @@ type ProfileStatus struct {
|
|||
|
||||
// Extensions is a list of enabled SSH features for the certificate.
|
||||
Extensions []string
|
||||
|
||||
// Cluster is a selected cluster
|
||||
Cluster string
|
||||
}
|
||||
|
||||
// IsExpired returns true if profile is not expired yet
|
||||
func (p *ProfileStatus) IsExpired(clock clockwork.Clock) bool {
|
||||
return p.ValidUntil.Sub(clock.Now()) <= 0
|
||||
}
|
||||
|
||||
// readProfile reads in the profile as well as the associated certificate
|
||||
|
@ -301,6 +310,7 @@ func readProfile(profileDir string, profileName string) (*ProfileStatus, error)
|
|||
ValidUntil: validUntil,
|
||||
Extensions: extensions,
|
||||
Roles: roles,
|
||||
Cluster: profile.SiteName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -336,6 +346,15 @@ func Status(profileDir string, proxyHost string) (*ProfileStatus, []*ProfileStat
|
|||
var profile *ProfileStatus
|
||||
var others []*ProfileStatus
|
||||
|
||||
// remove ports from proxy host, because profile name is stored
|
||||
// by host name
|
||||
if proxyHost != "" {
|
||||
proxyHost, err = utils.Host(proxyHost)
|
||||
if err != nil {
|
||||
return nil, nil, trace.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Construct the full path to the profile requested and make sure it exists.
|
||||
profileDir = FullProfilePath(profileDir)
|
||||
stat, err := os.Stat(profileDir)
|
||||
|
@ -381,6 +400,11 @@ func Status(profileDir string, proxyHost string) (*ProfileStatus, []*ProfileStat
|
|||
}
|
||||
ps, err := readProfile(profileDir, file.Name())
|
||||
if err != nil {
|
||||
// parts of profile are missing?
|
||||
// status skips these files
|
||||
if trace.IsNotFound(err) {
|
||||
continue
|
||||
}
|
||||
return nil, nil, trace.Wrap(err)
|
||||
}
|
||||
others = append(others, ps)
|
||||
|
@ -403,7 +427,7 @@ func (c *Config) LoadProfile(profileDir string, proxyName string) error {
|
|||
return trace.Wrap(err)
|
||||
}
|
||||
// apply the profile to the current configuration:
|
||||
c.SetProxy(cp.ProxyHost, cp.ProxyWebPort, cp.ProxySSHPort)
|
||||
c.SetProxy(cp.ProxyHost, cp.ProxyWebPort, cp.ProxySSHPort, cp.ProxyKubePort)
|
||||
c.Username = cp.Username
|
||||
c.SiteName = cp.SiteName
|
||||
c.LocalForwardPorts, err = ParsePortForwardSpec(cp.ForwardedPorts)
|
||||
|
@ -427,6 +451,7 @@ func (c *Config) SaveProfile(profileDir string, profileOptions ...ProfileOptions
|
|||
cp.Username = c.Username
|
||||
cp.ProxySSHPort = c.ProxySSHPort()
|
||||
cp.ProxyWebPort = c.ProxyWebPort()
|
||||
cp.ProxyKubePort = c.ProxyKubePort()
|
||||
cp.ForwardedPorts = c.LocalForwardPorts.ToStringSpec()
|
||||
cp.SiteName = c.SiteName
|
||||
|
||||
|
@ -446,8 +471,12 @@ func (c *Config) SaveProfile(profileDir string, profileOptions ...ProfileOptions
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) SetProxy(host string, webPort, sshPort int) {
|
||||
c.ProxyHostPort = fmt.Sprintf("%s:%d,%d", host, webPort, sshPort)
|
||||
func (c *Config) SetProxy(host string, webPort, sshPort, kubePort int) {
|
||||
if kubePort != 0 {
|
||||
c.ProxyHostPort = fmt.Sprintf("%s:%d,%d,%d", host, webPort, sshPort, kubePort)
|
||||
} else {
|
||||
c.ProxyHostPort = fmt.Sprintf("%s:%d,%d", host, webPort, sshPort)
|
||||
}
|
||||
}
|
||||
|
||||
// ProxyHost returns the hostname of the proxy server (without any port numbers)
|
||||
|
@ -472,6 +501,10 @@ func (c *Config) ProxyWebHostPort() string {
|
|||
return net.JoinHostPort(c.ProxyHost(), strconv.Itoa(c.ProxyWebPort()))
|
||||
}
|
||||
|
||||
func (c *Config) ProxyKubeHostPort() string {
|
||||
return net.JoinHostPort(c.ProxyHost(), strconv.Itoa(c.ProxyKubePort()))
|
||||
}
|
||||
|
||||
// ProxyWebPort returns the port number of teleport HTTP proxy stored in the config
|
||||
// usually 3080 by default.
|
||||
func (c *Config) ProxyWebPort() (retval int) {
|
||||
|
@ -506,6 +539,23 @@ func (c *Config) ProxySSHPort() (retval int) {
|
|||
return retval
|
||||
}
|
||||
|
||||
// ProxyKubePort returns the port number of teleport Kubernetes proxy stored in the config
|
||||
// usually 3026 by default.
|
||||
func (c *Config) ProxyKubePort() (retval int) {
|
||||
retval = defaults.KubeProxyListenPort
|
||||
_, port, err := net.SplitHostPort(c.ProxyHostPort)
|
||||
if err == nil && len(port) > 0 {
|
||||
ports := strings.Split(port, ",")
|
||||
if len(ports) > 2 {
|
||||
retval, err = strconv.Atoi(ports[2])
|
||||
if err != nil {
|
||||
log.Warnf("invalid proxy Kubernetes port: '%v': %v", ports, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return retval
|
||||
}
|
||||
|
||||
// ProxySpecified returns true if proxy has been specified
|
||||
func (c *Config) ProxySpecified() bool {
|
||||
return len(c.ProxyHostPort) > 0
|
||||
|
|
|
@ -43,7 +43,7 @@ func (s *APITestSuite) TestConfig(c *check.C) {
|
|||
c.Assert(conf.ProxySSHHostPort(), check.Equals, "example.org:3023")
|
||||
c.Assert(conf.ProxyWebHostPort(), check.Equals, "example.org:3080")
|
||||
|
||||
conf.SetProxy("example.org", 100, 200)
|
||||
conf.SetProxy("example.org", 100, 200, 0)
|
||||
c.Assert(conf.ProxyWebHostPort(), check.Equals, "example.org:100")
|
||||
c.Assert(conf.ProxySSHHostPort(), check.Equals, "example.org:200")
|
||||
|
||||
|
@ -54,6 +54,12 @@ func (s *APITestSuite) TestConfig(c *check.C) {
|
|||
conf.ProxyHostPort = "example.org:,200"
|
||||
c.Assert(conf.ProxySSHHostPort(), check.Equals, "example.org:200")
|
||||
c.Assert(conf.ProxyWebHostPort(), check.Equals, "example.org:3080")
|
||||
|
||||
conf.SetProxy("example.org", 100, 200, 300)
|
||||
c.Assert(conf.ProxyWebHostPort(), check.Equals, "example.org:100")
|
||||
c.Assert(conf.ProxySSHHostPort(), check.Equals, "example.org:200")
|
||||
c.Assert(conf.ProxyKubeHostPort(), check.Equals, "example.org:300")
|
||||
|
||||
}
|
||||
|
||||
func (s *APITestSuite) TestNew(c *check.C) {
|
||||
|
|
|
@ -37,9 +37,10 @@ type ClientProfile struct {
|
|||
//
|
||||
// proxy configuration
|
||||
//
|
||||
ProxyHost string `yaml:"proxy_host,omitempty"`
|
||||
ProxySSHPort int `yaml:"proxy_port,omitempty"`
|
||||
ProxyWebPort int `yaml:"proxy_web_port,omitempty"`
|
||||
ProxyHost string `yaml:"proxy_host,omitempty"`
|
||||
ProxySSHPort int `yaml:"proxy_port,omitempty"`
|
||||
ProxyWebPort int `yaml:"proxy_web_port,omitempty"`
|
||||
ProxyKubePort int `yaml:"proxy_kube_port,omitempty"`
|
||||
|
||||
//
|
||||
// auth/identity
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
|
||||
"github.com/gravitational/teleport"
|
||||
"github.com/gravitational/teleport/lib/client"
|
||||
"github.com/gravitational/teleport/lib/defaults"
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
|
@ -22,8 +21,10 @@ func UpdateKubeconfig(tc *client.TeleportClient) error {
|
|||
return trace.Wrap(err)
|
||||
}
|
||||
clusterName := tc.ProxyHost()
|
||||
// TODO: unhardcode the port
|
||||
clusterAddr := fmt.Sprintf("https://%v:%v", tc.ProxyHost(), defaults.KubeProxyListenPort)
|
||||
if tc.SiteName != "" && tc.SiteName != clusterName {
|
||||
clusterName = fmt.Sprintf("%v.%v", tc.SiteName, tc.ProxyHost())
|
||||
}
|
||||
clusterAddr := fmt.Sprintf("https://%v:%v", clusterName, tc.ProxyKubePort())
|
||||
|
||||
creds, err := tc.LocalAgent().GetKey()
|
||||
if err != nil {
|
||||
|
|
|
@ -280,7 +280,7 @@ func (f *Forwarder) setupContext(ctx auth.AuthContext, req *http.Request, isRemo
|
|||
return nil, trace.Wrap(err)
|
||||
}
|
||||
for _, remoteCluster := range f.Tunnel.GetSites() {
|
||||
if strings.HasSuffix(req.Host, remoteCluster.GetName()+".") {
|
||||
if strings.HasPrefix(req.Host, remoteCluster.GetName()+".") {
|
||||
f.Debugf("Going to proxy to cluster: %v based on matching host suffix %v.", remoteCluster.GetName(), req.Host)
|
||||
targetCluster = remoteCluster
|
||||
isRemoteCluster = remoteCluster.GetName() != f.ClusterName
|
||||
|
@ -561,9 +561,9 @@ type clusterSession struct {
|
|||
func (f *Forwarder) getOrCreateClusterSession(ctx authContext) (*clusterSession, error) {
|
||||
client := f.getClusterSession(ctx)
|
||||
if client != nil {
|
||||
f.Debugf("Returning existing creds for %v.", ctx)
|
||||
return client, nil
|
||||
}
|
||||
f.Debugf("Requesting new creds for %v.", ctx)
|
||||
return f.newClusterSession(ctx)
|
||||
}
|
||||
|
||||
|
|
|
@ -10,5 +10,11 @@ EXAMPLES:
|
|||
$ tsh --proxy=host.example.com:8080,8023 login
|
||||
|
||||
Use port 8080 and 3023 (default) for SSH proxy:
|
||||
$ tsh --proxy=host.example.com:8080 login`
|
||||
$ tsh --proxy=host.example.com:8080 login
|
||||
|
||||
Login and select cluster "two":
|
||||
$ tsh --proxy=host.example.com login two
|
||||
|
||||
Select cluster "two" using existing credentials and proxy:
|
||||
$ tsh login two`
|
||||
)
|
||||
|
|
|
@ -44,6 +44,7 @@ import (
|
|||
"github.com/gravitational/trace"
|
||||
|
||||
gops "github.com/google/gops/agent"
|
||||
"github.com/jonboulle/clockwork"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
|
@ -146,6 +147,11 @@ func main() {
|
|||
Run(cmd_line, false)
|
||||
}
|
||||
|
||||
const (
|
||||
clusterEnvVar = "TELEPORT_SITE"
|
||||
clusterHelp = "Specify the cluster to connect"
|
||||
)
|
||||
|
||||
// Run executes TSH client. same as main() but easier to test
|
||||
func Run(args []string, underTest bool) {
|
||||
var cf CLIConf
|
||||
|
@ -159,7 +165,7 @@ func Run(args []string, underTest bool) {
|
|||
app.Flag("proxy", "SSH proxy address").Envar("TELEPORT_PROXY").StringVar(&cf.Proxy)
|
||||
app.Flag("nocache", "do not cache cluster discovery locally").Hidden().BoolVar(&cf.NoCache)
|
||||
app.Flag("user", fmt.Sprintf("SSH proxy user [%s]", localUser)).Envar("TELEPORT_USER").StringVar(&cf.Username)
|
||||
app.Flag("cluster", "Specify the cluster to connect").Envar("TELEPORT_SITE").StringVar(&cf.SiteName)
|
||||
|
||||
app.Flag("ttl", "Minutes to live for a SSH session").Int32Var(&cf.MinsToLive)
|
||||
app.Flag("identity", "Identity file").Short('i').StringVar(&cf.IdentityFileIn)
|
||||
app.Flag("compat", "OpenSSH compatibility flag").Hidden().StringVar(&cf.Compatibility)
|
||||
|
@ -182,20 +188,26 @@ func Run(args []string, underTest bool) {
|
|||
ssh.Flag("forward", "Forward localhost connections to remote server").Short('L').StringsVar(&cf.LocalForwardPorts)
|
||||
ssh.Flag("local", "Execute command on localhost after connecting to SSH node").Default("false").BoolVar(&cf.LocalExec)
|
||||
ssh.Flag("tty", "Allocate TTY").Short('t').BoolVar(&cf.Interactive)
|
||||
ssh.Flag("cluster", clusterHelp).Envar(clusterEnvVar).StringVar(&cf.SiteName)
|
||||
|
||||
// join
|
||||
join := app.Command("join", "Join the active SSH session")
|
||||
join.Flag("cluster", clusterHelp).Envar(clusterEnvVar).StringVar(&cf.SiteName)
|
||||
join.Arg("session-id", "ID of the session to join").Required().StringVar(&cf.SessionID)
|
||||
// play
|
||||
play := app.Command("play", "Replay the recorded SSH session")
|
||||
play.Flag("cluster", clusterHelp).Envar(clusterEnvVar).StringVar(&cf.SiteName)
|
||||
play.Arg("session-id", "ID of the session to play").Required().StringVar(&cf.SessionID)
|
||||
// scp
|
||||
scp := app.Command("scp", "Secure file copy")
|
||||
scp.Flag("cluster", clusterHelp).Envar(clusterEnvVar).StringVar(&cf.SiteName)
|
||||
scp.Arg("from, to", "Source and destination to copy").Required().StringsVar(&cf.CopySpec)
|
||||
scp.Flag("recursive", "Recursive copy of subdirectories").Short('r').BoolVar(&cf.RecursiveCopy)
|
||||
scp.Flag("port", "Port to connect to on the remote host").Short('P').Int32Var(&cf.NodePort)
|
||||
scp.Flag("quiet", "Quiet mode").Short('q').BoolVar(&cf.Quiet)
|
||||
// ls
|
||||
ls := app.Command("ls", "List remote SSH nodes")
|
||||
ls.Flag("cluster", clusterHelp).Envar(clusterEnvVar).StringVar(&cf.SiteName)
|
||||
ls.Arg("labels", "List of labels to filter node list").StringVar(&cf.UserHost)
|
||||
// clusters
|
||||
clusters := app.Command("clusters", "List available Teleport clusters")
|
||||
|
@ -208,6 +220,7 @@ func Run(args []string, underTest bool) {
|
|||
login.Flag("format", fmt.Sprintf("Identity format [%s] or %s (for OpenSSH compatibility)",
|
||||
client.DefaultIdentityFormat,
|
||||
client.IdentityFormatOpenSSH)).Default(string(client.DefaultIdentityFormat)).StringVar((*string)(&cf.IdentityFormat))
|
||||
login.Arg("cluster", clusterHelp).StringVar(&cf.SiteName)
|
||||
login.Alias(loginUsageFooter)
|
||||
|
||||
// logout deletes obtained session certificates in ~/.tsh
|
||||
|
@ -215,6 +228,7 @@ func Run(args []string, underTest bool) {
|
|||
|
||||
// bench
|
||||
bench := app.Command("bench", "Run shell or execute a command on a remote SSH node").Hidden()
|
||||
bench.Flag("cluster", clusterHelp).Envar(clusterEnvVar).StringVar(&cf.SiteName)
|
||||
bench.Arg("[user@]host", "Remote hostname and the login to use").Required().StringVar(&cf.UserHost)
|
||||
bench.Arg("command", "Command to execute on a remote host").Required().StringsVar(&cf.RemoteCommand)
|
||||
bench.Flag("port", "SSH port on a remote host").Short('p').Int32Var(&cf.NodePort)
|
||||
|
@ -281,7 +295,6 @@ func Run(args []string, underTest bool) {
|
|||
case clusters.FullCommand():
|
||||
onListSites(&cf)
|
||||
case login.FullCommand():
|
||||
refuseArgs(login.FullCommand(), args)
|
||||
onLogin(&cf)
|
||||
case logout.FullCommand():
|
||||
refuseArgs(logout.FullCommand(), args)
|
||||
|
@ -320,12 +333,47 @@ func onLogin(cf *CLIConf) {
|
|||
utils.FatalError(trace.BadParameter("invalid identity format: %s", cf.IdentityFormat))
|
||||
}
|
||||
|
||||
// Get the status of the active profile ~/.tsh/profile as well as the status
|
||||
// of any other proxies the user is logged into.
|
||||
profile, profiles, err := client.Status("", cf.Proxy)
|
||||
if err != nil {
|
||||
if !trace.IsNotFound(err) {
|
||||
utils.FatalError(err)
|
||||
}
|
||||
}
|
||||
|
||||
// make the teleport client and retrieve the certificate from the proxy:
|
||||
tc, err = makeClient(cf, true)
|
||||
if err != nil {
|
||||
utils.FatalError(err)
|
||||
}
|
||||
|
||||
// client is already logged in and profile is not expired
|
||||
if profile != nil && !profile.IsExpired(clockwork.NewRealClock()) {
|
||||
switch {
|
||||
// in case if nothing is specified, print current status
|
||||
case cf.Proxy == "" && cf.SiteName == "":
|
||||
printProfiles(profile, profiles)
|
||||
return
|
||||
// in case if parameters match, print current status
|
||||
case host(cf.Proxy) == host(profile.ProxyURL.Host) && cf.SiteName == profile.Cluster:
|
||||
printProfiles(profile, profiles)
|
||||
return
|
||||
// proxy is unspecified or the same as the currently provided proxy,
|
||||
// but cluster is specified, treat this as selecting a new cluster
|
||||
// for the same proxy
|
||||
case (cf.Proxy == "" || host(cf.Proxy) == host(profile.ProxyURL.Host)) && cf.SiteName != "":
|
||||
tc.SaveProfile("")
|
||||
if err := kubeclient.UpdateKubeconfig(tc); err != nil {
|
||||
utils.FatalError(err)
|
||||
}
|
||||
onStatus(cf)
|
||||
return
|
||||
// otherwise just passthrough to standard login
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
if cf.Username == "" {
|
||||
cf.Username = tc.Username
|
||||
}
|
||||
|
@ -350,11 +398,8 @@ func onLogin(cf *CLIConf) {
|
|||
|
||||
// regular login (without -i flag)
|
||||
tc.SaveProfile("")
|
||||
if tc.SiteName != "" {
|
||||
fmt.Printf("\nYou are now logged into %s as %s\n", tc.SiteName, tc.Username)
|
||||
} else {
|
||||
fmt.Printf("\nYou are now logged in\n")
|
||||
}
|
||||
|
||||
onStatus(cf)
|
||||
}
|
||||
|
||||
// onLogout deletes a "session certificate" from ~/.tsh for a given proxy
|
||||
|
@ -849,6 +894,9 @@ func printStatus(p *client.ProfileStatus, isActive bool) {
|
|||
|
||||
fmt.Printf("%vProfile URL: %v\n", prefix, p.ProxyURL.String())
|
||||
fmt.Printf(" Logged in as: %v\n", p.Username)
|
||||
if p.Cluster != "" {
|
||||
fmt.Printf(" Cluster: %v\n", p.Cluster)
|
||||
}
|
||||
fmt.Printf(" Roles: %v*\n", strings.Join(p.Roles, ", "))
|
||||
fmt.Printf(" Logins: %v\n", strings.Join(p.Logins, ", "))
|
||||
fmt.Printf(" Valid until: %v [%v]\n", p.ValidUntil, humanDuration)
|
||||
|
@ -862,9 +910,16 @@ func onStatus(cf *CLIConf) {
|
|||
// of any other proxies the user is logged into.
|
||||
profile, profiles, err := client.Status("", cf.Proxy)
|
||||
if err != nil {
|
||||
if trace.IsNotFound(err) {
|
||||
fmt.Printf("Not logged in.\n")
|
||||
return
|
||||
}
|
||||
utils.FatalError(err)
|
||||
}
|
||||
printProfiles(profile, profiles)
|
||||
}
|
||||
|
||||
func printProfiles(profile *client.ProfileStatus, profiles []*client.ProfileStatus) {
|
||||
// Print the active profile.
|
||||
if profile != nil {
|
||||
printStatus(profile, true)
|
||||
|
@ -882,3 +937,14 @@ func onStatus(cf *CLIConf) {
|
|||
fmt.Printf(" https://gravitaitonal.com/teleport/docs/enteprise\n")
|
||||
}
|
||||
}
|
||||
|
||||
// host is a utility function that extracts
|
||||
// host from the host:port pair, in case of any error
|
||||
// returns the original value
|
||||
func host(in string) string {
|
||||
out, err := utils.Host(in)
|
||||
if err != nil {
|
||||
return in
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue