Skip to content

Commit 314ba9c

Browse files
joaopapereiraSamze
andauthored
[main] Multiple commands changed to allow canary steps (#3400)
* Fix error message for push command Signed-off-by: João Pereira <[email protected]> * Display current step in the canary deployment Signed-off-by: João Pereira <[email protected]> * Add --instance-steps to the restart command Signed-off-by: João Pereira <[email protected]> * Add flag --instance-steps to restage command Signed-off-by: João Pereira <[email protected]> * Add --instance-steps to rollback Signed-off-by: João Pereira <[email protected]> * Add --instance-steps to copy-source command Signed-off-by: João Pereira <[email protected]> * Fix rollback that was not setting the correct values on the deployment Signed-off-by: João Pereira <[email protected]> * Add min capi version check on instance steps commands Signed-off-by: João Pereira <[email protected]> * Fix pointer reference * Add version to copy-source canary step test * Update canary step version requirement * Fix continue deployment test * Fix CAPI versions in tests * Fixes for canary integration tests * Fix flakey canary test * Add missing continue to copy-source canary test --------- Signed-off-by: João Pereira <[email protected]> Co-authored-by: Sam Gunaratne <[email protected]> Co-authored-by: Sam Gunaratne <[email protected]>
1 parent ed4f404 commit 314ba9c

16 files changed

+541
-57
lines changed

api/cloudcontroller/ccversion/minimum_version.go

+2-4
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,6 @@ const (
44
MinSupportedV2ClientVersion = "2.128.0"
55
MinSupportedClientVersionV9 = "3.160.0"
66

7-
MinVersionUpdateServiceNameWhenPlanNotVisibleV2 = "2.131.0"
8-
MinVersionUpdateServiceInstanceMaintenanceInfoV2 = "2.135.0"
9-
MinVersionMaintenanceInfoInSummaryV2 = "2.138.0"
10-
117
MinVersionCreateServiceBrokerV3 = "3.72.0"
128
MinVersionCreateSpaceScopedServiceBrokerV3 = "3.75.0"
139

@@ -16,4 +12,6 @@ const (
1612

1713
MinVersionLogRateLimitingV3 = "3.125.0"
1814
MinVersionPerRouteOpts = "3.183.0"
15+
16+
MinVersionCanarySteps = "3.189.0"
1917
)

command/v7/copy_source_command.go

+31-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
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"
9+
"code.cloudfoundry.org/cli/api/cloudcontroller/ccversion"
610
"code.cloudfoundry.org/cli/api/logcache"
711
"code.cloudfoundry.org/cli/command"
812
"code.cloudfoundry.org/cli/command/flag"
913
"code.cloudfoundry.org/cli/command/translatableerror"
1014
"code.cloudfoundry.org/cli/command/v7/shared"
15+
"code.cloudfoundry.org/cli/resources"
1116
"code.cloudfoundry.org/cli/util/configv3"
1217
)
1318

@@ -16,12 +21,13 @@ type CopySourceCommand struct {
1621

1722
RequiredArgs flag.CopySourceArgs `positional-args:"yes"`
1823
usage interface{} `usage:"CF_NAME copy-source SOURCE_APP DESTINATION_APP [-s TARGET_SPACE [-o TARGET_ORG]] [--no-restart] [--strategy STRATEGY] [--no-wait]"`
19-
Strategy flag.DeploymentStrategy `long:"strategy" description:"Deployment strategy can be canary, rolling or null"`
24+
InstanceSteps string `long:"instance-steps" description:"An array of percentage steps to deploy when using deployment strategy canary. (e.g. 20,40,60)"`
2025
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."`
2126
NoWait bool `long:"no-wait" description:"Exit when the first instance of the web process is healthy"`
2227
NoRestart bool `long:"no-restart" description:"Do not restage the destination application"`
2328
Organization string `short:"o" long:"organization" description:"Org that contains the destination application"`
2429
Space string `short:"s" long:"space" description:"Space that contains the destination application"`
30+
Strategy flag.DeploymentStrategy `long:"strategy" description:"Deployment strategy can be canary, rolling or null"`
2531
relatedCommands interface{} `related_commands:"apps, push, restage, restart, target"`
2632
envCFStagingTimeout interface{} `environmentName:"CF_STAGING_TIMEOUT" environmentDescription:"Max wait time for staging, in minutes" environmentDefault:"15"`
2733
envCFStartupTimeout interface{} `environmentName:"CF_STARTUP_TIMEOUT" environmentDescription:"Max wait time for app instance startup, in minutes" environmentDefault:"5"`
@@ -57,6 +63,18 @@ func (cmd *CopySourceCommand) ValidateFlags() error {
5763
return translatableerror.IncorrectUsageError{Message: "--max-in-flight must be greater than or equal to 1"}
5864
}
5965

66+
if cmd.Strategy.Name != constant.DeploymentStrategyCanary && cmd.InstanceSteps != "" {
67+
return translatableerror.RequiredFlagsError{Arg1: "--instance-steps", Arg2: "--strategy=canary"}
68+
}
69+
70+
if len(cmd.InstanceSteps) > 0 && !validateInstanceSteps(cmd.InstanceSteps) {
71+
return translatableerror.ParseArgumentError{ArgumentName: "--instance-steps", ExpectedType: "list of weights"}
72+
}
73+
74+
if len(cmd.InstanceSteps) > 0 {
75+
return command.MinimumCCAPIVersionCheck(cmd.Config.APIVersion(), ccversion.MinVersionCanarySteps, "--instance-steps")
76+
}
77+
6078
return nil
6179
}
6280

@@ -178,6 +196,18 @@ func (cmd CopySourceCommand) Execute(args []string) error {
178196
opts.MaxInFlight = *cmd.MaxInFlight
179197
}
180198

199+
if cmd.InstanceSteps != "" {
200+
if len(cmd.InstanceSteps) > 0 {
201+
for _, v := range strings.Split(cmd.InstanceSteps, ",") {
202+
parsedInt, err := strconv.ParseInt(v, 0, 64)
203+
if err != nil {
204+
return err
205+
}
206+
opts.CanarySteps = append(opts.CanarySteps, resources.CanaryStep{InstanceWeight: parsedInt})
207+
}
208+
}
209+
}
210+
181211
err = cmd.Stager.StageAndStart(targetApp, targetSpace, targetOrg, pkg.GUID, opts)
182212
if err != nil {
183213
return mapErr(cmd.Config, targetApp.Name, err)

command/v7/copy_source_command_test.go

+56
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,29 @@ var _ = Describe("copy-source Command", func() {
324324
Expect(opts.NoWait).To(Equal(false))
325325
Expect(opts.AppAction).To(Equal(constant.ApplicationRestarting))
326326
})
327+
328+
When("instance steps is provided", func() {
329+
BeforeEach(func() {
330+
cmd.Strategy = flag.DeploymentStrategy{Name: constant.DeploymentStrategyCanary}
331+
cmd.InstanceSteps = "1,2,4"
332+
333+
fakeConfig.APIVersionReturns("3.999.0")
334+
})
335+
336+
It("starts the new app", func() {
337+
Expect(executeErr).ToNot(HaveOccurred())
338+
Expect(fakeAppStager.StageAndStartCallCount()).To(Equal(1))
339+
340+
inputApp, inputSpace, inputOrg, inputDropletGuid, opts := fakeAppStager.StageAndStartArgsForCall(0)
341+
Expect(inputApp).To(Equal(targetApp))
342+
Expect(inputDropletGuid).To(Equal("target-package-guid"))
343+
Expect(inputSpace).To(Equal(cmd.Config.TargetedSpace()))
344+
Expect(inputOrg).To(Equal(cmd.Config.TargetedOrganization()))
345+
Expect(opts.Strategy).To(Equal(constant.DeploymentStrategyCanary))
346+
Expect(opts.AppAction).To(Equal(constant.ApplicationRestarting))
347+
Expect(opts.CanarySteps).To(Equal([]resources.CanaryStep{{InstanceWeight: 1}, {InstanceWeight: 2}, {InstanceWeight: 4}}))
348+
})
349+
})
327350
})
328351

