From c7d2a620834efd68348366bea68908b24b42e9fd Mon Sep 17 00:00:00 2001 From: Kaixiang-AWS Date: Wed, 12 Feb 2020 13:42:05 -0800 Subject: [PATCH] feat(codebuild): Bind artifacts produced by CodeBuild (#3431) AWS CodeBuild is able to produce multiple artifacts to S3 location as defined by users in their project configuration. However, these artifacts are not visible to downstream stages, since they are not Spinnaker artifacts. The two tasks added in this PR will fetch S3 location from CodeBuild and then bind it to the stage output, which allows downstream stages to consume it as needed. --- .../spinnaker/orca/igor/IgorService.java | 4 + .../orca/igor/pipeline/AwsCodeBuildStage.java | 6 +- .../tasks/GetAwsCodeBuildArtifactsTask.java | 56 ++++++++++++ .../GetAwsCodeBuildArtifactsTaskSpec.groovy | 86 +++++++++++++++++++ 4 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 orca-igor/src/main/groovy/com/netflix/spinnaker/orca/igor/tasks/GetAwsCodeBuildArtifactsTask.java create mode 100644 orca-igor/src/test/groovy/com/netflix/spinnaker/orca/igor/tasks/GetAwsCodeBuildArtifactsTaskSpec.groovy 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 6c55da6384..07db140237 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); + @GET("/codebuild/builds/artifacts/{account}/{buildId}") + List getAwsCodeBuildArtifacts( + @Path("account") String account, @Path("buildId") String buildId); + @POST("/codebuild/builds/stop/{account}/{buildId}") AwsCodeBuildExecution stopAwsCodeBuild( @Path("account") String account, @Path("buildId") String buildId); 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 31e1ec3995..a1d17d1ca4 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 @@ -17,12 +17,14 @@ import com.netflix.spinnaker.orca.CancellableStage; import com.netflix.spinnaker.orca.TaskResult; +import com.netflix.spinnaker.orca.igor.tasks.GetAwsCodeBuildArtifactsTask; 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 com.netflix.spinnaker.orca.pipeline.tasks.artifacts.BindProducedArtifactsTask; import java.util.HashMap; import java.util.Map; import javax.annotation.Nonnull; @@ -40,7 +42,9 @@ public class AwsCodeBuildStage implements StageDefinitionBuilder, CancellableSta public void taskGraph(@Nonnull Stage stage, @Nonnull TaskNode.Builder builder) { builder .withTask("startAwsCodeBuildTask", StartAwsCodeBuildTask.class) - .withTask("monitorAwsCodeBuildTask", MonitorAwsCodeBuildTask.class); + .withTask("monitorAwsCodeBuildTask", MonitorAwsCodeBuildTask.class) + .withTask("getAwsCodeBuildArtifactsTask", GetAwsCodeBuildArtifactsTask.class) + .withTask("bindProducedArtifacts", BindProducedArtifactsTask.class); } @Override diff --git a/orca-igor/src/main/groovy/com/netflix/spinnaker/orca/igor/tasks/GetAwsCodeBuildArtifactsTask.java b/orca-igor/src/main/groovy/com/netflix/spinnaker/orca/igor/tasks/GetAwsCodeBuildArtifactsTask.java new file mode 100644 index 0000000000..29028e1a9d --- /dev/null +++ b/orca-igor/src/main/groovy/com/netflix/spinnaker/orca/igor/tasks/GetAwsCodeBuildArtifactsTask.java @@ -0,0 +1,56 @@ +/* + * 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.kork.artifacts.model.Artifact; +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.AwsCodeBuildStageDefinition; +import com.netflix.spinnaker.orca.pipeline.model.Stage; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import javax.annotation.Nonnull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +public class GetAwsCodeBuildArtifactsTask extends RetryableIgorTask { + private final IgorService igorService; + + @Override + public @Nonnull TaskResult tryExecute(@Nonnull AwsCodeBuildStageDefinition stageDefinition) { + List artifacts = + igorService.getAwsCodeBuildArtifacts( + stageDefinition.getAccount(), getBuildId(stageDefinition.getBuildInfo().getArn())); + Map> outputs = Collections.singletonMap("artifacts", artifacts); + return TaskResult.builder(ExecutionStatus.SUCCEEDED).outputs(outputs).build(); + } + + @Override + public @Nonnull AwsCodeBuildStageDefinition mapStage(@Nonnull Stage stage) { + return stage.mapTo(AwsCodeBuildStageDefinition.class); + } + + private String getBuildId(String arn) { + return arn.split("/")[1]; + } +} diff --git a/orca-igor/src/test/groovy/com/netflix/spinnaker/orca/igor/tasks/GetAwsCodeBuildArtifactsTaskSpec.groovy b/orca-igor/src/test/groovy/com/netflix/spinnaker/orca/igor/tasks/GetAwsCodeBuildArtifactsTaskSpec.groovy new file mode 100644 index 0000000000..cc74a89518 --- /dev/null +++ b/orca-igor/src/test/groovy/com/netflix/spinnaker/orca/igor/tasks/GetAwsCodeBuildArtifactsTaskSpec.groovy @@ -0,0 +1,86 @@ +/* + * 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.kork.artifacts.model.Artifact +import com.netflix.spinnaker.orca.ExecutionStatus +import com.netflix.spinnaker.orca.TaskResult +import com.netflix.spinnaker.orca.igor.IgorService +import com.netflix.spinnaker.orca.pipeline.model.Execution +import com.netflix.spinnaker.orca.pipeline.model.Stage +import retrofit.RetrofitError +import spock.lang.Specification +import spock.lang.Subject + +class GetAwsCodeBuildArtifactsTaskSpec extends Specification { + String ACCOUNT = "my-account" + String BUILD_ID = "test:c7715bbf-5c12-44d6-87ef-8149473e02f7" + String ARN = "arn:aws:codebuild:us-west-2:123456789012:build/$BUILD_ID" + + Execution execution = Mock(Execution) + IgorService igorService = Mock(IgorService) + + @Subject + GetAwsCodeBuildArtifactsTask task = new GetAwsCodeBuildArtifactsTask(igorService) + + def "fetches artifacts from igor and returns success"() { + given: + def artifacts = [ + Artifact.builder().reference("abc").name("abc").build(), + Artifact.builder().reference("def").name("def").build() + ] + def stage = new Stage(execution, "awsCodeBuild", [ + account: ACCOUNT, + buildInfo: [ + arn: ARN + ], + ]) + + when: + TaskResult result = task.execute(stage) + + then: + 1 * igorService.getAwsCodeBuildArtifacts(ACCOUNT, BUILD_ID) >> artifacts + 0 * igorService._ + result.getStatus() == ExecutionStatus.SUCCEEDED + result.getOutputs().get("artifacts") == artifacts + } + + def "task returns RUNNING when communcation with igor fails"() { + given: + def stage = new Stage(execution, "awsCodeBuild", [ + account: ACCOUNT, + buildInfo: [ + arn: ARN + ], + ]) + + when: + TaskResult result = task.execute(stage) + + then: + 1 * igorService.getAwsCodeBuildArtifacts(ACCOUNT, BUILD_ID) >> { throw stubRetrofitError() } + 0 * igorService._ + result.getStatus() == ExecutionStatus.RUNNING + } + + def stubRetrofitError() { + return Stub(RetrofitError) { + getKind() >> RetrofitError.Kind.NETWORK + } + } +}