diff --git a/orca-igor/src/main/groovy/com/netflix/spinnaker/orca/igor/IgorService.java b/orca-igor/src/main/groovy/com/netflix/spinnaker/orca/igor/IgorService.java index 913e062617..6c55da6384 100644 --- a/orca-igor/src/main/groovy/com/netflix/spinnaker/orca/igor/IgorService.java +++ b/orca-igor/src/main/groovy/com/netflix/spinnaker/orca/igor/IgorService.java @@ -96,6 +96,10 @@ AwsCodeBuildExecution startAwsCodeBuild( AwsCodeBuildExecution getAwsCodeBuildExecution( @Path("account") String account, @Path("buildId") String buildId); + @POST("/codebuild/builds/stop/{account}/{buildId}") + AwsCodeBuildExecution stopAwsCodeBuild( + @Path("account") String account, @Path("buildId") String buildId); + @GET("/delivery-config/manifest") Map getDeliveryConfigManifest( @Query("scmType") String repoType, diff --git a/orca-igor/src/main/groovy/com/netflix/spinnaker/orca/igor/model/AwsCodeBuildExecution.java b/orca-igor/src/main/groovy/com/netflix/spinnaker/orca/igor/model/AwsCodeBuildExecution.java index 782c5546ac..a0e89115a7 100644 --- a/orca-igor/src/main/groovy/com/netflix/spinnaker/orca/igor/model/AwsCodeBuildExecution.java +++ b/orca-igor/src/main/groovy/com/netflix/spinnaker/orca/igor/model/AwsCodeBuildExecution.java @@ -64,7 +64,7 @@ public enum Status { FAILED(ExecutionStatus.TERMINAL), FAULT(ExecutionStatus.TERMINAL), TIMED_OUT(ExecutionStatus.TERMINAL), - STOPPED(ExecutionStatus.CANCELED), + STOPPED(ExecutionStatus.TERMINAL), UNKNOWN(ExecutionStatus.TERMINAL); @Getter private ExecutionStatus executionStatus; diff --git a/orca-igor/src/main/groovy/com/netflix/spinnaker/orca/igor/pipeline/AwsCodeBuildStage.java b/orca-igor/src/main/groovy/com/netflix/spinnaker/orca/igor/pipeline/AwsCodeBuildStage.java index 254d1b27b7..31e1ec3995 100644 --- a/orca-igor/src/main/groovy/com/netflix/spinnaker/orca/igor/pipeline/AwsCodeBuildStage.java +++ b/orca-igor/src/main/groovy/com/netflix/spinnaker/orca/igor/pipeline/AwsCodeBuildStage.java @@ -15,11 +15,16 @@ */ package com.netflix.spinnaker.orca.igor.pipeline; +import com.netflix.spinnaker.orca.CancellableStage; +import com.netflix.spinnaker.orca.TaskResult; import com.netflix.spinnaker.orca.igor.tasks.MonitorAwsCodeBuildTask; import com.netflix.spinnaker.orca.igor.tasks.StartAwsCodeBuildTask; +import com.netflix.spinnaker.orca.igor.tasks.StopAwsCodeBuildTask; import com.netflix.spinnaker.orca.pipeline.StageDefinitionBuilder; import com.netflix.spinnaker.orca.pipeline.TaskNode; import com.netflix.spinnaker.orca.pipeline.model.Stage; +import java.util.HashMap; +import java.util.Map; import javax.annotation.Nonnull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -28,11 +33,35 @@ @Component @RequiredArgsConstructor @Slf4j -public class AwsCodeBuildStage implements StageDefinitionBuilder { +public class AwsCodeBuildStage implements StageDefinitionBuilder, CancellableStage { + private final StopAwsCodeBuildTask stopAwsCodeBuildTask; + @Override public void taskGraph(@Nonnull Stage stage, @Nonnull TaskNode.Builder builder) { builder .withTask("startAwsCodeBuildTask", StartAwsCodeBuildTask.class) .withTask("monitorAwsCodeBuildTask", MonitorAwsCodeBuildTask.class); } + + @Override + public Result cancel(Stage stage) { + log.info( + String.format( + "Cancelling stage (stageId: %s, executionId: %s context: %s)", + stage.getId(), stage.getExecution().getId(), stage.getContext())); + + try { + TaskResult result = stopAwsCodeBuildTask.execute(stage); + Map context = new HashMap<>(); + context.put("buildInfo", result.getContext().get("buildInfo")); + stage.setContext(context); + } catch (Exception e) { + log.error( + String.format( + "Failed to cancel stage (stageId: %s, executionId: %s), e: %s", + stage.getId(), stage.getExecution().getId(), e.getMessage()), + e); + } + return new Result(stage, stage.getContext()); + } } diff --git a/orca-igor/src/main/groovy/com/netflix/spinnaker/orca/igor/tasks/StopAwsCodeBuildTask.java b/orca-igor/src/main/groovy/com/netflix/spinnaker/orca/igor/tasks/StopAwsCodeBuildTask.java new file mode 100644 index 0000000000..4d817a706e --- /dev/null +++ b/orca-igor/src/main/groovy/com/netflix/spinnaker/orca/igor/tasks/StopAwsCodeBuildTask.java @@ -0,0 +1,40 @@ +package com.netflix.spinnaker.orca.igor.tasks; + +import com.netflix.spinnaker.orca.ExecutionStatus; +import com.netflix.spinnaker.orca.Task; +import com.netflix.spinnaker.orca.TaskResult; +import com.netflix.spinnaker.orca.igor.IgorService; +import com.netflix.spinnaker.orca.igor.model.AwsCodeBuildExecution; +import com.netflix.spinnaker.orca.igor.model.AwsCodeBuildStageDefinition; +import com.netflix.spinnaker.orca.pipeline.model.Stage; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nonnull; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class StopAwsCodeBuildTask implements Task { + private final IgorService igorService; + + @Override + @Nonnull + public TaskResult execute(@Nonnull Stage stage) { + AwsCodeBuildStageDefinition stageDefinition = stage.mapTo(AwsCodeBuildStageDefinition.class); + AwsCodeBuildExecution execution = stageDefinition.getBuildInfo(); + if (execution != null) { + AwsCodeBuildExecution latestDetails = + igorService.stopAwsCodeBuild( + stageDefinition.getAccount(), getBuildId(execution.getArn())); + Map context = new HashMap<>(); + context.put("buildInfo", latestDetails); + return TaskResult.builder(ExecutionStatus.SUCCEEDED).context(context).build(); + } + return TaskResult.SUCCEEDED; + } + + private String getBuildId(String arn) { + return arn.split("/")[1]; + } +} diff --git a/orca-igor/src/test/groovy/com/netflix/spinnaker/orca/igor/pipeline/AwsCodeBuildStageSpec.groovy b/orca-igor/src/test/groovy/com/netflix/spinnaker/orca/igor/pipeline/AwsCodeBuildStageSpec.groovy index 9b597b1bb0..43f10c93c3 100644 --- a/orca-igor/src/test/groovy/com/netflix/spinnaker/orca/igor/pipeline/AwsCodeBuildStageSpec.groovy +++ b/orca-igor/src/test/groovy/com/netflix/spinnaker/orca/igor/pipeline/AwsCodeBuildStageSpec.groovy @@ -16,6 +16,7 @@ package com.netflix.spinnaker.orca.igor.pipeline +import com.netflix.spinnaker.orca.igor.tasks.MonitorAwsCodeBuildTask import com.netflix.spinnaker.orca.igor.tasks.StartAwsCodeBuildTask import spock.lang.Specification @@ -45,4 +46,25 @@ class AwsCodeBuildStageSpec extends Specification { it.implementingClass == StartAwsCodeBuildTask }.size() == 1 } + + def "should wait for completion"() { + given: + def awsCodeBuildStage = new AwsCodeBuildStage() + + def stage = stage { + type = "awsCodeBuild" + context = [ + account: ACCOUNT, + projectName: PROJECT_NAME, + ] + } + + when: + def tasks = awsCodeBuildStage.buildTaskGraph(stage) + + then: + tasks.findAll { + it.implementingClass == MonitorAwsCodeBuildTask + }.size() == 1 + } } diff --git a/orca-igor/src/test/groovy/com/netflix/spinnaker/orca/igor/tasks/MonitorAwsCodeBuildTaskSpec.groovy b/orca-igor/src/test/groovy/com/netflix/spinnaker/orca/igor/tasks/MonitorAwsCodeBuildTaskSpec.groovy index 14c313cad8..6d0d30c67d 100644 --- a/orca-igor/src/test/groovy/com/netflix/spinnaker/orca/igor/tasks/MonitorAwsCodeBuildTaskSpec.groovy +++ b/orca-igor/src/test/groovy/com/netflix/spinnaker/orca/igor/tasks/MonitorAwsCodeBuildTaskSpec.groovy @@ -65,7 +65,7 @@ class MonitorAwsCodeBuildTaskSpec extends Specification { "FAILED" | ExecutionStatus.TERMINAL "FAULT" | ExecutionStatus.TERMINAL "TIMED_OUT" | ExecutionStatus.TERMINAL - "STOPPED" | ExecutionStatus.CANCELED + "STOPPED" | ExecutionStatus.TERMINAL "UNKNOWN" | ExecutionStatus.TERMINAL } diff --git a/orca-igor/src/test/groovy/com/netflix/spinnaker/orca/igor/tasks/StopAwsCodeBuildTaskSpec.groovy b/orca-igor/src/test/groovy/com/netflix/spinnaker/orca/igor/tasks/StopAwsCodeBuildTaskSpec.groovy new file mode 100644 index 0000000000..a73831e1b6 --- /dev/null +++ b/orca-igor/src/test/groovy/com/netflix/spinnaker/orca/igor/tasks/StopAwsCodeBuildTaskSpec.groovy @@ -0,0 +1,71 @@ +/* + * Copyright 2020 Amazon.com, 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.igor.tasks + +import com.netflix.spinnaker.orca.ExecutionStatus +import com.netflix.spinnaker.orca.TaskResult +import com.netflix.spinnaker.orca.igor.IgorService +import com.netflix.spinnaker.orca.igor.model.AwsCodeBuildExecution +import com.netflix.spinnaker.orca.pipeline.model.Execution +import com.netflix.spinnaker.orca.pipeline.model.Stage +import spock.lang.Specification +import spock.lang.Subject + +class StopAwsCodeBuildTaskSpec extends Specification { + def ACCOUNT = "codebuild-account" + def PROJECT_NAME = "test" + def ARN = "arn:aws:codebuild:us-west-2:123456789012:build/test:c7715bbf-5c12-44d6-87ef-8149473e02f7" + + Execution execution = Mock(Execution) + IgorService igorService = Mock(IgorService) + + @Subject + StopAwsCodeBuildTask task = new StopAwsCodeBuildTask(igorService) + + def "should stop a build"() { + given: + def igorResponse = new AwsCodeBuildExecution(ARN, null, null) + def stage = new Stage(execution, "awsCodeBuild", [ + account: ACCOUNT, + buildInfo: [ + arn: ARN + ], + ]) + + when: + TaskResult result = task.execute(stage) + + then: + 1 * igorService.stopAwsCodeBuild(ACCOUNT, _) >> igorResponse + result.status == ExecutionStatus.SUCCEEDED + result.context.buildInfo.arn == igorResponse.arn + } + + def "should do nothing if buildInfo doesn't exist"() { + given: + def igorResponse = new AwsCodeBuildExecution(ARN, null, null) + def stage = new Stage(execution, "awsCodeBuild", [account: ACCOUNT, projectName: PROJECT_NAME]) + + when: + TaskResult result = task.execute(stage) + + then: + 0 * igorService.stopAwsCodeBuild(ACCOUNT, _) + result.status == ExecutionStatus.SUCCEEDED + result.context.buildInfo == null + } +}