diff --git a/pkg/cmd/project/list/list.go b/pkg/cmd/project/list/list.go index 702e80ce..9e4b76ff 100644 --- a/pkg/cmd/project/list/list.go +++ b/pkg/cmd/project/list/list.go @@ -1,30 +1,60 @@ package list import ( + "errors" + "fmt" + "github.com/MakeNowJust/heredoc/v2" "github.com/OctopusDeploy/cli/pkg/apiclient" "github.com/OctopusDeploy/cli/pkg/constants" "github.com/OctopusDeploy/cli/pkg/factory" "github.com/OctopusDeploy/cli/pkg/output" + "github.com/OctopusDeploy/cli/pkg/util/flag" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projectgroups" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/services" "github.com/spf13/cobra" ) +const ( + FlagGroup = "group" +) + +type ListFlags struct { + Group *flag.Flag[string] +} + +func NewListFlags() *ListFlags { + return &ListFlags{ + Group: flag.New[string](FlagGroup, false), + } +} + +type GetAllProjectsCallback func() ([]*projects.Project, error) +type GetProjectGroupCallback func(idOrName string) (*projectgroups.ProjectGroup, error) +type GetProjectsInGroupCallback func(projectGroup *projectgroups.ProjectGroup) ([]*projects.Project, error) + func NewCmdList(f factory.Factory) *cobra.Command { + listFlags := NewListFlags() + cmd := &cobra.Command{ Use: "list", Short: "List projects", Long: "List projects in Octopus Deploy", Example: heredoc.Docf(` %[1]s project list - %[1]s project ls + %[1]s project list --group 'Default Project Group' + %[1]s project ls -g ProjectGroups-1 `, constants.ExecutableName), Aliases: []string{"ls"}, RunE: func(cmd *cobra.Command, args []string) error { - return listRun(cmd, f) + return listRun(cmd, f, listFlags) }, } + flags := cmd.Flags() + flags.StringVarP(&listFlags.Group.Value, listFlags.Group.Name, "g", "", "list only the projects in this project group") + return cmd } @@ -35,18 +65,22 @@ type ProjectAsJson struct { ProjectTags []string `json:"ProjectTags,omitempty"` } -func listRun(cmd *cobra.Command, f factory.Factory) error { +func listRun(cmd *cobra.Command, f factory.Factory, flags *ListFlags) error { client, err := f.GetSpacedClient(apiclient.NewRequester(cmd)) if err != nil { return err } - allProjects, err := client.Projects.GetAll() + projectsToList, err := getProjects( + flags.Group.Value, + client.Projects.GetAll, + client.ProjectGroups.GetByIDOrName, + client.ProjectGroups.GetProjects) if err != nil { return err } - return output.PrintArray(allProjects, cmd, output.Mappers[*projects.Project]{ + return output.PrintArray(projectsToList, cmd, output.Mappers[*projects.Project]{ Json: func(p *projects.Project) any { return ProjectAsJson{ Id: p.GetID(), @@ -66,3 +100,31 @@ func listRun(cmd *cobra.Command, f factory.Factory) error { }, }) } + +// getProjects lists every project in the space, or only the projects in the +// named group when the group filter is supplied. The group is resolved rather +// than the project list filtered client side, so the server only sends back the +// projects that were asked for. +func getProjects( + group string, + getAllProjects GetAllProjectsCallback, + getProjectGroup GetProjectGroupCallback, + getProjectsInGroup GetProjectsInGroupCallback) ([]*projects.Project, error) { + if group == "" { + return getAllProjects() + } + + projectGroup, err := getProjectGroup(group) + if err != nil { + if errors.Is(err, services.ErrItemNotFound) { + return nil, fmt.Errorf("cannot find a project group with name or ID of '%s'", group) + } + return nil, err + } + + if projectGroup == nil { + return nil, fmt.Errorf("cannot find a project group with name or ID of '%s'", group) + } + + return getProjectsInGroup(projectGroup) +} diff --git a/pkg/cmd/project/list/list_test.go b/pkg/cmd/project/list/list_test.go new file mode 100644 index 00000000..12d34a37 --- /dev/null +++ b/pkg/cmd/project/list/list_test.go @@ -0,0 +1,96 @@ +package list + +import ( + "errors" + "testing" + + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projectgroups" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/services" + "github.com/stretchr/testify/assert" +) + +func newProject(name string) *projects.Project { + return projects.NewProject(name, "Lifecycles-1", "ProjectGroups-1") +} + +func TestGetProjects_WithoutGroupListsEverything(t *testing.T) { + getAllProjects := func() ([]*projects.Project, error) { + return []*projects.Project{newProject("foo"), newProject("bar")}, nil + } + getProjectGroup := func(idOrName string) (*projectgroups.ProjectGroup, error) { + t.Errorf("did not expect a project group lookup, got '%s'", idOrName) + return nil, nil + } + getProjectsInGroup := func(projectGroup *projectgroups.ProjectGroup) ([]*projects.Project, error) { + t.Error("did not expect the projects in a group to be requested") + return nil, nil + } + + result, err := getProjects("", getAllProjects, getProjectGroup, getProjectsInGroup) + + assert.NoError(t, err) + assert.Equal(t, []string{"foo", "bar"}, projectNames(result)) +} + +func TestGetProjects_WithGroupListsOnlyThatGroup(t *testing.T) { + getAllProjects := func() ([]*projects.Project, error) { + t.Error("did not expect every project to be requested") + return nil, nil + } + getProjectGroup := func(idOrName string) (*projectgroups.ProjectGroup, error) { + assert.Equal(t, "Default Project Group", idOrName) + return projectgroups.NewProjectGroup("Default Project Group"), nil + } + getProjectsInGroup := func(projectGroup *projectgroups.ProjectGroup) ([]*projects.Project, error) { + assert.Equal(t, "Default Project Group", projectGroup.Name) + return []*projects.Project{newProject("foo")}, nil + } + + result, err := getProjects("Default Project Group", getAllProjects, getProjectGroup, getProjectsInGroup) + + assert.NoError(t, err) + assert.Equal(t, []string{"foo"}, projectNames(result)) +} + +func TestGetProjects_WithUnknownGroupReportsTheGroup(t *testing.T) { + getProjectGroup := func(idOrName string) (*projectgroups.ProjectGroup, error) { + return nil, services.ErrItemNotFound + } + + result, err := getProjects("Nope", nil, getProjectGroup, nil) + + assert.Nil(t, result) + assert.EqualError(t, err, "cannot find a project group with name or ID of 'Nope'") +} + +func TestGetProjects_WithNilGroupReportsTheGroup(t *testing.T) { + getProjectGroup := func(idOrName string) (*projectgroups.ProjectGroup, error) { + return nil, nil + } + + result, err := getProjects("Nope", nil, getProjectGroup, nil) + + assert.Nil(t, result) + assert.EqualError(t, err, "cannot find a project group with name or ID of 'Nope'") +} + +func TestGetProjects_SurfacesOtherLookupErrors(t *testing.T) { + expected := errors.New("the remote server returned 401 unauthorized") + getProjectGroup := func(idOrName string) (*projectgroups.ProjectGroup, error) { + return nil, expected + } + + result, err := getProjects("Default Project Group", nil, getProjectGroup, nil) + + assert.Nil(t, result) + assert.Equal(t, expected, err) +} + +func projectNames(items []*projects.Project) []string { + names := make([]string, 0, len(items)) + for _, item := range items { + names = append(names, item.Name) + } + return names +}