Skip to content

[main] Multiple commands changed to allow canary steps #3400

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Mar 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1828e54
Fix error message for push command
joaopapereira Feb 5, 2025
4a99027
Display current step in the canary deployment
joaopapereira Feb 5, 2025
1e2ab6c
Add --instance-steps to the restart command
joaopapereira Feb 12, 2025
ecdc90b
Add flag --instance-steps to restage command
joaopapereira Feb 18, 2025
e7931ef
Add --instance-steps to rollback
joaopapereira Feb 19, 2025
42cb1fe
Add --instance-steps to copy-source command
joaopapereira Feb 19, 2025
15e2367
Fix rollback that was not setting the correct values on the deployment
joaopapereira Feb 20, 2025
12e4524
Add min capi version check on instance steps commands
joaopapereira Feb 20, 2025
701812c
Merge branch 'main' into main-restart-canary-steps
Samze Mar 12, 2025
fa8670e
Fix pointer reference
Samze Mar 13, 2025
e47b6a6
Add version to copy-source canary step test
Samze Mar 13, 2025
e9c4744
Update canary step version requirement
Samze Mar 13, 2025
1894f8e
Fix continue deployment test
Samze Mar 13, 2025
1272d35
Fix CAPI versions in tests
Samze Mar 13, 2025
af8c745
Fixes for canary integration tests
Samze Mar 13, 2025
da9fa8a
Merge branch 'main' into main-restart-canary-steps
Samze Mar 18, 2025
d215054
Merge branch 'main' into main-restart-canary-steps
Samze Mar 19, 2025
376ef4c
Merge branch 'main' into main-restart-canary-steps
Samze Mar 20, 2025
9c1a411
Fix flakey canary test
Samze Mar 20, 2025
35bc4d7
Add missing continue to copy-source canary test
Samze Mar 20, 2025
23647ce
Merge branch 'main' into main-restart-canary-steps
Samze Mar 26, 2025
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
6 changes: 2 additions & 4 deletions api/cloudcontroller/ccversion/minimum_version.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@ const (
MinSupportedV2ClientVersion = "2.128.0"
MinSupportedClientVersionV9 = "3.160.0"

MinVersionUpdateServiceNameWhenPlanNotVisibleV2 = "2.131.0"
MinVersionUpdateServiceInstanceMaintenanceInfoV2 = "2.135.0"
MinVersionMaintenanceInfoInSummaryV2 = "2.138.0"

MinVersionCreateServiceBrokerV3 = "3.72.0"
MinVersionCreateSpaceScopedServiceBrokerV3 = "3.75.0"

Expand All @@ -16,4 +12,6 @@ const (

MinVersionLogRateLimitingV3 = "3.125.0"
MinVersionPerRouteOpts = "3.183.0"

MinVersionCanarySteps = "3.189.0"
)
32 changes: 31 additions & 1 deletion command/v7/copy_source_command.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package v7

import (
"strconv"
"strings"

"code.cloudfoundry.org/cli/actor/v7action"
"code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/constant"
"code.cloudfoundry.org/cli/api/cloudcontroller/ccversion"
"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"
"code.cloudfoundry.org/cli/resources"
"code.cloudfoundry.org/cli/util/configv3"
)

Expand All @@ -16,12 +21,13 @@ 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"`
InstanceSteps string `long:"instance-steps" description:"An array of percentage steps to deploy when using deployment strategy canary. (e.g. 20,40,60)"`
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"`
Space string `short:"s" long:"space" description:"Space that contains the destination application"`
Strategy flag.DeploymentStrategy `long:"strategy" description:"Deployment strategy can be canary, rolling or null"`
relatedCommands interface{} `related_commands:"apps, push, restage, restart, target"`
envCFStagingTimeout interface{} `environmentName:"CF_STAGING_TIMEOUT" environmentDescription:"Max wait time for staging, in minutes" environmentDefault:"15"`
envCFStartupTimeout interface{} `environmentName:"CF_STARTUP_TIMEOUT" environmentDescription:"Max wait time for app instance startup, in minutes" environmentDefault:"5"`
Expand Down Expand Up @@ -57,6 +63,18 @@ func (cmd *CopySourceCommand) ValidateFlags() error {
return translatableerror.IncorrectUsageError{Message: "--max-in-flight must be greater than or equal to 1"}
}

if cmd.Strategy.Name != constant.DeploymentStrategyCanary && cmd.InstanceSteps != "" {
return translatableerror.RequiredFlagsError{Arg1: "--instance-steps", Arg2: "--strategy=canary"}
}

if len(cmd.InstanceSteps) > 0 && !validateInstanceSteps(cmd.InstanceSteps) {
return translatableerror.ParseArgumentError{ArgumentName: "--instance-steps", ExpectedType: "list of weights"}
}

if len(cmd.InstanceSteps) > 0 {
return command.MinimumCCAPIVersionCheck(cmd.Config.APIVersion(), ccversion.MinVersionCanarySteps, "--instance-steps")
}

return nil
}

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

