Skip to content
59 changes: 36 additions & 23 deletions api/gen/proto/go/teleport/machineid/v1/bot_instance_service.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions api/proto/teleport/machineid/v1/bot_instance_service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ syntax = "proto3";
package teleport.machineid.v1;

import "google/protobuf/empty.proto";
import "teleport/legacy/types/types.proto";
import "teleport/machineid/v1/bot_instance.proto";

option go_package = "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1;machineidv1";
Expand Down Expand Up @@ -45,6 +46,8 @@ message ListBotInstancesRequest {
string page_token = 3;
// A search term used to filter the results. If non-empty, it's used to match against supported fields.
string filter_search_term = 4;
// The sort config to use for the results. If empty, the default sort field and order is used.
types.SortBy sort = 5;
}

// Response for ListBotInstances.
Expand Down
2 changes: 1 addition & 1 deletion lib/auth/authclient/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -1231,7 +1231,7 @@ type Cache interface {
GetBotInstance(ctx context.Context, botName, instanceID string) (*machineidv1.BotInstance, error)

// ListBotInstances returns a page of BotInstance resources.
ListBotInstances(ctx context.Context, botName string, pageSize int, lastToken string, search string) ([]*machineidv1.BotInstance, string, error)
ListBotInstances(ctx context.Context, botName string, pageSize int, lastToken string, search string, sort *types.SortBy) ([]*machineidv1.BotInstance, string, error)
}

type NodeWrapper struct {
Expand Down
4 changes: 2 additions & 2 deletions lib/auth/machineid/machineidv1/bot_instance_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ type BotInstancesCache interface {
GetBotInstance(ctx context.Context, botName, instanceID string) (*pb.BotInstance, error)

// ListBotInstances returns a page of BotInstance resources.
ListBotInstances(ctx context.Context, botName string, pageSize int, lastToken string, search string) ([]*pb.BotInstance, string, error)
ListBotInstances(ctx context.Context, botName string, pageSize int, lastToken string, search string, sort *types.SortBy) ([]*pb.BotInstance, string, error)
}

// BotInstanceServiceConfig holds configuration options for the BotInstance gRPC
Expand Down Expand Up @@ -157,7 +157,7 @@ func (b *BotInstanceService) ListBotInstances(ctx context.Context, req *pb.ListB
return nil, trace.Wrap(err)
}

res, nextToken, err := b.cache.ListBotInstances(ctx, req.FilterBotName, int(req.PageSize), req.PageToken, req.FilterSearchTerm)
res, nextToken, err := b.cache.ListBotInstances(ctx, req.FilterBotName, int(req.PageSize), req.PageToken, req.FilterSearchTerm, req.Sort)
if err != nil {
return nil, trace.Wrap(err)
}
Expand Down
51 changes: 45 additions & 6 deletions lib/cache/bot_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"slices"
"strings"
"time"

"github.com/gravitational/trace"
"google.golang.org/protobuf/proto"
Expand All @@ -34,7 +35,8 @@ import (
type botInstanceIndex string

const (
botInstanceNameIndex botInstanceIndex = "name"
botInstanceNameIndex botInstanceIndex = "name"
botInstanceActiveAtIndex botInstanceIndex = "active_at_latest"
)

func keyForNameIndex(botInstance *machineidv1.BotInstance) string {
Expand All @@ -48,6 +50,22 @@ func makeNameIndexKey(botName string, instanceID string) string {
return botName + "/" + instanceID
}

func keyForActiveAtIndex(botInstance *machineidv1.BotInstance) string {
var recordedAt time.Time

initialHeartbeatTime := botInstance.GetStatus().GetInitialHeartbeat().GetRecordedAt()
if initialHeartbeatTime != nil {
recordedAt = initialHeartbeatTime.AsTime()
}

latestHeartbeats := botInstance.GetStatus().GetLatestHeartbeats()
if len(latestHeartbeats) > 0 {
recordedAt = latestHeartbeats[len(latestHeartbeats)-1].GetRecordedAt().AsTime()
}

return recordedAt.Format(time.RFC3339) + "/" + botInstance.GetMetadata().GetName()
}

func newBotInstanceCollection(upstream services.BotInstance, w types.WatchKind) (*collection[*machineidv1.BotInstance, botInstanceIndex], error) {
if upstream == nil {
return nil, trace.BadParameter("missing parameter upstream (BotInstance)")
Expand All @@ -60,12 +78,14 @@ func newBotInstanceCollection(upstream services.BotInstance, w types.WatchKind)
map[botInstanceIndex]func(*machineidv1.BotInstance) string{
// Index on a combination of bot name and instance name
botInstanceNameIndex: keyForNameIndex,
// Index on a combination of most recent heartbeat time and instance name
botInstanceActiveAtIndex: keyForActiveAtIndex,
}),
fetcher: func(ctx context.Context, loadSecrets bool) ([]*machineidv1.BotInstance, error) {
var out []*machineidv1.BotInstance
clientutils.IterateResources(ctx,
func(ctx context.Context, limit int, start string) ([]*machineidv1.BotInstance, string, error) {
return upstream.ListBotInstances(ctx, "", limit, start, "")
return upstream.ListBotInstances(ctx, "", limit, start, "", nil)
},
func(hcc *machineidv1.BotInstance) error {
out = append(out, hcc)
Expand Down Expand Up @@ -97,23 +117,42 @@ func (c *Cache) GetBotInstance(ctx context.Context, botName, instanceID string)
}

// ListBotInstances returns a page of BotInstance resources.
func (c *Cache) ListBotInstances(ctx context.Context, botName string, pageSize int, lastToken string, search string) ([]*machineidv1.BotInstance, string, error) {
func (c *Cache) ListBotInstances(ctx context.Context, botName string, pageSize int, lastToken string, search string, sort *types.SortBy) ([]*machineidv1.BotInstance, string, error) {
ctx, span := c.Tracer.Start(ctx, "cache/ListBotInstances")
defer span.End()

index := botInstanceNameIndex
keyFn := keyForNameIndex
var isDesc bool
if sort != nil {
isDesc = sort.IsDesc

switch sort.Field {
case "bot_name":
index = botInstanceNameIndex
keyFn = keyForNameIndex
case "active_at_latest":
index = botInstanceActiveAtIndex
keyFn = keyForActiveAtIndex
default:
return nil, "", trace.BadParameter("unsupported sort %q but expected bot_name or active_at_latest", sort.Field)
}
}

lister := genericLister[*machineidv1.BotInstance, botInstanceIndex]{
cache: c,
collection: c.collections.botInstances,
index: botInstanceNameIndex,
index: index,
isDesc: isDesc,
defaultPageSize: defaults.DefaultChunkSize,
upstreamList: func(ctx context.Context, limit int, start string) ([]*machineidv1.BotInstance, string, error) {
return c.Config.BotInstanceService.ListBotInstances(ctx, botName, limit, start, search)
return c.Config.BotInstanceService.ListBotInstances(ctx, botName, limit, start, search, sort)
},
filter: func(b *machineidv1.BotInstance) bool {
return matchBotInstance(b, botName, search)
},
nextToken: func(b *machineidv1.BotInstance) string {
return keyForNameIndex(b)
return keyFn(b)
},
}
out, next, err := lister.list(ctx,
Expand Down
Loading
Loading