Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion lib/web/machineid.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package web

import (
"cmp"
"context"
"net/http"
"strconv"
Expand Down Expand Up @@ -502,7 +503,10 @@ func (h *Handler) listBotInstancesV2(_ http.ResponseWriter, r *http.Request, _ h
}

if authentication != nil {
uiInstance.JoinMethodLatest = authentication.GetJoinMethod()
uiInstance.JoinMethodLatest = cmp.Or(
authentication.GetJoinAttrs().GetMeta().GetJoinMethod(),
authentication.GetJoinMethod(),
)
}

if heartbeat != nil {
Expand Down
130 changes: 84 additions & 46 deletions tool/tctl/common/bots_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,17 @@ import (
"google.golang.org/protobuf/types/known/fieldmaskpb"

"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/constants"
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
machineidv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1"
"github.com/gravitational/teleport/api/mfa"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/utils/clientutils"
"github.com/gravitational/teleport/lib/asciitable"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/auth/machineid/machineidv1"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/itertools/stream"
"github.com/gravitational/teleport/lib/service/servicecfg"
"github.com/gravitational/teleport/lib/utils"
commonclient "github.com/gravitational/teleport/tool/tctl/common/client"
Expand All @@ -73,6 +75,12 @@ type BotsCommand struct {
addLogins string
setLogins string

search string
query string

sortIndex string
sortOrder string

botsList *kingpin.CmdClause
botsAdd *kingpin.CmdClause
botsRemove *kingpin.CmdClause
Expand Down Expand Up @@ -126,6 +134,11 @@ func (c *BotsCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIF

c.botsInstancesList = c.botsInstances.Command("list", "List bot instances.").Alias("ls")
c.botsInstancesList.Arg("name", "The name of the bot from which to list instances. If unset, lists instances from all bots.").StringVar(&c.botName)
c.botsInstancesList.Flag("format", "Output format, 'text' or 'json'").Default(teleport.Text).EnumVar(&c.format, teleport.Text, teleport.JSON)
c.botsInstancesList.Flag("search", "Fuzzy search query used to filter bot instances").StringVar(&c.search)
c.botsInstancesList.Flag("query", "An expression in the Teleport predicate language used to filter bot instances").StringVar(&c.query)
c.botsInstancesList.Flag("sort-index", "Request sort index, 'bot_name', 'active_at_latest', 'version_latest' or 'host_name_latest'").Default("bot_name").StringVar(&c.sortIndex)
c.botsInstancesList.Flag("sort-order", "Request sort order, 'ascending' or 'descending'").Default("ascending").StringVar(&c.sortOrder)

c.botsInstancesAdd = c.botsInstances.Command("add", "Join a new instance onto an existing bot.").Alias("join")
c.botsInstancesAdd.Arg("name", "The name of the existing bot for which to add a new instance.").Required().StringVar(&c.botName)
Expand All @@ -138,8 +151,8 @@ func (c *BotsCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIF
}

// TryRun attempts to run subcommands.
func (c *BotsCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) {
var commandFunc func(ctx context.Context, client *authclient.Client) error
func (c *BotsCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (bool, error) {
var commandFunc func(ctx context.Context, client botsCommandClient) error
switch cmd {
case c.botsList.FullCommand():
commandFunc = c.ListBots
Expand Down Expand Up @@ -170,9 +183,22 @@ func (c *BotsCommand) TryRun(ctx context.Context, cmd string, clientFunc commonc
return true, trace.Wrap(err)
}

type botsCommandClient interface {
BotServiceClient() machineidv1pb.BotServiceClient
BotInstanceServiceClient() machineidv1pb.BotInstanceServiceClient

GetToken(ctx context.Context, name string) (types.ProvisionToken, error)
UpsertToken(ctx context.Context, token types.ProvisionToken) error
GetUser(ctx context.Context, name string, withSecrets bool) (types.User, error)
GetRole(context.Context, string) (types.Role, error)
UpsertLock(ctx context.Context, lock types.Lock) error
GetProxies() ([]types.Server, error)
PerformMFACeremony(ctx context.Context, in *proto.CreateAuthenticateChallengeRequest, promptOpts ...mfa.PromptOpt) (*proto.MFAAuthenticateResponse, error)
}

// ListBots writes a listing of the cluster's certificate renewal bots
// to standard out.
func (c *BotsCommand) ListBots(ctx context.Context, client *authclient.Client) error {
func (c *BotsCommand) ListBots(ctx context.Context, client botsCommandClient) error {
var bots []*machineidv1pb.Bot
req := &machineidv1pb.ListBotsRequest{}
for {
Expand Down Expand Up @@ -255,7 +281,7 @@ Please note:
`))

// AddBot adds a new certificate renewal bot to the cluster.
func (c *BotsCommand) AddBot(ctx context.Context, client *authclient.Client) error {
func (c *BotsCommand) AddBot(ctx context.Context, client botsCommandClient) error {
// Prompt for admin action MFA if required, allowing reuse for UpsertToken and CreateBot.
mfaResponse, err := mfa.PerformAdminActionMFACeremony(ctx, client.PerformMFACeremony, true /*allowReuse*/)
if err == nil {
Expand Down Expand Up @@ -344,7 +370,7 @@ func (c *BotsCommand) AddBot(ctx context.Context, client *authclient.Client) err
return trace.Wrap(outputToken(c.stdout, c.format, client, bot, token))
}

func (c *BotsCommand) RemoveBot(ctx context.Context, client *authclient.Client) error {
func (c *BotsCommand) RemoveBot(ctx context.Context, client botsCommandClient) error {
_, err := client.BotServiceClient().DeleteBot(ctx, &machineidv1pb.DeleteBotRequest{
BotName: c.botName,
})
Expand All @@ -357,7 +383,7 @@ func (c *BotsCommand) RemoveBot(ctx context.Context, client *authclient.Client)
return nil
}

func (c *BotsCommand) LockBot(ctx context.Context, client *authclient.Client) error {
func (c *BotsCommand) LockBot(ctx context.Context, client botsCommandClient) error {
lockExpiry, err := computeLockExpiry(c.lockExpires, c.lockTTL)
if err != nil {
return trace.Wrap(err)
Expand Down Expand Up @@ -459,14 +485,9 @@ func (c *BotsCommand) updateBotLogins(ctx context.Context, bot *machineidv1pb.Bo
return trace.Wrap(mask.Append(&machineidv1pb.Bot{}, "spec.traits"))
}

// clientRoleGetter is a minimal mockable interface for the client API
type clientRoleGetter interface {
GetRole(context.Context, string) (types.Role, error)
}

// updateBotRoles applies updates from CLI arguments to a bot's roles, updating
// the field mask as necessary if any updates were made.
func (c *BotsCommand) updateBotRoles(ctx context.Context, client clientRoleGetter, bot *machineidv1pb.Bot, mask *fieldmaskpb.FieldMask) error {
func (c *BotsCommand) updateBotRoles(ctx context.Context, client botsCommandClient, bot *machineidv1pb.Bot, mask *fieldmaskpb.FieldMask) error {
currentRoles := make(map[string]struct{})
for _, role := range bot.Spec.Roles {
currentRoles[role] = struct{}{}
Expand Down Expand Up @@ -510,7 +531,7 @@ func (c *BotsCommand) updateBotRoles(ctx context.Context, client clientRoleGette
}

// UpdateBot performs various updates to existing bot users and roles.
func (c *BotsCommand) UpdateBot(ctx context.Context, client *authclient.Client) error {
func (c *BotsCommand) UpdateBot(ctx context.Context, client botsCommandClient) error {
bot, err := client.BotServiceClient().GetBot(ctx, &machineidv1pb.GetBotRequest{
BotName: c.botName,
})
Expand Down Expand Up @@ -561,27 +582,47 @@ func (c *BotsCommand) UpdateBot(ctx context.Context, client *authclient.Client)
}

// ListBotInstances lists bot instances, possibly filtering for a specific bot
func (c *BotsCommand) ListBotInstances(ctx context.Context, client *authclient.Client) error {
var instances []*machineidv1pb.BotInstance
req := &machineidv1pb.ListBotInstancesRequest{}

if c.botName != "" {
req.FilterBotName = c.botName
}

for {
// TODO(nicholasmarais1158) Use ListBotInstancesV2 instead.
//nolint:staticcheck // SA1019
resp, err := client.BotInstanceServiceClient().ListBotInstances(ctx, req)
if err != nil {
return trace.Wrap(err)
func (c *BotsCommand) ListBotInstances(ctx context.Context, client botsCommandClient) error {
pageFunc := func(ctx context.Context, pageSize int, pageToken string) ([]*machineidv1pb.BotInstance, string, error) {
resp, err := client.BotInstanceServiceClient().ListBotInstancesV2(ctx, &machineidv1pb.ListBotInstancesV2Request{
PageSize: int32(pageSize),
PageToken: pageToken,
SortField: c.sortIndex,
SortDesc: c.sortOrder == "descending",
Filter: &machineidv1pb.ListBotInstancesV2Request_Filters{
BotName: c.botName,
SearchTerm: c.search,
Query: c.query,
},
})
return resp.GetBotInstances(), resp.GetNextPageToken(), trace.Wrap(err)
}

fallbackFunc := func(ctx context.Context) ([]*machineidv1pb.BotInstance, error) {
if c.query != "" {
return nil, trace.NotImplemented("fallback not supported for requests with a query")
}
fallbackPageFunc := func(ctx context.Context, pageSize int, pageToken string) ([]*machineidv1pb.BotInstance, string, error) {
// Needed for backwards compatibility
//nolint:staticcheck // SA1019
resp, err := client.BotInstanceServiceClient().ListBotInstances(ctx, &machineidv1pb.ListBotInstancesRequest{
FilterBotName: c.botName,
PageSize: int32(pageSize),
PageToken: pageToken,
FilterSearchTerm: c.search,
Sort: &types.SortBy{
Field: c.sortIndex,
IsDesc: c.sortOrder == "descending",
},
})
return resp.GetBotInstances(), resp.GetNextPageToken(), trace.Wrap(err)
}
return stream.Collect(clientutils.Resources(ctx, fallbackPageFunc))
}

instances = append(instances, resp.BotInstances...)
if resp.NextPageToken == "" {
break
}
req.PageToken = resp.NextPageToken
instances, err := clientutils.CollectWithFallback(ctx, pageFunc, fallbackFunc)
if err != nil {
return trace.Wrap(err)
}

if c.format == teleport.JSON {
Expand Down Expand Up @@ -610,29 +651,24 @@ func (c *BotsCommand) ListBotInstances(ctx context.Context, client *authclient.C
return nil
}

t := asciitable.MakeTable([]string{"ID", "Join Method", "Hostname", "Joined", "Last Seen", "Generation"})
t := asciitable.MakeTable([]string{"ID", "Join Method", "Version", "Hostname", "Last Seen"})
for _, i := range instances {
var (
joinMethod string
hostname string
generation string
version string
)

joined := i.Status.InitialAuthentication.AuthenticatedAt.AsTime().Format(time.RFC3339)
initialJoinMethod := cmp.Or(
i.Status.InitialAuthentication.GetJoinAttrs().GetMeta().GetJoinMethod(),
i.Status.InitialAuthentication.JoinMethod,
)

lastSeen := i.Status.InitialAuthentication.AuthenticatedAt.AsTime()

if len(i.Status.LatestAuthentications) == 0 {
generation = "n/a"
} else {
if len(i.Status.LatestAuthentications) > 0 {
auth := i.Status.LatestAuthentications[len(i.Status.LatestAuthentications)-1]

generation = fmt.Sprint(auth.Generation)

authJM := cmp.Or(
auth.GetJoinAttrs().GetMeta().GetJoinMethod(),
auth.JoinMethod,
Expand All @@ -650,11 +686,13 @@ func (c *BotsCommand) ListBotInstances(ctx context.Context, client *authclient.C
}

if len(i.Status.LatestHeartbeats) == 0 {
hostname = "n/a"
hostname = "-"
version = "-"
} else {
hb := i.Status.LatestHeartbeats[len(i.Status.LatestHeartbeats)-1]

hostname = hb.Hostname
version = hb.Version

if hb.RecordedAt.AsTime().After(lastSeen) {
lastSeen = hb.RecordedAt.AsTime()
Expand All @@ -663,7 +701,7 @@ func (c *BotsCommand) ListBotInstances(ctx context.Context, client *authclient.C

t.AddRow([]string{
fmt.Sprintf("%s/%s", i.Spec.BotName, i.Spec.InstanceId), joinMethod,
hostname, joined, lastSeen.Format(time.RFC3339), generation,
version, hostname, lastSeen.Format(time.RFC3339),
})
}
fmt.Fprintln(c.stdout, t.AsBuffer().String())
Expand All @@ -679,7 +717,7 @@ func (c *BotsCommand) ListBotInstances(ctx context.Context, client *authclient.C
}

// AddBotInstance begins onboarding a new instance of an existing bot.
func (c *BotsCommand) AddBotInstance(ctx context.Context, client *authclient.Client) error {
func (c *BotsCommand) AddBotInstance(ctx context.Context, client botsCommandClient) error {
// A bit of a misnomer but makes the terminology a bit more consistent. This
// doesn't directly create a bot instance, but creates token that allows a
// bot to join, which creates a new instance.
Expand Down Expand Up @@ -762,7 +800,7 @@ To onboard a new instance for this bot, run:
> {{.executable}} bots instances add {{.instance.Spec.BotName}}
`))

func (c *BotsCommand) ShowBotInstance(ctx context.Context, client *authclient.Client) error {
func (c *BotsCommand) ShowBotInstance(ctx context.Context, client botsCommandClient) error {
botName, instanceID, err := parseInstanceID(c.instanceID)
if err != nil {
return trace.Wrap(err)
Expand Down Expand Up @@ -815,7 +853,7 @@ type botJSONResponse struct {
}

// outputToken writes token information to stdout, depending on the token format.
func outputToken(wr io.Writer, format string, client *authclient.Client, bot *machineidv1pb.Bot, token types.ProvisionToken) error {
func outputToken(wr io.Writer, format string, client botsCommandClient, bot *machineidv1pb.Bot, token types.ProvisionToken) error {
if format == teleport.JSON {
tokenTTL := time.Duration(0)
if exp := token.Expiry(); !exp.IsZero() {
Expand Down
Loading
Loading