329352
When("the no-wait flag is set", func() {
@@ -440,5 +463,38 @@ var _ = Describe("copy-source Command", func() {
440463
translatableerror.IncorrectUsageError{
441464
Message: "--max-in-flight must be greater than or equal to 1",
442465
}),
466+
467+
Entry("instance-steps no strategy provided",
468+
func() {
469+
cmd.InstanceSteps = "1,2,3"
470+
},
471+
translatableerror.RequiredFlagsError{
472+
Arg1: "--instance-steps",
473+
Arg2: "--strategy=canary",
474+
}),
475+
476+
Entry("instance-steps a valid list of ints",
477+
func() {
478+
cmd.Strategy = flag.DeploymentStrategy{Name: constant.DeploymentStrategyCanary}
479+
cmd.InstanceSteps = "some,thing,not,right"
480+
},
481+
translatableerror.ParseArgumentError{
482+
ArgumentName: "--instance-steps",
483+
ExpectedType: "list of weights",
484+
}),
485+
486+
Entry("instance-steps used when CAPI does not support canary steps",
487+
func() {
488+
cmd.InstanceSteps = "1,2,3"
489+
cmd.Strategy.Name = constant.DeploymentStrategyCanary
490+
fakeConfig = &commandfakes.FakeConfig{}
491+
fakeConfig.APIVersionReturns("3.0.0")
492+
cmd.Config = fakeConfig
493+
},
494+
translatableerror.MinimumCFAPIVersionNotMetError{
495+
Command: "--instance-steps",
496+
CurrentVersion: "3.0.0",
497+
MinimumVersion: "3.189.0",
498+
}),
443499
)
444500
})