if cmd.InstanceSteps != "" {
if len(cmd.InstanceSteps) > 0 {
for _, v := range strings.Split(cmd.InstanceSteps, ",") {
parsedInt, err := strconv.ParseInt(v, 0, 64)
if err != nil {
return err
}
opts.CanarySteps = append(opts.CanarySteps, resources.CanaryStep{InstanceWeight: parsedInt})
}
}
}

err = cmd.Stager.StageAndStart(targetApp, targetSpace, targetOrg, pkg.GUID, opts)
if err != nil {
return mapErr(cmd.Config, targetApp.Name, err)
Expand Down
56 changes: 56 additions & 0 deletions command/v7/copy_source_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,29 @@ var _ = Describe("copy-source Command", func() {
Expect(opts.NoWait).To(Equal(false))
Expect(opts.AppAction).To(Equal(constant.ApplicationRestarting))
})

When("instance steps is provided", func() {
BeforeEach(func() {
cmd.Strategy = flag.DeploymentStrategy{Name: constant.DeploymentStrategyCanary}
cmd.InstanceSteps = "1,2,4"

fakeConfig.APIVersionReturns("3.999.0")
})

It("starts the new app", func() {
Expect(executeErr).ToNot(HaveOccurred())
Expect(fakeAppStager.StageAndStartCallCount()).To(Equal(1))

inputApp, inputSpace, inputOrg, inputDropletGuid, opts := fakeAppStager.StageAndStartArgsForCall(0)
Expect(inputApp).To(Equal(targetApp))
Expect(inputDropletGuid).To(Equal("target-package-guid"))
Expect(inputSpace).To(Equal(cmd.Config.TargetedSpace()))
Expect(inputOrg).To(Equal(cmd.Config.TargetedOrganization()))
Expect(opts.Strategy).To(Equal(constant.DeploymentStrategyCanary))
Expect(opts.AppAction).To(Equal(constant.ApplicationRestarting))
Expect(opts.CanarySteps).To(Equal([]resources.CanaryStep{{InstanceWeight: 1}, {InstanceWeight: 2}, {InstanceWeight: 4}}))
})
})
})

When("the no-wait flag is set", func() {
Expand Down Expand Up @@ -440,5 +463,38 @@ var _ = Describe("copy-source Command", func() {
translatableerror.IncorrectUsageError{
Message: "--max-in-flight must be greater than or equal to 1",
}),

Entry("instance-steps no strategy provided",
func() {
cmd.InstanceSteps = "1,2,3"
},
translatableerror.RequiredFlagsError{
Arg1: "--instance-steps",
Arg2: "--strategy=canary",
}),

Entry("instance-steps a valid list of ints",
func() {
cmd.Strategy = flag.DeploymentStrategy{Name: constant.DeploymentStrategyCanary}
cmd.InstanceSteps = "some,thing,not,right"
},
translatableerror.ParseArgumentError{
ArgumentName: "--instance-steps",
ExpectedType: "list of weights",
}),

Entry("instance-steps used when CAPI does not support canary steps",
func() {
cmd.InstanceSteps = "1,2,3"
cmd.Strategy.Name = constant.DeploymentStrategyCanary
fakeConfig = &commandfakes.FakeConfig{}
fakeConfig.APIVersionReturns("3.0.0")
cmd.Config = fakeConfig
},
translatableerror.MinimumCFAPIVersionNotMetError{
Command: "--instance-steps",
CurrentVersion: "3.0.0",
MinimumVersion: "3.189.0",
}),
)
})
11 changes: 8 additions & 3 deletions command/v7/push_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strconv"
"strings"

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

