diff --git a/pkg/cmd/dependencies.go b/pkg/cmd/dependencies.go index e0d7660d..d6050245 100644 --- a/pkg/cmd/dependencies.go +++ b/pkg/cmd/dependencies.go @@ -32,6 +32,18 @@ func NewDependencies(f factory.Factory, cmd *cobra.Command) *Dependencies { panic(err) } + return newDependencies(f, cmd, client) +} + +func NewSystemDependencies(f factory.Factory, cmd *cobra.Command) *Dependencies { + client, err := f.GetSystemClient() + if err != nil { + panic(err) + } + return newDependencies(f, cmd, client) +} + +func newDependencies(f factory.Factory, cmd *cobra.Command, client *client.Client) *Dependencies { return &Dependencies{ Ask: f.Ask, CmdPath: cmd.CommandPath(), diff --git a/pkg/cmd/projectgroup/create/create.go b/pkg/cmd/projectgroup/create/create.go index 2dcd105c..8c558d43 100644 --- a/pkg/cmd/projectgroup/create/create.go +++ b/pkg/cmd/projectgroup/create/create.go @@ -120,7 +120,7 @@ func PromptMissing(opts *CreateOptions) error { if opts.Description.Value == "" { if err := opts.Ask(&survey.Input{ Message: messagePrefix + "Description", - Help: "A short, memorable, unique name for this project.", + Help: "A short, memorable, description for this project group.", }, &opts.Description.Value); err != nil { return err } diff --git a/pkg/cmd/space/create/create.go b/pkg/cmd/space/create/create.go index b35a2f07..102f1cf0 100644 --- a/pkg/cmd/space/create/create.go +++ b/pkg/cmd/space/create/create.go @@ -1,9 +1,13 @@ package create import ( + "errors" "fmt" + "github.com/OctopusDeploy/cli/pkg/cmd" + "github.com/OctopusDeploy/cli/pkg/cmd/tenant/shared" "github.com/OctopusDeploy/cli/pkg/util" - "io" + "github.com/OctopusDeploy/cli/pkg/util/flag" + "strings" "github.com/AlecAivazis/survey/v2" "github.com/MakeNowJust/heredoc/v2" @@ -12,14 +16,60 @@ import ( "github.com/OctopusDeploy/cli/pkg/output" "github.com/OctopusDeploy/cli/pkg/question" "github.com/OctopusDeploy/cli/pkg/validation" - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/spaces" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/teams" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/users" "github.com/spf13/cobra" ) +const ( + FlagName = "name" + FlagDescription = "description" + FlagTeam = "team" + FlagUser = "user" +) + +type CreateFlags struct { + Name *flag.Flag[string] + Description *flag.Flag[string] + Teams *flag.Flag[[]string] + Users *flag.Flag[[]string] +} + +type CreateOptions struct { + *cmd.Dependencies + *CreateFlags + GetAllSpacesCallback shared.GetAllSpacesCallback + GetAllTeamsCallback shared.GetAllTeamsCallback + GetAllUsersCallback shared.GetAllUsersCallback +} + +func NewCreateOptions(f factory.Factory, flags *CreateFlags, dependencies *cmd.Dependencies) *CreateOptions { + client, err := f.GetSystemClient() + dependencies.Client = client // override the default space client + if err != nil { + panic(err) + } + return &CreateOptions{ + CreateFlags: flags, + Dependencies: dependencies, + GetAllSpacesCallback: func() ([]*spaces.Space, error) { return shared.GetAllSpaces(*client) }, + GetAllTeamsCallback: func() ([]*teams.Team, error) { return shared.GetAllTeams(*client) }, + GetAllUsersCallback: func() ([]*users.User, error) { return shared.GetAllUsers(*client) }, + } +} + +func NewCreateFlags() *CreateFlags { + return &CreateFlags{ + Name: flag.New[string](FlagName, false), + Description: flag.New[string](FlagDescription, false), + Teams: flag.New[[]string](FlagTeam, false), + Users: flag.New[[]string](FlagUser, false), + } +} + func NewCmdCreate(f factory.Factory) *cobra.Command { + createFlags := NewCreateFlags() cmd := &cobra.Command{ Use: "create", Short: "Creates a space in an instance of Octopus Deploy", @@ -27,78 +77,101 @@ func NewCmdCreate(f factory.Factory) *cobra.Command { Example: fmt.Sprintf(heredoc.Doc(` $ %s space create" `), constants.ExecutableName), - RunE: func(cmd *cobra.Command, args []string) error { - return createRun(f, cmd.OutOrStdout()) + RunE: func(c *cobra.Command, args []string) error { + opts := NewCreateOptions(f, createFlags, cmd.NewSystemDependencies(f, c)) + + return createRun(opts) }, } + flags := cmd.Flags() + flags.StringVarP(&createFlags.Name.Value, createFlags.Name.Name, "n", "", "Name of the space") + flags.StringVarP(&createFlags.Description.Value, createFlags.Description.Name, "d", "", "Description of the space") + flags.StringSliceVarP(&createFlags.Teams.Value, createFlags.Teams.Name, "t", nil, "The teams to manage the space (can be specified multiple times)") + flags.StringSliceVarP(&createFlags.Users.Value, createFlags.Users.Name, "u", nil, "The users to manage the space (can be specified multiple times)") + return cmd } -func createRun(f factory.Factory, _ io.Writer) error { - systemClient, err := f.GetSystemClient() - if err != nil { - return err +func createRun(opts *CreateOptions) error { + if !opts.NoPrompt { + if err := PromptMissing(opts); err != nil { + return err + } } + space := spaces.NewSpace(opts.Name.Value) - existingSpaces, err := systemClient.Spaces.GetAll() + allTeams, err := opts.Client.Teams.GetAll() if err != nil { return err } - spaceNames := util.SliceTransform(existingSpaces, func(s *spaces.Space) string { return s.Name }) - - var name string - err = f.Ask(&survey.Input{ - Help: "The name of the space being created.", - Message: "Name", - }, &name, survey.WithValidator(survey.ComposeValidators( - survey.MaxLength(20), - survey.MinLength(1), - survey.Required, - validation.NotEquals(spaceNames, "a space with this name already exists"), - ))) + allUsers, err := opts.Client.Users.GetAll() if err != nil { return err } - space := spaces.NewSpace(name) - selectedTeams, err := selectTeams(f.Ask, systemClient, existingSpaces, "Select one or more teams to manage this space:") - if err != nil { - return err - } + for _, team := range opts.Teams.Value { + team, err := findTeam(allTeams, team) + if err != nil { + return err + } - for _, team := range selectedTeams { space.SpaceManagersTeams = append(space.SpaceManagersTeams, team.ID) } - selectedUsers, err := selectUsers(f.Ask, systemClient, "Select one or more users to manage this space:") - if err != nil { - return err - } + for _, user := range opts.Users.Value { + user, err := findUser(allUsers, user) + if err != nil { + return err + } - for _, user := range selectedUsers { - space.SpaceManagersTeamMembers = append(space.SpaceManagersTeams, user.ID) + space.SpaceManagersTeamMembers = append(space.SpaceManagersTeamMembers, user.GetID()) } - createdSpace, err := systemClient.Spaces.Add(space) + space.Description = opts.Description.Value + + createdSpace, err := opts.Client.Spaces.Add(space) if err != nil { return err } fmt.Printf("%s The space, \"%s\" %s was created successfully.\n", output.Green("✔"), createdSpace.Name, output.Dimf("(%s)", createdSpace.ID)) + + if !opts.NoPrompt { + autoCmd := flag.GenerateAutomationCmd(opts.CmdPath, opts.Name, opts.Description, opts.Teams, opts.Users) + fmt.Fprintf(opts.Out, "\nAutomation Command: %s\n", autoCmd) + } return nil } -func selectTeams(ask question.Asker, client *client.Client, existingSpaces []*spaces.Space, message string) ([]*teams.Team, error) { - systemTeams, err := client.Teams.Get(teams.TeamsQuery{ - IncludeSystem: true, - }) +func findTeam(allTeams []*teams.Team, identifier string) (*teams.Team, error) { + for _, team := range allTeams { + if strings.EqualFold(identifier, team.ID) || strings.EqualFold(identifier, team.Name) { + return team, nil + } + } + + return nil, errors.New(fmt.Sprintf("Cannot find team '%s'", identifier)) +} + +func findUser(allUsers []*users.User, identifier string) (*users.User, error) { + for _, user := range allUsers { + if strings.EqualFold(identifier, user.ID) || strings.EqualFold(identifier, user.Username) || strings.EqualFold(identifier, user.DisplayName) { + return user, nil + } + } + + return nil, errors.New(fmt.Sprintf("Cannot find user '%s'", identifier)) +} + +func selectTeams(ask question.Asker, getAllTeamsCallback shared.GetAllTeamsCallback, existingSpaces []*spaces.Space, message string) ([]*teams.Team, error) { + systemTeams, err := getAllTeamsCallback() if err != nil { return []*teams.Team{}, err } - return question.MultiSelectMap(ask, message, systemTeams.Items, func(team *teams.Team) string { + return question.MultiSelectMap(ask, message, systemTeams, func(team *teams.Team) string { if len(team.SpaceID) == 0 { return fmt.Sprintf("%s %s", team.Name, output.Dim("(System Team)")) } @@ -111,8 +184,8 @@ func selectTeams(ask question.Asker, client *client.Client, existingSpaces []*sp }, false) } -func selectUsers(ask question.Asker, client *client.Client, message string) ([]*users.User, error) { - existingUsers, err := client.Users.GetAll() +func selectUsers(ask question.Asker, getAllUsersCallback shared.GetAllUsersCallback, message string) ([]*users.User, error) { + existingUsers, err := getAllUsersCallback() if err != nil { return nil, err } @@ -121,3 +194,59 @@ func selectUsers(ask question.Asker, client *client.Client, message string) ([]* return fmt.Sprintf("%s %s", existingUser.DisplayName, output.Dimf("(%s)", existingUser.Username)) }, false) } + +func PromptMissing(opts *CreateOptions) error { + existingSpaces, err := opts.GetAllSpacesCallback() + if err != nil { + return err + } + + spaceNames := util.SliceTransform(existingSpaces, func(s *spaces.Space) string { return s.Name }) + if opts.Name.Value == "" { + err = opts.Ask(&survey.Input{ + Help: "The name of the space being created.", + Message: "Name", + }, &opts.Name.Value, survey.WithValidator(survey.ComposeValidators( + survey.MaxLength(20), + survey.MinLength(1), + survey.Required, + validation.NotEquals(spaceNames, "a space with this name already exists"), + ))) + if err != nil { + return err + } + } + + if opts.Description.Value == "" { + if err := opts.Ask(&survey.Input{ + Message: "Description", + Help: "A short, memorable, description for this space.", + }, &opts.Description.Value); err != nil { + return err + } + } + + if len(opts.Teams.Value) == 0 { + selectedTeams, err := selectTeams(opts.Ask, opts.GetAllTeamsCallback, existingSpaces, "Select one or more teams to manage this space:") + if err != nil { + return err + } + + for _, team := range selectedTeams { + opts.Teams.Value = append(opts.Teams.Value, team.Name) + } + } + + if len(opts.Users.Value) == 0 { + selectedUsers, err := selectUsers(opts.Ask, opts.GetAllUsersCallback, "Select one or more users to manage this space:") + if err != nil { + return err + } + + for _, user := range selectedUsers { + opts.Users.Value = append(opts.Users.Value, user.Username) + } + } + + return nil +} diff --git a/pkg/cmd/space/create/create_test.go b/pkg/cmd/space/create/create_test.go new file mode 100644 index 00000000..1f3c3c67 --- /dev/null +++ b/pkg/cmd/space/create/create_test.go @@ -0,0 +1,88 @@ +package create_test + +import ( + "fmt" + "github.com/OctopusDeploy/cli/pkg/cmd" + "github.com/OctopusDeploy/cli/pkg/cmd/space/create" + "github.com/OctopusDeploy/cli/pkg/output" + "github.com/OctopusDeploy/cli/test/testutil" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/spaces" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/teams" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/users" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestPromptMissing_AllOptionsSupplied(t *testing.T) { + pa := []*testutil.PA{} + + asker, checkRemainingPrompts := testutil.NewMockAsker(t, pa) + flags := create.NewCreateFlags() + flags.Name.Value = "The Final Frontier" + flags.Description.Value = "Where no person has gone before" + flags.Teams.Value = []string{"Crew"} + flags.Users.Value = []string{"James T. Kirk"} + + opts := &create.CreateOptions{ + CreateFlags: flags, + Dependencies: &cmd.Dependencies{Ask: asker}, + GetAllSpacesCallback: func() ([]*spaces.Space, error) { + return []*spaces.Space{ + spaces.NewSpace("Explored space")}, nil + }, + } + create.PromptMissing(opts) + checkRemainingPrompts() +} + +func TestPromptMissing_NoOptionsSupplied(t *testing.T) { + captain := users.NewUser("james.kirk@enterprise.alphaquad.com", "James T. Kirk") + captain.ID = "Users-1" + vulcan := users.NewUser("spock@enterprise.alphaquad.com", "Spock") + vulcan.ID = "Users-2" + bridgeTeam := teams.NewTeam("Bridge crew") + bridgeTeam.ID = "Teams-1" + engineeringTeam := teams.NewTeam("Engineering") + engineeringTeam.ID = "Teams-2" + + pa := []*testutil.PA{ + testutil.NewInputPrompt("Name", "The name of the space being created.", "Cray cray space"), + testutil.NewInputPrompt("Description", "A short, memorable, description for this space.", "Crazy description"), + testutil.NewMultiSelectPrompt("Select one or more teams to manage this space:", "", []string{formatTeam(bridgeTeam), formatTeam(engineeringTeam)}, []string{formatTeam(bridgeTeam)}), + testutil.NewMultiSelectPrompt("Select one or more users to manage this space:", "", []string{formatUser(captain), formatUser(vulcan)}, []string{formatUser(vulcan)}), + } + + asker, checkRemainingPrompts := testutil.NewMockAsker(t, pa) + flags := create.NewCreateFlags() + + opts := &create.CreateOptions{ + CreateFlags: flags, + Dependencies: &cmd.Dependencies{Ask: asker}, + GetAllSpacesCallback: func() ([]*spaces.Space, error) { + return []*spaces.Space{ + spaces.NewSpace("Explored space")}, nil + }, + GetAllTeamsCallback: func() ([]*teams.Team, error) { + + return []*teams.Team{ + bridgeTeam, + engineeringTeam, + }, nil + }, + GetAllUsersCallback: func() ([]*users.User, error) { return []*users.User{captain, vulcan}, nil }, + } + create.PromptMissing(opts) + checkRemainingPrompts() + assert.Equal(t, "Cray cray space", flags.Name.Value) + assert.Equal(t, "Crazy description", flags.Description.Value) + assert.Equal(t, []string{bridgeTeam.Name}, flags.Teams.Value) + assert.Equal(t, []string{vulcan.Username}, flags.Users.Value) +} + +func formatUser(user *users.User) string { + return fmt.Sprintf("%s (%s)", user.DisplayName, output.Dimf(user.Username)) +} + +func formatTeam(team *teams.Team) string { + return fmt.Sprintf("%s %s", team.Name, output.Dimf("(System Team)")) +} diff --git a/pkg/cmd/tenant/shared/shared.go b/pkg/cmd/tenant/shared/shared.go index 3e8b46e4..a19340cc 100644 --- a/pkg/cmd/tenant/shared/shared.go +++ b/pkg/cmd/tenant/shared/shared.go @@ -3,15 +3,47 @@ package shared import ( "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/spaces" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/teams" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tenants" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/users" ) +type GetAllSpacesCallback func() ([]*spaces.Space, error) +type GetAllTeamsCallback func() ([]*teams.Team, error) +type GetAllUsersCallback func() ([]*users.User, error) type GetAllTenantsCallback func() ([]*tenants.Tenant, error) type GetTenantCallback func(identifier string) (*tenants.Tenant, error) type GetAllProjectsCallback func() ([]*projects.Project, error) type GetProjectCallback func(identifier string) (*projects.Project, error) type GetProjectProgression func(project *projects.Project) (*projects.Progression, error) +func GetAllTeams(client client.Client) ([]*teams.Team, error) { + res, err := client.Teams.Get(teams.TeamsQuery{IncludeSystem: true}) + if err != nil { + return nil, err + } + return res.Items, nil +} + +func GetAllUsers(client client.Client) ([]*users.User, error) { + res, err := client.Users.GetAll() + if err != nil { + return nil, err + } + + return res, nil +} + +func GetAllSpaces(client client.Client) ([]*spaces.Space, error) { + res, err := client.Spaces.GetAll() + if err != nil { + return nil, err + } + + return res, nil +} + func GetAllTenants(client client.Client) ([]*tenants.Tenant, error) { res, err := client.Tenants.GetAll() if err != nil {