-
Notifications
You must be signed in to change notification settings - Fork 10
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: project create #115
feat: project create #115
Changes from 17 commits
f3e7fca
c283bde
9653db3
88b416b
98d10ca
f0a5a76
5f50b52
d55ff9b
16acf77
5a94d1f
4d79676
4085d57
b0311f2
15f4428
86c5baa
c86ad72
5d97470
b00c777
086a0aa
cf38f22
a69591d
f79b8f7
2c0d949
57d6f46
487aaa7
91a4f0c
3b5aa46
ec7cbd3
2f2b6d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
package cmd | ||
|
||
import ( | ||
"io" | ||
|
||
"github.com/OctopusDeploy/cli/pkg/factory" | ||
"github.com/OctopusDeploy/cli/pkg/question" | ||
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client" | ||
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/spaces" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
type Dependable interface { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I completely understand we're trying to fit within the patterns and idioms of golang... but |
||
Commit() error | ||
GenerateAutomationCmd() | ||
} | ||
|
||
type Dependencies struct { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps From a quick look, it appears as though this struct is meant to carry services (client, asker) and options (space, noprompt) that are commonly used across many commands? If so, some possible naming suggestions?
... something to think about anyway There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. tag @slewis74 |
||
Out io.Writer | ||
Client *client.Client | ||
Host string | ||
Space *spaces.Space | ||
NoPrompt bool | ||
Ask question.Asker | ||
CmdPath string | ||
ShowMessagePrefix bool | ||
} | ||
|
||
func NewDependencies(f factory.Factory, cmd *cobra.Command) *Dependencies { | ||
client, err := f.GetSpacedClient() | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
return &Dependencies{ | ||
Ask: f.Ask, | ||
CmdPath: cmd.CommandPath(), | ||
Out: cmd.OutOrStdout(), | ||
Client: client, | ||
Host: f.GetCurrentHost(), | ||
NoPrompt: !f.IsPromptEnabled(), | ||
Space: f.GetCurrentSpace(), | ||
} | ||
} | ||
|
||
func NewDependenciesFromExisting(opts *Dependencies, cmdPath string) *Dependencies { | ||
return &Dependencies{ | ||
Ask: opts.Ask, | ||
CmdPath: cmdPath, | ||
Out: opts.Out, | ||
Client: opts.Client, | ||
Host: opts.Host, | ||
NoPrompt: opts.NoPrompt, | ||
Space: opts.Space, | ||
ShowMessagePrefix: true, | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,293 @@ | ||
package create | ||
|
||
import ( | ||
"fmt" | ||
"github.com/AlecAivazis/survey/v2" | ||
"github.com/MakeNowJust/heredoc/v2" | ||
"github.com/OctopusDeploy/cli/pkg/cmd" | ||
"github.com/OctopusDeploy/cli/pkg/factory" | ||
"github.com/OctopusDeploy/cli/pkg/question" | ||
"github.com/OctopusDeploy/cli/pkg/question/selectors" | ||
"github.com/OctopusDeploy/cli/pkg/util/flag" | ||
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projectgroups" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
const ( | ||
FlagGroup = "group" | ||
FlagName = "name" | ||
FlagDescription = "description" | ||
FlagLifecycle = "lifecycle" | ||
FlagConfigAsCode = "cac" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not sure I am happy with name. Suggestions? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I found when doing On the other hand, the "brand name" that keeps showing up on twitter/linkedin/customer forums is always Config As Code, so 🤷 ? tag @jbristowe |
||
FlagGitUrl = "git-url" | ||
FlagGitBranch = "git-branch" | ||
FlagGitLibraryCredentials = "git-credentials" | ||
FlagGitUsername = "git-username" | ||
FlagGitPassword = "git-password" | ||
FlagGitCredentialStorage = "git-cred-store" | ||
FlagGitInitialCommitMessage = "git-initial-commit" | ||
FlagGitBasePath = "git-base-path" | ||
|
||
DefaultGitCommitMessage = "Initial commit of deployment process" | ||
DefaultBasePath = ".octopus/" | ||
DefaultBranch = "main" | ||
GitPersistenceType = "VersionControlled" | ||
|
||
GitStorageProject = "project" | ||
GitStorageLibrary = "library" | ||
) | ||
|
||
type CreateFlags struct { | ||
Group *flag.Flag[string] | ||
Name *flag.Flag[string] | ||
Description *flag.Flag[string] | ||
Lifecycle *flag.Flag[string] | ||
ConfigAsCode *flag.Flag[bool] | ||
|
||
GitUrl *flag.Flag[string] | ||
GitBranch *flag.Flag[string] | ||
GitCredentials *flag.Flag[string] | ||
GitUsername *flag.Flag[string] | ||
GitPassword *flag.Flag[string] | ||
GitStorage *flag.Flag[string] | ||
GitInitialCommitMessage *flag.Flag[string] | ||
GitBasePath *flag.Flag[string] | ||
} | ||
|
||
func NewCreateFlags() *CreateFlags { | ||
return &CreateFlags{ | ||
Group: flag.New[string](FlagGroup, false), | ||
Name: flag.New[string](FlagName, false), | ||
Description: flag.New[string](FlagDescription, false), | ||
Lifecycle: flag.New[string](FlagLifecycle, false), | ||
ConfigAsCode: flag.New[bool](FlagConfigAsCode, false), | ||
GitUrl: flag.New[string](FlagGitUrl, false), | ||
GitBranch: flag.New[string](FlagGitBranch, false), | ||
GitCredentials: flag.New[string](FlagGitLibraryCredentials, false), | ||
GitUsername: flag.New[string](FlagGitUsername, false), | ||
GitPassword: flag.New[string](FlagGitPassword, true), | ||
GitStorage: flag.New[string](FlagGitCredentialStorage, false), | ||
GitInitialCommitMessage: flag.New[string](FlagGitInitialCommitMessage, false), | ||
GitBasePath: flag.New[string](FlagGitBasePath, false), | ||
} | ||
} | ||
|
||
func NewCmdCreate(f factory.Factory) *cobra.Command { | ||
createFlags := NewCreateFlags() | ||
|
||
cmd := &cobra.Command{ | ||
Use: "create", | ||
Short: "Creates a new project in Octopus Deploy", | ||
Long: "Creates a new project in Octopus Deploy.", | ||
Example: heredoc.Doc(` | ||
$ octopus project create .... fill this in later | ||
`), | ||
RunE: func(c *cobra.Command, _ []string) error { | ||
opts := NewCreateOptions(createFlags, cmd.NewDependencies(f, c)) | ||
|
||
return createRun(opts) | ||
}, | ||
} | ||
|
||
flags := cmd.Flags() | ||
flags.StringVarP(&createFlags.Name.Value, createFlags.Name.Name, "n", "", "Name of the project") | ||
flags.StringVarP(&createFlags.Description.Value, createFlags.Description.Name, "d", "", "Description of the project") | ||
flags.StringVarP(&createFlags.Group.Value, createFlags.Group.Name, "g", "", "Project group of the project") | ||
flags.StringVarP(&createFlags.Lifecycle.Value, createFlags.Lifecycle.Name, "l", "", "Lifecycle of the project") | ||
flags.BoolVar(&createFlags.ConfigAsCode.Value, createFlags.ConfigAsCode.Name, false, "Use Config As Code for the project") | ||
|
||
flags.StringVarP(&createFlags.GitUrl.Value, createFlags.GitUrl.Name, "u", "", "Url of the Git repository for storing project configuration") | ||
flags.StringVarP(&createFlags.GitBranch.Value, createFlags.GitBranch.Name, "b", "", "The default branch to use for Config As Code, default is main.") | ||
flags.StringVar(&createFlags.GitCredentials.Value, createFlags.GitCredentials.Name, "", "The Id or name of the Git credentials stored in Octopus") | ||
flags.StringVar(&createFlags.GitUsername.Value, createFlags.GitUsername.Name, "", "The username to authenticate with Git") | ||
flags.StringVar(&createFlags.GitPassword.Value, createFlags.GitPassword.Name, "", "The password to authenticate with Git") | ||
flags.StringVar(&createFlags.GitStorage.Value, createFlags.GitStorage.Name, "", "The location to store the supplied Git credentials. Options are library or project. Default is library") | ||
flags.StringVar(&createFlags.GitInitialCommitMessage.Value, createFlags.GitInitialCommitMessage.Name, "", "The initial commit message for configuring Config As Code.") | ||
flags.SortFlags = false | ||
|
||
return cmd | ||
} | ||
|
||
func createRun(opts *CreateOptions) error { | ||
if !opts.NoPrompt { | ||
optsArray, err := PromptMissing(opts) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
for _, o := range optsArray { | ||
if err := o.Commit(); err != nil { | ||
return err | ||
} | ||
} | ||
|
||
if !opts.NoPrompt { | ||
benPearce1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
fmt.Fprintln(opts.Out, "\nAutomation Commands:") | ||
for _, o := range optsArray { | ||
o.GenerateAutomationCmd() | ||
} | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func PromptMissing(opts *CreateOptions) ([]cmd.Dependable, error) { | ||
nestedOpts := []cmd.Dependable{} | ||
|
||
question.AskName(opts.Ask, "", "project", &opts.Name.Value) | ||
|
||
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 nil, err | ||
} | ||
opts.Lifecycle.Value = lc.Name | ||
} | ||
|
||
value, projectGroupOpt, err := AskProjectGroups(opts.Ask, opts.Group.Value, opts.GetAllGroupsCallback, opts.CreateProjectGroupCallback) | ||
if err != nil { | ||
return nil, err | ||
} | ||
opts.Group.Value = value | ||
if projectGroupOpt != nil { | ||
nestedOpts = append(nestedOpts, projectGroupOpt) | ||
} | ||
|
||
err = PromptForConfigAsCode(opts) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
nestedOpts = append(nestedOpts, opts) | ||
return nestedOpts, nil | ||
} | ||
|
||
func AskProjectGroups(ask question.Asker, value string, getAllGroupsCallback GetAllGroupsCallback, createProjectGroupCallback CreateProjectGroupCallback) (string, cmd.Dependable, error) { | ||
if value != "" { | ||
return value, nil, nil | ||
} | ||
var shouldCreateNewProjectGroup bool | ||
ask(&survey.Confirm{ | ||
Message: "Would you like to create a new Project Group?", | ||
Default: false, | ||
}, &shouldCreateNewProjectGroup) | ||
|
||
if shouldCreateNewProjectGroup { | ||
return createProjectGroupCallback() | ||
} | ||
|
||
g, err := selectors.Select(ask, "You have not specified a Project group for this project. Please select one:", getAllGroupsCallback, func(pg *projectgroups.ProjectGroup) string { | ||
return pg.Name | ||
}) | ||
if err != nil { | ||
return "", nil, err | ||
} | ||
return g.Name, nil, nil | ||
|
||
} | ||
|
||
func PromptForConfigAsCode(opts *CreateOptions) error { | ||
if !opts.ConfigAsCode.Value { | ||
opts.Ask(&survey.Confirm{ | ||
Message: "Would you like to use Config as Code?", | ||
Default: false, | ||
}, &opts.ConfigAsCode.Value) | ||
} | ||
|
||
if opts.ConfigAsCode.Value { | ||
if opts.GitStorage.Value == "" { | ||
selectedOption, err := selectors.SelectOptions[string](opts.Ask, "Select where to store the Git credentials", getGitStorageOptions) | ||
|
||
if err != nil { | ||
return err | ||
} | ||
opts.GitStorage.Value = selectedOption.Value | ||
} | ||
|
||
if opts.GitUrl.Value == "" { | ||
if err := opts.Ask(&survey.Input{ | ||
Message: "Git URL", | ||
Help: "The URL of the Git repository to store configuration.", | ||
}, &opts.GitUrl.Value, survey.WithValidator(survey.ComposeValidators( | ||
survey.MaxLength(200), | ||
survey.MinLength(1), | ||
survey.Required, | ||
))); err != nil { | ||
return err | ||
} | ||
} | ||
|
||
if opts.GitBasePath.Value == "" { | ||
if err := opts.Ask(&survey.Input{ | ||
Message: "Git repository base path", | ||
Help: "The path in the repository where Config As Code settings are stored. Default value: '" + DefaultBasePath + "'", | ||
}, &opts.GitBasePath.Value, survey.WithValidator(survey.ComposeValidators( | ||
survey.MaxLength(200), | ||
))); err != nil { | ||
return err | ||
} | ||
} | ||
|
||
if opts.GitBranch.Value == "" { | ||
if err := opts.Ask(&survey.Input{ | ||
Message: "Git branch", | ||
Help: "The default branch to use. Default value: '" + DefaultBranch + "'", | ||
}, &opts.GitBranch.Value, survey.WithValidator(survey.ComposeValidators( | ||
survey.MaxLength(200), | ||
))); err != nil { | ||
return err | ||
} | ||
} | ||
|
||
if opts.GitInitialCommitMessage.Value == "" { | ||
if err := opts.Ask(&survey.Input{ | ||
Message: "Initial Git commit message", | ||
Help: "The commit message used in initializing. Default value: '" + DefaultGitCommitMessage + "'", | ||
benPearce1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}, &opts.GitInitialCommitMessage.Value, survey.WithValidator(survey.ComposeValidators( | ||
survey.MaxLength(50), | ||
))); err != nil { | ||
return err | ||
} | ||
} | ||
|
||
if opts.GitStorage.Value == GitStorageLibrary { | ||
benPearce1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
panic("library storage not currently supported") | ||
} else { | ||
if opts.GitUsername.Value == "" { | ||
if err := opts.Ask(&survey.Input{ | ||
Message: "Git username", | ||
Help: "The Git username.", | ||
}, &opts.GitUsername.Value, survey.WithValidator(survey.ComposeValidators( | ||
survey.MaxLength(200), | ||
survey.MinLength(1), | ||
survey.Required, | ||
))); err != nil { | ||
return err | ||
} | ||
} | ||
|
||
if opts.GitPassword.Value == "" { | ||
if err := opts.Ask(&survey.Password{ | ||
Message: "Git password", | ||
Help: "The Git password.", | ||
}, &opts.GitPassword.Value, survey.WithValidator(survey.ComposeValidators( | ||
survey.MaxLength(200), | ||
survey.MinLength(1), | ||
survey.Required, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IIRC both I ran into this when selecting environments for release deployment, which is a multi-select with min-items(1); Required did the same job but produced a better error message so I used that instead |
||
))); err != nil { | ||
return err | ||
} | ||
} | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func getGitStorageOptions() []*selectors.SelectOption[string] { | ||
return []*selectors.SelectOption[string]{ | ||
{Display: "Project", Value: GitStorageProject}, | ||
{Display: "Library", Value: GitStorageLibrary}, | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
q for @domenicsim1
Given that this isn't a command itself, should it go under the
cmd
directory structure, or should it go under just pkg or pkg/somethingelse?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe, although it is suppose to be the basis of cmds. Not sure yet, happy to leave it for now.