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
72 changes: 67 additions & 5 deletions pkg/cmd/project/list/list.go
Original file line number Diff line number Diff line change
@@ -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
}

Expand All @@ -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(),
Expand All @@ -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)
}
96 changes: 96 additions & 0 deletions pkg/cmd/project/list/list_test.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading