diff --git a/orca-core/src/main/java/com/netflix/spinnaker/orca/pipeline/util/ArtifactResolver.java b/orca-core/src/main/java/com/netflix/spinnaker/orca/pipeline/util/ArtifactResolver.java index b14aadbee6..543ae8b6b7 100644 --- a/orca-core/src/main/java/com/netflix/spinnaker/orca/pipeline/util/ArtifactResolver.java +++ b/orca-core/src/main/java/com/netflix/spinnaker/orca/pipeline/util/ArtifactResolver.java @@ -37,14 +37,7 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -60,11 +53,14 @@ public class ArtifactResolver { private final ObjectMapper objectMapper; private final ExecutionRepository executionRepository; + private final ContextParameterProcessor contextParameterProcessor; @Autowired - public ArtifactResolver(ObjectMapper objectMapper, ExecutionRepository executionRepository) { + public ArtifactResolver(ObjectMapper objectMapper, ExecutionRepository executionRepository, + ContextParameterProcessor contextParameterProcessor) { this.objectMapper = objectMapper; this.executionRepository = executionRepository; + this.contextParameterProcessor = contextParameterProcessor; } public @Nonnull @@ -106,6 +102,26 @@ List getAllArtifacts(@Nonnull Execution execution) { return emittedArtifacts; } + /** + * Used to fully resolve a bound artifact on a stage that can either select + * an expected artifact ID for an expected artifact defined in a prior stage + * or as a trigger constraint OR define an inline expression-evaluable default artifact. + * @param stage The stage containing context to evaluate expressions on the bound artifact. + * @param id An expected artifact id. Either id or artifact must be specified. + * @param artifact An inline default artifact. Either id or artifact must be specified. + * @return A bound artifact with expressions evaluated. + */ + public @Nullable Artifact getBoundArtifactForStage(Stage stage, @Nullable String id, @Nullable Artifact artifact) { + Artifact boundArtifact = id != null ? getBoundArtifactForId(stage, id) : artifact; + Map boundArtifactMap = objectMapper.convertValue(boundArtifact, new TypeReference>() { + }); + + Map evaluatedBoundArtifactMap = contextParameterProcessor.process(boundArtifactMap, + contextParameterProcessor.buildExecutionContext(stage, true), true); + + return objectMapper.convertValue(evaluatedBoundArtifactMap, Artifact.class); + } + public @Nullable Artifact getBoundArtifactForId( @Nonnull Stage stage, @Nullable String id) { diff --git a/orca-core/src/test/groovy/com/netflix/spinnaker/orca/pipeline/util/ArtifactResolverSpec.groovy b/orca-core/src/test/groovy/com/netflix/spinnaker/orca/pipeline/util/ArtifactResolverSpec.groovy index de30f34846..c1717d3498 100644 --- a/orca-core/src/test/groovy/com/netflix/spinnaker/orca/pipeline/util/ArtifactResolverSpec.groovy +++ b/orca-core/src/test/groovy/com/netflix/spinnaker/orca/pipeline/util/ArtifactResolverSpec.groovy @@ -24,12 +24,38 @@ import com.netflix.spinnaker.orca.pipeline.model.DefaultTrigger import com.netflix.spinnaker.orca.pipeline.persistence.ExecutionRepository import spock.lang.Specification import spock.lang.Unroll + import static com.netflix.spinnaker.orca.test.model.ExecutionBuilder.pipeline import static com.netflix.spinnaker.orca.test.model.ExecutionBuilder.stage class ArtifactResolverSpec extends Specification { def makeArtifactResolver() { - return new ArtifactResolver(new ObjectMapper(), Mock(ExecutionRepository)) + return new ArtifactResolver(new ObjectMapper(), Mock(ExecutionRepository), + new ContextParameterProcessor()) + } + + def "should resolve expressions in stage-inlined artifacts"() { + setup: + def execution = pipeline { + stage { + name = "upstream stage" + type = "stage1" + refId = "1" + } + } + + execution.trigger = new DefaultTrigger('manual') + execution.trigger.other['buildNumber'] = 100 + execution.trigger.artifacts.add(Artifact.builder().type('http/file').name('build/libs/my-jar-100.jar').build()) + + when: + def artifact = makeArtifactResolver().getBoundArtifactForStage(execution.stages[0], null, Artifact.builder() + .type('http/file') + .name('build/libs/my-jar-${trigger[\'buildNumber\']}.jar') + .build()) + + then: + artifact.name == 'build/libs/my-jar-100.jar' } def "should find upstream artifacts in small pipeline"() { @@ -45,28 +71,28 @@ class ArtifactResolverSpec extends Specification { artifacts.find { it.type == "extra" } != null where: - execution = pipeline { - stage { - name = "upstream stage" - type = "stage1" - refId = "1" - outputs.artifacts = [new Artifact(type: "1")] - } - stage { - name = "upstream stage" - type = "stage2" - refId = "2" - requisiteStageRefIds = ["1"] - outputs.artifacts = [new Artifact(type: "2"), new Artifact(type: "extra")] - } - stage { - name = "desired" - requisiteStageRefIds = ["2"] - } + execution = pipeline { + stage { + name = "upstream stage" + type = "stage1" + refId = "1" + outputs.artifacts = [new Artifact(type: "1")] + } + stage { + name = "upstream stage" + type = "stage2" + refId = "2" + requisiteStageRefIds = ["1"] + outputs.artifacts = [new Artifact(type: "2"), new Artifact(type: "extra")] + } + stage { + name = "desired" + requisiteStageRefIds = ["2"] } + } } - def "should find upstream artifacts only" () { + def "should find upstream artifacts only"() { when: def desired = execution.getStages().find { it.name == "desired" } def artifactResolver = makeArtifactResolver() @@ -98,7 +124,7 @@ class ArtifactResolverSpec extends Specification { } } - def "should find artifacts from trigger and upstream stages" () { + def "should find artifacts from trigger and upstream stages"() { when: def execution = pipeline { stage { @@ -124,7 +150,7 @@ class ArtifactResolverSpec extends Specification { artifacts.find { it.type == "trigger" } != null } - def "should find no artifacts" () { + def "should find no artifacts"() { when: def execution = pipeline { stage { @@ -146,7 +172,7 @@ class ArtifactResolverSpec extends Specification { artifacts.size == 0 } - def "should find a bound artifact from upstream stages" () { + def "should find a bound artifact from upstream stages"() { when: def execution = pipeline { stage { @@ -154,8 +180,8 @@ class ArtifactResolverSpec extends Specification { type = "stage1" refId = "1" outputs.resolvedExpectedArtifacts = [ - new ExpectedArtifact(id: "1", boundArtifact: new Artifact(type: "correct")), - new ExpectedArtifact(id: "2", boundArtifact: new Artifact(type: "incorrect")) + new ExpectedArtifact(id: "1", boundArtifact: new Artifact(type: "correct")), + new ExpectedArtifact(id: "2", boundArtifact: new Artifact(type: "incorrect")) ] } stage { @@ -175,7 +201,7 @@ class ArtifactResolverSpec extends Specification { artifact.type == "correct" } - def "should find a bound artifact from a trigger" () { + def "should find a bound artifact from a trigger"() { when: def execution = pipeline { stage { @@ -183,7 +209,7 @@ class ArtifactResolverSpec extends Specification { type = "stage1" refId = "1" outputs.resolvedExpectedArtifacts = [ - new ExpectedArtifact(id: "2", boundArtifact: new Artifact(type: "incorrect")) + new ExpectedArtifact(id: "2", boundArtifact: new Artifact(type: "incorrect")) ] } stage { @@ -237,7 +263,7 @@ class ArtifactResolverSpec extends Specification { new ExpectedArtifact(matchArtifact: new Artifact(type: "docker/.*", name: "none")) | [new Artifact(type: "docker/image", name: "bad"), new Artifact(type: "docker/image", name: "image")] } - def "should find all artifacts from an execution, in reverse order" () { + def "should find all artifacts from an execution, in reverse order"() { when: def execution = pipeline { stage { diff --git a/orca-front50/src/test/groovy/com/netflix/spinnaker/orca/front50/DependentPipelineStarterSpec.groovy b/orca-front50/src/test/groovy/com/netflix/spinnaker/orca/front50/DependentPipelineStarterSpec.groovy index 71b4c1597d..c4dc2ac118 100644 --- a/orca-front50/src/test/groovy/com/netflix/spinnaker/orca/front50/DependentPipelineStarterSpec.groovy +++ b/orca-front50/src/test/groovy/com/netflix/spinnaker/orca/front50/DependentPipelineStarterSpec.groovy @@ -43,7 +43,7 @@ class DependentPipelineStarterSpec extends Specification { ObjectMapper mapper = OrcaObjectMapper.newInstance() ExecutionRepository executionRepository = Mock(ExecutionRepository) - ArtifactResolver artifactResolver = Spy(ArtifactResolver, constructorArgs: [mapper, executionRepository]) + ArtifactResolver artifactResolver = Spy(ArtifactResolver, constructorArgs: [mapper, executionRepository, new ContextParameterProcessor()]) def "should only propagate credentials when explicitly provided"() { setup: diff --git a/orca-igor/src/test/groovy/com/netflix/spinnaker/orca/igor/tasks/MonitorJenkinsJobTaskSpec.groovy b/orca-igor/src/test/groovy/com/netflix/spinnaker/orca/igor/tasks/MonitorJenkinsJobTaskSpec.groovy index 1437ba32cd..d4fdce14bb 100644 --- a/orca-igor/src/test/groovy/com/netflix/spinnaker/orca/igor/tasks/MonitorJenkinsJobTaskSpec.groovy +++ b/orca-igor/src/test/groovy/com/netflix/spinnaker/orca/igor/tasks/MonitorJenkinsJobTaskSpec.groovy @@ -26,6 +26,7 @@ import com.netflix.spinnaker.orca.pipeline.model.Stage import com.netflix.spinnaker.orca.pipeline.persistence.ExecutionRepository import com.netflix.spinnaker.orca.pipeline.tasks.artifacts.BindProducedArtifactsTask import com.netflix.spinnaker.orca.pipeline.util.ArtifactResolver +import com.netflix.spinnaker.orca.pipeline.util.ContextParameterProcessor import retrofit.RetrofitError import retrofit.client.Response import spock.lang.Shared @@ -35,7 +36,7 @@ import spock.lang.Unroll class MonitorJenkinsJobTaskSpec extends Specification { def executionRepository = Mock(ExecutionRepository) - def artifactResolver = new ArtifactResolver(new ObjectMapper(), executionRepository) + def artifactResolver = new ArtifactResolver(new ObjectMapper(), executionRepository, new ContextParameterProcessor()) @Subject MonitorJenkinsJobTask task = new MonitorJenkinsJobTask() diff --git a/orca-web/src/test/groovy/com/netflix/spinnaker/orca/controllers/OperationsControllerSpec.groovy b/orca-web/src/test/groovy/com/netflix/spinnaker/orca/controllers/OperationsControllerSpec.groovy index 3014aec3c6..3365c53f05 100644 --- a/orca-web/src/test/groovy/com/netflix/spinnaker/orca/controllers/OperationsControllerSpec.groovy +++ b/orca-web/src/test/groovy/com/netflix/spinnaker/orca/controllers/OperationsControllerSpec.groovy @@ -481,7 +481,7 @@ class OperationsControllerSpec extends Specification { startedPipeline } executionRepository.retrievePipelinesForPipelineConfigId(*_) >> Observable.empty() - ArtifactResolver realArtifactResolver = new ArtifactResolver(mapper, executionRepository) + ArtifactResolver realArtifactResolver = new ArtifactResolver(mapper, executionRepository, new ContextParameterProcessor()) // can't use @subject, since we need to test the behavior of otherwise mocked-out 'artifactResolver' def tempController = new OperationsController(