sessionsearch[28]: add resource property filters to tctl recordings search (#66428)

Expose SSH, Kubernetes, and database-specific resource property filters on
`tctl recordings search`, and map them into the session search
ResourceProperties oneof.

Reject mixed resource property variants in a single search request, since the
API accepts only one ResourceProperties variant at a time.

Signed-off-by: Tiago Silva <tiago.silva@goteleport.com>
This commit is contained in:
Tiago Silva
2026-05-05 12:09:02 +00:00
committed by GitHub
parent f7bc2ce5a3
commit 6fc31cb2db
2 changed files with 181 additions and 0 deletions
+79
View File
@@ -109,6 +109,16 @@ type RecordingsCommand struct {
searchResourceKind string
// searchResourceName filters results by the resource name.
searchResourceName string
// searchServerHostname filters SSH results by server hostname.
searchServerHostname string
// searchServerAddr filters SSH results by server address.
searchServerAddr string
// searchPodNamespace filters Kubernetes results by pod namespace.
searchPodNamespace string
// searchPodName filters Kubernetes results by pod name.
searchPodName string
// searchDatabaseName filters database results by database name.
searchDatabaseName string
// searchSeverity filters results by minimum severity level (low/medium/high/critical).
searchSeverity string
// searchMode controls which search strategy to use: hybrid (default), keyword, or embedding.
@@ -143,6 +153,11 @@ func (c *RecordingsCommand) Initialize(app *kingpin.Application, t *tctlcfg.Glob
c.recordingsSearch.Flag("role", "Filter by role held during the session. Can be specified multiple times.").StringsVar(&c.searchRoles)
c.recordingsSearch.Flag("resource-kind", "Filter by Teleport resource type (node, kube_cluster, db).").StringVar(&c.searchResourceKind)
c.recordingsSearch.Flag("resource-name", "Filter by resource name.").StringVar(&c.searchResourceName)
c.recordingsSearch.Flag("server-hostname", "Filter SSH sessions by server hostname.").StringVar(&c.searchServerHostname)
c.recordingsSearch.Flag("server-addr", "Filter SSH sessions by server address.").StringVar(&c.searchServerAddr)
c.recordingsSearch.Flag("pod-namespace", "Filter Kubernetes sessions by pod namespace.").StringVar(&c.searchPodNamespace)
c.recordingsSearch.Flag("pod-name", "Filter Kubernetes sessions by pod name.").StringVar(&c.searchPodName)
c.recordingsSearch.Flag("database-name", "Filter database sessions by database name.").StringVar(&c.searchDatabaseName)
c.recordingsSearch.Flag("severity", "Minimum severity level to include (low, medium, high, critical).").StringVar(&c.searchSeverity)
c.recordingsSearch.Flag("search-mode", "Search strategy to use when search queries are provided.").Default(searchModeHybrid).EnumVar(&c.searchMode, searchModeHybrid, searchModeKeyword, searchModeEmbedding)
c.recordingsSearch.Flag("limit", "Maximum number of results to return.").Default(defaults.TshTctlSessionListLimit).Uint32Var(&c.searchLimit)
@@ -290,6 +305,11 @@ func (c *RecordingsCommand) SearchRecordings(ctx context.Context, tc *authclient
UserRoles: c.searchRoles,
MaxResults: c.searchLimit,
}
resourceProperties, err := c.buildSearchResourceProperties()
if err != nil {
return trace.Wrap(err)
}
req.ResourceProperties = resourceProperties
if c.searchUsername != "" {
req.Username = &c.searchUsername
}
@@ -354,6 +374,65 @@ func showSessionSummaries(ctx context.Context, sessions []*sessionsearchv1pb.Ses
}
}
func (c *RecordingsCommand) buildSearchResourceProperties() (*sessionsearchv1pb.ResourceProperties, error) {
sshSet := c.searchServerHostname != "" || c.searchServerAddr != ""
kubernetesSet := c.searchPodNamespace != "" || c.searchPodName != ""
databaseSet := c.searchDatabaseName != ""
var variants []string
if sshSet {
variants = append(variants, "SSH")
}
if kubernetesSet {
variants = append(variants, "Kubernetes")
}
if databaseSet {
variants = append(variants, "Database")
}
if len(variants) > 1 {
return nil, trace.BadParameter("resource property filters can only target one session kind at a time, got %s", strings.Join(variants, ", "))
}
switch {
case sshSet:
props := &sessionsearchv1pb.SSHProperties{}
if c.searchServerHostname != "" {
props.ServerHostname = &c.searchServerHostname
}
if c.searchServerAddr != "" {
props.ServerAddr = &c.searchServerAddr
}
return &sessionsearchv1pb.ResourceProperties{
Type: &sessionsearchv1pb.ResourceProperties_Ssh{
Ssh: props,
},
}, nil
case kubernetesSet:
props := &sessionsearchv1pb.KubernetesProperties{}
if c.searchPodNamespace != "" {
props.PodNamespace = &c.searchPodNamespace
}
if c.searchPodName != "" {
props.PodName = &c.searchPodName
}
return &sessionsearchv1pb.ResourceProperties{
Type: &sessionsearchv1pb.ResourceProperties_Kubernetes{
Kubernetes: props,
},
}, nil
case databaseSet:
return &sessionsearchv1pb.ResourceProperties{
Type: &sessionsearchv1pb.ResourceProperties_Database{
Database: &sessionsearchv1pb.DatabaseProperties{
DatabaseName: &c.searchDatabaseName,
},
},
}, nil
default:
return nil, nil
}
}
// checkSessionSearchEnabled returns an error if session search is not active on this cluster.
func checkSessionSearchEnabled(ctx context.Context, sc sessionsearchv1pb.SessionSearchServiceClient) error {
resp, err := sc.IsEnabled(ctx, &sessionsearchv1pb.IsEnabledRequest{})
+102
View File
@@ -0,0 +1,102 @@
/*
* Teleport
* Copyright (C) 2026 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package common
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/lib/service/servicecfg"
"github.com/gravitational/teleport/lib/utils"
tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config"
)
func TestRecordingsSearchResourcePropertyFlags(t *testing.T) {
var c RecordingsCommand
app := utils.InitCLIParser("tctl", GlobalHelpString)
c.Initialize(app, &tctlcfg.GlobalCLIFlags{}, servicecfg.MakeDefaultConfig())
selectedCmd, err := app.Parse([]string{
"recordings", "search",
"--server-hostname=host-1",
"--server-addr=10.0.0.1:3022",
"--pod-namespace=prod",
"--pod-name=api-7fd",
"--database-name=postgres",
})
require.NoError(t, err)
require.Equal(t, c.recordingsSearch.FullCommand(), selectedCmd)
require.Equal(t, "host-1", c.searchServerHostname)
require.Equal(t, "10.0.0.1:3022", c.searchServerAddr)
require.Equal(t, "prod", c.searchPodNamespace)
require.Equal(t, "api-7fd", c.searchPodName)
require.Equal(t, "postgres", c.searchDatabaseName)
}
func TestBuildSearchResourceProperties(t *testing.T) {
t.Run("none", func(t *testing.T) {
got, err := (&RecordingsCommand{}).buildSearchResourceProperties()
require.NoError(t, err)
require.Nil(t, got)
})
t.Run("ssh", func(t *testing.T) {
got, err := (&RecordingsCommand{
searchServerHostname: "host-1",
searchServerAddr: "10.0.0.1:3022",
}).buildSearchResourceProperties()
require.NoError(t, err)
ssh := got.GetSsh()
require.NotNil(t, ssh)
require.Equal(t, "host-1", ssh.GetServerHostname())
require.Equal(t, "10.0.0.1:3022", ssh.GetServerAddr())
})
t.Run("kubernetes", func(t *testing.T) {
got, err := (&RecordingsCommand{
searchPodNamespace: "prod",
searchPodName: "api-7fd",
}).buildSearchResourceProperties()
require.NoError(t, err)
kubernetes := got.GetKubernetes()
require.NotNil(t, kubernetes)
require.Equal(t, "prod", kubernetes.GetPodNamespace())
require.Equal(t, "api-7fd", kubernetes.GetPodName())
})
t.Run("database", func(t *testing.T) {
got, err := (&RecordingsCommand{
searchDatabaseName: "postgres",
}).buildSearchResourceProperties()
require.NoError(t, err)
database := got.GetDatabase()
require.NotNil(t, database)
require.Equal(t, "postgres", database.GetDatabaseName())
})
t.Run("mixed variants", func(t *testing.T) {
got, err := (&RecordingsCommand{
searchServerHostname: "host-1",
searchPodName: "api-7fd",
}).buildSearchResourceProperties()
require.ErrorContains(t, err, "resource property filters can only target one session kind")
require.Nil(t, got)
})
}