Skip to content
Draft
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
2 changes: 1 addition & 1 deletion pkg/cmd/channel/delete/delete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ func TestChannelDelete(t *testing.T) {

// No DELETE request is expected; api.Close() asserts nothing further was requested.
_, err := testutil.ReceivePair(cmdReceiver)
assert.EqualError(t, err, "no channel found with name of Channels-99")
assert.EqualError(t, err, "cannot find a channel in project 'Fire Project' with the ID or name of 'Channels-99'")

assert.Equal(t, "", stdErr.String())
}},
Expand Down
4 changes: 2 additions & 2 deletions pkg/cmd/channel/view/view_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ func TestChannelView(t *testing.T) {
})

_, err := testutil.ReceivePair(cmdReceiver)
assert.EqualError(t, err, "no channel found with name of Channels-99")
assert.EqualError(t, err, "cannot find a channel in project 'Fire Project' with the ID or name of 'Channels-99'")

assert.Equal(t, "", stdOut.String())
assert.Equal(t, "", stdErr.String())
Expand All @@ -262,7 +262,7 @@ func TestChannelView(t *testing.T) {
})

_, err := testutil.ReceivePair(cmdReceiver)
assert.EqualError(t, err, "no channel found with name of Nonexistent")
assert.EqualError(t, err, "cannot find a channel in project 'Fire Project' with the ID or name of 'Nonexistent'")

assert.Equal(t, "", stdOut.String())
assert.Equal(t, "", stdErr.String())
Expand Down
8 changes: 8 additions & 0 deletions pkg/cmd/release/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,14 @@ func createRun(cmd *cobra.Command, f factory.Factory, flags *CreateFlags) error
return err
}
options.ProjectName = project.GetName()

if options.ChannelName != "" { // the executions API only matches channels by name, so resolve any ID we were given
channel, err := selectors.FindChannel(octopus, project, options.ChannelName)
if err != nil {
return err
}
options.ChannelName = channel.Name
}
}
}

