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

[v15] Add a tsh sessions ls command #37969

Merged
merged 1 commit into from Feb 9, 2024
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
59 changes: 3 additions & 56 deletions tool/tsh/common/kube.go
Expand Up @@ -512,7 +512,7 @@ type kubeSessionsCommand struct {

func newKubeSessionsCommand(parent *kingpin.CmdClause) *kubeSessionsCommand {
c := &kubeSessionsCommand{
CmdClause: parent.Command("sessions", "Get a list of active Kubernetes sessions."),
CmdClause: parent.Command("sessions", "Get a list of active Kubernetes sessions. (DEPRECATED: use tsh sessions ls --kind=kube instead)"),
}
c.Flag("format", defaults.FormatFlagDescription(defaults.DefaultFormats...)).Short('f').Default(teleport.Text).EnumVar(&c.format, defaults.DefaultFormats...)
c.Flag("cluster", clusterHelp).Short('c').StringVar(&c.siteName)
Expand All @@ -539,61 +539,8 @@ func (c *kubeSessionsCommand) run(cf *CLIConf) error {
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", "Command"})
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(),
strings.Join(s.GetCommand(), " "),
})
}

tableOutput := table.AsBuffer().String()
fmt.Fprintln(output, tableOutput)
filteredSessions := sortAndFilterSessions(sessions, []types.SessionKind{types.KubernetesSessionKind})
return trace.Wrap(serializeSessions(filteredSessions, strings.ToLower(c.format), cf.Stdout()))
}

type kubeCredentialsCommand struct {
Expand Down
98 changes: 98 additions & 0 deletions tool/tsh/common/tsh.go
Expand Up @@ -414,6 +414,9 @@ type CLIConf struct {
// JoinMode is the participant mode someone is joining a session as.
JoinMode string

// SessionKinds is the kind of active sessions to list.
SessionKinds []string

// displayParticipantRequirements is set if verbose participant requirement information should be printed for moderated sessions.
displayParticipantRequirements bool

Expand Down Expand Up @@ -943,12 +946,19 @@ func Run(ctx context.Context, args []string, opts ...CliOption) error {
ls.Flag("search", searchHelp).StringVar(&cf.SearchKeywords)
ls.Flag("query", queryHelp).StringVar(&cf.PredicateExpression)
ls.Flag("all", "List nodes from all clusters and proxies.").Short('R').BoolVar(&cf.ListAll)

// clusters
clusters := app.Command("clusters", "List available Teleport clusters.")
clusters.Flag("format", defaults.FormatFlagDescription(defaults.DefaultFormats...)).Short('f').Default(teleport.Text).EnumVar(&cf.Format, defaults.DefaultFormats...)
clusters.Flag("quiet", "Quiet mode").Short('q').BoolVar(&cf.Quiet)
clusters.Flag("verbose", "Verbose table output, shows full label output").Short('v').BoolVar(&cf.Verbose)

// sessions
sessions := app.Command("sessions", "Operate on active sessions.")
sessionsList := sessions.Command("ls", "List active sessions.")
sessionsList.Flag("format", defaults.FormatFlagDescription(defaults.DefaultFormats...)).Short('f').Default(teleport.Text).EnumVar(&cf.Format, defaults.DefaultFormats...)
sessionsList.Flag("kind", "Filter by session kind(s)").Default("ssh", "k8s", "db", "app", "desktop").EnumsVar(&cf.SessionKinds, "ssh", "k8s", "kube", "db", "app", "desktop")

// login logs in with remote proxy and obtains a "session certificate" which gets
// stored in ~/.tsh directory
login := app.Command("login", "Log in to a cluster and retrieve the session certificate.")
Expand Down Expand Up @@ -1354,6 +1364,8 @@ func Run(ctx context.Context, args []string, opts ...CliOption) error {
err = onListNodes(&cf)
case clusters.FullCommand():
err = onListClusters(&cf)
case sessionsList.FullCommand():
err = onListSessions(&cf)
case login.FullCommand():
err = onLogin(&cf)
case logout.FullCommand():
Expand Down Expand Up @@ -3052,6 +3064,92 @@ func onListClusters(cf *CLIConf) error {
return nil
}

func onListSessions(cf *CLIConf) error {
tc, err := makeClient(cf)
if err != nil {
return trace.Wrap(err)
}

clt, err := tc.ConnectToCluster(cf.Context)
if err != nil {
return trace.Wrap(err)
}
defer clt.Close()

sessions, err := clt.AuthClient.GetActiveSessionTrackers(cf.Context)
if err != nil {
return trace.Wrap(err)
}

kinds := map[string]types.SessionKind{
"ssh": types.SSHSessionKind,
"db": types.DatabaseSessionKind,
"app": types.AppSessionKind,
"desktop": types.WindowsDesktopSessionKind,
"k8s": types.KubernetesSessionKind,
// tsh commands often use "kube" to mean kubernetes,
// so add an alias to make it more intuitive
"kube": types.KubernetesSessionKind,
}

var filter []types.SessionKind
for _, k := range cf.SessionKinds {
filter = append(filter, kinds[k])
}
sessions = sortAndFilterSessions(sessions, filter)
return trace.Wrap(serializeSessions(sessions, strings.ToLower(cf.Format), cf.Stdout()))
}

func sortAndFilterSessions(sessions []types.SessionTracker, kinds []types.SessionKind) []types.SessionTracker {
filtered := slices.DeleteFunc(sessions, func(st types.SessionTracker) bool {
return !slices.Contains(kinds, st.GetSessionKind())
})
sort.Slice(filtered, func(i, j int) bool {
return filtered[i].GetCreated().Before(filtered[j].GetCreated())
})
return filtered
}

func serializeSessions(sessions []types.SessionTracker, format string, w io.Writer) error {
switch format {
case teleport.Text, "":
printSessions(w, sessions)
case teleport.JSON:
out, err := utils.FastMarshalIndent(sessions, "", " ")
if err != nil {
return trace.Wrap(err)
}
fmt.Fprintln(w, string(out))
case teleport.YAML:
out, err := yaml.Marshal(sessions)
if err != nil {
return trace.Wrap(err)
}
fmt.Fprintln(w, string(out))
default:
return trace.BadParameter("unsupported format %q", format)
}
return nil
}

func printSessions(output io.Writer, sessions []types.SessionTracker) {
table := asciitable.MakeTable([]string{"ID", "Kind", "Created", "Hostname", "Address", "Login", "Command"})
for _, s := range sessions {
table.AddRow([]string{
s.GetSessionID(),
string(s.GetSessionKind()),
s.GetCreated().Format(time.RFC3339),
s.GetHostname(),
s.GetAddress(),
s.GetLogin(),
strings.Join(s.GetCommand(), " "),
})
}

tableOutput := table.AsBuffer().String()
fmt.Fprintln(output, tableOutput)
}

type clusterInfo struct {
ClusterName string `json:"cluster_name"`
Status string `json:"status"`
Expand Down
4 changes: 3 additions & 1 deletion tool/tsh/common/tsh_test.go
Expand Up @@ -4463,7 +4463,9 @@ func TestSerializeKubeSessions(t *testing.T) {
})
require.NoError(t, err)
testSerialization(t, expected, func(f string) (string, error) {
return serializeKubeSessions([]types.SessionTracker{tracker}, f)
var b bytes.Buffer
err := serializeSessions([]types.SessionTracker{tracker}, f, &b)
return b.String(), err
})
}

Expand Down