mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-19 10:10:29 +08:00
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:
@@ -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{})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user