command/v7/push_command.go

+8-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"strconv"
88
"strings"
99

10+
"code.cloudfoundry.org/cli/api/cloudcontroller/ccversion"
1011
"github.com/cloudfoundry/bosh-cli/director/template"
1112
log "github.com/sirupsen/logrus"
1213
"gopkg.in/yaml.v2"
@@ -574,15 +575,19 @@ func (cmd PushCommand) ValidateFlags() error {
574575
return translatableerror.IncorrectUsageError{Message: "--max-in-flight must be greater than or equal to 1"}
575576
case len(cmd.InstanceSteps) > 0 && cmd.Strategy.Name != constant.DeploymentStrategyCanary:
576577
return translatableerror.ArgumentCombinationError{Args: []string{"--instance-steps", "--strategy=rolling or --strategy not provided"}}
577-
case len(cmd.InstanceSteps) > 0 && !cmd.validateInstanceSteps():
578+
case len(cmd.InstanceSteps) > 0 && !validateInstanceSteps(cmd.InstanceSteps):
578579
return translatableerror.ParseArgumentError{ArgumentName: "--instance-steps", ExpectedType: "list of weights"}
579580
}
580581

582+
if len(cmd.InstanceSteps) > 0 {
583+
return command.MinimumCCAPIVersionCheck(cmd.Config.APIVersion(), ccversion.MinVersionCanarySteps, "--instance-steps")
584+
}
585+
581586
return nil
582587
}
583588

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

command/v7/push_command_test.go

