Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Nested Commands & Project Group Create #109

Merged
merged 1 commit into from
Oct 11, 2022
Merged
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
6 changes: 6 additions & 0 deletions pkg/cmd/nested_opts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package cmd

type NestedOpts interface {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does the same job as the TaskExecutor abstraction.

The advantage this has is that it's simpler because you're sticking commands directly in the collection, rather than having a separation between gather input / perform action.
The disadvantage this has is that the questions are bound (via a closure) to the commit. You can't ask the question differently in a nested command that you could at the root level.

The reason I added that separation for TaskExecutor was to avoid circular package references, to help provide some code structure as the project grew, and to allow for nested questions to be asked differently, but share the underlying 'send to server' bit.

I don't mind that you've gone a different way here, but it would be good to understand.

  • Why didn't you just use the TaskExecutor pattern?
  • This appears to force nested questions to be asked in the same way as they are at the root. Is this worth it?
  • Have we thought through whether there are any other differences that might be significant?

If the team would like to go with this approach, then it obsoletes the TaskExecutor design. We should pay down our tech-debt and delete all that stuff.

Commit() error
GenerateAutomationCmd()
}
133 changes: 96 additions & 37 deletions pkg/cmd/project/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ package create

import (
"fmt"
"io"

"github.com/AlecAivazis/survey/v2"
"github.com/MakeNowJust/heredoc/v2"
"github.com/OctopusDeploy/cli/pkg/cmd"
projectGroupCreate "github.com/OctopusDeploy/cli/pkg/cmd/projectgroup/create"
"github.com/OctopusDeploy/cli/pkg/factory"
"github.com/OctopusDeploy/cli/pkg/output"
"github.com/OctopusDeploy/cli/pkg/question"
Expand All @@ -13,7 +17,6 @@ import (
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/spaces"
"github.com/spf13/cobra"
"io"
)

const (
Expand All @@ -38,6 +41,44 @@ type CreateOptions struct {
Space *spaces.Space
NoPrompt bool
Ask question.Asker
CmdPath string
}

func (co *CreateOptions) Commit() error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the commit pattern for each command

lifecycle, err := co.Client.Lifecycles.GetByIDOrName(co.Lifecycle.Value)
if err != nil {
return err
}

projectGroup, err := co.Client.ProjectGroups.GetByIDOrName(co.Group.Value)
if err != nil {
return err
}

project := projects.NewProject(co.Name.Value, lifecycle.ID, projectGroup.ID)
project.Description = co.Description.Value

createdProject, err := co.Client.Projects.Add(project)
if err != nil {
return err
}

_, err = fmt.Fprintf(co.Out, "\nSuccessfully created project %s (%s), with lifecycle %s in project group %s.\n", createdProject.Name, createdProject.Slug, co.Lifecycle.Value, co.Group.Value)
if err != nil {
return err
}

link := output.Bluef("%s/app#/%s/projects/%s", co.Host, co.Space.GetID(), createdProject.GetID())
fmt.Fprintf(co.Out, "View this project on Octopus Deploy: %s\n", link)

return nil
}

func (co *CreateOptions) GenerateAutomationCmd() {
if !co.NoPrompt {
autoCmd := flag.GenerateAutomationCmd(co.CmdPath, co.Name, co.Description, co.Group, co.Lifecycle)
fmt.Fprintf(co.Out, "%s\n", autoCmd)
}
}

func NewCreateFlags() *CreateFlags {
Expand Down Expand Up @@ -66,6 +107,7 @@ func NewCmdCreate(f factory.Factory) *cobra.Command {
if err != nil {
return err
}
opts.CmdPath = cmd.CommandPath()
opts.Out = cmd.OutOrStdout()
opts.Client = client
opts.Host = f.GetCurrentHost()
Expand All @@ -77,52 +119,43 @@ func NewCmdCreate(f factory.Factory) *cobra.Command {
}

flags := cmd.Flags()
flags.StringVarP(&opts.CreateFlags.Name.Value, opts.CreateFlags.Name.Name, "n", "", "Name of the project")
flags.StringVarP(&opts.CreateFlags.Description.Value, opts.CreateFlags.Description.Name, "d", "", "Description of the project")
flags.StringVarP(&opts.CreateFlags.Group.Value, opts.CreateFlags.Group.Name, "g", "", "Project group of the project")
flags.StringVarP(&opts.CreateFlags.Lifecycle.Value, opts.CreateFlags.Lifecycle.Name, "l", "", "Lifecycle of the project")
flags.StringVarP(&opts.Name.Value, opts.Name.Name, "n", "", "Name of the project")
flags.StringVarP(&opts.Description.Value, opts.Description.Name, "d", "", "Description of the project")
flags.StringVarP(&opts.Group.Value, opts.Group.Name, "g", "", "Project group of the project")
flags.StringVarP(&opts.Lifecycle.Value, opts.Lifecycle.Name, "l", "", "Lifecycle of the project")
flags.SortFlags = false

return cmd
}

func createRun(opts *CreateOptions) error {
if !opts.NoPrompt {
if err := PromptMissing(opts); err != nil {
optsArray, err := PromptMissing(opts)
if err != nil {
return err
}
}

lifecycle, err := opts.Client.Lifecycles.GetByIDOrName(opts.CreateFlags.Lifecycle.Value)
if err != nil {
return err
}

projectGroup, err := opts.Client.ProjectGroups.GetByIDOrName(opts.CreateFlags.Group.Value)
if err != nil {
return err
}

project := projects.NewProject(opts.CreateFlags.Name.Value, lifecycle.ID, projectGroup.ID)
project.Description = opts.CreateFlags.Description.Value
for _, o := range optsArray {
if err := o.Commit(); err != nil {
return err
}
}

createdProject, err := opts.Client.Projects.Add(project)
if err != nil {
return err
}
if !opts.NoPrompt {
fmt.Fprintln(opts.Out, "\nAutomation Commands:")
for _, o := range optsArray {
o.GenerateAutomationCmd()
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

_, err = fmt.Fprintf(opts.Out, "Successfully created project %s (%s), with lifecycle %s in project group %s.\n", createdProject.Name, createdProject.Slug, opts.Lifecycle.Value, opts.Group.Value)
if err != nil {
return err
}

link := output.Bluef("%s/app#/%s/projects/%s", opts.Host, opts.Space.GetID(), createdProject.GetID())
fmt.Fprintf(opts.Out, "\nView this project on Octopus Deploy: %s\n", link)

return nil
}

func PromptMissing(opts *CreateOptions) error {
func PromptMissing(opts *CreateOptions) ([]cmd.NestedOpts, error) {
nestedOpts := []cmd.NestedOpts{}

if opts.Name.Value == "" {
if err := opts.Ask(&survey.Input{
Message: "Name",
Expand All @@ -132,25 +165,51 @@ func PromptMissing(opts *CreateOptions) error {
survey.MinLength(1),
survey.Required,
))); err != nil {
return err
return nil, err
}
}

if opts.Lifecycle.Value == "" {
lc, err := selectors.Lifecycle("You have not specified a Lifecycle for this project. Please select one:", opts.Client, opts.Ask)
if err != nil {
return err
return nil, err
}
opts.Lifecycle.Value = lc.Name
}

if opts.Group.Value == "" {
g, err := selectors.ProjectGroup("You have not specified a Project group for this project. Please select one:", opts.Client, opts.Ask)
if err != nil {
return err
var shouldCreateNewProjectGroup bool
opts.Ask(&survey.Confirm{
Message: "Would you like to create a new Project Group?",
Default: false,
}, &shouldCreateNewProjectGroup)

if shouldCreateNewProjectGroup {
optValues := projectGroupCreate.NewCreateFlags()
projectGroupCreateOpts := projectGroupCreate.CreateOptions{
Host: opts.Host,
Ask: opts.Ask,
Out: opts.Out,
CreateFlags: optValues,
Client: opts.Client,
Space: opts.Space,
NoPrompt: opts.NoPrompt,
CmdPath: "octopus project-group create",
ShowMessagePrefix: true,
}
projectGroupCreate.PromptMissing(&projectGroupCreateOpts)
opts.Group.Value = projectGroupCreateOpts.Name.Value
nestedOpts = append(nestedOpts, &projectGroupCreateOpts)
} else {
g, err := selectors.ProjectGroup("You have not specified a Project group for this project. Please select one:", opts.Client, opts.Ask)
if err != nil {
return nil, err
}
opts.Group.Value = g.Name
}
opts.Group.Value = g.Name

}

return nil
nestedOpts = append(nestedOpts, opts)
return nestedOpts, nil
}
155 changes: 155 additions & 0 deletions pkg/cmd/projectgroup/create/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package create

import (
"fmt"
"io"

"github.com/AlecAivazis/survey/v2"
"github.com/MakeNowJust/heredoc/v2"
"github.com/OctopusDeploy/cli/pkg/factory"
"github.com/OctopusDeploy/cli/pkg/output"
"github.com/OctopusDeploy/cli/pkg/question"
"github.com/OctopusDeploy/cli/pkg/util/flag"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projectgroups"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/spaces"
"github.com/spf13/cobra"
)

const (
FlagName = "name"
FlagDescription = "description"
)

type CreateFlags struct {
Name *flag.Flag[string]
Description *flag.Flag[string]
}

type CreateOptions struct {
*CreateFlags
Out io.Writer
Client *client.Client
Host string
Space *spaces.Space
NoPrompt bool
Ask question.Asker
CmdPath string
ShowMessagePrefix bool
}

func (co *CreateOptions) Commit() error {
projectGroup := projectgroups.NewProjectGroup(co.Name.Value)
projectGroup.Description = co.Description.Value

createdGroupProject, err := co.Client.ProjectGroups.Add(projectGroup)
if err != nil {
return err
}
_, err = fmt.Fprintf(co.Out, "\nSuccessfully created project group %s.\n", createdGroupProject.Name)
if err != nil {
return err
}
link := output.Bluef("%s/app#/%s/projectGroups/%s", co.Host, co.Space.GetID(), createdGroupProject.GetID())
fmt.Fprintf(co.Out, "View this project group on Octopus Deploy: %s\n", link)
return nil
}

func (co *CreateOptions) GenerateAutomationCmd() {
if !co.NoPrompt {
autoCmd := flag.GenerateAutomationCmd(co.CmdPath, co.Name, co.Description)
fmt.Fprintf(co.Out, "%s\n", autoCmd)
}
}

func NewCreateFlags() *CreateFlags {
return &CreateFlags{
Name: flag.New[string](FlagName, false),
Description: flag.New[string](FlagDescription, false),
}
}

func NewCmdCreate(f factory.Factory) *cobra.Command {
opts := &CreateOptions{
Ask: f.Ask,
CreateFlags: NewCreateFlags(),
}
cmd := &cobra.Command{
Use: "create",
Short: "Creates a new project group in Octopus Deploy",
Long: "Creates a new project group in Octopus Deploy.",
Example: heredoc.Doc(`
$ octopus project-group create
`),
RunE: func(cmd *cobra.Command, args []string) error {
client, err := f.GetSpacedClient()
if err != nil {
return err
}
opts.CmdPath = cmd.CommandPath()
opts.Out = cmd.OutOrStdout()
opts.Client = client
opts.Host = f.GetCurrentHost()
opts.NoPrompt = !f.IsPromptEnabled()
opts.Space = f.GetCurrentSpace()

return createRun(opts)
},
}

flags := cmd.Flags()
flags.StringVarP(&opts.Name.Value, opts.CreateFlags.Name.Name, "n", "", "Name of the project group")
flags.StringVarP(&opts.Description.Value, opts.CreateFlags.Description.Name, "d", "", "Description of the project group")
flags.SortFlags = false

return cmd
}

func createRun(opts *CreateOptions) error {
if !opts.NoPrompt {
if err := PromptMissing(opts); err != nil {
return err
}
}

if err := opts.Commit(); err != nil {
return err
}
if !opts.NoPrompt {
fmt.Fprint(opts.Out, "Automation Command: ")
opts.GenerateAutomationCmd()
}

return nil
}

func PromptMissing(opts *CreateOptions) error {
messagePrefix := ""
if opts.ShowMessagePrefix {
messagePrefix = "Project Group "
}

if opts.Name.Value == "" {
if err := opts.Ask(&survey.Input{
Message: messagePrefix + "Name",
Help: "A short, memorable, unique name for this project group.",
}, &opts.Name.Value, survey.WithValidator(survey.ComposeValidators(
survey.MaxLength(200),
survey.MinLength(1),
survey.Required,
))); err != nil {
return err
}
}

if opts.Description.Value == "" {
if err := opts.Ask(&survey.Input{
Message: messagePrefix + "Description",
Help: "A short, memorable, unique name for this project.",
}, &opts.Description.Value); err != nil {
return err
}
}

return nil
}
30 changes: 30 additions & 0 deletions pkg/cmd/projectgroup/project-group.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package projectgroup

import (
"fmt"
"github.com/MakeNowJust/heredoc/v2"
createCmd "github.com/OctopusDeploy/cli/pkg/cmd/projectgroup/create"
"github.com/OctopusDeploy/cli/pkg/constants"
"github.com/OctopusDeploy/cli/pkg/constants/annotations"
"github.com/OctopusDeploy/cli/pkg/factory"
"github.com/spf13/cobra"
)

func NewCmdProjectGroup(f factory.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "project-group <command>",
Short: "Manage project groups",
Long: `Work with Octopus Deploy project groups.`,
Example: fmt.Sprintf(heredoc.Doc(`
$ %s project-group list
$ %s project-group ls
`), constants.ExecutableName, constants.ExecutableName),
Annotations: map[string]string{
annotations.IsCore: "true",
},
}

cmd.AddCommand(createCmd.NewCmdCreate(f))

return cmd
}
Loading