if len(cmd.InstanceSteps) > 0 {
return command.MinimumCCAPIVersionCheck(cmd.Config.APIVersion(), ccversion.MinVersionCanarySteps, "--instance-steps")
}

return nil
}

func (cmd PushCommand) validateInstanceSteps() bool {
for _, v := range strings.Split(cmd.InstanceSteps, ",") {
func validateInstanceSteps(instanceSteps string) bool {
for _, v := range strings.Split(instanceSteps, ",") {
_, err := strconv.ParseInt(v, 0, 64)
if err != nil {
return false
Expand Down
17 changes: 17 additions & 0 deletions command/v7/push_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,9 @@ var _ = Describe("push Command", func() {
When("canary strategy is provided", func() {
BeforeEach(func() {
cmd.Strategy = flag.DeploymentStrategy{Name: "canary"}
fakeConfig = &commandfakes.FakeConfig{}
fakeConfig.APIVersionReturns("4.0.0")
cmd.Config = fakeConfig
})

It("should succeed", func() {
Expand Down Expand Up @@ -1440,5 +1443,19 @@ var _ = Describe("push Command", func() {
Args: []string{
"--instance-steps", "--strategy=rolling or --strategy not provided",
}}),

Entry("instance-steps used when CAPI does not support canary steps",
func() {
cmd.InstanceSteps = "1,2,3"
cmd.Strategy.Name = constant.DeploymentStrategyCanary
fakeConfig = &commandfakes.FakeConfig{}
fakeConfig.APIVersionReturns("3.0.0")
cmd.Config = fakeConfig
},
translatableerror.MinimumCFAPIVersionNotMetError{
Command: "--instance-steps",
CurrentVersion: "3.0.0",
MinimumVersion: "3.189.0",
}),
)
})
28 changes: 27 additions & 1 deletion command/v7/restage_command.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
package v7

import (
"strconv"
"strings"

"code.cloudfoundry.org/cli/actor/actionerror"
"code.cloudfoundry.org/cli/actor/v7action"
"code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/constant"
"code.cloudfoundry.org/cli/api/cloudcontroller/ccversion"
"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"
"code.cloudfoundry.org/cli/resources"
)

type RestageCommand struct {
BaseCommand

RequiredArgs flag.AppName `positional-args:"yes"`
Strategy flag.DeploymentStrategy `long:"strategy" description:"Deployment strategy can be canary, rolling or null."`
InstanceSteps string `long:"instance-steps" description:"An array of percentage steps to deploy when using deployment strategy canary. (e.g. 20,40,60)"`
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"`
Strategy flag.DeploymentStrategy `long:"strategy" description:"Deployment strategy can be canary, rolling or null."`
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"`
envCFStagingTimeout interface{} `environmentName:"CF_STAGING_TIMEOUT" environmentDescription:"Max wait time for staging, in minutes" environmentDefault:"15"`
Expand Down Expand Up @@ -93,6 +99,18 @@ func (cmd RestageCommand) Execute(args []string) error {
opts.MaxInFlight = *cmd.MaxInFlight
}

if cmd.InstanceSteps != "" {
if len(cmd.InstanceSteps) > 0 {
for _, v := range strings.Split(cmd.InstanceSteps, ",") {
parsedInt, err := strconv.ParseInt(v, 0, 64)
if err != nil {
return err
}
opts.CanarySteps = append(opts.CanarySteps, resources.CanaryStep{InstanceWeight: parsedInt})
}
}
}

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)
Expand All @@ -107,6 +125,14 @@ func (cmd RestageCommand) ValidateFlags() error {
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"}
case cmd.Strategy.Name != constant.DeploymentStrategyCanary && cmd.InstanceSteps != "":
return translatableerror.RequiredFlagsError{Arg1: "--instance-steps", Arg2: "--strategy=canary"}
case len(cmd.InstanceSteps) > 0 && !validateInstanceSteps(cmd.InstanceSteps):
return translatableerror.ParseArgumentError{ArgumentName: "--instance-steps", ExpectedType: "list of weights"}
}

if len(cmd.InstanceSteps) > 0 {
return command.MinimumCCAPIVersionCheck(cmd.Config.APIVersion(), ccversion.MinVersionCanarySteps, "--instance-steps")
}

return nil
Expand Down
67 changes: 67 additions & 0 deletions command/v7/restage_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,30 @@ var _ = Describe("restage Command", func() {
})
})

When("canary strategy is provided", func() {
BeforeEach(func() {
cmd.Strategy = flag.DeploymentStrategy{Name: constant.DeploymentStrategyCanary}
cmd.InstanceSteps = "1,2,4"
fakeConfig = &commandfakes.FakeConfig{}
fakeConfig.APIVersionReturns("4.0.0")
cmd.Config = fakeConfig
})

It("starts the app with the current droplet", func() {
Expect(executeErr).ToNot(HaveOccurred())
Expect(fakeAppStager.StageAndStartCallCount()).To(Equal(1))

inputApp, inputSpace, inputOrg, inputDropletGuid, opts := fakeAppStager.StageAndStartArgsForCall(0)
Expect(inputApp).To(Equal(app))
Expect(inputDropletGuid).To(Equal("earliest-package-guid"))
Expect(inputSpace).To(Equal(cmd.Config.TargetedSpace()))
Expect(inputOrg).To(Equal(cmd.Config.TargetedOrganization()))
Expect(opts.Strategy).To(Equal(constant.DeploymentStrategyCanary))
Expect(opts.AppAction).To(Equal(constant.ApplicationRestarting))
Expect(opts.CanarySteps).To(Equal([]resources.CanaryStep{{InstanceWeight: 1}, {InstanceWeight: 2}, {InstanceWeight: 4}}))
})
})

It("displays that it's restaging", func() {
Expect(testUI.Out).To(Say("Restaging app some-app in org some-org / space some-space as steve..."))
})
Expand Down Expand Up @@ -226,5 +250,48 @@ var _ = Describe("restage Command", func() {
translatableerror.IncorrectUsageError{
Message: "--max-in-flight must be greater than or equal to 1",
}),

Entry("instance-steps provided with rolling deployment",
func() {
cmd.Strategy = flag.DeploymentStrategy{Name: constant.DeploymentStrategyRolling}
cmd.InstanceSteps = "1,2,3"
},
translatableerror.RequiredFlagsError{
Arg1: "--instance-steps",
Arg2: "--strategy=canary",
}),

Entry("instance-steps no strategy provided",
func() {
cmd.InstanceSteps = "1,2,3"
},
translatableerror.RequiredFlagsError{
Arg1: "--instance-steps",
Arg2: "--strategy=canary",
}),

Entry("instance-steps a valid list of ints",
func() {
cmd.Strategy = flag.DeploymentStrategy{Name: constant.DeploymentStrategyCanary}
cmd.InstanceSteps = "some,thing,not,right"
},
translatableerror.ParseArgumentError{
ArgumentName: "--instance-steps",
ExpectedType: "list of weights",
}),

Entry("instance-steps used when CAPI does not support canary steps",
func() {
cmd.InstanceSteps = "1,2,3"
cmd.Strategy.Name = constant.DeploymentStrategyCanary
fakeConfig = &commandfakes.FakeConfig{}
fakeConfig.APIVersionReturns("3.0.0")
cmd.Config = fakeConfig
},
translatableerror.MinimumCFAPIVersionNotMetError{
Command: "--instance-steps",
CurrentVersion: "3.0.0",
MinimumVersion: "3.189.0",
}),
)
})
Loading
Loading