Skip to content
Open
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
3 changes: 3 additions & 0 deletions pkg/cmd/release/progression/shared/shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ func GetReleaseID(octopus *client.Client, spaceID string, projectIdentifier stri
if err != nil {
return "", err
}
if selectedProject == nil {
return "", fmt.Errorf("unable to find project '%s'", projectIdentifier)
}

selectedRelease, err := FindRelease(octopus, selectedProject, version)
if err != nil {
Expand Down
2 changes: 2 additions & 0 deletions pkg/cmd/release/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
cmdDeploy "github.com/OctopusDeploy/cli/pkg/cmd/release/deploy"
cmdList "github.com/OctopusDeploy/cli/pkg/cmd/release/list"
cmdProgression "github.com/OctopusDeploy/cli/pkg/cmd/release/progression"
cmdSnapshotVariables "github.com/OctopusDeploy/cli/pkg/cmd/release/snapshot_variables"
"github.com/OctopusDeploy/cli/pkg/constants"
"github.com/OctopusDeploy/cli/pkg/constants/annotations"
"github.com/OctopusDeploy/cli/pkg/factory"
Expand All @@ -29,6 +30,7 @@ func NewCmdRelease(f factory.Factory) *cobra.Command {
cmd.AddCommand(cmdList.NewCmdList(f))
cmd.AddCommand(cmdDelete.NewCmdDelete(f))
cmd.AddCommand(cmdProgression.NewCmdProgression(f))
cmd.AddCommand(cmdSnapshotVariables.NewCmdSnapshotVariables(f))

return cmd
}
139 changes: 139 additions & 0 deletions pkg/cmd/release/snapshot_variables/snapshot_variables.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package snapshot_variables

import (
"errors"
"fmt"

"github.com/MakeNowJust/heredoc/v2"
"github.com/OctopusDeploy/cli/pkg/cmd"
"github.com/OctopusDeploy/cli/pkg/cmd/release/progression/shared"
"github.com/OctopusDeploy/cli/pkg/constants"
"github.com/OctopusDeploy/cli/pkg/factory"
"github.com/OctopusDeploy/cli/pkg/output"
"github.com/OctopusDeploy/cli/pkg/question/selectors"
"github.com/OctopusDeploy/cli/pkg/util/flag"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/releases"
"github.com/spf13/cobra"
)

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

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

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

type SnapshotVariablesOptions struct {
*SnapshotVariablesFlags
*cmd.Dependencies
}

func NewSnapshotVariablesOptions(flags *SnapshotVariablesFlags, dependencies *cmd.Dependencies) *SnapshotVariablesOptions {
return &SnapshotVariablesOptions{
SnapshotVariablesFlags: flags,
Dependencies: dependencies,
}
}

func NewCmdSnapshotVariables(f factory.Factory) *cobra.Command {
snapshotVariablesFlags := NewSnapshotVariablesFlags()

cmd := &cobra.Command{
Use: "snapshot-variables",
Short: "Update the variable snapshot for a release",
Long: "Update the variable snapshot for a release in Octopus Deploy",
Example: heredoc.Docf(`
$ %[1]s release snapshot-variables --project MyProject --version 1.2.3
$ %[1]s release snapshot-variables -p MyProject -v 1.2.3
`, constants.ExecutableName),
RunE: func(c *cobra.Command, args []string) error {
opts := NewSnapshotVariablesOptions(snapshotVariablesFlags, cmd.NewDependencies(f, c))
return snapshotVariablesRun(opts)
},
}

flags := cmd.Flags()
flags.StringVarP(&snapshotVariablesFlags.Project.Value, snapshotVariablesFlags.Project.Name, "p", "", "Name or ID of the project")
flags.StringVarP(&snapshotVariablesFlags.Version.Value, snapshotVariablesFlags.Version.Name, "v", "", "Release version/number")

flags.SortFlags = false

return cmd
}

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

if opts.Project.Value == "" {
return errors.New("project must be specified")
}
if opts.Version.Value == "" {
return errors.New("version must be specified")
}

releaseID, err := shared.GetReleaseID(opts.Client, opts.Client.GetSpaceID(), opts.Project.Value, opts.Version.Value)
if err != nil {
return err
}

if _, err := releases.UpdateSnapshotVariables(opts.Client, opts.Client.GetSpaceID(), releaseID); err != nil {
return err
}

fmt.Fprintf(opts.Out, "Successfully updated variable snapshot for release '%s' (%s)\n", opts.Version.Value, output.Dim(releaseID))
link := output.Bluef("%s/app#/%s/releases/%s", opts.Host, opts.Space.GetID(), releaseID)
fmt.Fprintf(opts.Out, "View this release on Octopus Deploy: %s\n", link)

if !opts.NoPrompt {
autoCmd := flag.GenerateAutomationCmd(opts.CmdPath, opts.GetSpaceNameOrEmpty(), opts.Project, opts.Version)
fmt.Fprintf(opts.Out, "\nAutomation Command: %s\n", autoCmd)
}

return nil
}

func PromptMissing(opts *SnapshotVariablesOptions) error {
var selectedProject *projects.Project
var err error

if opts.Project.Value == "" {
selectedProject, err = selectors.Project("Select the project containing the release", opts.Client, opts.Ask)
if err != nil {
return err
}
opts.Project.Value = selectedProject.GetName()
} else {
selectedProject, err = selectors.FindProject(opts.Client, opts.Project.Value)
if err != nil {
return err
}
if selectedProject == nil {
return fmt.Errorf("unable to find project '%s'", opts.Project.Value)
}
}

if opts.Version.Value == "" {
selectedRelease, err := shared.SelectRelease(opts.Client, selectedProject, opts.Ask, "Update Variables for")
if err != nil {
return err
}
opts.Version.Value = selectedRelease.Version
}

return nil
}
177 changes: 177 additions & 0 deletions pkg/cmd/release/snapshot_variables/snapshot_variables_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package snapshot_variables_test

import (
"bytes"
"testing"

"github.com/AlecAivazis/survey/v2"
cmdRoot "github.com/OctopusDeploy/cli/pkg/cmd/root"
"github.com/OctopusDeploy/cli/pkg/question"
"github.com/OctopusDeploy/cli/test/fixtures"
"github.com/OctopusDeploy/cli/test/testutil"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/releases"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/resources"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
)

var rootResource = testutil.NewRootResource()

func TestReleaseSnapshotVariables(t *testing.T) {
const spaceID = "Spaces-1"
const fireProjectID = "Projects-22"

space1 := fixtures.NewSpace(spaceID, "Default Space")

fireProject := fixtures.NewProject(spaceID, fireProjectID, "Fire Project", "Lifecycles-1", "ProjectGroups-1", "")
rDefault21 := fixtures.NewRelease(spaceID, "Releases-21", "2.1", fireProjectID, "Channels-1")
rDefault20 := fixtures.NewRelease(spaceID, "Releases-20", "2.0", fireProjectID, "Channels-1")

expectProjectLookup := func(t *testing.T, api *testutil.MockHttpServer) {
api.ExpectRequest(t, "GET", "/api/").RespondWith(rootResource)
api.ExpectRequest(t, "GET", "/api/Spaces-1").RespondWith(rootResource)
api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/Fire Project").RespondWithStatus(404, "NotFound", nil)
api.ExpectRequest(t, "GET", "/api/Spaces-1/projects?partialName=Fire+Project").
RespondWith(resources.Resources[*projects.Project]{
Items: []*projects.Project{fireProject},
})
}

tests := []struct {
name string
run func(t *testing.T, api *testutil.MockHttpServer, qa *testutil.AskMocker, rootCmd *cobra.Command, stdOut *bytes.Buffer, stdErr *bytes.Buffer)
}{
{"noprompt: missing --project returns clear error", func(t *testing.T, api *testutil.MockHttpServer, qa *testutil.AskMocker, rootCmd *cobra.Command, stdOut *bytes.Buffer, stdErr *bytes.Buffer) {
cmdReceiver := testutil.GoBegin2(func() (*cobra.Command, error) {
defer api.Close()
rootCmd.SetArgs([]string{"release", "snapshot-variables", "--version", "2.1", "--no-prompt"})
return rootCmd.ExecuteC()
})

api.ExpectRequest(t, "GET", "/api/").RespondWith(rootResource)
api.ExpectRequest(t, "GET", "/api/Spaces-1").RespondWith(rootResource)

_, err := testutil.ReceivePair(cmdReceiver)
assert.EqualError(t, err, "project must be specified")
}},

{"noprompt: missing --version returns clear error", func(t *testing.T, api *testutil.MockHttpServer, qa *testutil.AskMocker, rootCmd *cobra.Command, stdOut *bytes.Buffer, stdErr *bytes.Buffer) {
cmdReceiver := testutil.GoBegin2(func() (*cobra.Command, error) {
defer api.Close()
rootCmd.SetArgs([]string{"release", "snapshot-variables", "--project", fireProject.Name, "--no-prompt"})
return rootCmd.ExecuteC()
})

api.ExpectRequest(t, "GET", "/api/").RespondWith(rootResource)
api.ExpectRequest(t, "GET", "/api/Spaces-1").RespondWith(rootResource)

_, err := testutil.ReceivePair(cmdReceiver)
assert.EqualError(t, err, "version must be specified")
}},

{"noprompt: posts to snapshot-variables endpoint and prints success", func(t *testing.T, api *testutil.MockHttpServer, qa *testutil.AskMocker, rootCmd *cobra.Command, stdOut *bytes.Buffer, stdErr *bytes.Buffer) {
cmdReceiver := testutil.GoBegin2(func() (*cobra.Command, error) {
defer api.Close()
rootCmd.SetArgs([]string{"release", "snapshot-variables", "--project", fireProject.Name, "--version", "2.1", "--no-prompt"})
return rootCmd.ExecuteC()
})

expectProjectLookup(t, api)
api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/Projects-22/releases/2.1").RespondWith(rDefault21)
api.ExpectRequest(t, "POST", "/api/Spaces-1/releases/Releases-21/snapshot-variables").RespondWith(rDefault21)

_, err := testutil.ReceivePair(cmdReceiver)
assert.Nil(t, err)
assert.Contains(t, stdOut.String(), "Successfully updated variable snapshot for release '2.1'")
assert.Contains(t, stdOut.String(), "Releases-21")
assert.NotContains(t, stdOut.String(), "Automation Command:")
assert.Equal(t, "", stdErr.String())
}},

{"noprompt: --releaseNumber is not a recognized flag", func(t *testing.T, api *testutil.MockHttpServer, qa *testutil.AskMocker, rootCmd *cobra.Command, stdOut *bytes.Buffer, stdErr *bytes.Buffer) {
cmdReceiver := testutil.GoBegin2(func() (*cobra.Command, error) {
defer api.Close()
rootCmd.SetArgs([]string{"release", "snapshot-variables", "--project", fireProject.Name, "--releaseNumber", "2.1", "--no-prompt"})
return rootCmd.ExecuteC()
})

_, err := testutil.ReceivePair(cmdReceiver)
assert.Error(t, err)
assert.Contains(t, err.Error(), "unknown flag: --releaseNumber")
}},

{"noprompt: server returns non-2xx status returns wrapped error", func(t *testing.T, api *testutil.MockHttpServer, qa *testutil.AskMocker, rootCmd *cobra.Command, stdOut *bytes.Buffer, stdErr *bytes.Buffer) {
cmdReceiver := testutil.GoBegin2(func() (*cobra.Command, error) {
defer api.Close()
rootCmd.SetArgs([]string{"release", "snapshot-variables", "--project", fireProject.Name, "--version", "2.1", "--no-prompt"})
return rootCmd.ExecuteC()
})

expectProjectLookup(t, api)
api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/Projects-22/releases/2.1").RespondWith(rDefault21)
api.ExpectRequest(t, "POST", "/api/Spaces-1/releases/Releases-21/snapshot-variables").
RespondWithStatus(500, "500 Internal Server Error", &core.APIError{ErrorMessage: "boom"})

_, err := testutil.ReceivePair(cmdReceiver)
assert.Error(t, err)
assert.Contains(t, err.Error(), "Octopus API error")
assert.Contains(t, err.Error(), "boom")
}},

{"interactive: prompts for project and release then posts", func(t *testing.T, api *testutil.MockHttpServer, qa *testutil.AskMocker, rootCmd *cobra.Command, stdOut *bytes.Buffer, stdErr *bytes.Buffer) {
cmdReceiver := testutil.GoBegin2(func() (*cobra.Command, error) {
defer api.Close()
rootCmd.SetArgs([]string{"release", "snapshot-variables"})
return rootCmd.ExecuteC()
})

api.ExpectRequest(t, "GET", "/api/").RespondWith(rootResource)
api.ExpectRequest(t, "GET", "/api/Spaces-1").RespondWith(rootResource)

api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/all").RespondWith([]*projects.Project{fireProject})
_ = qa.ExpectQuestion(t, &survey.Select{
Message: "Select the project containing the release",
Options: []string{fireProject.Name},
}).AnswerWith(fireProject.Name)

api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/Projects-22/releases").
RespondWith(resources.Resources[*releases.Release]{
Items: []*releases.Release{rDefault21, rDefault20},
})
_ = qa.ExpectQuestion(t, &survey.Select{
Message: "Select Release to Update Variables for Progression for",
Options: []string{rDefault21.Version, rDefault20.Version},
}).AnswerWith(rDefault21.Version)

api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/Fire Project").RespondWithStatus(404, "NotFound", nil)
api.ExpectRequest(t, "GET", "/api/Spaces-1/projects?partialName=Fire+Project").
RespondWith(resources.Resources[*projects.Project]{
Items: []*projects.Project{fireProject},
})
api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/Projects-22/releases/2.1").RespondWith(rDefault21)
api.ExpectRequest(t, "POST", "/api/Spaces-1/releases/Releases-21/snapshot-variables").RespondWith(rDefault21)

_, err := testutil.ReceivePair(cmdReceiver)
assert.Nil(t, err)
assert.Contains(t, stdOut.String(), "Successfully updated variable snapshot for release '2.1'")
assert.Contains(t, stdOut.String(), "Automation Command:")
assert.Contains(t, stdOut.String(), "--project 'Fire Project'")
assert.Contains(t, stdOut.String(), "--version '2.1'")
}},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
api, qa := testutil.NewMockServerAndAsker()
askProvider := question.NewAskProvider(qa.AsAsker())
fac := testutil.NewMockFactoryWithSpaceAndPrompt(api, space1, askProvider)
rootCmd := cmdRoot.NewCmdRoot(fac, nil, askProvider)
rootCmd.SetOut(stdout)
rootCmd.SetErr(stderr)
test.run(t, api, qa, rootCmd, stdout, stderr)
})
}
}
2 changes: 2 additions & 0 deletions pkg/cmd/runbook/snapshot/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
cmdCreate "github.com/OctopusDeploy/cli/pkg/cmd/runbook/snapshot/create"
cmdList "github.com/OctopusDeploy/cli/pkg/cmd/runbook/snapshot/list"
cmdPublish "github.com/OctopusDeploy/cli/pkg/cmd/runbook/snapshot/publish"
cmdSnapshotVariables "github.com/OctopusDeploy/cli/pkg/cmd/runbook/snapshot/snapshot_variables"
"github.com/OctopusDeploy/cli/pkg/constants"
"github.com/OctopusDeploy/cli/pkg/factory"
"github.com/spf13/cobra"
Expand All @@ -24,5 +25,6 @@ func NewCmdSnapshot(f factory.Factory) *cobra.Command {
cmd.AddCommand(cmdList.NewCmdList(f))
cmd.AddCommand(cmdCreate.NewCmdCreate(f))
cmd.AddCommand(cmdPublish.NewCmdPublish(f))
cmd.AddCommand(cmdSnapshotVariables.NewCmdSnapshotVariables(f))
return cmd
}
Loading