Expand Down
64 changes: 64 additions & 0 deletions pkg/cmd/release/create/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1209,6 +1209,7 @@ func TestReleaseCreate_AutomationMode(t *testing.T) {

protectedBranchNamePatterns := []string{}
cacProject := fixtures.NewProject(space1.ID, cacProjectID, "CaC Project", "Lifecycles-1", "ProjectGroups-1", depProcess.ID)
betaChannel := fixtures.NewChannel(space1.ID, "Channels-31", "BetaChannel", cacProjectID)
cacProject.PersistenceSettings = projects.NewGitPersistenceSettings(
".octopus",
credentials.NewAnonymous(),
Expand Down Expand Up @@ -1588,6 +1589,53 @@ func TestReleaseCreate_AutomationMode(t *testing.T) {
assert.EqualError(t, err, "cannot specify both --release-notes and --release-notes-file at the same time")
}},

{"release creation specifying the project and channel by ID", func(t *testing.T, api *testutil.MockHttpServer, rootCmd *cobra.Command, stdOut *bytes.Buffer, stdErr *bytes.Buffer) {
cmdReceiver := testutil.GoBegin2(func() (*cobra.Command, error) {
defer api.Close()
rootCmd.SetArgs([]string{"release", "create", "--project", cacProjectID, "--channel", betaChannel.ID})
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/"+cacProjectID).RespondWith(cacProject)

api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/"+cacProjectID+"/channels").RespondWith(resources.Resources[*channels.Channel]{
Items: []*channels.Channel{betaChannel},
})

req := api.ExpectRequest(t, "POST", "/api/Spaces-1/releases/create/v1")

// the executions API only matches channels by name, so the ID must have been resolved before we got here
requestBody, err := testutil.ReadJson[releases.CreateReleaseCommandV1](req.Request.Body)
assert.Nil(t, err)

assert.Equal(t, releases.CreateReleaseCommandV1{
SpaceID: "Spaces-1",
ProjectIDOrName: cacProject.Name,
ChannelIDOrName: betaChannel.Name,
}, requestBody)

req.RespondWith(&releases.CreateReleaseResponseV1{
ReleaseID: "Releases-999",
ReleaseVersion: "1.2.3",
})

releaseInfo := releases.NewRelease(betaChannel.ID, cacProject.ID, "1.2.3")
api.ExpectRequest(t, "GET", "/api/Spaces-1/releases/Releases-999").RespondWith(releaseInfo)
api.ExpectRequest(t, "GET", "/api/Spaces-1/channels/"+betaChannel.ID).RespondWith(betaChannel)

_, err = testutil.ReceivePair(cmdReceiver)
assert.Nil(t, err)

assert.Equal(t, heredoc.Doc(`
Successfully created release version 1.2.3 using channel BetaChannel

View this release on Octopus Deploy: http://server/app#/Spaces-1/releases/Releases-999
`), stdOut.String())
assert.Equal(t, "", stdErr.String())
}},

{"release creation with all the flags", func(t *testing.T, api *testutil.MockHttpServer, rootCmd *cobra.Command, stdOut *bytes.Buffer, stdErr *bytes.Buffer) {
cmdReceiver := testutil.GoBegin2(func() (*cobra.Command, error) {
defer api.Close()
Expand All @@ -1611,6 +1659,10 @@ func TestReleaseCreate_AutomationMode(t *testing.T) {
api.ExpectRequest(t, "GET", "/api/Spaces-1").RespondWith(rootResource)
api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/"+cacProject.GetName()).RespondWith(cacProject)

api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/"+cacProjectID+"/channels").RespondWith(resources.Resources[*channels.Channel]{
Items: []*channels.Channel{betaChannel},
})

req := api.ExpectRequest(t, "POST", "/api/Spaces-1/releases/create/v1")

// check that it sent the server the right request body
Expand Down Expand Up @@ -1682,6 +1734,10 @@ func TestReleaseCreate_AutomationMode(t *testing.T) {
api.ExpectRequest(t, "GET", "/api/Spaces-1").RespondWith(rootResource)
api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/"+cacProject.GetName()).RespondWith(cacProject)

api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/"+cacProjectID+"/channels").RespondWith(resources.Resources[*channels.Channel]{
Items: []*channels.Channel{betaChannel},
})

req := api.ExpectRequest(t, "POST", "/api/Spaces-1/releases/create/v1")

// check that it sent the server the right request body
Expand Down Expand Up @@ -1748,6 +1804,10 @@ func TestReleaseCreate_AutomationMode(t *testing.T) {
api.ExpectRequest(t, "GET", "/api/Spaces-1").RespondWith(rootResource)
api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/"+cacProject.GetName()).RespondWith(cacProject)

api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/"+cacProjectID+"/channels").RespondWith(resources.Resources[*channels.Channel]{
Items: []*channels.Channel{betaChannel},
})

req := api.ExpectRequest(t, "POST", "/api/Spaces-1/releases/create/v1")

// check that it sent the server the right request body
Expand Down Expand Up @@ -1817,6 +1877,10 @@ func TestReleaseCreate_AutomationMode(t *testing.T) {
api.ExpectRequest(t, "GET", "/api/Spaces-1").RespondWith(rootResource)
api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/"+cacProject.GetName()).RespondWith(cacProject)

api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/"+cacProjectID+"/channels").RespondWith(resources.Resources[*channels.Channel]{
Items: []*channels.Channel{betaChannel},
})

req := api.ExpectRequest(t, "POST", "/api/Spaces-1/releases/create/v1")

// check that it sent the server the right request body
Expand Down
53 changes: 45 additions & 8 deletions pkg/cmd/release/deploy/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/releases"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/spaces"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tenants"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/variables"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -237,6 +238,15 @@ func deployRun(cmd *cobra.Command, f factory.Factory, flags *DeployFlags) error
options.ForcePackageDownloadWasSpecified = true
}

// the executions API only matches tenants by name, so resolve any IDs we were given
if len(options.Tenants) > 0 {
selectedTenants, err := selectors.FindTenants(octopus, options.Tenants)
if err != nil {
return err
}
options.Tenants = util.SliceTransform(selectedTenants, func(t *tenants.Tenant) string { return t.Name })
}

if f.IsPromptEnabled() {
now := time.Now
if cmd.Context() != nil { // allow context to override the definition of 'now' for testing
Expand Down Expand Up @@ -319,6 +329,13 @@ func deployRun(cmd *cobra.Command, f factory.Factory, flags *DeployFlags) error
options.ProjectName = project.GetName()
}

// the executions API only matches environments by name, so resolve any IDs we were given
if len(options.Environments) > 0 {
options.Environments, err = resolveEnvironmentNames(octopus, f.GetCurrentSpace(), options.Environments)
if err != nil {
return err
}
}
}

