diff --git a/commands/blue_green_deploy_command.go b/commands/blue_green_deploy_command.go index 5ad6880..88c0984 100644 --- a/commands/blue_green_deploy_command.go +++ b/commands/blue_green_deploy_command.go @@ -38,32 +38,33 @@ func (c *BlueGreenDeployCommand) GetPluginCommand() plugin.Command { Perform action on an active deploy operation cf deploy -i OPERATION_ID -a ACTION [-u URL]`, Options: map[string]string{ - extDescriptorsOpt: "Extension descriptors", - deployServiceURLOpt: "Deploy service URL, by default 'deploy-service.'", - versionRuleOpt: "Version rule (HIGHER, SAME_HIGHER, ALL)", - operationIDOpt: "Active deploy operation ID", - actionOpt: "Action to perform on active deploy operation (abort, retry, resume, monitor)", - forceOpt: "Force deploy without confirmation for aborting conflicting processes", - util.GetShortOption(noStartOpt): "Do not start apps", - util.GetShortOption(namespaceOpt): "(EXPERIMENTAL) Namespace for the MTA, applied on app names, app routes and service names", - util.GetShortOption(applyNamespaceAppNamesOpt): "(EXPERIMENTAL) Apply namespace to application names: (true, false)", - util.GetShortOption(applyNamespaceServiceNamesOpt): "(EXPERIMENTAL) Apply namespace to service names: (true, false)", - util.GetShortOption(applyNamespaceAppRoutesOpt): "(EXPERIMENTAL) Apply namespace to application routes: (true, false)", - util.GetShortOption(applyNamespaceAsSuffix): "(EXPERIMENTAL) Apply namespace as a suffix rather than a prefix: (true, false)", - util.GetShortOption(deleteServicesOpt): "Recreate changed services / delete discontinued services", - util.GetShortOption(deleteServiceKeysOpt): "Delete existing service keys and apply the new ones", - util.GetShortOption(deleteServiceBrokersOpt): "Delete discontinued service brokers", - util.GetShortOption(keepFilesOpt): "Keep files used for deployment", - util.GetShortOption(noRestartSubscribedAppsOpt): "Do not restart subscribed apps, updated during the deployment", - util.GetShortOption(noConfirmOpt): "Do not require confirmation for deleting the previously deployed MTA apps", - util.GetShortOption(noFailOnMissingPermissionsOpt): "Do not fail on missing permissions for admin operations", - util.GetShortOption(abortOnErrorOpt): "Auto-abort the process on any errors", - util.GetShortOption(retriesOpt): "Retry the operation N times in case a non-content error occurs (default 3)", - util.GetShortOption(skipIdleStart): "Directly start the new MTA version as 'live', skipping the 'idle' phase of the resources. Do not require further confirmation or testing before deleting the old version", - util.GetShortOption(stageTimeoutOpt): "Stage app timeout in seconds", - util.GetShortOption(uploadTimeoutOpt): "Upload app timeout in seconds", - util.GetShortOption(taskExecutionTimeoutOpt): "Task execution timeout in seconds", - util.CombineFullAndShortParameters(startTimeoutOpt, timeoutOpt): "Start app timeout in seconds", + extDescriptorsOpt: "Extension descriptors", + deployServiceURLOpt: "Deploy service URL, by default 'deploy-service.'", + versionRuleOpt: "Version rule (HIGHER, SAME_HIGHER, ALL)", + operationIDOpt: "Active deploy operation ID", + actionOpt: "Action to perform on active deploy operation (abort, retry, resume, monitor)", + forceOpt: "Force deploy without confirmation for aborting conflicting processes", + util.GetShortOption(noStartOpt): "Do not start apps", + util.GetShortOption(namespaceOpt): "(EXPERIMENTAL) Namespace for the MTA, applied on app names, app routes and service names", + util.GetShortOption(applyNamespaceAppNamesOpt): "(EXPERIMENTAL) Apply namespace to application names: (true, false)", + util.GetShortOption(applyNamespaceServiceNamesOpt): "(EXPERIMENTAL) Apply namespace to service names: (true, false)", + util.GetShortOption(applyNamespaceAppRoutesOpt): "(EXPERIMENTAL) Apply namespace to application routes: (true, false)", + util.GetShortOption(applyNamespaceAsSuffix): "(EXPERIMENTAL) Apply namespace as a suffix rather than a prefix: (true, false)", + util.GetShortOption(deleteServicesOpt): "Recreate changed services / delete discontinued services", + util.GetShortOption(deleteServiceKeysOpt): "Delete existing service keys and apply the new ones", + util.GetShortOption(deleteServiceBrokersOpt): "Delete discontinued service brokers", + util.GetShortOption(keepFilesOpt): "Keep files used for deployment", + util.GetShortOption(noRestartSubscribedAppsOpt): "Do not restart subscribed apps, updated during the deployment", + util.GetShortOption(noConfirmOpt): "Do not require confirmation for deleting the previously deployed MTA apps", + util.GetShortOption(noFailOnMissingPermissionsOpt): "Do not fail on missing permissions for admin operations", + util.GetShortOption(abortOnErrorOpt): "Auto-abort the process on any errors", + util.GetShortOption(retriesOpt): "Retry the operation N times in case a non-content error occurs (default 3)", + util.GetShortOption(skipIdleStart): "Directly start the new MTA version as 'live', skipping the 'idle' phase of the resources. Do not require further confirmation or testing before deleting the old version", + util.GetShortOption(stageTimeoutOpt): "Stage app timeout in seconds", + util.GetShortOption(uploadTimeoutOpt): "Upload app timeout in seconds", + util.GetShortOption(taskExecutionTimeoutOpt): "Task execution timeout in seconds", + util.CombineFullAndShortParameters(startTimeoutOpt, timeoutOpt): "Start app timeout in seconds", + util.GetShortOption(shouldBackupPreviousVersionOpt): "(EXPERIMENTAL) Backup previous version of applications, use new cli command \"rollback-mta\" to rollback to the previous version", }, }, } diff --git a/commands/deploy_command.go b/commands/deploy_command.go index f23e8fd..8b1ac95 100644 --- a/commands/deploy_command.go +++ b/commands/deploy_command.go @@ -29,29 +29,30 @@ import ( ) const ( - extDescriptorsOpt = "e" - timeoutOpt = "t" - versionRuleOpt = "version-rule" - noStartOpt = "no-start" - deleteServiceKeysOpt = "delete-service-keys" - keepFilesOpt = "keep-files" - skipOwnershipValidationOpt = "skip-ownership-validation" - moduleOpt = "m" - resourceOpt = "r" - allModulesOpt = "all-modules" - allResourcesOpt = "all-resources" - strategyOpt = "strategy" - skipTestingPhase = "skip-testing-phase" - skipIdleStart = "skip-idle-start" - startTimeoutOpt = "apps-start-timeout" - stageTimeoutOpt = "apps-stage-timeout" - uploadTimeoutOpt = "apps-upload-timeout" - taskExecutionTimeoutOpt = "apps-task-execution-timeout" - applyNamespaceAppNamesOpt = "apply-namespace-app-names" - applyNamespaceServiceNamesOpt = "apply-namespace-service-names" - applyNamespaceAppRoutesOpt = "apply-namespace-app-routes" - applyNamespaceAsSuffix = "apply-namespace-as-suffix" - maxNamespaceSize = 36 + extDescriptorsOpt = "e" + timeoutOpt = "t" + versionRuleOpt = "version-rule" + noStartOpt = "no-start" + deleteServiceKeysOpt = "delete-service-keys" + keepFilesOpt = "keep-files" + skipOwnershipValidationOpt = "skip-ownership-validation" + moduleOpt = "m" + resourceOpt = "r" + allModulesOpt = "all-modules" + allResourcesOpt = "all-resources" + strategyOpt = "strategy" + skipTestingPhase = "skip-testing-phase" + skipIdleStart = "skip-idle-start" + startTimeoutOpt = "apps-start-timeout" + stageTimeoutOpt = "apps-stage-timeout" + uploadTimeoutOpt = "apps-upload-timeout" + taskExecutionTimeoutOpt = "apps-task-execution-timeout" + applyNamespaceAppNamesOpt = "apply-namespace-app-names" + applyNamespaceServiceNamesOpt = "apply-namespace-service-names" + applyNamespaceAppRoutesOpt = "apply-namespace-app-routes" + applyNamespaceAsSuffix = "apply-namespace-as-suffix" + maxNamespaceSize = 36 + shouldBackupPreviousVersionOpt = "backup-previous-version" ) type listFlag struct { @@ -114,37 +115,38 @@ func (c *DeployCommand) GetPluginCommand() plugin.Command { | cf deploy [-e EXT_DESCRIPTOR[,...]] [-t TIMEOUT] [--version-rule VERSION_RULE] [-u MTA_CONTROLLER_URL] [--retries RETRIES] [--no-start] [--namespace NAMESPACE] [--apply-namespace-app-names true/false] [--apply-namespace-service-names true/false] [--apply-namespace-app-routes true/false] [--apply-namespace-as-suffix true/false ] [--delete-services] [--delete-service-keys] [--delete-service-brokers] [--keep-files] [--no-restart-subscribed-apps] [--do-not-fail-on-missing-permissions] [--abort-on-error] [--strategy STRATEGY] [--skip-testing-phase] [--skip-idle-start] [--apps-start-timeout TIMEOUT] [--apps-stage-timeout TIMEOUT] [--apps-upload-timeout TIMEOUT] [--apps-task-execution-timeout TIMEOUT]`, Options: map[string]string{ - extDescriptorsOpt: "Extension descriptors", - deployServiceURLOpt: "Deploy service URL, by default 'deploy-service.'", - versionRuleOpt: "Version rule (HIGHER, SAME_HIGHER, ALL)", - operationIDOpt: "Active deploy operation ID", - actionOpt: "Action to perform on active deploy operation (abort, retry, resume, monitor)", - forceOpt: "Force deploy without confirmation for aborting conflicting processes", - moduleOpt: "Deploy list of modules which are contained in the deployment descriptor, in the current location", - resourceOpt: "Deploy list of resources which are contained in the deployment descriptor, in the current location", - util.GetShortOption(noStartOpt): "Do not start apps", - util.GetShortOption(namespaceOpt): "(EXPERIMENTAL) Namespace for the MTA, applied on app names, app routes and service names", - util.GetShortOption(applyNamespaceAppNamesOpt): "(EXPERIMENTAL) Apply namespace to application names: (true, false)", - util.GetShortOption(applyNamespaceServiceNamesOpt): "(EXPERIMENTAL) Apply namespace to service names: (true, false)", - util.GetShortOption(applyNamespaceAppRoutesOpt): "(EXPERIMENTAL) Apply namespace to application routes: (true, false)", - util.GetShortOption(applyNamespaceAsSuffix): "(EXPERIMENTAL) Apply namespace as a suffix rather than a prefix: (true, false)", - util.GetShortOption(deleteServicesOpt): "Recreate changed services / delete discontinued services", - util.GetShortOption(deleteServiceKeysOpt): "Delete existing service keys and apply the new ones", - util.GetShortOption(deleteServiceBrokersOpt): "Delete discontinued service brokers", - util.GetShortOption(keepFilesOpt): "Keep files used for deployment", - util.GetShortOption(noRestartSubscribedAppsOpt): "Do not restart subscribed apps, updated during the deployment", - util.GetShortOption(noFailOnMissingPermissionsOpt): "Do not fail on missing permissions for admin operations", - util.GetShortOption(abortOnErrorOpt): "Auto-abort the process on any errors", - util.GetShortOption(allModulesOpt): "Deploy all modules which are contained in the deployment descriptor, in the current location", - util.GetShortOption(allResourcesOpt): "Deploy all resources which are contained in the deployment descriptor, in the current location", - util.GetShortOption(retriesOpt): "Retry the operation N times in case a non-content error occurs (default 3)", - util.GetShortOption(strategyOpt): "Specify the deployment strategy when updating an mta (default, blue-green, (EXPERIMENTAL) incremental-blue-green)", - util.GetShortOption(skipTestingPhase): "(STRATEGY: BLUE-GREEN, (EXPERIMENTAL) INCREMENTAL-BLUE-GREEN) Do not require confirmation for deleting the previously deployed MTA app", - util.GetShortOption(skipIdleStart): "(STRATEGY: BLUE-GREEN, (EXPERIMENTAL) INCREMENTAL-BLUE-GREEN) Directly start the new MTA version as 'live', skipping the 'idle' phase of the resources. Do not require further confirmation or testing before deleting the old version", - util.GetShortOption(stageTimeoutOpt): "Stage app timeout in seconds", - util.GetShortOption(uploadTimeoutOpt): "Upload app timeout in seconds", - util.GetShortOption(taskExecutionTimeoutOpt): "Task execution timeout in seconds", - util.CombineFullAndShortParameters(startTimeoutOpt, timeoutOpt): "Start app timeout in seconds", + extDescriptorsOpt: "Extension descriptors", + deployServiceURLOpt: "Deploy service URL, by default 'deploy-service.'", + versionRuleOpt: "Version rule (HIGHER, SAME_HIGHER, ALL)", + operationIDOpt: "Active deploy operation ID", + actionOpt: "Action to perform on active deploy operation (abort, retry, resume, monitor)", + forceOpt: "Force deploy without confirmation for aborting conflicting processes", + moduleOpt: "Deploy list of modules which are contained in the deployment descriptor, in the current location", + resourceOpt: "Deploy list of resources which are contained in the deployment descriptor, in the current location", + util.GetShortOption(noStartOpt): "Do not start apps", + util.GetShortOption(namespaceOpt): "(EXPERIMENTAL) Namespace for the MTA, applied on app names, app routes and service names", + util.GetShortOption(applyNamespaceAppNamesOpt): "(EXPERIMENTAL) Apply namespace to application names: (true, false)", + util.GetShortOption(applyNamespaceServiceNamesOpt): "(EXPERIMENTAL) Apply namespace to service names: (true, false)", + util.GetShortOption(applyNamespaceAppRoutesOpt): "(EXPERIMENTAL) Apply namespace to application routes: (true, false)", + util.GetShortOption(applyNamespaceAsSuffix): "(EXPERIMENTAL) Apply namespace as a suffix rather than a prefix: (true, false)", + util.GetShortOption(deleteServicesOpt): "Recreate changed services / delete discontinued services", + util.GetShortOption(deleteServiceKeysOpt): "Delete existing service keys and apply the new ones", + util.GetShortOption(deleteServiceBrokersOpt): "Delete discontinued service brokers", + util.GetShortOption(keepFilesOpt): "Keep files used for deployment", + util.GetShortOption(noRestartSubscribedAppsOpt): "Do not restart subscribed apps, updated during the deployment", + util.GetShortOption(noFailOnMissingPermissionsOpt): "Do not fail on missing permissions for admin operations", + util.GetShortOption(abortOnErrorOpt): "Auto-abort the process on any errors", + util.GetShortOption(allModulesOpt): "Deploy all modules which are contained in the deployment descriptor, in the current location", + util.GetShortOption(allResourcesOpt): "Deploy all resources which are contained in the deployment descriptor, in the current location", + util.GetShortOption(retriesOpt): "Retry the operation N times in case a non-content error occurs (default 3)", + util.GetShortOption(strategyOpt): "Specify the deployment strategy when updating an mta (default, blue-green, (EXPERIMENTAL) incremental-blue-green)", + util.GetShortOption(skipTestingPhase): "(STRATEGY: BLUE-GREEN, (EXPERIMENTAL) INCREMENTAL-BLUE-GREEN) Do not require confirmation for deleting the previously deployed MTA app", + util.GetShortOption(skipIdleStart): "(STRATEGY: BLUE-GREEN, (EXPERIMENTAL) INCREMENTAL-BLUE-GREEN) Directly start the new MTA version as 'live', skipping the 'idle' phase of the resources. Do not require further confirmation or testing before deleting the old version", + util.GetShortOption(stageTimeoutOpt): "Stage app timeout in seconds", + util.GetShortOption(uploadTimeoutOpt): "Upload app timeout in seconds", + util.GetShortOption(taskExecutionTimeoutOpt): "Task execution timeout in seconds", + util.CombineFullAndShortParameters(startTimeoutOpt, timeoutOpt): "Start app timeout in seconds", + util.GetShortOption(shouldBackupPreviousVersionOpt): "(STRATEGY: BLUE-GREEN, (EXPERIMENTAL) INCREMENTAL-BLUE-GREEN) Backup previous version of applications, use new cli command \"rollback-mta\" to rollback to the previous version", }, }, } @@ -174,7 +176,7 @@ func deployProcessParametersSetter() ProcessParametersSetter { var lastSetValue string = "" for i := 0; i < len(os.Args); i++ { arg := os.Args[i] - if arg == "-t" { + if arg == "-t" { if i+1 < len(os.Args) { lastSetValue = os.Args[i+1] i++ @@ -223,6 +225,7 @@ func (c *DeployCommand) defineCommandOptions(flags *flag.FlagSet) { flags.String(stageTimeoutOpt, "", "") flags.String(uploadTimeoutOpt, "", "") flags.String(taskExecutionTimeoutOpt, "", "") + flags.Bool(shouldBackupPreviousVersionOpt, false, "") } func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string, flags *flag.FlagSet, cfTarget util.CloudFoundryTarget) ExecutionStatus { @@ -236,7 +239,7 @@ func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string, mtaElementsCalculator := createMtaElementsCalculator(flags) - rawMtaArchive, err := c.getMtaArchive(positionalArgs, mtaElementsCalculator) + rawMtaArchive, err := c.getMtaArchive(positionalArgs, mtaElementsCalculator) if err != nil { ui.Failed("Error retrieving MTA: %s", err.Error()) return Failure @@ -658,61 +661,61 @@ func (deployCommandLineArgumentsParser) determinePositionalArgumentsToValidate(p type deployCommandFlagsValidator struct{} func (deployCommandFlagsValidator) ValidateParsedFlags(flags *flag.FlagSet) error { - var err error - - flags.Visit(func(f *flag.Flag) { - switch f.Name { - case strategyOpt: - if f.Value.String() == "" { - err = errors.New("strategy flag defined but no argument specified") - return - } else if !util.Contains(AvailableStrategies(), f.Value.String()) { - err = fmt.Errorf("%s is not a valid deployment strategy, available strategies: %v", f.Value.String(), AvailableStrategies()) - return - } - case namespaceOpt: - if len(f.Value.String()) > maxNamespaceSize { - err = fmt.Errorf("Invalid value for namespace. The namespace cannot be more than %d symbols.", maxNamespaceSize) - return - } - case timeoutOpt, startTimeoutOpt, stageTimeoutOpt, uploadTimeoutOpt, taskExecutionTimeoutOpt: - if e := ValidateTimeoutOption(f.Name, flags, 259200); e != nil { - err = e - return - } - case applyNamespaceAppNamesOpt, applyNamespaceServiceNamesOpt, applyNamespaceAppRoutesOpt, applyNamespaceAsSuffix: + var err error + + flags.Visit(func(f *flag.Flag) { + switch f.Name { + case strategyOpt: + if f.Value.String() == "" { + err = errors.New("strategy flag defined but no argument specified") + return + } else if !util.Contains(AvailableStrategies(), f.Value.String()) { + err = fmt.Errorf("%s is not a valid deployment strategy, available strategies: %v", f.Value.String(), AvailableStrategies()) + return + } + case namespaceOpt: + if len(f.Value.String()) > maxNamespaceSize { + err = fmt.Errorf("Invalid value for namespace. The namespace cannot be more than %d symbols.", maxNamespaceSize) + return + } + case timeoutOpt, startTimeoutOpt, stageTimeoutOpt, uploadTimeoutOpt, taskExecutionTimeoutOpt: + if e := ValidateTimeoutOption(f.Name, flags, 259200); e != nil { + err = e + return + } + case applyNamespaceAppNamesOpt, applyNamespaceServiceNamesOpt, applyNamespaceAppRoutesOpt, applyNamespaceAsSuffix: if e := ValidateBooleanFlag(f.Name, flags); e != nil { - err = e - return - } - } - }) - if err != nil { - return err - } - return NewDefaultCommandFlagsValidator(nil).ValidateParsedFlags(flags) + err = e + return + } + } + }) + if err != nil { + return err + } + return NewDefaultCommandFlagsValidator(nil).ValidateParsedFlags(flags) } func ValidateTimeoutOption(optionName string, flags *flag.FlagSet, maxAllowedValue int) error { - optionValueStr := flags.Lookup(optionName).Value.String() + optionValueStr := flags.Lookup(optionName).Value.String() - optionValue, err := strconv.Atoi(optionValueStr) - if err != nil || optionValue < 0 || optionValue > maxAllowedValue { - return fmt.Errorf("Invalid value for %s: %s. Value must be in the range 0 to %d.", optionName, optionValueStr, maxAllowedValue) - } - return nil + optionValue, err := strconv.Atoi(optionValueStr) + if err != nil || optionValue < 0 || optionValue > maxAllowedValue { + return fmt.Errorf("Invalid value for %s: %s. Value must be in the range 0 to %d.", optionName, optionValueStr, maxAllowedValue) + } + return nil } func ValidateBooleanFlag(flagName string, flags *flag.FlagSet) error { - flagValueStr := flags.Lookup(flagName).Value.String() + flagValueStr := flags.Lookup(flagName).Value.String() - if flagValueStr == "" { - return fmt.Errorf("%s flag defined but no argument specified", flagName) - } + if flagValueStr == "" { + return fmt.Errorf("%s flag defined but no argument specified", flagName) + } - if flagValueStr != "true" && flagValueStr != "false" { - return fmt.Errorf("Invalid value for %s: %s. Expected true or false.", flagName, flagValueStr) - } + if flagValueStr != "true" && flagValueStr != "false" { + return fmt.Errorf("Invalid value for %s: %s. Expected true or false.", flagName, flagValueStr) + } - return nil + return nil } diff --git a/commands/deployment_strategy.go b/commands/deployment_strategy.go index 2abe61e..fbea589 100644 --- a/commands/deployment_strategy.go +++ b/commands/deployment_strategy.go @@ -20,9 +20,10 @@ func (d *DeployCommandDeploymentStrategy) CreateProcessBuilder() *util.ProcessBu } type BlueGreenDeployCommandDeploymentStrategy struct { - noConfirm bool - skipIdleStart bool - incrementalDeploy bool + noConfirm bool + skipIdleStart bool + incrementalDeploy bool + shouldBackupPreviousVersion bool } func (b *BlueGreenDeployCommandDeploymentStrategy) CreateProcessBuilder() *util.ProcessBuilder { @@ -32,21 +33,22 @@ func (b *BlueGreenDeployCommandDeploymentStrategy) CreateProcessBuilder() *util. processBuilder.Parameter("skipIdleStart", strconv.FormatBool(b.skipIdleStart)) processBuilder.Parameter("keepOriginalAppNamesAfterDeploy", strconv.FormatBool(true)) processBuilder.Parameter("shouldApplyIncrementalInstancesUpdate", strconv.FormatBool(b.incrementalDeploy)) + processBuilder.Parameter("shouldBackupPreviousVersion", strconv.FormatBool(b.shouldBackupPreviousVersion)) return processBuilder } func NewDeploymentStrategy(flags *flag.FlagSet, typeProvider ProcessTypeProvider) DeploymentStrategy { if typeProvider.GetProcessType() == (blueGreenDeployCommandProcessTypeProvider{}).GetProcessType() { - return &BlueGreenDeployCommandDeploymentStrategy{GetBoolOpt(noConfirmOpt, flags), GetBoolOpt(skipIdleStart, flags), isIncrementalBlueGreen(flags)} + return &BlueGreenDeployCommandDeploymentStrategy{GetBoolOpt(noConfirmOpt, flags), GetBoolOpt(skipIdleStart, flags), isIncrementalBlueGreen(flags), GetBoolOpt(shouldBackupPreviousVersionOpt, flags)} } strategy := GetStringOpt(strategyOpt, flags) if strategy == "default" { return &DeployCommandDeploymentStrategy{} } if GetBoolOpt(skipIdleStart, flags) { - return &BlueGreenDeployCommandDeploymentStrategy{true, true, isIncrementalBlueGreen(flags)} + return &BlueGreenDeployCommandDeploymentStrategy{true, true, isIncrementalBlueGreen(flags), GetBoolOpt(shouldBackupPreviousVersionOpt, flags)} } - return &BlueGreenDeployCommandDeploymentStrategy{GetBoolOpt(skipTestingPhase, flags), false, isIncrementalBlueGreen(flags)} + return &BlueGreenDeployCommandDeploymentStrategy{GetBoolOpt(skipTestingPhase, flags), false, isIncrementalBlueGreen(flags), GetBoolOpt(shouldBackupPreviousVersionOpt, flags)} } func isIncrementalBlueGreen(flags *flag.FlagSet) bool { diff --git a/commands/deployment_strategy_test.go b/commands/deployment_strategy_test.go index 6cc9bd5..179a4d4 100644 --- a/commands/deployment_strategy_test.go +++ b/commands/deployment_strategy_test.go @@ -15,11 +15,12 @@ var _ = Describe("Deployment Strategy", func() { const noConfirmOpt = "noConfirm" const keepOriginalNamesAfterDeploy = "keepOriginalAppNamesAfterDeploy" const skipIdleStart = "skipIdleStart" + const shouldBackupPreviousVersion = "shouldBackupPreviousVersion" var deployProcessTypeProvider = &fakes.FakeDeployCommandProcessTypeProvider{} var bgDeployProcessTypeProvider = &fakes.FakeBlueGreenCommandProcessTypeProvider{} - var createFlags = func(noConfirm bool, skipIdleStart bool, strategy string) *flag.FlagSet { + var createFlags = func(noConfirm bool, skipIdleStart bool, strategy string, backupPreviousVersion bool) *flag.FlagSet { flags := flag.NewFlagSet("", flag.ContinueOnError) flags.SetOutput(io.Discard) @@ -27,11 +28,12 @@ var _ = Describe("Deployment Strategy", func() { flags.Bool("no-confirm", noConfirm, "") flags.Bool("skip-testing-phase", true, "") flags.Bool("skip-idle-start", skipIdleStart, "") + flags.Bool("backup-previous-version", backupPreviousVersion, "") return flags } var testInputAndOperationProcessTypesMatch = func(provider commands.ProcessTypeProvider) { - flags := createFlags(false, false, "default") + flags := createFlags(false, false, "default", false) processBuilder := commands.NewDeploymentStrategy(flags, provider).CreateProcessBuilder() operation := processBuilder.Build() Expect(operation.ProcessType).To(Equal(provider.GetProcessType())) @@ -45,7 +47,7 @@ var _ = Describe("Deployment Strategy", func() { Context("with a blue-green deploy command and --no-confirm flag", func() { It("should build a blue-green deploy operation with the noConfirm parameter set to true", func() { - flags := createFlags(true, false, "default") + flags := createFlags(true, false, "default", false) processBuilder := commands.NewDeploymentStrategy(flags, bgDeployProcessTypeProvider).CreateProcessBuilder() operation := processBuilder.Build() @@ -56,7 +58,7 @@ var _ = Describe("Deployment Strategy", func() { Context("with a blue-green deploy command and --skip-idle-start flag", func() { It("should build a blue-green deploy operation with the skipIdleStart parameter set to true", func() { - flags := createFlags(false, true, "default") + flags := createFlags(false, true, "default", false) processBuilder := commands.NewDeploymentStrategy(flags, bgDeployProcessTypeProvider).CreateProcessBuilder() operation := processBuilder.Build() @@ -73,7 +75,7 @@ var _ = Describe("Deployment Strategy", func() { Context("with a deploy command with strategy flag set to blue-green", func() { It("should build a blue-green deploy operation", func() { - flags := createFlags(false, false, "blue-green") + flags := createFlags(false, false, "blue-green", false) processBuilder := commands.NewDeploymentStrategy(flags, deployProcessTypeProvider).CreateProcessBuilder() operation := processBuilder.Build() @@ -85,7 +87,7 @@ var _ = Describe("Deployment Strategy", func() { Context("with a deploy command with strategy flag set to blue-green and --no-confirm flag present", func() { It("should build a blue-green deploy operation with the noConfirm parameter set to true", func() { - flags := createFlags(true, false, "blue-green") + flags := createFlags(true, false, "blue-green", false) processBuilder := commands.NewDeploymentStrategy(flags, deployProcessTypeProvider).CreateProcessBuilder() operation := processBuilder.Build() @@ -98,7 +100,7 @@ var _ = Describe("Deployment Strategy", func() { Context("with a deploy command with strategy flag set to blue-green and skip-idl-start set to true", func() { It("should build a blue-green deploy operation", func() { - flags := createFlags(false, true, "blue-green") + flags := createFlags(false, true, "blue-green", false) processBuilder := commands.NewDeploymentStrategy(flags, deployProcessTypeProvider).CreateProcessBuilder() operation := processBuilder.Build() @@ -108,4 +110,41 @@ var _ = Describe("Deployment Strategy", func() { Expect(operation.Parameters[skipIdleStart]).To(Equal(strconv.FormatBool(true))) }) }) + + Context("with a deploy command with strategy flag set to blue-green and backup-previous-version set to true", func() { + It("should build a blue-green deploy operation with set backup-previous-version flag", func() { + flags := createFlags(true, false, "blue-green", true) + + processBuilder := commands.NewDeploymentStrategy(flags, deployProcessTypeProvider).CreateProcessBuilder() + operation := processBuilder.Build() + + Expect(operation.ProcessType).To(Equal(bgDeployProcessTypeProvider.GetProcessType())) + Expect(operation.Parameters[shouldBackupPreviousVersion]).To(Equal(strconv.FormatBool(true))) + }) + }) + + Context("with a deploy command with strategy flag set to incremental-blue-green and backup-previous-version set to true", func() { + It("should build a blue-green deploy operation with set incremental-blue-green to true and backup-previous-version to true", func() { + flags := createFlags(true, false, "incremental-blue-green", true) + + processBuilder := commands.NewDeploymentStrategy(flags, deployProcessTypeProvider).CreateProcessBuilder() + operation := processBuilder.Build() + + Expect(operation.ProcessType).To(Equal(bgDeployProcessTypeProvider.GetProcessType())) + Expect(operation.Parameters[shouldBackupPreviousVersion]).To(Equal(strconv.FormatBool(true))) + Expect(operation.Parameters["shouldApplyIncrementalInstancesUpdate"]).To(Equal(strconv.FormatBool(true))) + }) + }) + + Context("with a deploy command with default strategy flag and backup-previous-version flag", func() { + It("should build a deploy operation without backup-previous-version flag", func() { + flags := createFlags(false, false, "default", true) + + processBuilder := commands.NewDeploymentStrategy(flags, deployProcessTypeProvider).CreateProcessBuilder() + operation := processBuilder.Build() + + Expect(operation.ProcessType).To(Equal(deployProcessTypeProvider.GetProcessType())) + Expect(operation.Parameters).NotTo(HaveKey(shouldBackupPreviousVersion)) + }) + }) }) diff --git a/commands/rollback_mta command_test.go b/commands/rollback_mta command_test.go new file mode 100644 index 0000000..d3b4427 --- /dev/null +++ b/commands/rollback_mta command_test.go @@ -0,0 +1,235 @@ +package commands_test + +import ( + "fmt" + + pluginFakes "code.cloudfoundry.org/cli/plugin/pluginfakes" + cliFakes "github.com/cloudfoundry-incubator/multiapps-cli-plugin/cli/fakes" + "github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/baseclient" + "github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/models" + "github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/mtaclient" + mtaFake "github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/mtaclient/fakes" + mtaV2Fake "github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/mtaclient_v2/fakes" + mtaV2fake "github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/mtaclient_v2/fakes" + "github.com/cloudfoundry-incubator/multiapps-cli-plugin/commands" + "github.com/cloudfoundry-incubator/multiapps-cli-plugin/testutil" + "github.com/cloudfoundry-incubator/multiapps-cli-plugin/ui" + utilFakes "github.com/cloudfoundry-incubator/multiapps-cli-plugin/util/fakes" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("RollbackMtaCommand", func() { + const org = "test-org" + const space = "test-space" + const spaceId = "test-space-guid" + const user = "test-user" + const mtaID = "test" + const ongoingOperationId = "999" + + var name string + var cliConnection *pluginFakes.FakeCliConnection + var mtaClient *mtaFake.FakeMtaClientOperations + var testClientFactory *commands.TestClientFactory + var command *commands.RollbackMtaCommand + var oc = testutil.NewUIOutputCapturer() + var ex = testutil.NewUIExpector() + + var rollbackMtaOperation = models.Operation{ + State: "FINISHED", + ProcessID: testutil.ProcessID, + ProcessType: "ROLLBACK_MTA", + Messages: []*models.Message{&testutil.SimpleMessage}, + } + + var ongoingOperation = models.Operation{ + AcquiredLock: true, + Messages: []*models.Message{&testutil.SimpleMessage}, + MtaID: mtaID, + ProcessID: ongoingOperationId, + ProcessType: "ROLLBACK_MTA", + SpaceID: spaceId, + State: "RUNNING", + User: user, + } + + var ongoingOperations = []*models.Operation{&ongoingOperation} + + var getOutputLines = func(processID string, abortedProcessId string) []string { + lines := []string{} + lines = append(lines, + "Rollback multi-target app "+mtaID+" in org "+org+" / space "+space+" as "+user+"...") + if abortedProcessId != "" { + lines = append(lines, + "Executing action \"abort\" on operation "+abortedProcessId+"...", + "OK") + } + lines = append(lines, + "Test message", + "Process finished.", + "Use \"cf dmol -i "+processID+"\" to download the logs of the process.") + return lines + } + + BeforeEach(func() { + ui.DisableTerminalOutput(true) + name = command.GetPluginCommand().Name + cliConnection = cliFakes.NewFakeCliConnectionBuilder(). + CurrentOrg("test-org-guid", org, nil). + CurrentSpace(spaceId, space, nil). + Username(user, nil). + AccessToken("bearer test-token", nil).Build() + mtaClient = mtaFake.NewFakeMtaClientBuilder(). + GetMta(mtaID, nil, nil). + GetMtaOperations(&[]string{mtaID}[0], nil, nil, nil, nil). + StartMtaOperation(testutil.OperationResult, mtaclient.ResponseHeader{Location: "operations/1000?embed=messages"}, nil). + GetMtaOperation(testutil.ProcessID, "messages", &rollbackMtaOperation, nil).Build() + mtaV2Client := mtaV2fake.NewFakeMtaV2ClientBuilder(). + GetMtasForThisSpace(mtaID, nil, nil, nil).Build() + testClientFactory = commands.NewTestClientFactory(mtaClient, mtaV2Client, nil) + command = commands.NewRollbackMtaCommand() + testTokenFactory := commands.NewTestTokenFactory(cliConnection) + deployServiceURLCalculator := utilFakes.NewDeployServiceURLFakeCalculator("deploy-service.test.ondemand.com") + command.InitializeAll(name, cliConnection, testutil.NewCustomTransport(200), testClientFactory, testTokenFactory, deployServiceURLCalculator) + }) + + Describe("GetPluginCommand", func() { + It("returns the correct plugin command", func() { + pluginCmd := command.GetPluginCommand() + Expect(pluginCmd.Name).To(Equal("rollback-mta")) + Expect(pluginCmd.HelpText).To(Equal("Rollback of a multi-target app works only if [--backup-previous-version] flag was used during blue-green deployment and backup applications exists in the space")) + }) + }) + + Describe("Execute", func() { + Context("when operation ID and action ID are provided", func() { + It("executes the action", func() { + testClientFactory.MtaClient = mtaFake.NewFakeMtaClientBuilder(). + GetMtaOperations(&[]string{mtaID}[0], nil, nil, ongoingOperations, nil). + GetOperationActions(ongoingOperationId, []string{"abort"}, nil). + ExecuteAction(ongoingOperationId, "abort", mtaclient.ResponseHeader{Location: "operations/999?embed=messages"}, nil). + GetMtaOperation(testutil.ProcessID, "messages", &rollbackMtaOperation, nil).Build() + testClientFactory.MtaV2Client = mtaV2Fake.NewFakeMtaV2ClientBuilder(). + GetMtasForThisSpace(mtaID, nil, nil, nil).Build() + output, status := oc.CaptureOutputAndStatus(func() int { + return command.Execute([]string{"-i", "999", "-a", "abort"}).ToInt() + }) + ex.ExpectSuccessWithOutput(status, output, []string{"Executing action \"abort\" on operation 999...", "OK"}) + }) + + It("executes the action and fails with not found operation", func() { + testClientFactory.MtaClient = mtaFake.NewFakeMtaClientBuilder(). + GetMtaOperations(&[]string{mtaID}[0], nil, nil, []*models.Operation{}, nil). + GetOperationActions(ongoingOperationId, []string{"abort"}, fmt.Errorf("not-found")).Build() + testClientFactory.MtaV2Client = mtaV2Fake.NewFakeMtaV2ClientBuilder(). + GetMtasForThisSpace(mtaID, nil, nil, nil).Build() + output, status := oc.CaptureOutputAndStatus(func() int { + return command.Execute([]string{"-i", "999", "-a", "abort"}).ToInt() + }) + ex.ExpectFailure(status, output, "Multi-target app operation with ID 999 not found") + }) + }) + + Context("with a correct mta id provided and ongoing operation found and force option provided", func() { + It("should try to abort the conflicting process and fail it", func() { + testClientFactory.MtaClient = mtaFake.NewFakeMtaClientBuilder(). + GetMtaOperations(&[]string{mtaID}[0], nil, nil, ongoingOperations, nil). + GetOperationActions(ongoingOperationId, []string{"abort"}, nil). + ExecuteAction(ongoingOperationId, "abort", mtaclient.ResponseHeader{Location: "operations/999?embed=messages"}, fmt.Errorf("test-error")).Build() + testClientFactory.MtaV2Client = mtaV2Fake.NewFakeMtaV2ClientBuilder(). + GetMtasForThisSpace(mtaID, nil, nil, nil).Build() + output, status := oc.CaptureOutputAndStatus(func() int { + return command.Execute([]string{mtaID, "-f"}).ToInt() + }) + ex.ExpectFailureOnLine(status, output, "Could not execute action \"abort\" on operation 999: test-error", 3) + }) + + It("should try to abort the conflicting process and success", func() { + testClientFactory.MtaClient = mtaFake.NewFakeMtaClientBuilder(). + GetMtaOperations(&[]string{mtaID}[0], nil, nil, ongoingOperations, nil). + GetOperationActions(ongoingOperationId, []string{"abort"}, nil). + ExecuteAction(ongoingOperationId, "abort", mtaclient.ResponseHeader{Location: "operations/999?embed=messages"}, nil). + StartMtaOperation(testutil.OperationResult, mtaclient.ResponseHeader{Location: "operations/1000?embed=messages"}, nil). + GetMtaOperation(testutil.ProcessID, "messages", &rollbackMtaOperation, nil).Build() + testClientFactory.MtaV2Client = mtaV2Fake.NewFakeMtaV2ClientBuilder(). + GetMtasForThisSpace(mtaID, nil, nil, nil).Build() + output, status := oc.CaptureOutputAndStatus(func() int { + return command.Execute([]string{mtaID, "-f"}).ToInt() + }) + ex.ExpectSuccessWithOutput(status, output, getOutputLines(testutil.ProcessID, ongoingOperationId)) + }) + }) + + Context("with error during during getting mtas for space", func() { + It("should display error and exit with non-zero status", func() { + var clientError = baseclient.NewClientError(testutil.ClientError) + testClientFactory.MtaV2Client = mtaV2Fake.NewFakeMtaV2ClientBuilder(). + GetMtasForThisSpace("test-non-existing-id", nil, nil, clientError).Build() + output, status := oc.CaptureOutputAndStatus(func() int { + return command.Execute([]string{"test-non-existing-id", "-f"}).ToInt() + }) + ex.ExpectFailureOnLine(status, output, "Could not get multi-target app test-non-existing-id:", 2) + }) + }) + + Context("with an incorrect mta id provided", func() { + It("should display error and exit with non-zero status", func() { + mta_id := "non-existing-mta" + custom_error := testutil.NewCustomError(404, "mtas", "MTA with name \""+mta_id+"\" does not exist") + var clientError = baseclient.NewClientError(custom_error) + testClientFactory.MtaV2Client = mtaV2Fake.NewFakeMtaV2ClientBuilder(). + GetMtasForThisSpace(mta_id, nil, nil, clientError).Build() + output, status := oc.CaptureOutputAndStatus(func() int { + return command.Execute([]string{mta_id, "-f"}).ToInt() + }) + ex.ExpectFailureOnLine(status, output, "Multi-target app "+mta_id+" not found", 2) + }) + }) + + Context("with an incorrect mta id and namespace provided", func() { + It("should display error and exit with non-zero status", func() { + mta_id := "non-existing-mta" + namespace := "with-a-namespace" + custom_error := testutil.NewCustomError(404, "mtas", "MTA with name \""+mta_id+"\" and namespace \""+namespace+"\" does not exist") + var clientError = baseclient.NewClientError(custom_error) + testClientFactory.MtaV2Client = mtaV2Fake.NewFakeMtaV2ClientBuilder(). + GetMtasForThisSpace(mta_id, &namespace, nil, clientError).Build() + output, status := oc.CaptureOutputAndStatus(func() int { + return command.Execute([]string{mta_id, "-f", "--namespace", namespace}).ToInt() + }) + ex.ExpectFailureOnLine(status, output, "Multi-target app "+mta_id+" with namespace "+namespace+" not found", 2) + }) + }) + + Context("with a correct mta id provided and no ongoing operations", func() { + It("should proceed without trying to abort conflicting process", func() { + output, status := oc.CaptureOutputAndStatus(func() int { + return command.Execute([]string{"test", "-f"}).ToInt() + }) + ex.ExpectSuccessWithOutput(status, output, getOutputLines(testutil.ProcessID, "")) + }) + + It("should proceed without trying to abort conflicting process with more options", func() { + output, status := oc.CaptureOutputAndStatus(func() int { + return command.Execute([]string{"test", "-process-user-provided-services", "-do-not-fail-on-missing-permissions", "-f"}).ToInt() + }) + ex.ExpectSuccessWithOutput(status, output, getOutputLines(testutil.ProcessID, "")) + }) + }) + + Context("with a correct mta id provided but fail to start operation", func() { + It("should exit with non-zero status", func() { + testClientFactory.MtaClient = mtaFake.NewFakeMtaClientBuilder(). + GetMtaOperations(&[]string{mtaID}[0], nil, nil, []*models.Operation{}, nil). + StartMtaOperation(testutil.OperationResult, mtaclient.ResponseHeader{Location: "operations/1000?embed=messages"}, fmt.Errorf("server-error")).Build() + testClientFactory.MtaV2Client = mtaV2Fake.NewFakeMtaV2ClientBuilder(). + GetMtasForThisSpace(mtaID, nil, nil, nil).Build() + output, status := oc.CaptureOutputAndStatus(func() int { + return command.Execute([]string{mtaID, "-f"}).ToInt() + }) + ex.ExpectFailureOnLine(status, output, "Could not create rollback mta process: server-error", 2) + }) + }) + + }) +}) diff --git a/commands/rollback_mta_command.go b/commands/rollback_mta_command.go new file mode 100644 index 0000000..f88b642 --- /dev/null +++ b/commands/rollback_mta_command.go @@ -0,0 +1,163 @@ +package commands + +import ( + "flag" + "fmt" + "strconv" + "strings" + + "code.cloudfoundry.org/cli/cf/terminal" + "code.cloudfoundry.org/cli/plugin" + "github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/baseclient" + "github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/models" + "github.com/cloudfoundry-incubator/multiapps-cli-plugin/ui" + "github.com/cloudfoundry-incubator/multiapps-cli-plugin/util" +) + +const processUserProvidedServicesOpt = "process-user-provided-services" + +// RollbackMtaCommand is a command for rollback of deployed MTAs. +type RollbackMtaCommand struct { + *BaseCommand + processTypeProvider ProcessTypeProvider +} + +func NewRollbackMtaCommand() *RollbackMtaCommand { + baseCmd := &BaseCommand{flagsParser: NewProcessActionExecutorCommandArgumentsParser([]string{"MTA_ID"}), flagsValidator: NewDefaultCommandFlagsValidator(nil)} + rollbackMtaCmd := &RollbackMtaCommand{baseCmd, &rollbackMtaCommandProcessTypeProvider{}} + baseCmd.Command = rollbackMtaCmd + return rollbackMtaCmd +} + +// GetPluginCommand returns more information for the blue green deploy command. +func (c *RollbackMtaCommand) GetPluginCommand() plugin.Command { + return plugin.Command{ + Name: "rollback-mta", + HelpText: "Rollback of a multi-target app works only if [--backup-previous-version] flag was used during blue-green deployment and backup applications exists in the space", + UsageDetails: plugin.Usage{ + Usage: `Rollback of a multi-target app + cf rollback-mta MTA_ID [-t TIMEOUT] [-f] [--retries RETRIES] [--namespace NAMESPACE] [--do-not-fail-on-missing-permissions] [--abort-on-error] [--apps-start-timeout TIMEOUT] [--apps-stage-timeout TIMEOUT] [--apps-upload-timeout TIMEOUT] [--apps-task-execution-timeout TIMEOUT] + + Perform action on an active deploy operation + cf rollback-mta -i OPERATION_ID -a ACTION [-u URL]`, + Options: map[string]string{ + deployServiceURLOpt: "Deploy service URL, by default 'deploy-service.'", + operationIDOpt: "Active deploy operation ID", + actionOpt: "Action to perform on active deploy operation (abort, retry, resume, monitor)", + forceOpt: "Force deploy without confirmation for aborting conflicting processes", + util.GetShortOption(namespaceOpt): "(EXPERIMENTAL) Namespace for the MTA, applied on app names, app routes and service names", + util.GetShortOption(deleteServicesOpt): "Recreate changed services / delete discontinued services", + util.GetShortOption(noFailOnMissingPermissionsOpt): "Do not fail on missing permissions for admin operations", + util.GetShortOption(abortOnErrorOpt): "Auto-abort the process on any errors", + util.GetShortOption(processUserProvidedServicesOpt): "Enable processing of user provided services during rollback", + util.GetShortOption(retriesOpt): "Retry the operation N times in case a non-content error occurs (default 3)", + util.GetShortOption(stageTimeoutOpt): "Stage app timeout in seconds", + util.GetShortOption(uploadTimeoutOpt): "Upload app timeout in seconds", + util.GetShortOption(taskExecutionTimeoutOpt): "Task execution timeout in seconds", + util.CombineFullAndShortParameters(startTimeoutOpt, timeoutOpt): "Start app timeout in seconds", + }, + }, + } +} + +func (c *RollbackMtaCommand) defineCommandOptions(flags *flag.FlagSet) { + flags.Bool(forceOpt, false, "") + flags.String(operationIDOpt, "", "") + flags.String(namespaceOpt, "", "") + flags.Bool(deleteServicesOpt, false, "") + flags.String(actionOpt, "", "") + flags.Bool(noFailOnMissingPermissionsOpt, false, "") + flags.Bool(abortOnErrorOpt, false, "") + flags.Bool(processUserProvidedServicesOpt, false, "") + flags.Uint(retriesOpt, 3, "") + flags.String(startTimeoutOpt, "", "") + flags.String(stageTimeoutOpt, "", "") + flags.String(uploadTimeoutOpt, "", "") + flags.String(taskExecutionTimeoutOpt, "", "") +} + +func (c *RollbackMtaCommand) executeInternal(positionalArgs []string, dsHost string, flags *flag.FlagSet, cfTarget util.CloudFoundryTarget) ExecutionStatus { + operationID := GetStringOpt(operationIDOpt, flags) + actionID := GetStringOpt(actionOpt, flags) + retries := GetUintOpt(retriesOpt, flags) + + if operationID != "" || actionID != "" { + return c.ExecuteAction(operationID, actionID, retries, dsHost, cfTarget) + } + + force := GetBoolOpt(forceOpt, flags) + mtaID := positionalArgs[0] + if !force && !ui.Confirm("Really rollback multi-target app %s in org %s / space %s? (y/n)", terminal.EntityNameColor(mtaID), + terminal.EntityNameColor(cfTarget.Org.Name), + terminal.EntityNameColor(cfTarget.Space.Name)) { + ui.Warn("Rollback mta cancelled") + return Failure + } + + // Print initial message + ui.Say("Rollback multi-target app %s in org %s / space %s as %s...", + terminal.EntityNameColor(mtaID), terminal.EntityNameColor(cfTarget.Org.Name), + terminal.EntityNameColor(cfTarget.Space.Name), terminal.EntityNameColor(cfTarget.Username)) + + // Create rest client + mtaClient := c.NewMtaClient(dsHost, cfTarget) + // Create new REST client for mtas V2 api + mtaV2Client := c.NewMtaV2Client(dsHost, cfTarget) + + namespace := strings.TrimSpace(GetStringOpt(namespaceOpt, flags)) + + // Check if a deployed MTA with the specified ID exists + _, err := mtaV2Client.GetMtasForThisSpace(&mtaID, &namespace) + if err != nil { + ce, ok := err.(*baseclient.ClientError) + if ok && ce.Code == 404 && strings.Contains(fmt.Sprint(ce.Description), mtaID) { + if util.DiscardIfEmpty(namespace) != nil { + ui.Failed("Multi-target app %s with namespace %s not found", terminal.EntityNameColor(mtaID), terminal.EntityNameColor(namespace)) + } else { + ui.Failed("Multi-target app %s not found", terminal.EntityNameColor(mtaID)) + } + return Failure + } + ui.Failed("Could not get multi-target app %s: %s", terminal.EntityNameColor(mtaID), baseclient.NewClientError(err)) + return Failure + } + + // Check for an ongoing operation for this MTA ID and abort it + wasAborted, err := c.CheckOngoingOperation(mtaID, namespace, dsHost, force, cfTarget) + if err != nil { + ui.Failed(err.Error()) + return Failure + } + if !wasAborted { + return Failure + } + + processBuilder := util.NewProcessBuilder() + processBuilder.ProcessType(c.processTypeProvider.GetProcessType()) + processBuilder.Parameter("mtaId", mtaID) + processBuilder.Parameter("noFailOnMissingPermissions", strconv.FormatBool(GetBoolOpt(noFailOnMissingPermissionsOpt, flags))) + processBuilder.Parameter("abortOnError", strconv.FormatBool(GetBoolOpt(abortOnErrorOpt, flags))) + processBuilder.Parameter("processUserProvidedServices", strconv.FormatBool(GetBoolOpt(processUserProvidedServicesOpt, flags))) + processBuilder.Parameter("namespace", namespace) + processBuilder.Parameter("deleteServices", strconv.FormatBool(GetBoolOpt(deleteServicesOpt, flags))) + processBuilder.Parameter("appsStageTimeout", GetStringOpt(stageTimeoutOpt, flags)) + processBuilder.Parameter("appsUploadTimeout", GetStringOpt(uploadTimeoutOpt, flags)) + processBuilder.Parameter("appsTaskExecutionTimeout", GetStringOpt(taskExecutionTimeoutOpt, flags)) + operation := processBuilder.Build() + + // Create the new process + responseHeader, err := mtaClient.StartMtaOperation(*operation) + if err != nil { + ui.Failed("Could not create rollback mta process: %s", err) + return Failure + } + + executionMonitor := NewExecutionMonitorFromLocationHeader(c.name, responseHeader.Location.String(), retries, []*models.Message{}, mtaClient) + return executionMonitor.Monitor() +} + +type rollbackMtaCommandProcessTypeProvider struct{} + +func (rollbackMta rollbackMtaCommandProcessTypeProvider) GetProcessType() string { + return "ROLLBACK_MTA" +} diff --git a/multiapps_plugin.go b/multiapps_plugin.go index 2861c23..683b679 100644 --- a/multiapps_plugin.go +++ b/multiapps_plugin.go @@ -29,6 +29,7 @@ var Commands = []commands.Command{ commands.NewMtaCommand(), commands.NewMtaOperationsCommand(), commands.NewPurgeConfigCommand(), + commands.NewRollbackMtaCommand(), } // Run runs this plugin