diff --git a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/RollbackServerGroupStage.groovy b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/RollbackServerGroupStage.groovy index 890d2ed576..43cf34bc30 100644 --- a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/RollbackServerGroupStage.groovy +++ b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/RollbackServerGroupStage.groovy @@ -18,9 +18,13 @@ package com.netflix.spinnaker.orca.clouddriver.pipeline.servergroup import com.fasterxml.jackson.annotation.JsonIgnore -import com.netflix.spinnaker.orca.clouddriver.pipeline.providers.aws.ApplySourceServerGroupCapacityStage -import com.netflix.spinnaker.orca.clouddriver.pipeline.providers.aws.CaptureSourceServerGroupCapacityStage -import com.netflix.spinnaker.orca.kato.pipeline.support.ResizeStrategy +import com.netflix.frigga.Names +import com.netflix.spinnaker.orca.RetrySupport +import com.netflix.spinnaker.orca.clouddriver.OortService +import com.netflix.spinnaker.orca.clouddriver.pipeline.servergroup.rollback.ExplicitRollback +import com.netflix.spinnaker.orca.clouddriver.pipeline.servergroup.rollback.PreviousImageRollback +import com.netflix.spinnaker.orca.clouddriver.pipeline.servergroup.rollback.Rollback +import com.netflix.spinnaker.orca.clouddriver.pipeline.servergroup.rollback.TestRollback import com.netflix.spinnaker.orca.pipeline.StageDefinitionBuilder import com.netflix.spinnaker.orca.pipeline.model.Execution import com.netflix.spinnaker.orca.pipeline.model.Stage @@ -37,7 +41,7 @@ class RollbackServerGroupStage implements StageDefinitionBuilder { AutowireCapableBeanFactory autowireCapableBeanFactory @Override - def > List> aroundStages(Stage stage) { + > List> aroundStages(Stage stage) { def stageData = stage.mapTo(StageData) if (!stageData.rollbackType) { @@ -50,7 +54,9 @@ class RollbackServerGroupStage implements StageDefinitionBuilder { } static enum RollbackType { - EXPLICIT(ExplicitRollback) + EXPLICIT(ExplicitRollback), + PREVIOUS_IMAGE(PreviousImageRollback), + TEST(TestRollback) final Class implementationClass @@ -62,117 +68,4 @@ class RollbackServerGroupStage implements StageDefinitionBuilder { static class StageData { RollbackType rollbackType } - - static interface Rollback { - def > List> buildStages(Stage parentStage) - } - - static class ExplicitRollback implements Rollback { - String rollbackServerGroupName - String restoreServerGroupName - Integer targetHealthyRollbackPercentage - - @Autowired - @JsonIgnore - EnableServerGroupStage enableServerGroupStage - - @Autowired - @JsonIgnore - DisableServerGroupStage disableServerGroupStage - - @Autowired - @JsonIgnore - ResizeServerGroupStage resizeServerGroupStage - - @Autowired - @JsonIgnore - CaptureSourceServerGroupCapacityStage captureSourceServerGroupCapacityStage - - @Autowired - @JsonIgnore - ApplySourceServerGroupCapacityStage applySourceServerGroupCapacityStage - - @JsonIgnore - def > List> buildStages(Stage parentStage) { - def stages = [] - - Map enableServerGroupContext = new HashMap(parentStage.context) - enableServerGroupContext.targetHealthyDeployPercentage = targetHealthyRollbackPercentage - enableServerGroupContext.serverGroupName = restoreServerGroupName - stages << newStage( - parentStage.execution, enableServerGroupStage.type, "enable", enableServerGroupContext, parentStage, SyntheticStageOwner.STAGE_AFTER - ) - - stages << buildCaptureSourceServerGroupCapacityStage(parentStage, parentStage.mapTo(ResizeStrategy.Source)) - - Map resizeServerGroupContext = new HashMap(parentStage.context) + [ - action : ResizeStrategy.ResizeAction.scale_to_server_group.toString(), - source : { - def source = parentStage.mapTo(ResizeStrategy.Source) - source.serverGroupName = rollbackServerGroupName - return source - }.call(), - asgName : restoreServerGroupName, - pinMinimumCapacity: true, - targetHealthyDeployPercentage: targetHealthyRollbackPercentage - ] - stages << newStage( - parentStage.execution, resizeServerGroupStage.type, "resize", resizeServerGroupContext, parentStage, SyntheticStageOwner.STAGE_AFTER - ) - - Map disableServerGroupContext = new HashMap(parentStage.context) - disableServerGroupContext.serverGroupName = rollbackServerGroupName - stages << newStage( - parentStage.execution, disableServerGroupStage.type, "disable", disableServerGroupContext, parentStage, SyntheticStageOwner.STAGE_AFTER - ) - - stages << buildApplySourceServerGroupCapacityStage(parentStage, parentStage.mapTo(ResizeStrategy.Source)) - return stages - } - - Stage buildCaptureSourceServerGroupCapacityStage(Stage parentStage, - ResizeStrategy.Source source) { - Map captureSourceServerGroupCapacityContext = [ - useSourceCapacity: true, - source : [ - asgName : rollbackServerGroupName, - serverGroupName: rollbackServerGroupName, - region : source.region, - account : source.credentials, - cloudProvider : source.cloudProvider - ] - ] - return newStage( - parentStage.execution, - captureSourceServerGroupCapacityStage.type, - "snapshot", - captureSourceServerGroupCapacityContext, - parentStage, - SyntheticStageOwner.STAGE_AFTER - ) - } - - Stage buildApplySourceServerGroupCapacityStage(Stage parentStage, - ResizeStrategy.Source source) { - Map applySourceServerGroupCapacityContext = [ - credentials: source.credentials, - target : [ - asgName : restoreServerGroupName, - serverGroupName: restoreServerGroupName, - region : source.region, - account : source.credentials, - cloudProvider : source.cloudProvider - ] - ] - return newStage( - parentStage.execution, - applySourceServerGroupCapacityStage.type, - "apply", - applySourceServerGroupCapacityContext, - parentStage, - SyntheticStageOwner.STAGE_AFTER - ) - } - } - } diff --git a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/rollback/ExplicitRollback.groovy b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/rollback/ExplicitRollback.groovy new file mode 100644 index 0000000000..e429422dd0 --- /dev/null +++ b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/rollback/ExplicitRollback.groovy @@ -0,0 +1,139 @@ +/* + * Copyright 2017 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.netflix.spinnaker.orca.clouddriver.pipeline.servergroup.rollback + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.netflix.spinnaker.orca.clouddriver.pipeline.providers.aws.ApplySourceServerGroupCapacityStage +import com.netflix.spinnaker.orca.clouddriver.pipeline.providers.aws.CaptureSourceServerGroupCapacityStage +import com.netflix.spinnaker.orca.clouddriver.pipeline.servergroup.DisableServerGroupStage +import com.netflix.spinnaker.orca.clouddriver.pipeline.servergroup.EnableServerGroupStage +import com.netflix.spinnaker.orca.clouddriver.pipeline.servergroup.ResizeServerGroupStage +import com.netflix.spinnaker.orca.kato.pipeline.support.ResizeStrategy +import com.netflix.spinnaker.orca.pipeline.model.Execution +import com.netflix.spinnaker.orca.pipeline.model.Stage +import com.netflix.spinnaker.orca.pipeline.model.SyntheticStageOwner +import org.springframework.beans.factory.annotation.Autowired; + +import static com.netflix.spinnaker.orca.pipeline.StageDefinitionBuilder.newStage + +class ExplicitRollback implements Rollback { + String rollbackServerGroupName + String restoreServerGroupName + Integer targetHealthyRollbackPercentage + + @Autowired + @JsonIgnore + EnableServerGroupStage enableServerGroupStage + + @Autowired + @JsonIgnore + DisableServerGroupStage disableServerGroupStage + + @Autowired + @JsonIgnore + ResizeServerGroupStage resizeServerGroupStage + + @Autowired + @JsonIgnore + CaptureSourceServerGroupCapacityStage captureSourceServerGroupCapacityStage + + @Autowired + @JsonIgnore + ApplySourceServerGroupCapacityStage applySourceServerGroupCapacityStage + + @JsonIgnore + def > List> buildStages(Stage parentStage) { + def stages = [] + + Map enableServerGroupContext = new HashMap(parentStage.context) + enableServerGroupContext.targetHealthyDeployPercentage = targetHealthyRollbackPercentage + enableServerGroupContext.serverGroupName = restoreServerGroupName + stages << newStage( + parentStage.execution, enableServerGroupStage.type, "enable", enableServerGroupContext, parentStage, SyntheticStageOwner.STAGE_AFTER + ) + + stages << buildCaptureSourceServerGroupCapacityStage(parentStage, parentStage.mapTo(ResizeStrategy.Source)) + + Map resizeServerGroupContext = new HashMap(parentStage.context) + [ + action : ResizeStrategy.ResizeAction.scale_to_server_group.toString(), + source : { + def source = parentStage.mapTo(ResizeStrategy.Source) + source.serverGroupName = rollbackServerGroupName + return source + }.call(), + asgName : restoreServerGroupName, + pinMinimumCapacity : true, + targetHealthyDeployPercentage: targetHealthyRollbackPercentage + ] + stages << newStage( + parentStage.execution, resizeServerGroupStage.type, "resize", resizeServerGroupContext, parentStage, SyntheticStageOwner.STAGE_AFTER + ) + + Map disableServerGroupContext = new HashMap(parentStage.context) + disableServerGroupContext.serverGroupName = rollbackServerGroupName + stages << newStage( + parentStage.execution, disableServerGroupStage.type, "disable", disableServerGroupContext, parentStage, SyntheticStageOwner.STAGE_AFTER + ) + + stages << buildApplySourceServerGroupCapacityStage(parentStage, parentStage.mapTo(ResizeStrategy.Source)) + return stages + } + + Stage buildCaptureSourceServerGroupCapacityStage(Stage parentStage, + ResizeStrategy.Source source) { + Map captureSourceServerGroupCapacityContext = [ + useSourceCapacity: true, + source : [ + asgName : rollbackServerGroupName, + serverGroupName: rollbackServerGroupName, + region : source.region, + account : source.credentials, + cloudProvider : source.cloudProvider + ] + ] + return newStage( + parentStage.execution, + captureSourceServerGroupCapacityStage.type, + "snapshot", + captureSourceServerGroupCapacityContext, + parentStage, + SyntheticStageOwner.STAGE_AFTER + ) + } + + Stage buildApplySourceServerGroupCapacityStage(Stage parentStage, + ResizeStrategy.Source source) { + Map applySourceServerGroupCapacityContext = [ + credentials: source.credentials, + target : [ + asgName : restoreServerGroupName, + serverGroupName: restoreServerGroupName, + region : source.region, + account : source.credentials, + cloudProvider : source.cloudProvider + ] + ] + return newStage( + parentStage.execution, + applySourceServerGroupCapacityStage.type, + "apply", + applySourceServerGroupCapacityContext, + parentStage, + SyntheticStageOwner.STAGE_AFTER + ) + } +} diff --git a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/rollback/PreviousImageRollback.groovy b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/rollback/PreviousImageRollback.groovy new file mode 100644 index 0000000000..cac144f8fc --- /dev/null +++ b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/rollback/PreviousImageRollback.groovy @@ -0,0 +1,144 @@ +/* + * Copyright 2017 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.netflix.spinnaker.orca.clouddriver.pipeline.servergroup.rollback + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.netflix.frigga.Names +import com.netflix.spinnaker.orca.RetrySupport +import com.netflix.spinnaker.orca.clouddriver.OortService +import com.netflix.spinnaker.orca.clouddriver.pipeline.servergroup.CloneServerGroupStage +import com.netflix.spinnaker.orca.pipeline.model.Execution +import com.netflix.spinnaker.orca.pipeline.model.Stage +import com.netflix.spinnaker.orca.pipeline.model.SyntheticStageOwner +import org.springframework.beans.factory.annotation.Autowired; + +import static com.netflix.spinnaker.orca.pipeline.StageDefinitionBuilder.newStage + +class PreviousImageRollback implements Rollback { + String rollbackServerGroupName + String imageName + String imageId + Integer targetHealthyRollbackPercentage + + @Autowired + @JsonIgnore + CloneServerGroupStage cloneServerGroupStage + + @Autowired + @JsonIgnore + OortService oortService + + @Autowired + @JsonIgnore + RetrySupport retrySupport + + @Override + > List> buildStages(Stage parentStage) { + def stages = [] + + def parentStageContext = parentStage.context + + def imageName = this.imageName + def imageId = this.imageId + + if (!imageName) { + def imageDetails = getImageDetailsFromEntityTags( + parentStageContext.cloudProvider as String, + parentStageContext.credentials as String, + parentStageContext.region as String + ) + + imageName = imageDetails?.imageName + imageId = imageDetails?.imageId ?: imageId + } + + if (!imageName) { + throw new IllegalStateException("Unable to determine rollback image (serverGroupName: ${rollbackServerGroupName})") + } + + def names = Names.parseName(rollbackServerGroupName) + + Map cloneServerGroupContext = [ + targetHealthyDeployPercentage: targetHealthyRollbackPercentage, + imageId : imageId, + imageName : imageName, + amiName : imageName, + strategy : "redblack", + application : parentStageContext.moniker?.app ?: names.app, + stack : parentStageContext.moniker?.stack ?: names.stack, + freeFormDetails : parentStageContext.moniker?.detail ?: names.detail, + region : parentStageContext.region, + credentials : parentStageContext.credentials, + cloudProvider : parentStageContext.cloudProvider, + source: [ + asgName : rollbackServerGroupName, + serverGroupName : rollbackServerGroupName, + account : parentStageContext.credentials, + region : parentStageContext.region, + cloudProvider : parentStageContext.cloudProvider, + useSourceCapacity: true + ] + ] + stages << newStage( + parentStage.execution, cloneServerGroupStage.type, "clone", cloneServerGroupContext, parentStage, SyntheticStageOwner.STAGE_AFTER + ) + + return stages + } + + private ImageDetails getImageDetailsFromEntityTags(String cloudProvider, + String credentials, + String region) { + def entityTags = retrySupport.retry({ + oortService.getEntityTags( + cloudProvider, + "serverGroup", + rollbackServerGroupName, + credentials, + region + ) + }, 15, 2000, false) + + if (entityTags?.size() > 1) { + // this should _not_ happen + String id = Arrays.asList( + cloudProvider, + "serverGroup", + rollbackServerGroupName, + credentials, + region + ).join(":") + throw new IllegalStateException("More than one set of entity tags found for " + id); + } + + if (!entityTags) { + return null + } + + def previousServerGroup = entityTags[0].tags.find { it.name == "spinnaker:metadata" }?.value?.previousServerGroup + if (!previousServerGroup?.imageName) { + return null + } + + return new ImageDetails(imageId: previousServerGroup.imageId, imageName: previousServerGroup.imageName) + } + + static class ImageDetails { + String imageId + String imageName + } +} diff --git a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/rollback/Rollback.java b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/rollback/Rollback.java new file mode 100644 index 0000000000..c621081b0d --- /dev/null +++ b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/rollback/Rollback.java @@ -0,0 +1,26 @@ +/* + * Copyright 2017 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.netflix.spinnaker.orca.clouddriver.pipeline.servergroup.rollback; + +import com.netflix.spinnaker.orca.pipeline.model.Execution; +import com.netflix.spinnaker.orca.pipeline.model.Stage; + +import java.util.List; + +public interface Rollback { + > List> buildStages(Stage parentStage); +} diff --git a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/rollback/TestRollback.java b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/rollback/TestRollback.java new file mode 100644 index 0000000000..eaa01d0f12 --- /dev/null +++ b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/rollback/TestRollback.java @@ -0,0 +1,54 @@ +/* + * Copyright 2017 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.netflix.spinnaker.orca.clouddriver.pipeline.servergroup.rollback; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.netflix.spinnaker.orca.clouddriver.pipeline.servergroup.EnableServerGroupStage; +import com.netflix.spinnaker.orca.pipeline.WaitStage; +import com.netflix.spinnaker.orca.pipeline.model.Execution; +import com.netflix.spinnaker.orca.pipeline.model.Stage; +import com.netflix.spinnaker.orca.pipeline.model.SyntheticStageOwner; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static com.netflix.spinnaker.orca.pipeline.StageDefinitionBuilder.newStage; + +public class TestRollback implements Rollback { + private Long waitTime; + + @Autowired + @JsonIgnore + WaitStage waitStage; + + @Override + public > List> buildStages(Stage parentStage) { + Map waitContext = Collections.singletonMap("waitTime", waitTime); + + return Collections.singletonList( + newStage( + parentStage.getExecution(), waitStage.getType(), "wait", waitContext, parentStage, SyntheticStageOwner.STAGE_AFTER + ) + ); + } + + public void setWaitTime(Long waitTime) { + this.waitTime = waitTime; + } +} diff --git a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/tasks/servergroup/SpinnakerMetadataServerGroupTagGenerator.java b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/tasks/servergroup/SpinnakerMetadataServerGroupTagGenerator.java index 9633284cbc..4fa9d030e2 100644 --- a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/tasks/servergroup/SpinnakerMetadataServerGroupTagGenerator.java +++ b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/tasks/servergroup/SpinnakerMetadataServerGroupTagGenerator.java @@ -129,6 +129,10 @@ Map getPreviousServerGroup(String application, previousServerGroup.put("imageId", targetServerGroup.get("imageId")); previousServerGroup.put("cloudProvider", cloudProvider); + if (targetServerGroup.containsKey("buildInfo")) { + previousServerGroup.put("buildInfo", targetServerGroup.get("buildInfo")); + } + return previousServerGroup; } catch (RetrofitError e) { if (e.getKind() == RetrofitError.Kind.HTTP && e.getResponse().getStatus() == 404) { diff --git a/orca-clouddriver/src/test/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/RollbackServerGroupStageSpec.groovy b/orca-clouddriver/src/test/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/RollbackServerGroupStageSpec.groovy new file mode 100644 index 0000000000..0633743cef --- /dev/null +++ b/orca-clouddriver/src/test/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/RollbackServerGroupStageSpec.groovy @@ -0,0 +1,68 @@ +/* + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.netflix.spinnaker.orca.clouddriver.pipeline.servergroup + +import com.netflix.spinnaker.orca.clouddriver.pipeline.servergroup.rollback.TestRollback +import com.netflix.spinnaker.orca.pipeline.WaitStage +import com.netflix.spinnaker.orca.pipeline.model.SyntheticStageOwner +import org.springframework.beans.factory.config.AutowireCapableBeanFactory +import spock.lang.Shared +import spock.lang.Specification + +import static com.netflix.spinnaker.orca.test.model.ExecutionBuilder.stage + +class RollbackServerGroupStageSpec extends Specification { + @Shared + def waitStage = new WaitStage() + + def "should build stages appropriate for strategy"() { + given: + def autowireCapableBeanFactory = Stub(AutowireCapableBeanFactory) { + autowireBean(_) >> { TestRollback rollback -> + rollback.waitStage = waitStage + } + } + + def rollbackServerGroupStage = new RollbackServerGroupStage() + rollbackServerGroupStage.autowireCapableBeanFactory = autowireCapableBeanFactory + + def stage = stage { + type = "rollbackServerGroup" + context = [ + rollbackType : "TEST", + rollbackContext : [ + waitTime: 100, + ] + ] + } + + when: + def tasks = rollbackServerGroupStage.buildTaskGraph(stage) + def allStages = rollbackServerGroupStage.aroundStages(stage) + def beforeStages = allStages.findAll { it.syntheticStageOwner == SyntheticStageOwner.STAGE_BEFORE } + def afterStages = allStages.findAll { it.syntheticStageOwner == SyntheticStageOwner.STAGE_AFTER } + + then: + tasks.iterator().size() == 0 + beforeStages.isEmpty() + afterStages*.type == [ + "wait", + ] + afterStages[0].context == [waitTime: 100] + } +} diff --git a/orca-clouddriver/src/test/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/RollbackServerGroupStageSpec.groovy b/orca-clouddriver/src/test/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/rollback/ExplicitRollbackSpec.groovy similarity index 66% rename from orca-clouddriver/src/test/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/RollbackServerGroupStageSpec.groovy rename to orca-clouddriver/src/test/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/rollback/ExplicitRollbackSpec.groovy index 235098273f..ea2a4ae449 100644 --- a/orca-clouddriver/src/test/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/RollbackServerGroupStageSpec.groovy +++ b/orca-clouddriver/src/test/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/rollback/ExplicitRollbackSpec.groovy @@ -1,7 +1,7 @@ /* - * Copyright 2015 Netflix, Inc. + * Copyright 2017 Netflix, Inc. * - * Licensed under the Apache License, Version 2.0 (the "License") + * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * @@ -14,23 +14,22 @@ * limitations under the License. */ - -package com.netflix.spinnaker.orca.clouddriver.pipeline +package com.netflix.spinnaker.orca.clouddriver.pipeline.servergroup.rollback import com.netflix.spinnaker.orca.clouddriver.pipeline.providers.aws.ApplySourceServerGroupCapacityStage import com.netflix.spinnaker.orca.clouddriver.pipeline.providers.aws.CaptureSourceServerGroupCapacityStage import com.netflix.spinnaker.orca.clouddriver.pipeline.servergroup.DisableServerGroupStage import com.netflix.spinnaker.orca.clouddriver.pipeline.servergroup.EnableServerGroupStage import com.netflix.spinnaker.orca.clouddriver.pipeline.servergroup.ResizeServerGroupStage -import com.netflix.spinnaker.orca.clouddriver.pipeline.servergroup.RollbackServerGroupStage import com.netflix.spinnaker.orca.kato.pipeline.support.ResizeStrategy import com.netflix.spinnaker.orca.pipeline.model.SyntheticStageOwner -import org.springframework.beans.factory.config.AutowireCapableBeanFactory import spock.lang.Shared import spock.lang.Specification -import static com.netflix.spinnaker.orca.test.model.ExecutionBuilder.stage +import spock.lang.Subject + +import static com.netflix.spinnaker.orca.test.model.ExecutionBuilder.stage; -class RollbackServerGroupStageSpec extends Specification { +class ExplicitRollbackSpec extends Specification { @Shared def enableServerGroupStage = new EnableServerGroupStage() @@ -46,45 +45,36 @@ class RollbackServerGroupStageSpec extends Specification { @Shared def applySourceServerGroupCapacityStage = new ApplySourceServerGroupCapacityStage() + @Subject + def rollback = new ExplicitRollback( + enableServerGroupStage: enableServerGroupStage, + disableServerGroupStage: disableServerGroupStage, + resizeServerGroupStage: resizeServerGroupStage, + captureSourceServerGroupCapacityStage: captureSourceServerGroupCapacityStage, + applySourceServerGroupCapacityStage: applySourceServerGroupCapacityStage + ) + def "should inject enable, resize and disable stages corresponding to the server group being restored and rollbacked"() { given: - def autowireCapableBeanFactory = Stub(AutowireCapableBeanFactory) { - autowireBean(_) >> { RollbackServerGroupStage.ExplicitRollback rollback -> - rollback.enableServerGroupStage = enableServerGroupStage - rollback.disableServerGroupStage = disableServerGroupStage - rollback.resizeServerGroupStage = resizeServerGroupStage - rollback.captureSourceServerGroupCapacityStage = captureSourceServerGroupCapacityStage - rollback.applySourceServerGroupCapacityStage = applySourceServerGroupCapacityStage - } - } - - def rollbackServerGroupStage = new RollbackServerGroupStage() - rollbackServerGroupStage.autowireCapableBeanFactory = autowireCapableBeanFactory + rollback.rollbackServerGroupName = "servergroup-v002" + rollback.restoreServerGroupName = "servergroup-v001" + rollback.targetHealthyRollbackPercentage = 95 def stage = stage { type = "rollbackServerGroup" context = [ - rollbackType : "EXPLICIT", - rollbackContext: [ - restoreServerGroupName : "servergroup-v001", - rollbackServerGroupName : "servergroup-v002", - targetHealthyRollbackPercentage : 95 - ], - credentials : "test", - cloudProvider : "aws", - "region" : "us-west-1", - "targetHealthyDeployPercentage" : 95 + credentials : "test", + cloudProvider : "aws", + "region" : "us-west-1" ] } when: - def tasks = rollbackServerGroupStage.buildTaskGraph(stage) - def allStages = rollbackServerGroupStage.aroundStages(stage) + def allStages = rollback.buildStages(stage) def beforeStages = allStages.findAll { it.syntheticStageOwner == SyntheticStageOwner.STAGE_BEFORE } def afterStages = allStages.findAll { it.syntheticStageOwner == SyntheticStageOwner.STAGE_AFTER } then: - tasks.iterator().size() == 0 beforeStages.isEmpty() afterStages*.type == [ "enableServerGroup", @@ -94,7 +84,8 @@ class RollbackServerGroupStageSpec extends Specification { "applySourceServerGroupCapacity" ] afterStages[0].context == stage.context + [ - serverGroupName: "servergroup-v001" + serverGroupName: "servergroup-v001", + targetHealthyDeployPercentage: 95 ] afterStages[1].context == [ source : [ @@ -110,7 +101,8 @@ class RollbackServerGroupStageSpec extends Specification { action : "scale_to_server_group", source : new ResizeStrategy.Source(null, null, "us-west-1", null, "servergroup-v002", "test", "aws"), asgName : "servergroup-v001", - pinMinimumCapacity: true + pinMinimumCapacity: true, + targetHealthyDeployPercentage: 95 ] afterStages[3].context == stage.context + [ serverGroupName: "servergroup-v002" diff --git a/orca-clouddriver/src/test/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/rollback/PreviousImageRollbackSpec.groovy b/orca-clouddriver/src/test/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/rollback/PreviousImageRollbackSpec.groovy new file mode 100644 index 0000000000..3b305a2d30 --- /dev/null +++ b/orca-clouddriver/src/test/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/rollback/PreviousImageRollbackSpec.groovy @@ -0,0 +1,140 @@ +/* + * Copyright 2017 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.netflix.spinnaker.orca.clouddriver.pipeline.servergroup.rollback + +import com.netflix.spinnaker.orca.RetrySupport +import com.netflix.spinnaker.orca.clouddriver.OortService +import com.netflix.spinnaker.orca.clouddriver.pipeline.servergroup.CloneServerGroupStage +import com.netflix.spinnaker.orca.pipeline.model.SyntheticStageOwner +import spock.lang.Specification +import spock.lang.Subject +import spock.lang.Unroll + +import static com.netflix.spinnaker.orca.test.model.ExecutionBuilder.stage; + +class PreviousImageRollbackSpec extends Specification { + def cloneServerGroupStage = new CloneServerGroupStage() + def oortService = Mock(OortService) + + @Subject + def rollback = new PreviousImageRollback( + cloneServerGroupStage: cloneServerGroupStage, + oortService: oortService, + retrySupport: Spy(RetrySupport) { + _ * sleep(_) >> { /* do nothing */ } + } + ) + + def stage = stage { + type = "rollbackServerGroup" + context = [ + credentials : "test", + cloudProvider: "aws", + region : "us-west-1" + ] + } + + @Unroll + def "should inject clone stage with #imageSource"() { + given: + rollback.rollbackServerGroupName = "application-v002" + rollback.targetHealthyRollbackPercentage = 95 + rollback.imageName = imageName + + when: + def allStages = rollback.buildStages(stage) + def beforeStages = allStages.findAll { it.syntheticStageOwner == SyntheticStageOwner.STAGE_BEFORE } + def afterStages = allStages.findAll { it.syntheticStageOwner == SyntheticStageOwner.STAGE_AFTER } + + then: + (imageName ? 0 : 1) * oortService.getEntityTags(_, _, _, _, _) >> { + // should only call `getEntityTags()` if an image was not explicitly provided to the rollback + return [[ + tags: [ + [ + name : "spinnaker:metadata", + value: [ + previousServerGroup: [ + imageName: "previous_image_from_entity_tags", + imageId : "previous_image_from_entity_tags_id", + ] + ] + ] + ] + ]] + + } + + beforeStages.isEmpty() + afterStages*.type == [ + "cloneServerGroup", + ] + afterStages[0].context == [ + amiName : expectedImageName, + imageId : expectedImageId, + imageName : expectedImageName, + strategy : "redblack", + application : "application", + stack : null, + freeFormDetails : null, + targetHealthyDeployPercentage: 95, + region : "us-west-1", + credentials : "test", + cloudProvider : "aws", + source : [ + asgName : "application-v002", + serverGroupName : "application-v002", + account : "test", + region : "us-west-1", + cloudProvider : "aws", + useSourceCapacity: true + ] + ] + + where: + imageName | imageSource || expectedImageName || expectedImageId + "explicit_image" | "explicitly provided image" || "explicit_image" || null + null | "image fetched from `spinnaker:metadata` entity tag" || "previous_image_from_entity_tags" || "previous_image_from_entity_tags_id" + } + + def "should raise exception if multiple entity tags found"() { + when: + rollback.rollbackServerGroupName = "application-v002" + rollback.getImageDetailsFromEntityTags("aws", "test", "us-west-2") + + then: + 1 * oortService.getEntityTags(*_) >> { + return [ + [id: "1"], + [id: "2"] + ] + } + + def e = thrown(IllegalStateException) + e.message == "More than one set of entity tags found for aws:serverGroup:application-v002:test:us-west-2" + } + + def "should raise exception if no image found"() { + when: + rollback.rollbackServerGroupName = "application-v002" + rollback.buildStages(stage) + + then: + def e = thrown(IllegalStateException) + e.message == "Unable to determine rollback image (serverGroupName: application-v002)" + } +}