diff --git a/command/v7/copy_source_command.go b/command/v7/copy_source_command.go index 9d18a329d8..f8591df6cd 100644 --- a/command/v7/copy_source_command.go +++ b/command/v7/copy_source_command.go @@ -17,6 +17,7 @@ type CopySourceCommand struct { RequiredArgs flag.CopySourceArgs `positional-args:"yes"` usage interface{} `usage:"CF_NAME copy-source SOURCE_APP DESTINATION_APP [-s TARGET_SPACE [-o TARGET_ORG]] [--no-restart] [--strategy STRATEGY] [--no-wait]"` Strategy flag.DeploymentStrategy `long:"strategy" description:"Deployment strategy can be canary, rolling or null"` + MaxInFlight int `long:"max-in-flight" default:"-1" description:"Defines the maximum number of instances that will be actively being started. Only applies when --strategy flag is specified."` NoWait bool `long:"no-wait" description:"Exit when the first instance of the web process is healthy"` NoRestart bool `long:"no-restart" description:"Do not restage the destination application"` Organization string `short:"o" long:"organization" description:"Org that contains the destination application"` @@ -48,6 +49,14 @@ func (cmd *CopySourceCommand) ValidateFlags() error { } } + if cmd.Strategy.Name == constant.DeploymentStrategyDefault && cmd.MaxInFlight > 0 { + return translatableerror.RequiredFlagsError{Arg1: "--max-in-flight", Arg2: "--strategy"} + } + + if cmd.Strategy.Name != constant.DeploymentStrategyDefault && cmd.MaxInFlight < 1 { + return translatableerror.IncorrectUsageError{Message: "--max-in-flight must be greater than or equal to 1"} + } + return nil } @@ -160,9 +169,10 @@ func (cmd CopySourceCommand) Execute(args []string) error { cmd.UI.DisplayNewline() opts := shared.AppStartOpts{ - Strategy: cmd.Strategy.Name, - NoWait: cmd.NoWait, - AppAction: constant.ApplicationRestarting, + AppAction: constant.ApplicationRestarting, + MaxInFlight: cmd.MaxInFlight, + NoWait: cmd.NoWait, + Strategy: cmd.Strategy.Name, } err = cmd.Stager.StageAndStart(targetApp, targetSpace, targetOrg, pkg.GUID, opts) if err != nil { diff --git a/command/v7/copy_source_command_test.go b/command/v7/copy_source_command_test.go index 2afadbdb1b..f0df517f1d 100644 --- a/command/v7/copy_source_command_test.go +++ b/command/v7/copy_source_command_test.go @@ -125,49 +125,6 @@ var _ = Describe("copy-source Command", func() { }) }) - When("the target organization is specified but the targeted space isn't", func() { - BeforeEach(func() { - cmd.Organization = "some-other-organization" - }) - - It("returns an error", func() { - Expect(executeErr).To(MatchError(translatableerror.RequiredFlagsError{ - Arg1: "--organization, -o", - Arg2: "--space, -s", - })) - }) - }) - - When("the no restart and strategy flags are both provided", func() { - BeforeEach(func() { - cmd.NoRestart = true - cmd.Strategy = flag.DeploymentStrategy{Name: constant.DeploymentStrategyRolling} - }) - - It("returns an error", func() { - Expect(executeErr).To(MatchError(translatableerror.ArgumentCombinationError{ - Args: []string{ - "--no-restart", "--strategy", - }, - })) - }) - }) - - When("the no restart and no wait flags are both provided", func() { - BeforeEach(func() { - cmd.NoRestart = true - cmd.NoWait = true - }) - - It("returns an error", func() { - Expect(executeErr).To(MatchError(translatableerror.ArgumentCombinationError{ - Args: []string{ - "--no-restart", "--no-wait", - }, - })) - }) - }) - When("a target org and space is provided", func() { BeforeEach(func() { cmd.Organization = "destination-org" @@ -329,6 +286,7 @@ var _ = Describe("copy-source Command", func() { cmd.Strategy = flag.DeploymentStrategy{ Name: constant.DeploymentStrategyRolling, } + cmd.MaxInFlight = 5 }) It("stages and starts the app with the appropriate strategy", func() { @@ -338,9 +296,10 @@ var _ = Describe("copy-source Command", func() { Expect(spaceForApp).To(Equal(configv3.Space{Name: "some-space", GUID: "some-space-guid"})) Expect(orgForApp).To(Equal(configv3.Organization{Name: "some-org"})) Expect(pkgGUID).To(Equal("target-package-guid")) - Expect(opts.Strategy).To(Equal(constant.DeploymentStrategyRolling)) - Expect(opts.NoWait).To(Equal(false)) Expect(opts.AppAction).To(Equal(constant.ApplicationRestarting)) + Expect(opts.MaxInFlight).To(Equal(5)) + Expect(opts.NoWait).To(Equal(false)) + Expect(opts.Strategy).To(Equal(constant.DeploymentStrategyRolling)) }) }) @@ -349,6 +308,7 @@ var _ = Describe("copy-source Command", func() { cmd.Strategy = flag.DeploymentStrategy{ Name: constant.DeploymentStrategyCanary, } + cmd.MaxInFlight = 1 }) It("stages and starts the app with the appropriate strategy", func() { @@ -417,4 +377,64 @@ var _ = Describe("copy-source Command", func() { It("succeeds", func() { Expect(executeErr).To(Not(HaveOccurred())) }) + + DescribeTable("ValidateFlags returns an error", + func(setup func(), expectedErr error) { + setup() + err := cmd.ValidateFlags() + if expectedErr == nil { + Expect(err).To(BeNil()) + } else { + Expect(err).To(MatchError(expectedErr)) + } + }, + Entry("the target organization is specified but the targeted space isn't", + func() { + cmd.Organization = "some-other-organization" + }, + translatableerror.RequiredFlagsError{ + Arg1: "--organization, -o", + Arg2: "--space, -s", + }), + + Entry("the no restart and strategy flags are both provided", + func() { + cmd.NoRestart = true + cmd.Strategy = flag.DeploymentStrategy{Name: constant.DeploymentStrategyRolling} + }, + translatableerror.ArgumentCombinationError{ + Args: []string{ + "--no-restart", "--strategy", + }, + }), + + Entry("the no restart and no wait flags are both provided", + func() { + cmd.NoRestart = true + cmd.NoWait = true + }, + translatableerror.ArgumentCombinationError{ + Args: []string{ + "--no-restart", "--no-wait", + }, + }), + + Entry("max-in-flight is passed without strategy", + func() { + cmd.MaxInFlight = 10 + }, + translatableerror.RequiredFlagsError{ + Arg1: "--max-in-flight", + Arg2: "--strategy", + }), + + Entry("max-in-flight is smaller than 1", + func() { + cmd.Strategy = flag.DeploymentStrategy{Name: constant.DeploymentStrategyRolling} + cmd.MaxInFlight = 0 + }, + translatableerror.IncorrectUsageError{ + Message: "--max-in-flight must be greater than or equal to 1", + }), + ) }) diff --git a/integration/v7/isolated/copy_source_command_test.go b/integration/v7/isolated/copy_source_command_test.go index 8389142263..bd8829140c 100644 --- a/integration/v7/isolated/copy_source_command_test.go +++ b/integration/v7/isolated/copy_source_command_test.go @@ -449,6 +449,7 @@ func helpText(session *Session) { Eventually(session).Should(Say(`cf copy-source SOURCE_APP DESTINATION_APP \[-s TARGET_SPACE \[-o TARGET_ORG\]\] \[--no-restart\] \[--strategy STRATEGY\] \[--no-wait\]`)) Eventually(session).Should(Say("OPTIONS:")) Eventually(session).Should(Say(`--strategy\s+Deployment strategy can be canary, rolling or null`)) + Eventually(session).Should(Say(`--max-in-flight\s+Defines the maximum number of instances`)) Eventually(session).Should(Say(`--no-wait\s+ Exit when the first instance of the web process is healthy`)) Eventually(session).Should(Say(`--no-restart\s+Do not restage the destination application`)) Eventually(session).Should(Say(`--organization, -o\s+Org that contains the destination application`))