Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions api/cloudcontroller/ccversion/minimum_version.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ const (

MinVersionCNB = "3.168.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",
}),
)
})
10 changes: 7 additions & 3 deletions command/v7/push_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -580,15 +580,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