+17
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,9 @@ var _ = Describe("push Command", func() {
628628
When("canary strategy is provided", func() {
629629
BeforeEach(func() {
630630
cmd.Strategy = flag.DeploymentStrategy{Name: "canary"}
631+
fakeConfig = &commandfakes.FakeConfig{}
632+
fakeConfig.APIVersionReturns("4.0.0")
633+
cmd.Config = fakeConfig
631634
})
632635

633636
It("should succeed", func() {
@@ -1440,5 +1443,19 @@ var _ = Describe("push Command", func() {
14401443
Args: []string{
14411444
"--instance-steps", "--strategy=rolling or --strategy not provided",
14421445
}}),
1446+
1447+
Entry("instance-steps used when CAPI does not support canary steps",
1448+
func() {
1449+
cmd.InstanceSteps = "1,2,3"
1450+
cmd.Strategy.Name = constant.DeploymentStrategyCanary
1451+
fakeConfig = &commandfakes.FakeConfig{}
1452+
fakeConfig.APIVersionReturns("3.0.0")
1453+
cmd.Config = fakeConfig
1454+
},
1455+
translatableerror.MinimumCFAPIVersionNotMetError{
1456+
Command: "--instance-steps",
1457+
CurrentVersion: "3.0.0",
1458+
MinimumVersion: "3.189.0",
1459+
}),
14431460
)
14441461
})

command/v7/restage_command.go

+27-1
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,29 @@
11
package v7
22

33
import (
4+
"strconv"
5+
"strings"
6+
47
"code.cloudfoundry.org/cli/actor/actionerror"
58
"code.cloudfoundry.org/cli/actor/v7action"
69
"code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/constant"
10+
"code.cloudfoundry.org/cli/api/cloudcontroller/ccversion"
711
"code.cloudfoundry.org/cli/api/logcache"
812
"code.cloudfoundry.org/cli/command"
913
"code.cloudfoundry.org/cli/command/flag"
1014
"code.cloudfoundry.org/cli/command/translatableerror"
1115
"code.cloudfoundry.org/cli/command/v7/shared"
16+
"code.cloudfoundry.org/cli/resources"
1217
)
1318

1419
type RestageCommand struct {
1520
BaseCommand
1621

1722
RequiredArgs flag.AppName `positional-args:"yes"`
18-
Strategy flag.DeploymentStrategy `long:"strategy" description:"Deployment strategy can be canary, rolling or null."`
23+
InstanceSteps string `long:"instance-steps" description:"An array of percentage steps to deploy when using deployment strategy canary. (e.g. 20,40,60)"`
1924
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."`
2025
NoWait bool `long:"no-wait" description:"Exit when the first instance of the web process is healthy"`
26+
Strategy flag.DeploymentStrategy `long:"strategy" description:"Deployment strategy can be canary, rolling or null."`
2127
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"`
2228
relatedCommands interface{} `related_commands:"restart"`
2329
envCFStagingTimeout interface{} `environmentName:"CF_STAGING_TIMEOUT" environmentDescription:"Max wait time for staging, in minutes" environmentDefault:"15"`
@@ -93,6 +99,18 @@ func (cmd RestageCommand) Execute(args []string) error {
9399
opts.MaxInFlight = *cmd.MaxInFlight
94100
}
95101

102+
if cmd.InstanceSteps != "" {
103+
if len(cmd.InstanceSteps) > 0 {
104+
for _, v := range strings.Split(cmd.InstanceSteps, ",") {
105+
parsedInt, err := strconv.ParseInt(v, 0, 64)
106+
if err != nil {
107+
return err
108+
}
109+
opts.CanarySteps = append(opts.CanarySteps, resources.CanaryStep{InstanceWeight: parsedInt})
110+
}
111+
}
112+
}
113+
96114
err = cmd.Stager.StageAndStart(app, cmd.Config.TargetedSpace(), cmd.Config.TargetedOrganization(), pkg.GUID, opts)
97115
if err != nil {
98116
return mapErr(cmd.Config, cmd.RequiredArgs.AppName, err)
@@ -107,6 +125,14 @@ func (cmd RestageCommand) ValidateFlags() error {
107125
return translatableerror.RequiredFlagsError{Arg1: "--max-in-flight", Arg2: "--strategy"}
108126
case cmd.Strategy.Name != constant.DeploymentStrategyDefault && cmd.MaxInFlight != nil && *cmd.MaxInFlight < 1:
109127
return translatableerror.IncorrectUsageError{Message: "--max-in-flight must be greater than or equal to 1"}
128+
case cmd.Strategy.Name != constant.DeploymentStrategyCanary && cmd.InstanceSteps != "":
129+
return translatableerror.RequiredFlagsError{Arg1: "--instance-steps", Arg2: "--strategy=canary"}
130+
case len(cmd.InstanceSteps) > 0 && !validateInstanceSteps(cmd.InstanceSteps):
131+
return translatableerror.ParseArgumentError{ArgumentName: "--instance-steps", ExpectedType: "list of weights"}
132+
}
133+
134+
if len(cmd.InstanceSteps) > 0 {
135+
return command.MinimumCCAPIVersionCheck(cmd.Config.APIVersion(), ccversion.MinVersionCanarySteps, "--instance-steps")
110136
}
111137

112138
return nil

command/v7/restage_command_test.go

+67
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,30 @@ var _ = Describe("restage Command", func() {
120120
})
121121
})
122122

123+
When("canary strategy is provided", func() {
124+
BeforeEach(func() {
125+
cmd.Strategy = flag.DeploymentStrategy{Name: constant.DeploymentStrategyCanary}
126+
cmd.InstanceSteps = "1,2,4"
127+
fakeConfig = &commandfakes.FakeConfig{}
128+
fakeConfig.APIVersionReturns("4.0.0")
129+
cmd.Config = fakeConfig
130+
})
131+
132+
It("starts the app with the current droplet", func() {
133+
Expect(executeErr).ToNot(HaveOccurred())
134+
Expect(fakeAppStager.StageAndStartCallCount()).To(Equal(1))
135+
136+
inputApp, inputSpace, inputOrg, inputDropletGuid, opts := fakeAppStager.StageAndStartArgsForCall(0)
137+
Expect(inputApp).To(Equal(app))
138+
Expect(inputDropletGuid).To(Equal("earliest-package-guid"))
139+
Expect(inputSpace).To(Equal(cmd.Config.TargetedSpace()))
140+
Expect(inputOrg).To(Equal(cmd.Config.TargetedOrganization()))
141+
Expect(opts.Strategy).To(Equal(constant.DeploymentStrategyCanary))
142+
Expect(opts.AppAction).To(Equal(constant.ApplicationRestarting))
143+
Expect(opts.CanarySteps).To(Equal([]resources.CanaryStep{{InstanceWeight: 1}, {InstanceWeight: 2}, {InstanceWeight: 4}}))
144+
})
145+
})
146+
123147
It("displays that it's restaging", func() {
124148
Expect(testUI.Out).To(Say("Restaging app some-app in org some-org / space some-space as steve..."))
125149
})
@@ -226,5 +250,48 @@ var _ = Describe("restage Command", func() {
226250
translatableerror.IncorrectUsageError{
227251
Message: "--max-in-flight must be greater than or equal to 1",
228252
}),
253+
254+
Entry("instance-steps provided with rolling deployment",
255+
func() {
256+
cmd.Strategy = flag.DeploymentStrategy{Name: constant.DeploymentStrategyRolling}
257+
cmd.InstanceSteps = "1,2,3"
258+
},
259+
translatableerror.RequiredFlagsError{
260+
Arg1: "--instance-steps",
261+
Arg2: "--strategy=canary",
262+
}),
263+
264+
Entry("instance-steps no strategy provided",
265+
func() {
266+
cmd.InstanceSteps = "1,2,3"
267+
},
268+
translatableerror.RequiredFlagsError{
269+
Arg1: "--instance-steps",
270+
Arg2: "--strategy=canary",
271+
}),
272+
273+
Entry("instance-steps a valid list of ints",
274+
func() {
275+
cmd.Strategy = flag.DeploymentStrategy{Name: constant.DeploymentStrategyCanary}
276+
cmd.InstanceSteps = "some,thing,not,right"
277+
},
278+
translatableerror.ParseArgumentError{
279+
ArgumentName: "--instance-steps",
280+
ExpectedType: "list of weights",
281+
}),
282+
283+
Entry("instance-steps used when CAPI does not support canary steps",
284+
func() {
285+
cmd.InstanceSteps = "1,2,3"
286+
cmd.Strategy.Name = constant.DeploymentStrategyCanary
287+
fakeConfig = &commandfakes.FakeConfig{}
288+
fakeConfig.APIVersionReturns("3.0.0")
289+
cmd.Config = fakeConfig
290+
},
291+
translatableerror.MinimumCFAPIVersionNotMetError{
292+
Command: "--instance-steps",
293+
CurrentVersion: "3.0.0",
294+
MinimumVersion: "3.189.0",
295+
}),
229296
)
230297
})

0 commit comments

Comments
 (0)