Skip to content
Draft
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
157 changes: 87 additions & 70 deletions cmd/agent/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ import (
"context"
"fmt"
"os"
"slices"
"strings"

"github.com/alecthomas/kong"
"github.com/buildkite/cli/v3/internal/agent"
"github.com/buildkite/cli/v3/internal/cli"
bkIO "github.com/buildkite/cli/v3/internal/io"
"github.com/buildkite/cli/v3/pkg/cmd/factory"
"github.com/buildkite/cli/v3/pkg/cmd/validation"
"github.com/buildkite/cli/v3/pkg/output"
buildkite "github.com/buildkite/go-buildkite/v4"
tea "github.com/charmbracelet/bubbletea"
)

const (
Expand Down Expand Up @@ -79,6 +79,7 @@ func (c *ListCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {
f.SkipConfirm = globals.SkipConfirmation()
f.NoInput = globals.DisableInput()
f.Quiet = globals.IsQuiet()
f.NoPager = globals.DisablePager()

if err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {
return err
Expand All @@ -92,77 +93,91 @@ func (c *ListCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {

format := output.Format(c.Output)

// Skip TUI when using non-text format (JSON/YAML)
if format != output.FormatText {
agents := []buildkite.Agent{}
page := 1

for len(agents) < c.Limit && page < 50 {
opts := buildkite.AgentListOptions{
Name: c.Name,
Hostname: c.Hostname,
Version: c.Version,
ListOptions: buildkite.ListOptions{
Page: page,
PerPage: c.PerPage,
},
}

pageAgents, _, err := f.RestAPIClient.Agents.List(ctx, f.Config.OrganizationSlug(), &opts)
if err != nil {
return err
}

if len(pageAgents) == 0 {
break
}

filtered := filterAgents(pageAgents, c.State, c.Tags)
agents = append(agents, filtered...)
page++
agents := []buildkite.Agent{}
page := 1
hasMore := false

for len(agents) < c.Limit && page < 50 {
opts := buildkite.AgentListOptions{
Name: c.Name,
Hostname: c.Hostname,
Version: c.Version,
ListOptions: buildkite.ListOptions{
Page: page,
PerPage: c.PerPage,
},
}

pageAgents, _, err := f.RestAPIClient.Agents.List(ctx, f.Config.OrganizationSlug(), &opts)
if err != nil {
return err
}

if len(agents) > c.Limit {
agents = agents[:c.Limit]
if len(pageAgents) == 0 {
break
}

filtered := filterAgents(pageAgents, c.State, c.Tags)
agents = append(agents, filtered...)
page++

// If we have more than the limit, there are definitely more results, so we'll set hasMore to true, which will add a '+' to the total count display in the output
// this is just to make it clear to the user that there's more results available than what are shown, as otherwise if they set --limit to say 5
// then it'd say showing 5 out of 30 agents (assuming there are more than 30 agents)
if len(agents) >= c.Limit {
hasMore = true
}
}

totalFetched := len(agents)
if len(agents) > c.Limit {
agents = agents[:c.Limit]
}

if format != output.FormatText {
return output.Write(os.Stdout, agents, format)
}

loader := func(page int) tea.Cmd {
return func() tea.Msg {
opts := buildkite.AgentListOptions{
Name: c.Name,
Hostname: c.Hostname,
Version: c.Version,
ListOptions: buildkite.ListOptions{
Page: page,
PerPage: c.PerPage,
},
}

agents, resp, err := f.RestAPIClient.Agents.List(ctx, f.Config.OrganizationSlug(), &opts)
if err != nil {
return err
}

filtered := filterAgents(agents, c.State, c.Tags)

items := make([]agent.AgentListItem, len(filtered))
for i, a := range filtered {
a := a
items[i] = agent.AgentListItem{Agent: a}
}

return agent.NewAgentItemsMsg(items, resp.LastPage)
if len(agents) == 0 {
fmt.Println("No agents found")
return nil
}

headers := []string{"State", "Name", "Version", "Queue", "Hostname"}
rows := make([][]string, len(agents))
for i, agent := range agents {
queue := extractQueue(agent.Metadata)
rows[i] = []string{
agent.ConnectedState,
agent.Name,
agent.Version,
queue,
agent.Hostname,
}
}

model := agent.NewAgentList(loader, 1, c.PerPage, f.Quiet)
columnStyles := map[string]string{
"state": "bold",
"name": "bold",
"hostname": "dim",
"version": "italic",
"queue": "italic",
}
table := output.Table(headers, rows, columnStyles)

writer, cleanup := bkIO.Pager(f.NoPager)
defer func() {
_ = cleanup()
}()

totalDisplay := fmt.Sprintf("%d", totalFetched)
if hasMore {
totalDisplay = fmt.Sprintf("%d+", totalFetched)
}
fmt.Fprintf(writer, "Showing %d of %s agents in %s\n\n", len(agents), totalDisplay, f.Config.OrganizationSlug())
fmt.Fprint(writer, table)

p := tea.NewProgram(model, tea.WithAltScreen())
_, err = p.Run()
return err
return nil
}

func validateState(state string) error {
Expand All @@ -171,10 +186,8 @@ func validateState(state string) error {
}

normalized := strings.ToLower(state)
for _, valid := range validStates {
if normalized == valid {
return nil
}
if slices.Contains(validStates, normalized) {
return nil
}

return fmt.Errorf("invalid state %q: must be one of %s, %s, or %s", state, stateRunning, stateIdle, statePaused)
Expand Down Expand Up @@ -222,10 +235,14 @@ func matchesTags(a buildkite.Agent, tags []string) bool {
}

func hasTag(metadata []string, tag string) bool {
for _, meta := range metadata {
if meta == tag {
return true
return slices.Contains(metadata, tag)
}

func extractQueue(metadata []string) string {
for _, m := range metadata {
if after, ok := strings.CutPrefix(m, "queue="); ok {
return after
}
}
return false
return "default"
}
Loading