From dc79ee678397d10fc3d10714760d9014eae3ce33 Mon Sep 17 00:00:00 2001 From: Tim Ross Date: Sat, 16 Mar 2024 11:44:20 -0400 Subject: [PATCH] Improve node listing in tsh and tctl Converts both tools to use the ListUnifiedResources RPC instead of the ListResources RPC to improve performance. While there may only be marginal gains when listing the entire set of nodes, there are substantial improvements when filtering via predicate, labels, or search. tctl was also changed slightly to make use of server side filtering when tctl get node/foo is run. Prior to this change the entire node set was always retreived and tctl performed the filter client side if the user provided a hostname or UUID. --- lib/client/api.go | 39 ++++++++++++++++++--- tool/tctl/common/resource_command.go | 52 +++++++++++++++++++++++----- 2 files changed, 78 insertions(+), 13 deletions(-) diff --git a/lib/client/api.go b/lib/client/api.go index e6b3236354d03..fb121eb366757 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -2383,13 +2383,22 @@ func isRemoteDest(name string) bool { // ListNodesWithFilters returns all nodes that match the filters in the current cluster // that the logged in user has access to. func (tc *TeleportClient) ListNodesWithFilters(ctx context.Context) ([]types.Server, error) { - req := tc.ResourceFilter(types.KindNode) + req := proto.ListUnifiedResourcesRequest{ + Kinds: []string{types.KindNode}, + Labels: tc.Labels, + SearchKeywords: tc.SearchKeywords, + PredicateExpression: tc.PredicateExpression, + UseSearchAsRoles: tc.UseSearchAsRoles, + SortBy: types.SortBy{ + Field: types.ResourceKind, + }, + } + ctx, span := tc.Tracer.Start( ctx, "teleportClient/ListNodesWithFilters", oteltrace.WithSpanKind(oteltrace.SpanKindClient), oteltrace.WithAttributes( - attribute.String("resource", req.ResourceType), attribute.Int("limit", int(req.Limit)), attribute.String("predicate", req.PredicateExpression), attribute.StringSlice("keywords", req.SearchKeywords), @@ -2403,8 +2412,30 @@ func (tc *TeleportClient) ListNodesWithFilters(ctx context.Context) ([]types.Ser } defer clt.Close() - servers, err := client.GetAllResources[types.Server](ctx, clt.AuthClient, req) - return servers, trace.Wrap(err) + var servers []types.Server + for { + page, err := client.ListUnifiedResourcePage(ctx, clt.AuthClient, &req) + if err != nil { + return nil, trace.Wrap(err) + } + + for _, r := range page.Resources { + srv, ok := r.(types.Server) + if !ok { + log.Warnf("expected types.Server but received unexpected type %T", r) + continue + } + + servers = append(servers, srv) + } + + req.StartKey = page.NextKey + if req.StartKey == "" { + break + } + } + + return servers, nil } // GetClusterAlerts returns a list of matching alerts from the current cluster. diff --git a/tool/tctl/common/resource_command.go b/tool/tctl/common/resource_command.go index 8532d7e4dad79..0470f29acce94 100644 --- a/tool/tctl/common/resource_command.go +++ b/tool/tctl/common/resource_command.go @@ -1895,19 +1895,53 @@ func (rc *ResourceCommand) getCollection(ctx context.Context, client *auth.Clien } return &authorityCollection{cas: []types.CertAuthority{authority}}, nil case types.KindNode: - nodes, err := client.GetNodes(ctx, rc.namespace) - if err != nil { - return nil, trace.Wrap(err) + var search []string + if rc.ref.Name != "" { + search = []string{rc.ref.Name} } - if rc.ref.Name == "" { - return &serverCollection{servers: nodes}, nil + + req := proto.ListUnifiedResourcesRequest{ + Kinds: []string{types.KindNode}, + SearchKeywords: search, + SortBy: types.SortBy{Field: types.ResourceKind}, } - for _, node := range nodes { - if node.GetName() == rc.ref.Name || node.GetHostname() == rc.ref.Name { - return &serverCollection{servers: []types.Server{node}}, nil + + var collection serverCollection + for { + page, err := apiclient.ListUnifiedResourcePage(ctx, client, &req) + if err != nil { + return nil, trace.Wrap(err) + } + + for _, r := range page.Resources { + srv, ok := r.(types.Server) + if !ok { + log.Warnf("expected types.Server but received unexpected type %T", r) + continue + } + + if rc.ref.Name == "" { + collection.servers = append(collection.servers, srv) + continue + } + + if srv.GetName() == rc.ref.Name || srv.GetHostname() == rc.ref.Name { + collection.servers = []types.Server{srv} + return &collection, nil + } + } + + req.StartKey = page.NextKey + if req.StartKey == "" { + break } } - return nil, trace.NotFound("node with ID %q not found", rc.ref.Name) + + if len(collection.servers) == 0 && rc.ref.Name != "" { + return nil, trace.NotFound("node with ID %q not found", rc.ref.Name) + } + + return &collection, nil case types.KindAuthServer: servers, err := client.GetAuthServers() if err != nil {