Skip to content

Commit

Permalink
feat: release list and release create
Browse files Browse the repository at this point in the history
  • Loading branch information
borland authored Aug 25, 2022
1 parent 804a618 commit ce93d7e
Show file tree
Hide file tree
Showing 14 changed files with 1,163 additions and 168 deletions.
66 changes: 8 additions & 58 deletions pkg/cmd/release/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import (
"errors"
"fmt"
"github.com/AlecAivazis/survey/v2"
"github.com/MakeNowJust/heredoc/v2"
cliErrors "github.com/OctopusDeploy/cli/pkg/errors"
"github.com/OctopusDeploy/cli/pkg/executor"
"github.com/OctopusDeploy/cli/pkg/factory"
"github.com/OctopusDeploy/cli/pkg/output"
"github.com/OctopusDeploy/cli/pkg/question"
"github.com/OctopusDeploy/cli/pkg/surveyext"
Expand All @@ -17,16 +19,12 @@ import (
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/feeds"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/releases"
"github.com/spf13/cobra"
"io"
"os"
"regexp"
"sort"
"strings"
"unicode"

"github.com/MakeNowJust/heredoc/v2"
"github.com/OctopusDeploy/cli/pkg/factory"
"github.com/spf13/cobra"
)

const (
Expand Down Expand Up @@ -277,15 +275,6 @@ func createRun(cmd *cobra.Command, f factory.Factory, flags *CreateFlags) error
return nil
}

func quoteStringIfRequired(str string) string {
for _, c := range str {
if unicode.IsSpace(c) {
return fmt.Sprintf("\"%s\"", str)
}
}
return str
}

type StepPackageVersion struct {
// these 3 fields are the main ones for showing the user
PackageID string
Expand Down Expand Up @@ -748,13 +737,13 @@ func printPackageVersions(ioWriter io.Writer, packages []*StepPackageVersion) er

func AskQuestions(octopus *octopusApiClient.Client, stdout io.Writer, asker question.Asker, spinner factory.Spinner, options *executor.TaskOptionsCreateRelease) error {
if octopus == nil {
return errors.New("api client is required")
return cliErrors.NewArgumentNullOrEmptyError("octopus")
}
if asker == nil {
return errors.New("asker is required")
return cliErrors.NewArgumentNullOrEmptyError("asker")
}
if options == nil {
return errors.New("options is required")
return cliErrors.NewArgumentNullOrEmptyError("options")
}
// Note: we don't get here at all if no-prompt is enabled, so we know we are free to ask questions

Expand All @@ -765,12 +754,12 @@ func AskQuestions(octopus *octopusApiClient.Client, stdout io.Writer, asker ques
var err error
var selectedProject *projects.Project
if options.ProjectName == "" {
selectedProject, err = selectProject(octopus, asker, spinner)
selectedProject, err = util.SelectProject("Select the project in which the release will be created", octopus, asker, spinner)
if err != nil {
return err
}
} else { // project name is already provided, fetch the object because it's needed for further questions
selectedProject, err = findProject(octopus, spinner, options.ProjectName)
selectedProject, err = util.FindProject(octopus, spinner, options.ProjectName)
if err != nil {
return err
}
Expand Down Expand Up @@ -1116,45 +1105,6 @@ func findChannel(octopus *octopusApiClient.Client, spinner factory.Spinner, proj
return nil, fmt.Errorf("no channel found with name of %s", channelName)
}

func findProject(octopus *octopusApiClient.Client, spinner factory.Spinner, projectName string) (*projects.Project, error) {
// projectsQuery has "Name" but it's just an alias in the server for PartialName; we need to filter client side
spinner.Start()
projectsPage, err := octopus.Projects.Get(projects.ProjectsQuery{PartialName: projectName})
if err != nil {
spinner.Stop()
return nil, err
}
for projectsPage != nil && len(projectsPage.Items) > 0 {
for _, c := range projectsPage.Items { // server doesn't support channel search by exact name so we must emulate it
if strings.EqualFold(c.Name, projectName) {
spinner.Stop()
return c, nil
}
}
projectsPage, err = projectsPage.GetNextPage(octopus.Projects.GetClient())
if err != nil {
spinner.Stop()
return nil, err
} // if there are no more pages, then GetNextPage will return nil, which breaks us out of the loop
}

spinner.Stop()
return nil, fmt.Errorf("no project found with name of %s", projectName)
}

func selectProject(octopus *octopusApiClient.Client, ask question.Asker, spinner factory.Spinner) (*projects.Project, error) {
spinner.Start()
existingProjects, err := octopus.Projects.GetAll()
spinner.Stop()
if err != nil {
return nil, err
}

return question.SelectMap(ask, "Select the project in which the release will be created", existingProjects, func(p *projects.Project) string {
return p.Name
})
}

func selectGitReference(octopus *octopusApiClient.Client, ask question.Asker, spinner factory.Spinner, project *projects.Project) (*projects.GitReference, error) {
spinner.Start()
branches, err := octopus.Projects.GetGitBranches(project)
Expand Down
214 changes: 214 additions & 0 deletions pkg/cmd/release/delete/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
package delete

import (
"errors"
"fmt"
"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"
"github.com/OctopusDeploy/cli/pkg/util/flag"
octopusApiClient "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/releases"
"github.com/hashicorp/go-multierror"
"github.com/spf13/cobra"
)

const (
FlagProject = "project"
FlagVersion = "version"
)

type Flags struct {
Project *flag.Flag[string]
Version *flag.Flag[[]string]
}

func NewFlags() *Flags {
return &Flags{
Project: flag.New[string](FlagProject, false),
Version: flag.New[[]string](FlagVersion, false),
}
}

func NewCmdDelete(f factory.Factory) *cobra.Command {
cmdFlags := NewFlags()
cmd := &cobra.Command{
Use: "delete",
Short: "Delete a release in Octopus Deploy",
Long: "Delete a release in Octopus Deploy.",
Example: heredoc.Doc(`
$ octopus release delete myProject 2.0
$ octopus release delete --project myProject --version 2.0
$ octopus release rm "Other Project" -v 2.0
`),
Aliases: []string{"del", "rm"},
RunE: func(cmd *cobra.Command, args []string) error {
return deleteRun(cmd, f, cmdFlags, args)
},
}

flags := cmd.Flags()
flags.StringVarP(&cmdFlags.Project.Value, cmdFlags.Project.Name, "p", "", "Name or ID of the project to delete releases in")
flags.StringSliceVarP(&cmdFlags.Version.Value, cmdFlags.Version.Name, "v", make([]string, 0), "Release version to delete, can be specified multiple times")
return cmd
}

func deleteRun(cmd *cobra.Command, f factory.Factory, flags *Flags, args []string) error {
// command line arg interpretation depends on which flags are present.
// e.g. `release delete -p MyProject -v 2.0` means we don't need to look at args at
// e.g. `release delete -p MyProject 2.0` means args[0] is the version
// e.g. `release delete MyProject -v 2.0` means args[0] is the project
// e.g. `release delete MyProject 2.0` means args[0] is the project and args[1] is the version
projectNameOrID := flags.Project.Value
versionsToDelete := flags.Version.Value

possibleVersionArgs := args
if projectNameOrID == "" && len(args) > 0 {
projectNameOrID = args[0]
possibleVersionArgs = args[1:] // we've consumed this arg, take it out of consideration for a version
}
for _, a := range possibleVersionArgs {
versionsToDelete = append(versionsToDelete, a)
}

// now off we go
octopus, err := f.GetSpacedClient()
if err != nil {
return err
}
spinner := f.Spinner()

var selectedProject *projects.Project
var releasesToDelete []*releases.Release

if f.IsPromptEnabled() { // this would be AskQuestions if it were bigger
if projectNameOrID == "" {
selectedProject, err = util.SelectProject("Select the project to delete a release in", octopus, f.Ask, spinner)
if err != nil {
return err
}
} else { // project name is already provided, fetch the object because it's needed for further questions
selectedProject, err = util.FindProject(octopus, spinner, projectNameOrID)
if err != nil {
return err
}
cmd.Printf("Project %s\n", output.Cyan(selectedProject.Name))
}

if len(versionsToDelete) == 0 {
releasesToDelete, err = selectReleases(octopus, selectedProject, f.Ask, spinner)
if err != nil {
return err
}
} else {
releasesToDelete, err = findReleases(octopus, spinner, selectedProject, versionsToDelete)
if err != nil {
return err
}
}

if len(releasesToDelete) == 0 {
return nil // no work to do, just exit
}

// prompt for confirmation
cmd.Printf("You are about to delete the following releases:\n")
for _, r := range releasesToDelete {
cmd.Printf("%s\n", r.Version)
}

var isConfirmed bool
if err = f.Ask(&survey.Confirm{
Message: fmt.Sprintf("Confirm delete of %d release(s)", len(releasesToDelete)),
Default: false,
}, &isConfirmed); err != nil {
return err
}
if !isConfirmed {
return nil // nothing to be done here
}

} else { // we don't have the executions API backing us and allowing NameOrID; we need to do the lookups ourselves
// validation
if projectNameOrID == "" {
return errors.New("project must be specified")
}
if len(versionsToDelete) == 0 {
return errors.New("at least one release version must be specified")
}

selectedProject, err = util.FindProject(octopus, factory.NoSpinner, projectNameOrID)
if err != nil {
return err
}
releasesToDelete, err = findReleases(octopus, factory.NoSpinner, selectedProject, versionsToDelete)
if err != nil {
return err
}
}

if len(releasesToDelete) == 0 {
// no work to do, just exit
return nil
}

spinner.Start()
var releaseDeleteErrors = &multierror.Error{}
for _, r := range releasesToDelete {
err = octopus.Releases.DeleteByID(r.ID)
if err != nil {
wrappedErr := fmt.Errorf("failed to delete release %s: %s", r.Version, err)
cmd.PrintErr(fmt.Sprintf("%s\n", wrappedErr.Error()))
releaseDeleteErrors = multierror.Append(releaseDeleteErrors, wrappedErr)
}
}
spinner.Stop()

failedCount := releaseDeleteErrors.Len()
actuallyDeletedCount := len(releasesToDelete) - failedCount

if failedCount == 0 { // all good
cmd.Printf("Successfully deleted %d releases\n", actuallyDeletedCount)
} else if actuallyDeletedCount == 0 { // all bad
cmd.Printf("Failed to delete %d releases\n", failedCount)
} else { // partial
cmd.Printf("Deleted %d releases. %d releases failed\n", actuallyDeletedCount, failedCount)
}
return releaseDeleteErrors.ErrorOrNil()
}

func selectReleases(octopus *octopusApiClient.Client, project *projects.Project, ask question.Asker, spinner factory.Spinner) ([]*releases.Release, error) {
spinner.Start()
existingReleases, err := octopus.Projects.GetReleases(project) // gets all of them, no paging
spinner.Stop()
if err != nil {
return nil, err
}

return question.MultiSelectMap(ask, "Select Releases to delete", existingReleases, func(p *releases.Release) string {
return p.Version
})
}

func findReleases(octopus *octopusApiClient.Client, spinner factory.Spinner, project *projects.Project, versionStrings []string) ([]*releases.Release, error) {
spinner.Start()
existingReleases, err := octopus.Projects.GetReleases(project) // gets all of them, no paging
spinner.Stop()
if err != nil {
return nil, err
}

versionStringLookup := make(map[string]bool, len(versionStrings))
for _, s := range versionStrings {
versionStringLookup[s] = true
}

return util.SliceFilter(existingReleases, func(p *releases.Release) bool {
_, exists := versionStringLookup[p.Version]
return exists
}), nil
}
Loading

0 comments on commit ce93d7e

Please sign in to comment.