diff --git a/docs/azdo_help_reference.md b/docs/azdo_help_reference.md index 331706bd..7df1f247 100644 --- a/docs/azdo_help_reference.md +++ b/docs/azdo_help_reference.md @@ -336,6 +336,25 @@ Aliases pools ``` +#### `azdo pipelines pool list [ORGANIZATION] [flags]` + +List agent pools + +``` +-q, --jq expression Filter JSON output using a jq expression + --json fields[=*] Output JSON with the specified fields. Prefix a field with '-' to exclude it. + --max-items int Optional client-side cap on results + --name string Filter pools by name + --pool-type string Filter pools by type: {automation|deployment} +-t, --template string Format JSON output using a Go template; see "azdo help formatting" +``` + +Aliases + +``` +ls, l +``` + #### `azdo pipelines pool show [ORGANIZATION/]POOL [flags]` Show details of an agent pool diff --git a/docs/azdo_pipelines_pool.md b/docs/azdo_pipelines_pool.md index 5a0311bc..ea229fd0 100644 --- a/docs/azdo_pipelines_pool.md +++ b/docs/azdo_pipelines_pool.md @@ -6,6 +6,7 @@ of agents that target build, release, and other pipeline jobs. ### Available commands +* [azdo pipelines pool list](./azdo_pipelines_pool_list.md) * [azdo pipelines pool show](./azdo_pipelines_pool_show.md) ### ALIASES @@ -23,6 +24,12 @@ azdo pipelines pool show 'Default' # Show a pool in a specific organization azdo pipelines pool show 'myorg/Default' + +# List pools in the default organization +azdo pipelines pool list + +# List pools in a specific organization +azdo pipelines pool list myorg ``` ### See also diff --git a/docs/azdo_pipelines_pool_list.md b/docs/azdo_pipelines_pool_list.md new file mode 100644 index 00000000..121f54a4 --- /dev/null +++ b/docs/azdo_pipelines_pool_list.md @@ -0,0 +1,68 @@ +## Command `azdo pipelines pool list` + +``` +azdo pipelines pool list [ORGANIZATION] [flags] +``` + +List Azure DevOps agent pools for an organization. + + +### Options + + +* `-q`, `--jq` `expression` + + Filter JSON output using a jq expression + +* `--json` `fields` + + Output JSON with the specified fields. Prefix a field with '-' to exclude it. + +* `--max-items` `int` (default `0`) + + Optional client-side cap on results + +* `--name` `string` + + Filter pools by name + +* `--pool-type` `string` + + Filter pools by type: {automation|deployment} + +* `-t`, `--template` `string` + + Format JSON output using a Go template; see "azdo help formatting" + + +### ALIASES + +- `ls` +- `l` + +### JSON Fields + +`agentCloudId`, `autoProvision`, `autoSize`, `autoUpdate`, `createdBy`, `createdOn`, `id`, `isHosted`, `isLegacy`, `name`, `options`, `owner`, `poolType`, `properties`, `scope`, `size`, `targetSize` + +### Examples + +```bash +# List all pools in the default organization +azdo pipelines pool list + +# List pools in a specific organization +azdo pipelines pool list myorg + +# List pools filtered by name +azdo pipelines pool list myorg --name Default + +# List deployment pools +azdo pipelines pool list myorg --pool-type deployment + +# Output as JSON +azdo pipelines pool list myorg --json +``` + +### See also + +* [azdo pipelines pool](./azdo_pipelines_pool.md) diff --git a/internal/cmd/pipelines/pool/list/list.go b/internal/cmd/pipelines/pool/list/list.go new file mode 100644 index 00000000..8e8189b0 --- /dev/null +++ b/internal/cmd/pipelines/pool/list/list.go @@ -0,0 +1,165 @@ +package list + +import ( + "fmt" + "strings" + + "github.com/MakeNowJust/heredoc/v2" + "github.com/microsoft/azure-devops-go-api/azuredevops/v7/taskagent" + "github.com/spf13/cobra" + "go.uber.org/zap" + + "github.com/tmeckel/azdo-cli/internal/cmd/util" + "github.com/tmeckel/azdo-cli/internal/types" +) + +type opts struct { + orgArg string + name string + poolType string + maxItems int + exporter util.Exporter +} + +func NewCmd(ctx util.CmdContext) *cobra.Command { + opts := &opts{} + + cmd := &cobra.Command{ + Use: "list [ORGANIZATION]", + Short: "List agent pools", + Long: heredoc.Doc(` + List Azure DevOps agent pools for an organization. + `), + Example: heredoc.Doc(` + # List all pools in the default organization + azdo pipelines pool list + + # List pools in a specific organization + azdo pipelines pool list myorg + + # List pools filtered by name + azdo pipelines pool list myorg --name Default + + # List deployment pools + azdo pipelines pool list myorg --pool-type deployment + + # Output as JSON + azdo pipelines pool list myorg --json + `), + Aliases: []string{ + "ls", + "l", + }, + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + opts.orgArg = args[0] + } + return run(ctx, opts) + }, + } + + cmd.Flags().StringVar(&opts.name, "name", "", "Filter pools by name") + util.StringEnumFlag(cmd, &opts.poolType, "pool-type", "", "", []string{ + string(taskagent.TaskAgentPoolTypeValues.Automation), + string(taskagent.TaskAgentPoolTypeValues.Deployment), + }, "Filter pools by type") + cmd.Flags().IntVar(&opts.maxItems, "max-items", 0, "Optional client-side cap on results") + util.AddJSONFlags(cmd, &opts.exporter, []string{ + "id", "isHosted", "isLegacy", "name", "options", "poolType", + "scope", "size", "agentCloudId", "autoProvision", "autoSize", + "autoUpdate", "createdBy", "createdOn", "owner", "properties", + "targetSize", + }) + + return cmd +} + +func run(cmdCtx util.CmdContext, opts *opts) error { + ios, err := cmdCtx.IOStreams() + if err != nil { + return err + } + ios.StartProgressIndicator() + defer ios.StopProgressIndicator() + + if opts.maxItems < 0 { + return util.FlagErrorf("invalid --max-items value %d; must be >= 0", opts.maxItems) + } + + organization, err := util.ParseOrganizationArg(cmdCtx, opts.orgArg) + if err != nil { + return util.FlagErrorWrap(err) + } + + taskClient, err := cmdCtx.ClientFactory().TaskAgent(cmdCtx.Context(), organization) + if err != nil { + return fmt.Errorf("failed to create task agent client: %w", err) + } + + var poolType *taskagent.TaskAgentPoolType + if trimmed := strings.TrimSpace(opts.poolType); trimmed != "" { + value := taskagent.TaskAgentPoolType(strings.ToLower(trimmed)) + poolType = &value + } + + args := taskagent.GetAgentPoolsArgs{ + PoolName: types.NotZeroPtrOrNil(strings.TrimSpace(opts.name)), + PoolType: poolType, + } + + zap.L().Debug( + "listing agent pools", + zap.String("organization", organization), + zap.String("poolName", types.GetValue(args.PoolName, "")), + zap.String("poolType", string(types.GetValue(args.PoolType, taskagent.TaskAgentPoolType("")))), + ) + + resp, err := taskClient.GetAgentPools(cmdCtx.Context(), args) + if err != nil { + return fmt.Errorf("failed to list pools: %w", err) + } + + pools := []taskagent.TaskAgentPool{} + if resp != nil { + pools = *resp + } + + if opts.maxItems > 0 && len(pools) > opts.maxItems { + zap.L().Debug("truncating result set to max-items", zap.Int("maxItems", opts.maxItems)) + pools = pools[:opts.maxItems] + } + + ios.StopProgressIndicator() + + if opts.exporter != nil { + return opts.exporter.Write(ios, pools) + } + + tp, err := cmdCtx.Printer("list") + if err != nil { + return err + } + + tp.AddColumns("ID", "NAME", "POOL TYPE", "SCOPE", "SIZE", "IS HOSTED", "IS LEGACY", "AUTO PROVISION", "CREATED ON") + + for _, pool := range pools { + scope := "" + if pool.Scope != nil { + scope = pool.Scope.String() + } + + tp.AddField(fmt.Sprintf("%d", types.GetValue(pool.Id, 0))) + tp.AddField(types.GetValue(pool.Name, "")) + tp.AddField(string(types.GetValue(pool.PoolType, ""))) + tp.AddField(scope) + tp.AddField(fmt.Sprintf("%d", types.GetValue(pool.Size, 0))) + tp.AddField(fmt.Sprintf("%v", types.GetValue(pool.IsHosted, false))) + tp.AddField(fmt.Sprintf("%v", types.GetValue(pool.IsLegacy, false))) + tp.AddField(fmt.Sprintf("%v", types.GetValue(pool.AutoProvision, false))) + tp.AddField(util.FormatTimeShort(pool.CreatedOn)) + tp.EndRow() + } + + return tp.Render() +} diff --git a/internal/cmd/pipelines/pool/list/list_test.go b/internal/cmd/pipelines/pool/list/list_test.go new file mode 100644 index 00000000..ce18b803 --- /dev/null +++ b/internal/cmd/pipelines/pool/list/list_test.go @@ -0,0 +1,394 @@ +package list + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/google/uuid" + "github.com/microsoft/azure-devops-go-api/azuredevops/v7" + "github.com/microsoft/azure-devops-go-api/azuredevops/v7/taskagent" + "github.com/microsoft/azure-devops-go-api/azuredevops/v7/webapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/tmeckel/azdo-cli/internal/cmd/util" + "github.com/tmeckel/azdo-cli/internal/iostreams" + "github.com/tmeckel/azdo-cli/internal/mocks" + "github.com/tmeckel/azdo-cli/internal/printer" + "github.com/tmeckel/azdo-cli/internal/types" +) + +type fakeListDeps struct { + ctrl *gomock.Controller + cmd *mocks.MockCmdContext + clientFact *mocks.MockClientFactory + taskClient *mocks.MockTaskAgentClient + config *mocks.MockConfig + auth *mocks.MockAuthConfig + stdout *bytes.Buffer +} + +func setupFakeDeps(t *testing.T, organization string) *fakeListDeps { + t.Helper() + + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + + io, _, out, _ := iostreams.Test() + io.SetStdoutTTY(false) + io.SetStderrTTY(false) + + deps := &fakeListDeps{ + ctrl: ctrl, + cmd: mocks.NewMockCmdContext(ctrl), + clientFact: mocks.NewMockClientFactory(ctrl), + taskClient: mocks.NewMockTaskAgentClient(ctrl), + stdout: out, + } + + deps.cmd.EXPECT().IOStreams().Return(io, nil).AnyTimes() + deps.cmd.EXPECT().Context().Return(context.Background()).AnyTimes() + deps.cmd.EXPECT().ClientFactory().Return(deps.clientFact).AnyTimes() + if organization != "" { + deps.clientFact.EXPECT().TaskAgent(gomock.Any(), organization).Return(deps.taskClient, nil).AnyTimes() + } + + return deps +} + +func (d *fakeListDeps) setupDefaultOrg(org string) { + d.config = mocks.NewMockConfig(d.ctrl) + d.auth = mocks.NewMockAuthConfig(d.ctrl) + d.cmd.EXPECT().Config().Return(d.config, nil).AnyTimes() + d.config.EXPECT().Authentication().Return(d.auth).AnyTimes() + d.auth.EXPECT().GetDefaultOrganization().Return(org, nil).AnyTimes() +} + +func samplePool(id int, name string, poolType taskagent.TaskAgentPoolType) taskagent.TaskAgentPool { + scope := uuid.MustParse("a1b2c3d4-e5f6-7890-abcd-ef1234567890") + createdOn := azuredevops.Time{Time: time.Date(2024, 1, 15, 10, 30, 0, 0, time.UTC)} + return taskagent.TaskAgentPool{ + Id: types.ToPtr(id), + Name: types.ToPtr(name), + PoolType: &poolType, + Scope: &scope, + Size: types.ToPtr(3), + IsHosted: types.ToPtr(true), + IsLegacy: types.ToPtr(false), + AutoProvision: types.ToPtr(true), + AutoUpdate: types.ToPtr(true), + CreatedOn: &createdOn, + CreatedBy: &webapi.IdentityRef{ + Id: types.ToPtr("creator-id"), + DisplayName: types.ToPtr("Alice"), + UniqueName: types.ToPtr("alice@contoso.com"), + }, + } +} + +func TestNewCmd_RegistersAsListLeaf(t *testing.T) { + t.Parallel() + + cmd := NewCmd(nil) + assert.Equal(t, "list [ORGANIZATION]", cmd.Use) + assert.ElementsMatch(t, []string{"ls", "l"}, cmd.Aliases) + assert.NotNil(t, cmd.RunE) +} + +func TestNewCmd_RequiresAtMostOneArg(t *testing.T) { + t.Parallel() + + cmd := NewCmd(nil) + require.NoError(t, cmd.Args(cmd, []string{})) + require.NoError(t, cmd.Args(cmd, []string{"myorg"})) + require.Error(t, cmd.Args(cmd, []string{"myorg", "extra"})) +} + +func TestNewCmd_HasFlags(t *testing.T) { + t.Parallel() + + cmd := NewCmd(nil) + f := cmd.Flags() + + require.NotNil(t, f.Lookup("name")) + require.NotNil(t, f.Lookup("pool-type")) + require.NotNil(t, f.Lookup("max-items")) + assert.NotNil(t, f.Lookup("json")) + assert.NotNil(t, f.Lookup("jq")) + assert.NotNil(t, f.Lookup("template")) +} + +func TestNewCmd_RejectsInvalidPoolType(t *testing.T) { + t.Parallel() + + cmd := NewCmd(nil) + cmd.SetArgs([]string{"--pool-type", "invalid"}) + + err := cmd.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), "valid values are {automation|deployment}") +} + +func TestRun_BasicCall(t *testing.T) { + t.Parallel() + + deps := setupFakeDeps(t, "myorg") + pools := []taskagent.TaskAgentPool{samplePool(7, "Default", taskagent.TaskAgentPoolTypeValues.Automation)} + deps.taskClient.EXPECT().GetAgentPools(gomock.Any(), gomock.Any()).DoAndReturn( + func(_ context.Context, args taskagent.GetAgentPoolsArgs) (*[]taskagent.TaskAgentPool, error) { + assert.Nil(t, args.PoolName) + assert.Nil(t, args.PoolType) + return &pools, nil + }, + ) + + tp, err := printer.NewTablePrinter(deps.stdout, false, 200) + require.NoError(t, err) + deps.cmd.EXPECT().Printer("list").Return(tp, nil).AnyTimes() + + err = run(deps.cmd, &opts{orgArg: "myorg"}) + require.NoError(t, err) + + output := deps.stdout.String() + assert.Contains(t, output, "7\tDefault\tautomation\ta1b2c3d4-e5f6-7890-abcd-ef1234567890\t3\ttrue\tfalse\ttrue\t2024-01-15") + assert.Contains(t, output, "7") + assert.Contains(t, output, "Default") + assert.Contains(t, output, "automation") + assert.Contains(t, output, "a1b2c3d4-e5f6-7890-abcd-ef1234567890") + assert.Contains(t, output, "3") + assert.Contains(t, output, "true") + assert.Contains(t, output, "false") + assert.Contains(t, output, "2024-01-15") +} + +func TestRun_OrgFromArg(t *testing.T) { + t.Parallel() + + deps := setupFakeDeps(t, "explicit-org") + pools := []taskagent.TaskAgentPool{samplePool(1, "Default", taskagent.TaskAgentPoolTypeValues.Automation)} + deps.taskClient.EXPECT().GetAgentPools(gomock.Any(), gomock.Any()).Return(&pools, nil) + + tp, err := printer.NewTablePrinter(deps.stdout, false, 200) + require.NoError(t, err) + deps.cmd.EXPECT().Printer("list").Return(tp, nil).AnyTimes() + + err = run(deps.cmd, &opts{orgArg: "explicit-org"}) + require.NoError(t, err) + assert.Contains(t, deps.stdout.String(), "Default") +} + +func TestRun_OrgFromDefaultConfig(t *testing.T) { + t.Parallel() + + deps := setupFakeDeps(t, "") + deps.setupDefaultOrg("default-org") + deps.clientFact.EXPECT().TaskAgent(gomock.Any(), "default-org").Return(deps.taskClient, nil).AnyTimes() + pools := []taskagent.TaskAgentPool{samplePool(1, "Default", taskagent.TaskAgentPoolTypeValues.Automation)} + deps.taskClient.EXPECT().GetAgentPools(gomock.Any(), gomock.Any()).Return(&pools, nil) + + tp, err := printer.NewTablePrinter(deps.stdout, false, 200) + require.NoError(t, err) + deps.cmd.EXPECT().Printer("list").Return(tp, nil).AnyTimes() + + err = run(deps.cmd, &opts{}) + require.NoError(t, err) + assert.Contains(t, deps.stdout.String(), "Default") +} + +func TestRun_ProjectScopeRejected(t *testing.T) { + t.Parallel() + + deps := setupFakeDeps(t, "") + err := run(deps.cmd, &opts{orgArg: "org/project"}) + require.Error(t, err) + assert.Contains(t, err.Error(), "project scope not allowed") +} + +func TestRun_NoDefaultOrg(t *testing.T) { + t.Parallel() + + deps := setupFakeDeps(t, "") + deps.setupDefaultOrg("") + + err := run(deps.cmd, &opts{}) + require.Error(t, err) + assert.Contains(t, err.Error(), "no organization specified") +} + +func TestRun_InvalidMaxItemsNegative(t *testing.T) { + t.Parallel() + + deps := setupFakeDeps(t, "org") + err := run(deps.cmd, &opts{orgArg: "org", maxItems: -5}) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid --max-items") +} + +func TestRun_ClientFactoryError(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + + io, _, _, _ := iostreams.Test() + cmd := mocks.NewMockCmdContext(ctrl) + clientFact := mocks.NewMockClientFactory(ctrl) + cmd.EXPECT().IOStreams().Return(io, nil).AnyTimes() + cmd.EXPECT().Context().Return(context.Background()).AnyTimes() + cmd.EXPECT().ClientFactory().Return(clientFact).AnyTimes() + + cfg := mocks.NewMockConfig(ctrl) + auth := mocks.NewMockAuthConfig(ctrl) + cmd.EXPECT().Config().Return(cfg, nil).AnyTimes() + cfg.EXPECT().Authentication().Return(auth).AnyTimes() + auth.EXPECT().GetDefaultOrganization().Return("myorg", nil) + + clientFact.EXPECT().TaskAgent(gomock.Any(), "myorg").Return(nil, fmt.Errorf("connection failed")) + + err := run(cmd, &opts{}) + require.Error(t, err) + assert.Contains(t, err.Error(), "connection failed") +} + +func TestRun_SDKError(t *testing.T) { + t.Parallel() + + deps := setupFakeDeps(t, "org") + deps.taskClient.EXPECT().GetAgentPools(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("API error")) + + err := run(deps.cmd, &opts{orgArg: "org"}) + require.Error(t, err) + assert.Contains(t, err.Error(), "API error") +} + +func TestRun_FilterByName(t *testing.T) { + t.Parallel() + + deps := setupFakeDeps(t, "org") + pools := []taskagent.TaskAgentPool{samplePool(1, "Default", taskagent.TaskAgentPoolTypeValues.Automation)} + deps.taskClient.EXPECT().GetAgentPools(gomock.Any(), gomock.Any()).DoAndReturn( + func(_ context.Context, args taskagent.GetAgentPoolsArgs) (*[]taskagent.TaskAgentPool, error) { + require.NotNil(t, args.PoolName) + assert.Equal(t, "Default", *args.PoolName) + return &pools, nil + }, + ) + + tp, err := printer.NewTablePrinter(deps.stdout, false, 200) + require.NoError(t, err) + deps.cmd.EXPECT().Printer("list").Return(tp, nil).AnyTimes() + + err = run(deps.cmd, &opts{orgArg: "org", name: "Default"}) + require.NoError(t, err) +} + +func TestRun_PoolTypeFilter(t *testing.T) { + t.Parallel() + + deps := setupFakeDeps(t, "org") + pools := []taskagent.TaskAgentPool{samplePool(1, "Default", taskagent.TaskAgentPoolTypeValues.Deployment)} + deps.taskClient.EXPECT().GetAgentPools(gomock.Any(), gomock.Any()).DoAndReturn( + func(_ context.Context, args taskagent.GetAgentPoolsArgs) (*[]taskagent.TaskAgentPool, error) { + require.NotNil(t, args.PoolType) + assert.Equal(t, taskagent.TaskAgentPoolType("deployment"), *args.PoolType) + return &pools, nil + }, + ) + + tp, err := printer.NewTablePrinter(deps.stdout, false, 200) + require.NoError(t, err) + deps.cmd.EXPECT().Printer("list").Return(tp, nil).AnyTimes() + + err = run(deps.cmd, &opts{orgArg: "org", poolType: "Deployment"}) + require.NoError(t, err) +} + +func TestRun_MaxItemsCaps(t *testing.T) { + t.Parallel() + + deps := setupFakeDeps(t, "org") + pools := []taskagent.TaskAgentPool{ + samplePool(1, "pool-1", taskagent.TaskAgentPoolTypeValues.Automation), + samplePool(2, "pool-2", taskagent.TaskAgentPoolTypeValues.Automation), + } + deps.taskClient.EXPECT().GetAgentPools(gomock.Any(), gomock.Any()).Return(&pools, nil) + + tp, err := printer.NewTablePrinter(deps.stdout, false, 200) + require.NoError(t, err) + deps.cmd.EXPECT().Printer("list").Return(tp, nil).AnyTimes() + + err = run(deps.cmd, &opts{orgArg: "org", maxItems: 1}) + require.NoError(t, err) + + output := deps.stdout.String() + assert.Contains(t, output, "pool-1") + assert.NotContains(t, output, "pool-2") +} + +func TestRun_JSONOutput(t *testing.T) { + t.Parallel() + + deps := setupFakeDeps(t, "org") + pools := []taskagent.TaskAgentPool{samplePool(7, "Default", taskagent.TaskAgentPoolTypeValues.Automation)} + deps.taskClient.EXPECT().GetAgentPools(gomock.Any(), gomock.Any()).Return(&pools, nil) + + exporter := util.NewJSONExporter() + err := run(deps.cmd, &opts{orgArg: "org", exporter: exporter}) + require.NoError(t, err) + + var parsed []map[string]any + err = json.Unmarshal(deps.stdout.Bytes(), &parsed) + require.NoError(t, err) + require.Len(t, parsed, 1) + + assert.Equal(t, float64(7), parsed[0]["id"]) + assert.Equal(t, "Default", parsed[0]["name"]) + assert.Equal(t, "automation", parsed[0]["poolType"]) + assert.NotContains(t, parsed[0], "type") + assert.Equal(t, "a1b2c3d4-e5f6-7890-abcd-ef1234567890", parsed[0]["scope"]) + assert.Equal(t, float64(3), parsed[0]["size"]) + assert.Equal(t, true, parsed[0]["isHosted"]) + assert.Equal(t, false, parsed[0]["isLegacy"]) + assert.Equal(t, true, parsed[0]["autoProvision"]) + assert.Equal(t, true, parsed[0]["autoUpdate"]) + assert.Contains(t, parsed[0]["createdOn"], "2024-01-15") + createdBy, ok := parsed[0]["createdBy"].(map[string]any) + require.True(t, ok) + assert.Equal(t, "creator-id", createdBy["id"]) + assert.Equal(t, "Alice", createdBy["displayName"]) + assert.Equal(t, "alice@contoso.com", createdBy["uniqueName"]) +} + +func TestRun_JSONOutputEmpty(t *testing.T) { + t.Parallel() + + deps := setupFakeDeps(t, "org") + pools := []taskagent.TaskAgentPool{} + deps.taskClient.EXPECT().GetAgentPools(gomock.Any(), gomock.Any()).Return(&pools, nil) + + exporter := util.NewJSONExporter() + err := run(deps.cmd, &opts{orgArg: "org", exporter: exporter}) + require.NoError(t, err) + + assert.Equal(t, "[]\n", deps.stdout.String()) +} + +func TestRun_NullResponseIsEmpty(t *testing.T) { + t.Parallel() + + deps := setupFakeDeps(t, "org") + deps.taskClient.EXPECT().GetAgentPools(gomock.Any(), gomock.Any()).Return(nil, nil) + + tp, err := printer.NewTablePrinter(deps.stdout, false, 200) + require.NoError(t, err) + deps.cmd.EXPECT().Printer("list").Return(tp, nil).AnyTimes() + + err = run(deps.cmd, &opts{orgArg: "org"}) + require.NoError(t, err) + assert.Equal(t, "", deps.stdout.String()) +} diff --git a/internal/cmd/pipelines/pool/pool.go b/internal/cmd/pipelines/pool/pool.go index 107ae83e..dc69d907 100644 --- a/internal/cmd/pipelines/pool/pool.go +++ b/internal/cmd/pipelines/pool/pool.go @@ -4,6 +4,7 @@ import ( "github.com/MakeNowJust/heredoc/v2" "github.com/spf13/cobra" + "github.com/tmeckel/azdo-cli/internal/cmd/pipelines/pool/list" "github.com/tmeckel/azdo-cli/internal/cmd/pipelines/pool/show" "github.com/tmeckel/azdo-cli/internal/cmd/util" ) @@ -25,10 +26,17 @@ func NewCmd(ctx util.CmdContext) *cobra.Command { # Show a pool in a specific organization azdo pipelines pool show 'myorg/Default' + + # List pools in the default organization + azdo pipelines pool list + + # List pools in a specific organization + azdo pipelines pool list myorg `), Aliases: []string{"pools"}, } + cmd.AddCommand(list.NewCmd(ctx)) cmd.AddCommand(show.NewCmd(ctx)) return cmd }