Skip to content

Commit 9246cb4

Browse files
committed
Add --instance-steps to the restart command
Signed-off-by: João Pereira <[email protected]>
1 parent ffdfa4c commit 9246cb4

File tree

5 files changed

+177
-34
lines changed

5 files changed

+177
-34
lines changed

command/v7/push_command.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -574,15 +574,15 @@ func (cmd PushCommand) ValidateFlags() error {
574574
return translatableerror.IncorrectUsageError{Message: "--max-in-flight must be greater than or equal to 1"}
575575
case len(cmd.InstanceSteps) > 0 && cmd.Strategy.Name != constant.DeploymentStrategyCanary:
576576
return translatableerror.ArgumentCombinationError{Args: []string{"--instance-steps", "--strategy=rolling or --strategy not provided"}}
577-
case len(cmd.InstanceSteps) > 0 && !cmd.validateInstanceSteps():
577+
case len(cmd.InstanceSteps) > 0 && !validateInstanceSteps(cmd.InstanceSteps):
578578
return translatableerror.ParseArgumentError{ArgumentName: "--instance-steps", ExpectedType: "list of weights"}
579579
}
580580

581581
return nil
582582
}
583583

584-
func (cmd PushCommand) validateInstanceSteps() bool {
585-
for _, v := range strings.Split(cmd.InstanceSteps, ",") {
584+
func validateInstanceSteps(instanceSteps string) bool {
585+
for _, v := range strings.Split(instanceSteps, ",") {
586586
_, err := strconv.ParseInt(v, 0, 64)
587587
if err != nil {
588588
return false

command/v7/restart_command.go

+21
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
package v7
22

33
import (
4+
"strconv"
5+
"strings"
6+
47
"code.cloudfoundry.org/cli/actor/v7action"
58
"code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/constant"
69
"code.cloudfoundry.org/cli/api/logcache"
710
"code.cloudfoundry.org/cli/command"
811
"code.cloudfoundry.org/cli/command/flag"
912
"code.cloudfoundry.org/cli/command/translatableerror"
1013
"code.cloudfoundry.org/cli/command/v7/shared"
14+
"code.cloudfoundry.org/cli/resources"
1115
)
1216

1317
type RestartCommand struct {
1418
BaseCommand
1519

20+
InstanceSteps string `long:"instance-steps" description:"An array of percentage steps to deploy when using deployment strategy canary. (e.g. 20,40,60)"`
1621
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."`
1722
RequiredArgs flag.AppName `positional-args:"yes"`
1823
Strategy flag.DeploymentStrategy `long:"strategy" description:"Deployment strategy can be canary, rolling or null."`
@@ -89,6 +94,18 @@ func (cmd RestartCommand) Execute(args []string) error {
8994
opts.MaxInFlight = *cmd.MaxInFlight
9095
}
9196

97+
if cmd.InstanceSteps != "" {
98+
if len(cmd.InstanceSteps) > 0 {
99+
for _, v := range strings.Split(cmd.InstanceSteps, ",") {
100+
parsedInt, err := strconv.ParseInt(v, 0, 64)
101+
if err != nil {
102+
return err
103+
}
104+
opts.CanarySteps = append(opts.CanarySteps, resources.CanaryStep{InstanceWeight: parsedInt})
105+
}
106+
}
107+
}
108+
92109
if packageGUID != "" {
93110
err = cmd.Stager.StageAndStart(app, cmd.Config.TargetedSpace(), cmd.Config.TargetedOrganization(), packageGUID, opts)
94111
if err != nil {
@@ -110,6 +127,10 @@ func (cmd RestartCommand) ValidateFlags() error {
110127
return translatableerror.RequiredFlagsError{Arg1: "--max-in-flight", Arg2: "--strategy"}
111128
case cmd.Strategy.Name != constant.DeploymentStrategyDefault && cmd.MaxInFlight != nil && *cmd.MaxInFlight < 1:
112129
return translatableerror.IncorrectUsageError{Message: "--max-in-flight must be greater than or equal to 1"}
130+
case cmd.Strategy.Name != constant.DeploymentStrategyCanary && cmd.InstanceSteps != "":
131+
return translatableerror.RequiredFlagsError{Arg1: "--instance-steps", Arg2: "--strategy=canary"}
132+
case len(cmd.InstanceSteps) > 0 && !validateInstanceSteps(cmd.InstanceSteps):
133+
return translatableerror.ParseArgumentError{ArgumentName: "--instance-steps", ExpectedType: "list of weights"}
113134
}
114135

115136
return nil

command/v7/restart_command_test.go

+101-22
Original file line numberDiff line numberDiff line change
@@ -133,18 +133,43 @@ var _ = Describe("restart Command", func() {
133133
fakeActor.GetUnstagedNewestPackageGUIDReturns("package-guid", v7action.Warnings{}, nil)
134134
})
135135

136-
It("stages the new package and starts the app with the new droplet", func() {
137-
Expect(executeErr).ToNot(HaveOccurred())
138-
Expect(fakeAppStager.StageAndStartCallCount()).To(Equal(1))
136+
When("no strategy is provided", func() {
137+
It("stages the new package and starts the app with the new droplet", func() {
138+
Expect(executeErr).ToNot(HaveOccurred())
139+
Expect(fakeAppStager.StageAndStartCallCount()).To(Equal(1))
140+
141+
inputApp, inputSpace, inputOrg, inputPkgGUID, opts := fakeAppStager.StageAndStartArgsForCall(0)
142+
Expect(inputApp).To(Equal(app))
143+
Expect(inputSpace).To(Equal(cmd.Config.TargetedSpace()))
144+
Expect(inputOrg).To(Equal(cmd.Config.TargetedOrganization()))
145+
Expect(inputPkgGUID).To(Equal("package-guid"))
146+
Expect(opts.Strategy).To(Equal(strategy))
147+
Expect(opts.NoWait).To(Equal(noWait))
148+
Expect(opts.AppAction).To(Equal(constant.ApplicationRestarting))
149+
Expect(opts.CanarySteps).To(HaveLen(0))
150+
})
151+
})
139152

140-
inputApp, inputSpace, inputOrg, inputPkgGUID, opts := fakeAppStager.StageAndStartArgsForCall(0)
141-
Expect(inputApp).To(Equal(app))
142-
Expect(inputSpace).To(Equal(cmd.Config.TargetedSpace()))
143-
Expect(inputOrg).To(Equal(cmd.Config.TargetedOrganization()))
144-
Expect(inputPkgGUID).To(Equal("package-guid"))
145-
Expect(opts.Strategy).To(Equal(strategy))
146-
Expect(opts.NoWait).To(Equal(noWait))
147-
Expect(opts.AppAction).To(Equal(constant.ApplicationRestarting))
153+
When("canary strategy is provided", func() {
154+
BeforeEach(func() {
155+
cmd.Strategy = flag.DeploymentStrategy{Name: constant.DeploymentStrategyCanary}
156+
cmd.InstanceSteps = "1,2,4"
157+
})
158+
159+
It("starts the app with the current droplet", func() {
160+
Expect(executeErr).ToNot(HaveOccurred())
161+
Expect(fakeAppStager.StageAndStartCallCount()).To(Equal(1))
162+
163+
inputApp, inputSpace, inputOrg, inputPkgGUID, opts := fakeAppStager.StageAndStartArgsForCall(0)
164+
Expect(inputApp).To(Equal(app))
165+
Expect(inputSpace).To(Equal(cmd.Config.TargetedSpace()))
166+
Expect(inputOrg).To(Equal(cmd.Config.TargetedOrganization()))
167+
Expect(inputPkgGUID).To(Equal("package-guid"))
168+
Expect(opts.Strategy).To(Equal(constant.DeploymentStrategyCanary))
169+
Expect(opts.NoWait).To(Equal(noWait))
170+
Expect(opts.AppAction).To(Equal(constant.ApplicationRestarting))
171+
Expect(opts.CanarySteps).To(Equal([]resources.CanaryStep{{InstanceWeight: 1}, {InstanceWeight: 2}, {InstanceWeight: 4}}))
172+
})
148173
})
149174

150175
Context("staging and starting the app returns an error", func() {
@@ -163,18 +188,43 @@ var _ = Describe("restart Command", func() {
163188
fakeActor.GetUnstagedNewestPackageGUIDReturns("", v7action.Warnings{}, nil)
164189
})
165190

166-
It("starts the app with the current droplet", func() {
167-
Expect(executeErr).ToNot(HaveOccurred())
168-
Expect(fakeAppStager.StartAppCallCount()).To(Equal(1))
191+
When("no strategy is provided", func() {
192+
It("starts the app with the current droplet", func() {
193+
Expect(executeErr).ToNot(HaveOccurred())
194+
Expect(fakeAppStager.StartAppCallCount()).To(Equal(1))
195+
196+
inputApp, inputSpace, inputOrg, inputDropletGuid, opts := fakeAppStager.StartAppArgsForCall(0)
197+
Expect(inputApp).To(Equal(app))
198+
Expect(inputDropletGuid).To(Equal(""))
199+
Expect(inputSpace).To(Equal(cmd.Config.TargetedSpace()))
200+
Expect(inputOrg).To(Equal(cmd.Config.TargetedOrganization()))
201+
Expect(opts.Strategy).To(Equal(strategy))
202+
Expect(opts.NoWait).To(Equal(noWait))
203+
Expect(opts.AppAction).To(Equal(constant.ApplicationRestarting))
204+
Expect(opts.CanarySteps).To(HaveLen(0))
205+
})
206+
})
207+
208+
When("canary strategy is provided", func() {
209+
BeforeEach(func() {
210+
cmd.Strategy = flag.DeploymentStrategy{Name: constant.DeploymentStrategyCanary}
211+
cmd.InstanceSteps = "1,2,4"
212+
})
169213

170-
inputApp, inputSpace, inputOrg, inputDropletGuid, opts := fakeAppStager.StartAppArgsForCall(0)
171-
Expect(inputApp).To(Equal(app))
172-
Expect(inputDropletGuid).To(Equal(""))
173-
Expect(inputSpace).To(Equal(cmd.Config.TargetedSpace()))
174-
Expect(inputOrg).To(Equal(cmd.Config.TargetedOrganization()))
175-
Expect(opts.Strategy).To(Equal(strategy))
176-
Expect(opts.NoWait).To(Equal(noWait))
177-
Expect(opts.AppAction).To(Equal(constant.ApplicationRestarting))
214+
It("starts the app with the current droplet", func() {
215+
Expect(executeErr).ToNot(HaveOccurred())
216+
Expect(fakeAppStager.StartAppCallCount()).To(Equal(1))
217+
218+
inputApp, inputSpace, inputOrg, inputDropletGuid, opts := fakeAppStager.StartAppArgsForCall(0)
219+
Expect(inputApp).To(Equal(app))
220+
Expect(inputDropletGuid).To(Equal(""))
221+
Expect(inputSpace).To(Equal(cmd.Config.TargetedSpace()))
222+
Expect(inputOrg).To(Equal(cmd.Config.TargetedOrganization()))
223+
Expect(opts.Strategy).To(Equal(constant.DeploymentStrategyCanary))
224+
Expect(opts.NoWait).To(Equal(noWait))
225+
Expect(opts.AppAction).To(Equal(constant.ApplicationRestarting))
226+
Expect(opts.CanarySteps).To(Equal([]resources.CanaryStep{{InstanceWeight: 1}, {InstanceWeight: 2}, {InstanceWeight: 4}}))
227+
})
178228
})
179229

180230
When("starting the app returns an error", func() {
@@ -218,5 +268,34 @@ var _ = Describe("restart Command", func() {
218268
translatableerror.IncorrectUsageError{
219269
Message: "--max-in-flight must be greater than or equal to 1",
220270
}),
271+
272+
Entry("instance-steps provided with rolling deployment",
273+
func() {
274+
cmd.Strategy = flag.DeploymentStrategy{Name: constant.DeploymentStrategyRolling}
275+
cmd.InstanceSteps = "1,2,3"
276+
},
277+
translatableerror.RequiredFlagsError{
278+
Arg1: "--instance-steps",
279+
Arg2: "--strategy=canary",
280+
}),
281+
282+
Entry("instance-steps no strategy provided",
283+
func() {
284+
cmd.InstanceSteps = "1,2,3"
285+
},
286+
translatableerror.RequiredFlagsError{
287+
Arg1: "--instance-steps",
288+
Arg2: "--strategy=canary",
289+
}),
290+
291+
Entry("instance-steps a valid list of ints",
292+
func() {
293+
cmd.Strategy = flag.DeploymentStrategy{Name: constant.DeploymentStrategyCanary}
294+
cmd.InstanceSteps = "some,thing,not,right"
295+
},
296+
translatableerror.ParseArgumentError{
297+
ArgumentName: "--instance-steps",
298+
ExpectedType: "list of weights",
299+
}),
221300
)
222301
})

command/v7/shared/app_stager.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type AppStartOpts struct {
3333
MaxInFlight int
3434
NoWait bool
3535
Strategy constant.DeploymentStrategy
36+
CanarySteps []resources.CanaryStep
3637
}
3738

3839
type Stager struct {
@@ -65,7 +66,6 @@ func NewAppStager(actor stagingAndStartActor, ui command.UI, config command.Conf
6566
}
6667

6768
func (stager *Stager) StageAndStart(app resources.Application, space configv3.Space, organization configv3.Organization, packageGUID string, opts AppStartOpts) error {
68-
6969
droplet, err := stager.StageApp(app, packageGUID, space)
7070
if err != nil {
7171
return err
@@ -129,6 +129,9 @@ func (stager *Stager) StartApp(app resources.Application, space configv3.Space,
129129
deploymentGUID, warnings, err = stager.Actor.CreateDeployment(dep)
130130
default:
131131
dep.DropletGUID = resourceGuid
132+
if opts.Strategy == constant.DeploymentStrategyCanary && len(opts.CanarySteps) > 0 {
133+
dep.Options = resources.DeploymentOpts{CanaryDeploymentOptions: resources.CanaryDeploymentOptions{Steps: opts.CanarySteps}}
134+
}
132135
dep.Options.MaxInFlight = opts.MaxInFlight
133136
deploymentGUID, warnings, err = stager.Actor.CreateDeployment(dep)
134137
}

command/v7/shared/app_stager_test.go

+48-8
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,15 @@ var _ = Describe("app stager", func() {
3030
fakeActor *v7fakes.FakeActor
3131
fakeLogCacheClient *sharedactionfakes.FakeLogCacheClient
3232

33-
app resources.Application
34-
space configv3.Space
35-
organization configv3.Organization
36-
pkgGUID string
37-
strategy constant.DeploymentStrategy
38-
maxInFlight int
39-
noWait bool
40-
appAction constant.ApplicationAction
33+
app resources.Application
34+
space configv3.Space
35+
organization configv3.Organization
36+
pkgGUID string
37+
strategy constant.DeploymentStrategy
38+
maxInFlight int
39+
noWait bool
40+
appAction constant.ApplicationAction
41+
canaryWeightSteps []resources.CanaryStep
4142

4243
allLogsWritten chan bool
4344
closedTheStreams bool
@@ -113,6 +114,7 @@ var _ = Describe("app stager", func() {
113114
MaxInFlight: maxInFlight,
114115
NoWait: noWait,
115116
Strategy: strategy,
117+
CanarySteps: canaryWeightSteps,
116118
}
117119
executeErr = appStager.StageAndStart(app, space, organization, pkgGUID, opts)
118120
})
@@ -196,6 +198,24 @@ var _ = Describe("app stager", func() {
196198
Expect(string(dep.Strategy)).To(Equal("rolling"))
197199
})
198200
})
201+
202+
When("deployment strategy is canary", func() {
203+
BeforeEach(func() {
204+
strategy = constant.DeploymentStrategyCanary
205+
noWait = true
206+
maxInFlight = 5
207+
canaryWeightSteps = append(canaryWeightSteps, []resources.CanaryStep{{InstanceWeight: 1}, {InstanceWeight: 2}, {InstanceWeight: 3}}...)
208+
appStager = shared.NewAppStager(fakeActor, testUI, fakeConfig, fakeLogCacheClient)
209+
})
210+
211+
It("creates expected deployment", func() {
212+
Expect(fakeActor.CreateDeploymentCallCount()).To(Equal(1), "CreateDeployment...")
213+
dep := fakeActor.CreateDeploymentArgsForCall(0)
214+
Expect(dep.Options.MaxInFlight).To(Equal(5))
215+
Expect(string(dep.Strategy)).To(Equal("canary"))
216+
Expect(dep.Options.CanaryDeploymentOptions.Steps).To(Equal([]resources.CanaryStep{{InstanceWeight: 1}, {InstanceWeight: 2}, {InstanceWeight: 3}}))
217+
})
218+
})
199219
})
200220

201221
Context("StageApp", func() {
@@ -351,6 +371,7 @@ var _ = Describe("app stager", func() {
351371
noWait = true
352372
maxInFlight = 2
353373
appAction = constant.ApplicationRestarting
374+
canaryWeightSteps = nil
354375

355376
app = resources.Application{GUID: "app-guid", Name: "app-name", State: constant.ApplicationStarted}
356377
space = configv3.Space{Name: "some-space", GUID: "some-space-guid"}
@@ -368,6 +389,7 @@ var _ = Describe("app stager", func() {
368389
NoWait: noWait,
369390
MaxInFlight: maxInFlight,
370391
AppAction: appAction,
392+
CanarySteps: canaryWeightSteps,
371393
}
372394
executeErr = appStager.StartApp(app, space, organization, resourceGUID, opts)
373395
})
@@ -564,6 +586,24 @@ var _ = Describe("app stager", func() {
564586
})
565587
})
566588

589+
When("deployment is canary and steps are provided", func() {
590+
BeforeEach(func() {
591+
appAction = constant.ApplicationStarting
592+
strategy = constant.DeploymentStrategyCanary
593+
canaryWeightSteps = append(canaryWeightSteps, []resources.CanaryStep{{InstanceWeight: 1}, {InstanceWeight: 2}, {InstanceWeight: 3}}...)
594+
app = resources.Application{GUID: "app-guid", Name: "app-name", State: constant.ApplicationStopped}
595+
})
596+
597+
It("displays output for each step of starting", func() {
598+
Expect(executeErr).To(BeNil())
599+
Expect(fakeActor.CreateDeploymentCallCount()).To(Equal(1), "CreateDeployment...")
600+
dep := fakeActor.CreateDeploymentArgsForCall(0)
601+
Expect(dep.Options.MaxInFlight).To(Equal(2))
602+
Expect(string(dep.Strategy)).To(Equal("canary"))
603+
Expect(dep.Options.CanaryDeploymentOptions.Steps).To(Equal([]resources.CanaryStep{{InstanceWeight: 1}, {InstanceWeight: 2}, {InstanceWeight: 3}}))
604+
})
605+
})
606+
567607
When("a droplet guid is not provided", func() {
568608
BeforeEach(func() {
569609
resourceGUID = ""

0 commit comments

Comments
 (0)