diff --git a/actor/v7pushaction/actor.go b/actor/v7pushaction/actor.go index f2c74ef9cea..829b64e2a19 100644 --- a/actor/v7pushaction/actor.go +++ b/actor/v7pushaction/actor.go @@ -71,7 +71,7 @@ func NewActor(v3Actor V7Actor, sharedActor SharedActor) *Actor { SetDefaultBitsPathForPushPlan, SetupDropletPathForPushPlan, actor.SetupAllResourcesForPushPlan, - SetupDeploymentStrategyForPushPlan, + SetupDeploymentInformationForPushPlan, SetupNoStartForPushPlan, SetupNoWaitForPushPlan, SetupTaskAppForPushPlan, diff --git a/actor/v7pushaction/actor_test.go b/actor/v7pushaction/actor_test.go index d3ebc799729..32f314564a4 100644 --- a/actor/v7pushaction/actor_test.go +++ b/actor/v7pushaction/actor_test.go @@ -22,7 +22,7 @@ var _ = Describe("Actor", func() { SetDefaultBitsPathForPushPlan, SetupDropletPathForPushPlan, actor.SetupAllResourcesForPushPlan, - SetupDeploymentStrategyForPushPlan, + SetupDeploymentInformationForPushPlan, SetupNoStartForPushPlan, SetupNoWaitForPushPlan, SetupTaskAppForPushPlan, diff --git a/actor/v7pushaction/create_deployment_for_push_plan.go b/actor/v7pushaction/create_deployment_for_push_plan.go index 241568f8141..5b23178c3a5 100644 --- a/actor/v7pushaction/create_deployment_for_push_plan.go +++ b/actor/v7pushaction/create_deployment_for_push_plan.go @@ -8,10 +8,16 @@ import ( func (actor Actor) CreateDeploymentForApplication(pushPlan PushPlan, eventStream chan<- *PushEvent, progressBar ProgressBar) (PushPlan, Warnings, error) { eventStream <- &PushEvent{Plan: pushPlan, Event: StartingDeployment} - var dep resources.Deployment - dep.DropletGUID = pushPlan.DropletGUID - dep.Strategy = pushPlan.Strategy - dep.Relationships = resources.Relationships{constant.RelationshipTypeApplication: resources.Relationship{GUID: pushPlan.Application.GUID}} + dep := resources.Deployment{ + Strategy: pushPlan.Strategy, + DropletGUID: pushPlan.DropletGUID, + Relationships: resources.Relationships{constant.RelationshipTypeApplication: resources.Relationship{GUID: pushPlan.Application.GUID}}, + } + + if pushPlan.MaxInFlight != 0 { + dep.Options = resources.DeploymentOpts{MaxInFlight: pushPlan.MaxInFlight} + } + deploymentGUID, warnings, err := actor.V7Actor.CreateDeployment(dep) if err != nil { diff --git a/actor/v7pushaction/create_deployment_for_push_plan_test.go b/actor/v7pushaction/create_deployment_for_push_plan_test.go index 9bbcfa30578..46031d80dc8 100644 --- a/actor/v7pushaction/create_deployment_for_push_plan_test.go +++ b/actor/v7pushaction/create_deployment_for_push_plan_test.go @@ -6,6 +6,7 @@ import ( "code.cloudfoundry.org/cli/actor/v7action" . "code.cloudfoundry.org/cli/actor/v7pushaction" "code.cloudfoundry.org/cli/actor/v7pushaction/v7pushactionfakes" + "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/constant" "code.cloudfoundry.org/cli/resources" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -106,6 +107,51 @@ var _ = Describe("CreateDeploymentForApplication()", func() { Expect(events).To(ConsistOf(StartingDeployment)) }) }) + + When("strategy is provided", func() { + BeforeEach(func() { + fakeV7Actor.PollStartForDeploymentCalls(func(_ resources.Application, _ string, _ bool, handleInstanceDetails func(string)) (warnings v7action.Warnings, err error) { + handleInstanceDetails("Instances starting...") + return nil, nil + }) + + fakeV7Actor.CreateDeploymentReturns( + "some-deployment-guid", + v7action.Warnings{"some-deployment-warning"}, + nil, + ) + paramPlan.Strategy = "rolling" + paramPlan.MaxInFlight = 10 + }) + + It("waits for the app to start", func() { + Expect(fakeV7Actor.PollStartForDeploymentCallCount()).To(Equal(1)) + givenApp, givenDeploymentGUID, noWait, _ := fakeV7Actor.PollStartForDeploymentArgsForCall(0) + Expect(givenApp).To(Equal(resources.Application{GUID: "some-app-guid"})) + Expect(givenDeploymentGUID).To(Equal("some-deployment-guid")) + Expect(noWait).To(Equal(false)) + Expect(events).To(ConsistOf(StartingDeployment, InstanceDetails, WaitingForDeployment)) + Expect(fakeV7Actor.CreateDeploymentCallCount()).To(Equal(1)) + dep := fakeV7Actor.CreateDeploymentArgsForCall(0) + Expect(dep).To(Equal(resources.Deployment{ + Strategy: "rolling", + Options: resources.DeploymentOpts{MaxInFlight: 10}, + Relationships: resources.Relationships{ + constant.RelationshipTypeApplication: resources.Relationship{GUID: "some-app-guid"}, + }, + })) + }) + + It("returns errors and warnings", func() { + Expect(returnedPushPlan).To(Equal(paramPlan)) + Expect(executeErr).NotTo(HaveOccurred()) + Expect(warnings).To(ConsistOf("some-deployment-warning")) + }) + + It("records deployment events", func() { + Expect(events).To(ConsistOf(StartingDeployment, InstanceDetails, WaitingForDeployment)) + }) + }) }) Describe("waiting for app to start", func() { diff --git a/actor/v7pushaction/push_plan.go b/actor/v7pushaction/push_plan.go index eacb52d577a..876a8ec2259 100644 --- a/actor/v7pushaction/push_plan.go +++ b/actor/v7pushaction/push_plan.go @@ -20,6 +20,7 @@ type PushPlan struct { NoStart bool NoWait bool Strategy constant.DeploymentStrategy + MaxInFlight int TaskTypeApplication bool DockerImageCredentials v7action.DockerImageCredentials @@ -47,6 +48,7 @@ type FlagOverrides struct { HealthCheckType constant.HealthCheckType Instances types.NullInt Memory string + MaxInFlight *int NoStart bool NoWait bool ProvidedAppPath string diff --git a/actor/v7pushaction/setup_deployment_information_for_push_plan.go b/actor/v7pushaction/setup_deployment_information_for_push_plan.go new file mode 100644 index 00000000000..11e4dd26d8e --- /dev/null +++ b/actor/v7pushaction/setup_deployment_information_for_push_plan.go @@ -0,0 +1,13 @@ +package v7pushaction + +import "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/constant" + +func SetupDeploymentInformationForPushPlan(pushPlan PushPlan, overrides FlagOverrides) (PushPlan, error) { + pushPlan.Strategy = overrides.Strategy + + if overrides.Strategy != constant.DeploymentStrategyDefault && overrides.MaxInFlight != nil { + pushPlan.MaxInFlight = *overrides.MaxInFlight + } + + return pushPlan, nil +} diff --git a/actor/v7pushaction/setup_deployment_strategy_for_push_plan_test.go b/actor/v7pushaction/setup_deployment_information_for_push_plan_test.go similarity index 55% rename from actor/v7pushaction/setup_deployment_strategy_for_push_plan_test.go rename to actor/v7pushaction/setup_deployment_information_for_push_plan_test.go index 5d0d8df0aba..14515c9f446 100644 --- a/actor/v7pushaction/setup_deployment_strategy_for_push_plan_test.go +++ b/actor/v7pushaction/setup_deployment_information_for_push_plan_test.go @@ -9,7 +9,7 @@ import ( . "github.com/onsi/gomega" ) -var _ = Describe("SetupDeploymentStrategyForPushPlan", func() { +var _ = Describe("SetupDeploymentInformationForPushPlan", func() { var ( pushPlan PushPlan overrides FlagOverrides @@ -24,24 +24,47 @@ var _ = Describe("SetupDeploymentStrategyForPushPlan", func() { }) JustBeforeEach(func() { - expectedPushPlan, executeErr = SetupDeploymentStrategyForPushPlan(pushPlan, overrides) + expectedPushPlan, executeErr = SetupDeploymentInformationForPushPlan(pushPlan, overrides) }) When("flag overrides specifies strategy", func() { BeforeEach(func() { overrides.Strategy = "rolling" + maxInFlight := 5 + overrides.MaxInFlight = &maxInFlight }) It("sets the strategy on the push plan", func() { Expect(executeErr).ToNot(HaveOccurred()) Expect(expectedPushPlan.Strategy).To(Equal(constant.DeploymentStrategyRolling)) }) + + It("sets the max in flight on the push plan", func() { + Expect(executeErr).ToNot(HaveOccurred()) + Expect(expectedPushPlan.MaxInFlight).To(Equal(5)) + }) }) When("flag overrides does not specify strategy", func() { + BeforeEach(func() { + maxInFlight := 10 + overrides.MaxInFlight = &maxInFlight + }) It("leaves the strategy as its default value on the push plan", func() { Expect(executeErr).ToNot(HaveOccurred()) Expect(expectedPushPlan.Strategy).To(Equal(constant.DeploymentStrategyDefault)) }) + + It("does not set MaxInFlight", func() { + Expect(executeErr).ToNot(HaveOccurred()) + Expect(expectedPushPlan.MaxInFlight).To(Equal(0)) + }) + }) + + When("flag not provided", func() { + It("does not set MaxInFlight", func() { + Expect(executeErr).ToNot(HaveOccurred()) + Expect(expectedPushPlan.MaxInFlight).To(Equal(0)) + }) }) }) diff --git a/actor/v7pushaction/setup_deployment_strategy_for_push_plan.go b/actor/v7pushaction/setup_deployment_strategy_for_push_plan.go deleted file mode 100644 index 0e6c2526342..00000000000 --- a/actor/v7pushaction/setup_deployment_strategy_for_push_plan.go +++ /dev/null @@ -1,7 +0,0 @@ -package v7pushaction - -func SetupDeploymentStrategyForPushPlan(pushPlan PushPlan, overrides FlagOverrides) (PushPlan, error) { - pushPlan.Strategy = overrides.Strategy - - return pushPlan, nil -} diff --git a/command/translatableerror/convert_to_translatable_error.go b/command/translatableerror/convert_to_translatable_error.go index 4bcacac1e35..f8636a7c5ec 100644 --- a/command/translatableerror/convert_to_translatable_error.go +++ b/command/translatableerror/convert_to_translatable_error.go @@ -188,6 +188,13 @@ func ConvertToTranslatableError(err error) error { return RunTaskError{Message: "App is not staged."} } + if strings.Contains(e.Message, "Unknown field(s): 'options'") { + return MinimumCFAPIVersionNotMetError{ + Command: "'--max-in-flight' flag", + MinimumVersion: "3.173.0", + } + } + // JSON Errors case *json.SyntaxError: return JSONSyntaxError{Err: e} diff --git a/command/v7/copy_source_command.go b/command/v7/copy_source_command.go index 9d18a329d83..c4ae96eeef1 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" 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 != nil { + return translatableerror.RequiredFlagsError{Arg1: "--max-in-flight", Arg2: "--strategy"} + } + + if cmd.Strategy.Name != constant.DeploymentStrategyDefault && cmd.MaxInFlight != nil && *cmd.MaxInFlight < 1 { + return translatableerror.IncorrectUsageError{Message: "--max-in-flight must be greater than or equal to 1"} + } + return nil } @@ -160,10 +169,15 @@ func (cmd CopySourceCommand) Execute(args []string) error { cmd.UI.DisplayNewline() opts := shared.AppStartOpts{ - Strategy: cmd.Strategy.Name, - NoWait: cmd.NoWait, AppAction: constant.ApplicationRestarting, + NoWait: cmd.NoWait, + Strategy: cmd.Strategy.Name, } + + if cmd.MaxInFlight != nil { + opts.MaxInFlight = *cmd.MaxInFlight + } + err = cmd.Stager.StageAndStart(targetApp, targetSpace, targetOrg, pkg.GUID, opts) if err != nil { return mapErr(cmd.Config, targetApp.Name, err) diff --git a/command/v7/copy_source_command_test.go b/command/v7/copy_source_command_test.go index 2afadbdb1b0..9d3787dc6ff 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,8 @@ var _ = Describe("copy-source Command", func() { cmd.Strategy = flag.DeploymentStrategy{ Name: constant.DeploymentStrategyRolling, } + maxInFlight := 5 + cmd.MaxInFlight = &maxInFlight }) It("stages and starts the app with the appropriate strategy", func() { @@ -338,9 +297,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 +309,8 @@ var _ = Describe("copy-source Command", func() { cmd.Strategy = flag.DeploymentStrategy{ Name: constant.DeploymentStrategyCanary, } + maxInFlight := 1 + cmd.MaxInFlight = &maxInFlight }) It("stages and starts the app with the appropriate strategy", func() { @@ -417,4 +379,66 @@ 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() { + maxInFlight := 5 + cmd.MaxInFlight = &maxInFlight + }, + translatableerror.RequiredFlagsError{ + Arg1: "--max-in-flight", + Arg2: "--strategy", + }), + + Entry("max-in-flight is smaller than 1", + func() { + cmd.Strategy = flag.DeploymentStrategy{Name: constant.DeploymentStrategyRolling} + maxInFlight := 0 + cmd.MaxInFlight = &maxInFlight + }, + translatableerror.IncorrectUsageError{ + Message: "--max-in-flight must be greater than or equal to 1", + }), + ) }) diff --git a/command/v7/push_command.go b/command/v7/push_command.go index 12e0001b15e..cbf71d78e78 100644 --- a/command/v7/push_command.go +++ b/command/v7/push_command.go @@ -90,6 +90,7 @@ type PushCommand struct { Instances flag.Instances `long:"instances" short:"i" description:"Number of instances"` LogRateLimit string `long:"log-rate-limit" short:"l" description:"Log rate limit per second, in bytes (e.g. 128B, 4K, 1M). -l=-1 represents unlimited"` PathToManifest flag.ManifestPathWithExistenceCheck `long:"manifest" short:"f" description:"Path to manifest"` + MaxInFlight *int `long:"max-in-flight" description:"Defines the maximum number of instances that will be actively being started. Only applies when --strategy flag is specified."` Memory string `long:"memory" short:"m" description:"Memory limit (e.g. 256M, 1024M, 1G)"` NoManifest bool `long:"no-manifest" description:"Ignore manifest file"` NoRoute bool `long:"no-route" description:"Do not map a route to this app"` @@ -347,6 +348,7 @@ func (cmd PushCommand) GetFlagOverrides() (v7pushaction.FlagOverrides, error) { HealthCheckType: cmd.HealthCheckType.Type, HealthCheckTimeout: cmd.HealthCheckTimeout.Value, Instances: cmd.Instances.NullInt, + MaxInFlight: cmd.MaxInFlight, Memory: cmd.Memory, NoStart: cmd.NoStart, NoWait: cmd.NoWait, @@ -485,6 +487,11 @@ func (cmd PushCommand) ValidateFlags() error { } case !cmd.validBuildpacks(): return translatableerror.InvalidBuildpacksError{} + + case cmd.Strategy.Name == constant.DeploymentStrategyDefault && cmd.MaxInFlight != nil: + return translatableerror.RequiredFlagsError{Arg1: "--max-in-flight", Arg2: "--strategy"} + case cmd.Strategy.Name != constant.DeploymentStrategyDefault && cmd.MaxInFlight != nil && *cmd.MaxInFlight < 1: + return translatableerror.IncorrectUsageError{Message: "--max-in-flight must be greater than or equal to 1"} } return nil diff --git a/command/v7/push_command_test.go b/command/v7/push_command_test.go index 00a464644cc..ead21520928 100644 --- a/command/v7/push_command_test.go +++ b/command/v7/push_command_test.go @@ -1072,6 +1072,8 @@ var _ = Describe("push Command", func() { cmd.RandomRoute = false cmd.NoStart = true cmd.NoWait = true + maxInFlight := 1 + cmd.MaxInFlight = &maxInFlight cmd.Strategy = flag.DeploymentStrategy{Name: constant.DeploymentStrategyRolling} cmd.Instances = flag.Instances{NullInt: types.NullInt{Value: 10, IsSet: true}} cmd.PathToManifest = "/manifest/path" @@ -1108,6 +1110,7 @@ var _ = Describe("push Command", func() { Expect(overrides.Vars).To(Equal([]template.VarKV{{Name: "key", Value: "val"}})) Expect(overrides.Task).To(BeTrue()) Expect(overrides.LogRateLimit).To(Equal("512M")) + Expect(*overrides.MaxInFlight).To(Equal(1)) }) When("a docker image is provided", func() { @@ -1290,5 +1293,25 @@ var _ = Describe("push Command", func() { "--task", "--strategy=canary", }, }), + + Entry("max-in-flight is passed without strategy", + func() { + maxInFlight := 10 + cmd.MaxInFlight = &maxInFlight + }, + translatableerror.RequiredFlagsError{ + Arg1: "--max-in-flight", + Arg2: "--strategy", + }), + + Entry("max-in-flight is smaller than 1", + func() { + cmd.Strategy = flag.DeploymentStrategy{Name: constant.DeploymentStrategyRolling} + maxInFlight := 0 + cmd.MaxInFlight = &maxInFlight + }, + translatableerror.IncorrectUsageError{ + Message: "--max-in-flight must be greater than or equal to 1", + }), ) }) diff --git a/command/v7/restage_command.go b/command/v7/restage_command.go index 76b02c618bd..6fa503b2d81 100644 --- a/command/v7/restage_command.go +++ b/command/v7/restage_command.go @@ -16,6 +16,7 @@ type RestageCommand struct { RequiredArgs flag.AppName `positional-args:"yes"` Strategy flag.DeploymentStrategy `long:"strategy" description:"Deployment strategy can be canary, rolling or null."` + MaxInFlight *int `long:"max-in-flight" description:"Defines the maximum number of instances that will be actively being restaged. Only applies when --strategy flag is specified."` NoWait bool `long:"no-wait" description:"Exit when the first instance of the web process is healthy"` usage interface{} `usage:"CF_NAME restage APP_NAME\n\n This command will cause downtime unless you use '--strategy' flag.\n\nEXAMPLES:\n CF_NAME restage APP_NAME\n CF_NAME restage APP_NAME --strategy rolling\n CF_NAME restage APP_NAME --strategy canary --no-wait"` relatedCommands interface{} `related_commands:"restart"` @@ -52,6 +53,11 @@ func (cmd RestageCommand) Execute(args []string) error { return err } + err = cmd.ValidateFlags() + if err != nil { + return err + } + if len(cmd.Strategy.Name) <= 0 { cmd.UI.DisplayWarning("This action will cause app downtime.") } @@ -78,10 +84,15 @@ func (cmd RestageCommand) Execute(args []string) error { } opts := shared.AppStartOpts{ - Strategy: cmd.Strategy.Name, - NoWait: cmd.NoWait, AppAction: constant.ApplicationRestarting, + NoWait: cmd.NoWait, + Strategy: cmd.Strategy.Name, } + + if cmd.MaxInFlight != nil { + opts.MaxInFlight = *cmd.MaxInFlight + } + err = cmd.Stager.StageAndStart(app, cmd.Config.TargetedSpace(), cmd.Config.TargetedOrganization(), pkg.GUID, opts) if err != nil { return mapErr(cmd.Config, cmd.RequiredArgs.AppName, err) @@ -90,6 +101,17 @@ func (cmd RestageCommand) Execute(args []string) error { return nil } +func (cmd RestageCommand) ValidateFlags() error { + switch { + case cmd.Strategy.Name == constant.DeploymentStrategyDefault && cmd.MaxInFlight != nil: + return translatableerror.RequiredFlagsError{Arg1: "--max-in-flight", Arg2: "--strategy"} + case cmd.Strategy.Name != constant.DeploymentStrategyDefault && cmd.MaxInFlight != nil && *cmd.MaxInFlight < 1: + return translatableerror.IncorrectUsageError{Message: "--max-in-flight must be greater than or equal to 1"} + } + + return nil +} + func mapErr(config command.Config, appName string, err error) error { switch err.(type) { case actionerror.AllInstancesCrashedError: diff --git a/command/v7/restage_command_test.go b/command/v7/restage_command_test.go index 652e7740d84..8cddad4ca5e 100644 --- a/command/v7/restage_command_test.go +++ b/command/v7/restage_command_test.go @@ -8,6 +8,7 @@ import ( "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/constant" "code.cloudfoundry.org/cli/command/commandfakes" "code.cloudfoundry.org/cli/command/flag" + "code.cloudfoundry.org/cli/command/translatableerror" v7 "code.cloudfoundry.org/cli/command/v7" "code.cloudfoundry.org/cli/command/v7/shared/sharedfakes" "code.cloudfoundry.org/cli/command/v7/v7fakes" @@ -73,6 +74,10 @@ var _ = Describe("restage Command", func() { v7action.Warnings{"get-package-warning"}, nil, ) + + cmd.Strategy = flag.DeploymentStrategy{Name: constant.DeploymentStrategyRolling} + maxInFlight := 4 + cmd.MaxInFlight = &maxInFlight }) JustBeforeEach(func() { @@ -108,6 +113,7 @@ var _ = Describe("restage Command", func() { When("No strategy flag is given", func() { BeforeEach(func() { cmd.Strategy.Name = constant.DeploymentStrategyDefault + cmd.MaxInFlight = nil }) It("warns that there will be app downtime", func() { Expect(testUI.Err).To(Say("This action will cause app downtime.")) @@ -170,7 +176,8 @@ var _ = Describe("restage Command", func() { Expect(spaceForApp).To(Equal(fakeConfig.TargetedSpace())) Expect(orgForApp).To(Equal(fakeConfig.TargetedOrganization())) Expect(pkgGUID).To(Equal("earliest-package-guid")) - Expect(opts.Strategy).To(Equal(constant.DeploymentStrategyDefault)) + Expect(opts.Strategy).To(Equal(constant.DeploymentStrategyRolling)) + Expect(opts.MaxInFlight).To(Equal(4)) Expect(opts.NoWait).To(Equal(false)) Expect(opts.AppAction).To(Equal(constant.ApplicationRestarting)) }) @@ -188,4 +195,36 @@ var _ = Describe("restage 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("max-in-flight is passed without strategy", + func() { + cmd.Strategy = flag.DeploymentStrategy{Name: constant.DeploymentStrategyDefault} + maxInFlight := 10 + cmd.MaxInFlight = &maxInFlight + }, + translatableerror.RequiredFlagsError{ + Arg1: "--max-in-flight", + Arg2: "--strategy", + }), + + Entry("max-in-flight is smaller than 1", + func() { + cmd.Strategy = flag.DeploymentStrategy{Name: constant.DeploymentStrategyRolling} + maxInFlight := 0 + cmd.MaxInFlight = &maxInFlight + }, + translatableerror.IncorrectUsageError{ + Message: "--max-in-flight must be greater than or equal to 1", + }), + ) }) diff --git a/command/v7/restart_command.go b/command/v7/restart_command.go index 1a453d50b9e..dfbe9c5e4a2 100644 --- a/command/v7/restart_command.go +++ b/command/v7/restart_command.go @@ -6,12 +6,14 @@ import ( "code.cloudfoundry.org/cli/api/logcache" "code.cloudfoundry.org/cli/command" "code.cloudfoundry.org/cli/command/flag" + "code.cloudfoundry.org/cli/command/translatableerror" "code.cloudfoundry.org/cli/command/v7/shared" ) type RestartCommand struct { BaseCommand + MaxInFlight *int `long:"max-in-flight" description:"Defines the maximum number of instances that will be actively restarted at any given time. Only applies when --strategy flag is specified."` RequiredArgs flag.AppName `positional-args:"yes"` Strategy flag.DeploymentStrategy `long:"strategy" description:"Deployment strategy can be canary, rolling or null."` NoWait bool `long:"no-wait" description:"Exit when the first instance of the web process is healthy"` @@ -50,6 +52,11 @@ func (cmd RestartCommand) Execute(args []string) error { return err } + err = cmd.ValidateFlags() + if err != nil { + return err + } + app, warnings, err := cmd.Actor.GetApplicationByNameAndSpace(cmd.RequiredArgs.AppName, cmd.Config.TargetedSpace().GUID) cmd.UI.DisplayWarnings(warnings) if err != nil { @@ -77,6 +84,11 @@ func (cmd RestartCommand) Execute(args []string) error { NoWait: cmd.NoWait, AppAction: constant.ApplicationRestarting, } + + if cmd.MaxInFlight != nil { + opts.MaxInFlight = *cmd.MaxInFlight + } + if packageGUID != "" { err = cmd.Stager.StageAndStart(app, cmd.Config.TargetedSpace(), cmd.Config.TargetedOrganization(), packageGUID, opts) if err != nil { @@ -91,3 +103,14 @@ func (cmd RestartCommand) Execute(args []string) error { return nil } + +func (cmd RestartCommand) ValidateFlags() error { + switch true { + case cmd.Strategy.Name == constant.DeploymentStrategyDefault && cmd.MaxInFlight != nil: + return translatableerror.RequiredFlagsError{Arg1: "--max-in-flight", Arg2: "--strategy"} + case cmd.Strategy.Name != constant.DeploymentStrategyDefault && cmd.MaxInFlight != nil && *cmd.MaxInFlight < 1: + return translatableerror.IncorrectUsageError{Message: "--max-in-flight must be greater than or equal to 1"} + } + + return nil +} diff --git a/command/v7/restart_command_test.go b/command/v7/restart_command_test.go index 2cea1f43b97..4d4f8e828bd 100644 --- a/command/v7/restart_command_test.go +++ b/command/v7/restart_command_test.go @@ -8,6 +8,7 @@ import ( "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/constant" "code.cloudfoundry.org/cli/command/commandfakes" "code.cloudfoundry.org/cli/command/flag" + "code.cloudfoundry.org/cli/command/translatableerror" v7 "code.cloudfoundry.org/cli/command/v7" "code.cloudfoundry.org/cli/command/v7/shared/sharedfakes" "code.cloudfoundry.org/cli/command/v7/v7fakes" @@ -187,4 +188,35 @@ var _ = Describe("restart Command", func() { }) }) + 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("max-in-flight is passed without strategy", + func() { + maxInFlight := 10 + cmd.MaxInFlight = &maxInFlight + }, + translatableerror.RequiredFlagsError{ + Arg1: "--max-in-flight", + Arg2: "--strategy", + }), + + Entry("max-in-flight is smaller than 1", + func() { + cmd.Strategy = flag.DeploymentStrategy{Name: constant.DeploymentStrategyRolling} + maxInFlight := 0 + cmd.MaxInFlight = &maxInFlight + }, + translatableerror.IncorrectUsageError{ + Message: "--max-in-flight must be greater than or equal to 1", + }), + ) }) diff --git a/command/v7/rollback_command.go b/command/v7/rollback_command.go index 84119377363..14854c42a36 100644 --- a/command/v7/rollback_command.go +++ b/command/v7/rollback_command.go @@ -8,6 +8,7 @@ import ( "code.cloudfoundry.org/cli/cf/errors" "code.cloudfoundry.org/cli/command" "code.cloudfoundry.org/cli/command/flag" + "code.cloudfoundry.org/cli/command/translatableerror" "code.cloudfoundry.org/cli/command/v7/shared" ) @@ -16,6 +17,7 @@ type RollbackCommand struct { Force bool `short:"f" description:"Force rollback without confirmation"` RequiredArgs flag.AppName `positional-args:"yes"` + MaxInFlight *int `long:"max-in-flight" description:"Defines the maximum number of instances that will be actively being rolled back."` Strategy flag.DeploymentStrategy `long:"strategy" description:"Deployment strategy can be canary or rolling. When not specified, it defaults to rolling."` Version flag.Revision `long:"version" required:"true" description:"Roll back to the specified revision"` relatedCommands interface{} `related_commands:"revisions"` @@ -50,6 +52,11 @@ func (cmd RollbackCommand) Execute(args []string) error { return err } + err = cmd.ValidateFlags() + if err != nil { + return err + } + app, warnings, err := cmd.Actor.GetApplicationByNameAndSpace(cmd.RequiredArgs.AppName, cmd.Config.TargetedSpace().GUID) cmd.UI.DisplayWarnings(warnings) if err != nil { @@ -72,10 +79,6 @@ func (cmd RollbackCommand) Execute(args []string) error { return err } - if err != nil { - return err - } - // TODO Localization? if !cmd.Force { cmd.UI.DisplayTextWithFlavor("Rolling '{{.AppName}}' back to revision '{{.TargetRevision}}' will create a new revision. The new revision will use the settings from revision '{{.TargetRevision}}'.", map[string]interface{}{ @@ -108,9 +111,12 @@ func (cmd RollbackCommand) Execute(args []string) error { }) opts := shared.AppStartOpts{ - Strategy: constant.DeploymentStrategyRolling, - NoWait: false, AppAction: constant.ApplicationRollingBack, + NoWait: false, + Strategy: constant.DeploymentStrategyRolling, + } + if cmd.MaxInFlight != nil { + opts.MaxInFlight = *cmd.MaxInFlight } if cmd.Strategy.Name != "" { @@ -126,3 +132,12 @@ func (cmd RollbackCommand) Execute(args []string) error { return nil } + +func (cmd RollbackCommand) ValidateFlags() error { + switch { + case cmd.MaxInFlight != nil && *cmd.MaxInFlight < 1: + return translatableerror.IncorrectUsageError{Message: "--max-in-flight must be greater than or equal to 1"} + } + + return nil +} diff --git a/command/v7/rollback_command_test.go b/command/v7/rollback_command_test.go index 6f80f7ca15f..9edcfc564a9 100644 --- a/command/v7/rollback_command_test.go +++ b/command/v7/rollback_command_test.go @@ -8,6 +8,7 @@ import ( "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/constant" "code.cloudfoundry.org/cli/command/commandfakes" "code.cloudfoundry.org/cli/command/flag" + "code.cloudfoundry.org/cli/command/translatableerror" v7 "code.cloudfoundry.org/cli/command/v7" "code.cloudfoundry.org/cli/command/v7/shared/sharedfakes" "code.cloudfoundry.org/cli/command/v7/v7fakes" @@ -76,6 +77,8 @@ var _ = Describe("rollback Command", func() { }, Stager: fakeAppStager, } + maxInFlight := 5 + cmd.MaxInFlight = &maxInFlight }) JustBeforeEach(func() { @@ -220,6 +223,7 @@ var _ = Describe("rollback Command", func() { Expect(revisionGUID).To(Equal("some-1-guid")) Expect(opts.AppAction).To(Equal(constant.ApplicationRollingBack)) Expect(opts.Strategy).To(Equal(constant.DeploymentStrategyRolling)) + Expect(opts.MaxInFlight).To(Equal(5)) Expect(testUI.Out).To(Say("Rolling '%s' back to revision '1' will create a new revision. The new revision will use the settings from revision '1'.", app)) Expect(testUI.Out).To(Say("Are you sure you want to continue?")) @@ -286,4 +290,24 @@ var _ = Describe("rollback Command", func() { }) }) + 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("max-in-flight is smaller than 1", + func() { + maxInFlight := 0 + cmd.MaxInFlight = &maxInFlight + }, + translatableerror.IncorrectUsageError{ + Message: "--max-in-flight must be greater than or equal to 1", + }), + ) }) diff --git a/command/v7/shared/app_stager.go b/command/v7/shared/app_stager.go index 5364bbd31c2..2aed583033d 100644 --- a/command/v7/shared/app_stager.go +++ b/command/v7/shared/app_stager.go @@ -29,9 +29,10 @@ type AppStager interface { } type AppStartOpts struct { - Strategy constant.DeploymentStrategy - NoWait bool - AppAction constant.ApplicationAction + AppAction constant.ApplicationAction + MaxInFlight int + NoWait bool + Strategy constant.DeploymentStrategy } type Stager struct { @@ -124,9 +125,11 @@ func (stager *Stager) StartApp(app resources.Application, space configv3.Space, switch opts.AppAction { case constant.ApplicationRollingBack: dep.RevisionGUID = resourceGuid + dep.Options.MaxInFlight = opts.MaxInFlight deploymentGUID, warnings, err = stager.Actor.CreateDeployment(dep) default: dep.DropletGUID = resourceGuid + dep.Options.MaxInFlight = opts.MaxInFlight deploymentGUID, warnings, err = stager.Actor.CreateDeployment(dep) } diff --git a/command/v7/shared/app_stager_test.go b/command/v7/shared/app_stager_test.go index 9c6b936ea59..067ddb96310 100644 --- a/command/v7/shared/app_stager_test.go +++ b/command/v7/shared/app_stager_test.go @@ -35,6 +35,7 @@ var _ = Describe("app stager", func() { organization configv3.Organization pkgGUID string strategy constant.DeploymentStrategy + maxInFlight int noWait bool appAction constant.ApplicationAction @@ -58,6 +59,7 @@ var _ = Describe("app stager", func() { space = configv3.Space{Name: "some-space", GUID: "some-space-guid"} organization = configv3.Organization{Name: "some-org"} strategy = constant.DeploymentStrategyDefault + maxInFlight = 2 appAction = constant.ApplicationRestarting fakeActor.GetStreamingLogsForApplicationByNameAndSpaceStub = func(appName string, spaceGUID string, client sharedaction.LogCacheClient) (<-chan sharedaction.LogMessage, <-chan error, context.CancelFunc, v7action.Warnings, error) { @@ -107,9 +109,10 @@ var _ = Describe("app stager", func() { JustBeforeEach(func() { appStager = shared.NewAppStager(fakeActor, testUI, fakeConfig, fakeLogCacheClient) opts := shared.AppStartOpts{ - Strategy: strategy, - NoWait: noWait, - AppAction: appAction, + AppAction: appAction, + MaxInFlight: maxInFlight, + NoWait: noWait, + Strategy: strategy, } executeErr = appStager.StageAndStart(app, space, organization, pkgGUID, opts) }) @@ -173,13 +176,8 @@ var _ = Describe("app stager", func() { BeforeEach(func() { strategy = constant.DeploymentStrategyRolling noWait = true + maxInFlight = 5 appStager = shared.NewAppStager(fakeActor, testUI, fakeConfig, fakeLogCacheClient) - opts := shared.AppStartOpts{ - Strategy: strategy, - NoWait: noWait, - AppAction: appAction, - } - executeErr = appStager.StageAndStart(app, space, organization, pkgGUID, opts) }) It("Restages and starts the app", func() { @@ -190,6 +188,13 @@ var _ = Describe("app stager", func() { Expect(testUI.Out).To(Say("First instance restaged correctly, restaging remaining in the background")) }) + + It("creates expected deployment", func() { + Expect(fakeActor.CreateDeploymentCallCount()).To(Equal(1), "CreateDeployment...") + dep := fakeActor.CreateDeploymentArgsForCall(0) + Expect(dep.Options.MaxInFlight).To(Equal(5)) + Expect(string(dep.Strategy)).To(Equal("rolling")) + }) }) }) @@ -344,6 +349,7 @@ var _ = Describe("app stager", func() { strategy = constant.DeploymentStrategyDefault noWait = true + maxInFlight = 2 appAction = constant.ApplicationRestarting app = resources.Application{GUID: "app-guid", Name: "app-name", State: constant.ApplicationStarted} @@ -358,9 +364,10 @@ var _ = Describe("app stager", func() { JustBeforeEach(func() { appStager = shared.NewAppStager(fakeActor, testUI, fakeConfig, fakeLogCacheClient) opts := shared.AppStartOpts{ - Strategy: strategy, - NoWait: noWait, - AppAction: appAction, + Strategy: strategy, + NoWait: noWait, + MaxInFlight: maxInFlight, + AppAction: appAction, } executeErr = appStager.StartApp(app, space, organization, resourceGUID, opts) }) @@ -399,6 +406,7 @@ var _ = Describe("app stager", func() { dep := fakeActor.CreateDeploymentArgsForCall(0) Expect(dep.Relationships[constant.RelationshipTypeApplication].GUID).To(Equal(app.GUID)) Expect(dep.RevisionGUID).To(Equal("revision-guid")) + Expect(dep.Options.MaxInFlight).To(Equal(2)) Expect(testUI.Err).To(Say("create-deployment-warning")) Expect(testUI.Out).To(Say("Waiting for app to deploy...")) @@ -408,6 +416,10 @@ var _ = Describe("app stager", func() { }) When("the app starts successfully", func() { + BeforeEach(func() { + maxInFlight = 3 + }) + It("displays output for each step of deploying", func() { Expect(executeErr).To(BeNil()) @@ -416,6 +428,7 @@ var _ = Describe("app stager", func() { dep := fakeActor.CreateDeploymentArgsForCall(0) Expect(dep.Relationships[constant.RelationshipTypeApplication].GUID).To(Equal(app.GUID)) Expect(dep.DropletGUID).To(Equal("droplet-guid")) + Expect(dep.Options.MaxInFlight).To(Equal(3)) Expect(testUI.Err).To(Say("create-deployment-warning")) Expect(testUI.Out).To(Say("Waiting for app to deploy...")) @@ -627,5 +640,4 @@ var _ = Describe("app stager", func() { Expect(executeErr).To(Not(HaveOccurred())) }) }) - }) diff --git a/integration/v7/isolated/continue_deployment_test.go b/integration/v7/isolated/continue_deployment_test.go index 18ea3dab317..4c1bf73370e 100644 --- a/integration/v7/isolated/continue_deployment_test.go +++ b/integration/v7/isolated/continue_deployment_test.go @@ -87,7 +87,7 @@ var _ = Describe("Continue Deployment", func() { When("There is a canary deployment", func() { It("succeeds", func() { helpers.WithHelloWorldApp(func(appDir string) { - helpers.CF("push", appName, "-p", appDir, "--strategy=canary", "--no-wait").Wait() + helpers.CF("push", appName, "-p", appDir, "--strategy=canary").Wait() }) session := helpers.CF("continue-deployment", appName) diff --git a/integration/v7/isolated/copy_source_command_test.go b/integration/v7/isolated/copy_source_command_test.go index 83891422636..bd8829140ca 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`)) diff --git a/integration/v7/isolated/restage_command_test.go b/integration/v7/isolated/restage_command_test.go index 08ce275bc18..8f30228cc3b 100644 --- a/integration/v7/isolated/restage_command_test.go +++ b/integration/v7/isolated/restage_command_test.go @@ -31,8 +31,9 @@ var _ = Describe("restage command", func() { Eventually(session).Should(Say("ALIAS:")) Eventually(session).Should(Say("rg")) Eventually(session).Should(Say("OPTIONS:")) - Eventually(session).Should(Say("--strategy Deployment strategy can be canary, rolling or null")) - Eventually(session).Should(Say("--no-wait Exit when the first instance of the web process is healthy")) + Eventually(session).Should(Say(`--strategy\s+Deployment strategy can be canary, rolling or null`)) + Eventually(session).Should(Say("--max-in-flight")) + Eventually(session).Should(Say(`--no-wait\s+Exit when the first instance of the web process is healthy`)) Eventually(session).Should(Say("ENVIRONMENT:")) Eventually(session).Should(Say(`CF_STAGING_TIMEOUT=15\s+Max wait time for staging, in minutes`)) Eventually(session).Should(Say(`CF_STARTUP_TIMEOUT=5\s+Max wait time for app instance startup, in minutes`)) @@ -145,7 +146,7 @@ var _ = Describe("restage command", func() { userName, _ := helpers.GetCredentials() session := helpers.CustomCF(helpers.CFEnv{ EnvVars: map[string]string{"CF_STARTUP_TIMEOUT": "0.1"}, - }, "restage", appName, "--strategy", "rolling") + }, "restage", appName, "--strategy", "rolling", "--max-in-flight", "3") Consistently(session.Err).ShouldNot(Say(`This action will cause app downtime\.`)) Eventually(session).Should(Say(`Restaging app %s in org %s / space %s as %s\.\.\.`, appName, orgName, spaceName, userName)) Eventually(session).Should(Say(`Creating deployment for app %s\.\.\.`, appName)) diff --git a/integration/v7/isolated/restart_command_test.go b/integration/v7/isolated/restart_command_test.go index 394c8f147bf..bd7bd8a5923 100644 --- a/integration/v7/isolated/restart_command_test.go +++ b/integration/v7/isolated/restart_command_test.go @@ -51,8 +51,9 @@ var _ = Describe("restart command", func() { Eventually(session).Should(Say("ALIAS:")) Eventually(session).Should(Say("rs")) Eventually(session).Should(Say("OPTIONS:")) - Eventually(session).Should(Say("--strategy Deployment strategy can be canary, rolling or null.")) - Eventually(session).Should(Say("--no-wait Exit when the first instance of the web process is healthy")) + Eventually(session).Should(Say("--max-in-flight")) + Eventually(session).Should(Say(`--strategy\s+Deployment strategy can be canary, rolling or null.`)) + Eventually(session).Should(Say(`--no-wait\s+Exit when the first instance of the web process is healthy`)) Eventually(session).Should(Say("ENVIRONMENT:")) Eventually(session).Should(Say(`CF_STAGING_TIMEOUT=15\s+Max wait time for staging, in minutes`)) Eventually(session).Should(Say(`CF_STARTUP_TIMEOUT=5\s+Max wait time for app instance startup, in minutes`)) @@ -101,7 +102,7 @@ var _ = Describe("restart command", func() { }) }) It("creates a deploy", func() { - session := helpers.CF("restart", appName, "--strategy=rolling") + session := helpers.CF("restart", appName, "--strategy=rolling", "--max-in-flight=3") Eventually(session).Should(Say(`Restarting app %s in org %s / space %s as %s\.\.\.`, appName, orgName, spaceName, userName)) Eventually(session).Should(Say(`Creating deployment for app %s\.\.\.`, appName)) Eventually(session).Should(Say(`Waiting for app to deploy\.\.\.`)) diff --git a/integration/v7/isolated/rollback_command_test.go b/integration/v7/isolated/rollback_command_test.go index 75c12d6f621..056c2115b9e 100644 --- a/integration/v7/isolated/rollback_command_test.go +++ b/integration/v7/isolated/rollback_command_test.go @@ -34,9 +34,10 @@ var _ = Describe("rollback command", func() { Expect(session).To(Say("USAGE:")) Expect(session).To(Say(`cf rollback APP_NAME \[--version VERSION\]`)) Expect(session).To(Say("OPTIONS:")) - Expect(session).To(Say("-f Force rollback without confirmation")) - Expect(session).To(Say("--strategy Deployment strategy can be canary or rolling. When not specified, it defaults to rolling.")) - Expect(session).To(Say("--version Roll back to the specified revision")) + Expect(session).To(Say(`-f\s+Force rollback without confirmation`)) + Expect(session).To(Say("--max-in-flight")) + Expect(session).To(Say(`--strategy\s+Deployment strategy can be canary or rolling. When not specified, it defaults to rolling.`)) + Expect(session).To(Say(`--version\s+Roll back to the specified revision`)) Expect(session).To(Say("SEE ALSO:")) Expect(session).To(Say("revisions")) }) @@ -113,7 +114,7 @@ applications: When("the -f flag is provided", func() { It("does not prompt the user, and just rolls back", func() { - session := helpers.CF("rollback", appName, "--version", "1", "-f") + session := helpers.CF("rollback", appName, "--version", "1", "-f", "--max-in-flight", "3") Eventually(session).Should(Exit(0)) Expect(session).To(HaveRollbackOutput(appName, orgName, spaceName, userName)) diff --git a/integration/v7/push/help_test.go b/integration/v7/push/help_test.go index c86d0b9999b..32845920753 100644 --- a/integration/v7/push/help_test.go +++ b/integration/v7/push/help_test.go @@ -81,6 +81,7 @@ var _ = Describe("help", func() { Eventually(session).Should(Say(`--instances, -i`)) Eventually(session).Should(Say(`--log-rate-limit, -l\s+Log rate limit per second, in bytes \(e.g. 128B, 4K, 1M\). -l=-1 represents unlimited`)) Eventually(session).Should(Say(`--manifest, -f`)) + Eventually(session).Should(Say(`--max-in-flight`)) Eventually(session).Should(Say(`--memory, -m`)) Eventually(session).Should(Say(`--no-manifest`)) Eventually(session).Should(Say(`--no-route`)) diff --git a/integration/v7/push/rolling_push_test.go b/integration/v7/push/rolling_push_test.go index 689faf5244f..98e26aeca78 100644 --- a/integration/v7/push/rolling_push_test.go +++ b/integration/v7/push/rolling_push_test.go @@ -53,6 +53,30 @@ var _ = Describe("push with --strategy rolling", func() { Expect(session).To(Say(`#0\s+running`)) }) }) + + It("pushes the app and creates a new deployment with max in flight set", func() { + helpers.WithHelloWorldApp(func(appDir string) { + session := helpers.CustomCF(helpers.CFEnv{WorkingDirectory: appDir}, + PushCommandName, appName, "--strategy", "rolling", "--max-in-flight", "3", + ) + + Eventually(session).Should(Exit(0)) + Expect(session).To(Say(`Pushing app %s to org %s / space %s as %s\.\.\.`, appName, organization, space, userName)) + Expect(session).To(Say(`Packaging files to upload\.\.\.`)) + Expect(session).To(Say(`Uploading files\.\.\.`)) + Expect(session).To(Say(`100.00%`)) + Expect(session).To(Say(`Waiting for API to complete processing files\.\.\.`)) + Expect(session).To(Say(`Staging app and tracing logs\.\.\.`)) + Expect(session).To(Say(`Starting deployment for app %s\.\.\.`, appName)) + Expect(session).To(Say(`Waiting for app to deploy\.\.\.`)) + Expect(session).To(Say(`name:\s+%s`, appName)) + Expect(session).To(Say(`requested state:\s+started`)) + Expect(session).To(Say(`routes:\s+%s.%s`, appName, helpers.DefaultSharedDomain())) + Expect(session).To(Say(`type:\s+web`)) + Expect(session).To(Say(`start command:\s+%s`, helpers.StaticfileBuildpackStartCommand)) + Expect(session).To(Say(`#0\s+running`)) + }) + }) }) When("canceling the deployment", func() { diff --git a/resources/deployment_resource.go b/resources/deployment_resource.go index aa342468d15..6e5b6cc71a3 100644 --- a/resources/deployment_resource.go +++ b/resources/deployment_resource.go @@ -13,6 +13,7 @@ type Deployment struct { StatusValue constant.DeploymentStatusValue StatusReason constant.DeploymentStatusReason LastStatusChange string + Options DeploymentOpts RevisionGUID string DropletGUID string CreatedAt string @@ -22,6 +23,10 @@ type Deployment struct { Strategy constant.DeploymentStrategy } +type DeploymentOpts struct { + MaxInFlight int `json:"max_in_flight"` +} + // MarshalJSON converts a Deployment into a Cloud Controller Deployment. func (d Deployment) MarshalJSON() ([]byte, error) { type Revision struct { @@ -33,6 +38,7 @@ func (d Deployment) MarshalJSON() ([]byte, error) { var ccDeployment struct { Droplet *Droplet `json:"droplet,omitempty"` + Options *DeploymentOpts `json:"options,omitempty"` Revision *Revision `json:"revision,omitempty"` Strategy constant.DeploymentStrategy `json:"strategy,omitempty"` Relationships Relationships `json:"relationships,omitempty"` @@ -50,6 +56,14 @@ func (d Deployment) MarshalJSON() ([]byte, error) { ccDeployment.Strategy = d.Strategy } + var b DeploymentOpts + if d.Options != b { + ccDeployment.Options = &d.Options + if d.Options.MaxInFlight < 1 { + ccDeployment.Options.MaxInFlight = 1 + } + } + ccDeployment.Relationships = d.Relationships return json.Marshal(ccDeployment) @@ -72,6 +86,7 @@ func (d *Deployment) UnmarshalJSON(data []byte) error { Droplet Droplet `json:"droplet,omitempty"` NewProcesses []Process `json:"new_processes,omitempty"` Strategy constant.DeploymentStrategy `json:"strategy"` + Options DeploymentOpts `json:"options,omitempty"` } err := cloudcontroller.DecodeJSON(data, &ccDeployment) @@ -89,6 +104,7 @@ func (d *Deployment) UnmarshalJSON(data []byte) error { d.DropletGUID = ccDeployment.Droplet.GUID d.NewProcesses = ccDeployment.NewProcesses d.Strategy = ccDeployment.Strategy + d.Options = ccDeployment.Options return nil }