// the executor will raise errors if any required options are missing
Expand Down Expand Up @@ -474,18 +491,21 @@ func AskQuestions(octopus *octopusApiClient.Client, stdout io.Writer, asker ques

if len(deploymentEnvironmentIDs) == 0 { // if the Q&A process earlier hasn't loaded environments already, we need to load them now
if selectedChannel.Type == channels.ChannelTypeLifecycle {
selectedEnvironments, err := executionscommon.FindEnvironments(octopus, options.Environments)
selectedEnvironments, err := selectors.FindEnvironments(octopus, options.Environments)
if err != nil {
return err
}

deploymentEnvironmentIDs = util.SliceTransform(selectedEnvironments, func(env *environments.Environment) string { return env.ID })
options.Environments = util.SliceTransform(selectedEnvironments, func(env *environments.Environment) string { return env.Name })
} else if selectedChannel.Type == channels.ChannelTypeEphemeral {
deploymentEnvironmentIDs, err = findEphemeralEnvironmentIDs(octopus, space, options.Environments)

selectedEnvironments, err := findEphemeralEnvironments(octopus, space, options.Environments)
if err != nil {
return err
}

deploymentEnvironmentIDs = util.SliceTransform(selectedEnvironments, func(env *ephemeralenvironments.EphemeralEnvironment) string { return env.ID })
options.Environments = util.SliceTransform(selectedEnvironments, func(env *ephemeralenvironments.EphemeralEnvironment) string { return env.Name })
}
}

Expand Down Expand Up @@ -622,7 +642,7 @@ func AskQuestions(octopus *octopusApiClient.Client, stdout io.Writer, asker ques
return nil
}

func findEphemeralEnvironmentIDs(octopus *octopusApiClient.Client, space *spaces.Space, environments []string) ([]string, error) {
func findEphemeralEnvironments(octopus *octopusApiClient.Client, space *spaces.Space, environmentIdentifiers []string) ([]*ephemeralenvironments.EphemeralEnvironment, error) {
allEphemeralEnvironments, err := ephemeralenvironments.GetAll(octopus, space.ID)
if err != nil {
return nil, err
Expand All @@ -632,8 +652,8 @@ func findEphemeralEnvironmentIDs(octopus *octopusApiClient.Client, space *spaces
return nil, errors.New("no ephemeral environments exist to deploy to")
}

var selectedEnvironments []string
if len(environments) == 0 {
var selectedEnvironments []*ephemeralenvironments.EphemeralEnvironment
if len(environmentIdentifiers) == 0 {
return nil, nil
}

Expand All @@ -643,17 +663,33 @@ func findEphemeralEnvironmentIDs(octopus *octopusApiClient.Client, space *spaces
envMap[strings.ToLower(ephemeralEnv.Name)] = ephemeralEnv
}

for _, envIdentifier := range environments {
for _, envIdentifier := range environmentIdentifiers {
ephemeralEnv, found := envMap[strings.ToLower(envIdentifier)]
if !found {
return nil, fmt.Errorf("environment '%s' not found in ephemeral environments", envIdentifier)
}
selectedEnvironments = append(selectedEnvironments, ephemeralEnv.ID)
selectedEnvironments = append(selectedEnvironments, ephemeralEnv)
}

return selectedEnvironments, nil
}

// resolveEnvironmentNames maps environment names or IDs onto canonical environment names, because
// the executions API only matches environments by name. Ephemeral environments aren't part of the
// regular environment list, so they're looked up separately when the regular lookup comes up empty.
func resolveEnvironmentNames(octopus *octopusApiClient.Client, space *spaces.Space, environmentIdentifiers []string) ([]string, error) {
selectedEnvironments, err := selectors.FindEnvironments(octopus, environmentIdentifiers)
if err == nil {
return util.SliceTransform(selectedEnvironments, func(env *environments.Environment) string { return env.Name }), nil
}

ephemeralEnvironments, ephemeralErr := findEphemeralEnvironments(octopus, space, environmentIdentifiers)
if ephemeralErr != nil {
return nil, err // ephemeral environments are the rarer case; report why the regular lookup failed
}
return util.SliceTransform(ephemeralEnvironments, func(env *ephemeralenvironments.EphemeralEnvironment) string { return env.Name }), nil
}

func selectDeploymentEnvironmentsForEphemeralChannel(octopus *octopusApiClient.Client, stdout io.Writer, asker question.Asker, options *executor.TaskOptionsDeployRelease, selectedRelease *releases.Release) ([]string, error) {
var deploymentEnvironmentIds []string
var selectedEnvironments []*ephemeralenvironments.EphemeralEnvironment
Expand Down Expand Up @@ -721,6 +757,7 @@ func selectDeploymentEnvironmentsForLifecycleChannel(octopus *octopusApiClient.C
if err != nil {
return nil, err
}
options.Environments = []string{selectedEnvironment.Name}
_, _ = fmt.Fprintf(stdout, "Environment %s\n", output.Cyan(selectedEnvironment.Name))
}
selectedEnvironments = []*environments.Environment{selectedEnvironment}
Expand Down
Loading