Skip to content

Commit

Permalink
feat(artifacts): support 'use prior execution'
Browse files Browse the repository at this point in the history
  • Loading branch information
Lars Wander committed Nov 30, 2017
1 parent 1f3daf1 commit 999cb22
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,52 +16,107 @@

package com.netflix.spinnaker.orca.pipeline.util

import com.fasterxml.jackson.databind.ObjectMapper
import com.netflix.spinnaker.kork.artifacts.model.Artifact
import com.netflix.spinnaker.kork.artifacts.model.ExpectedArtifact
import groovy.util.logging.Slf4j
import com.netflix.spinnaker.orca.pipeline.persistence.ExecutionRepository
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
import rx.schedulers.Schedulers

import java.lang.reflect.Field

@Slf4j
@Component
class ArtifactResolver {

static void resolveArtifacts(Map pipeline) {
Set<Artifact> resolvedArtifacts = []
List<Artifact> receivedArtifacts = pipeline.receivedArtifacts ?: []
List<ExpectedArtifact> expectedArtifacts = pipeline.expectedArtifacts.findAll { e -> e.id in pipeline.trigger.expectedArtifactIds } ?: []
List<ExpectedArtifact> unresolvedExpectedArtifacts = []
@Autowired
private ObjectMapper objectMapper

for (ExpectedArtifact expectedArtifact : expectedArtifacts) {
List<Artifact> matches = receivedArtifacts.findAll { a -> expectedArtifact.matches((Artifact) a) }
switch (matches.size()) {
case 0:
unresolvedExpectedArtifacts.add(expectedArtifact)
continue
case 1:
resolvedArtifacts.add(matches[0])
continue
default:
throw new IllegalStateException("Expected artifact ${expectedArtifact} matches multiple incoming artifacts ${matches}")
}
void resolveArtifacts(ExecutionRepository repository, Map pipeline) {
List<ExpectedArtifact> expectedArtifacts = pipeline.expectedArtifacts?.collect { objectMapper.convertValue(it, ExpectedArtifact.class) } ?: []
List<Artifact> receivedArtifacts = pipeline.receivedArtifacts?.collect { objectMapper.convertValue(it, Artifact.class) } ?: []

if (!expectedArtifacts) {
return
}

def priorArtifacts = (List<Artifact>) repository.retrievePipelinesForPipelineConfigId((String) pipeline.get("id"), new ExecutionRepository.ExecutionCriteria())
.subscribeOn(Schedulers.io())
.toList()
.toBlocking()
.single()
.sort(startTimeOrId)
.getAt(0)
?.getTrigger()
?.get("artifacts")
?.collect { objectMapper.convertValue(it, Artifact.class) } ?: []

ResolveResult resolve = resolveExpectedArtifacts(expectedArtifacts, receivedArtifacts)

Set<Artifact> resolvedArtifacts = resolve.resolvedArtifacts
Set<ExpectedArtifact> unresolvedExpectedArtifacts = resolve.unresolvedExpectedArtifacts

for (ExpectedArtifact expectedArtifact : unresolvedExpectedArtifacts) {
Artifact resolved = null
if (expectedArtifact.usePriorArtifact) {
throw new UnsupportedOperationException("'usePriorArtifact' is not supported yet")
} else if (expectedArtifact.useDefaultArtifact && expectedArtifact.defaultArtifact) {
resolvedArtifacts.add(expectedArtifact.defaultArtifact)
resolved = resolveSingleArtifact(expectedArtifact, priorArtifacts);
}

if (!resolved && expectedArtifact.useDefaultArtifact && expectedArtifact.defaultArtifact) {
resolved = expectedArtifact.defaultArtifact
}

if (!resolved) {
throw new IllegalStateException("Unmatched expected artifact ${expectedArtifact} could not be resolved.")
} else {
throw new IllegalStateException("Unmatched expected artifact ${expectedArtifact} with no fallback behavior specified")
resolvedArtifacts.add(resolved)
}
}

pipeline.trigger.artifacts = resolvedArtifacts as List
pipeline.trigger.resolvedExpectedArtifacts = expectedArtifacts // Add the actual expectedArtifacts we included in the ids.
}

Artifact resolveSingleArtifact(ExpectedArtifact expectedArtifact, List<Artifact> possibleMatches) {
List<Artifact> matches = possibleMatches.findAll { a -> expectedArtifact.matches((Artifact) a) }
switch (matches.size()) {
case 0:
return null
case 1:
return matches[0]
default:
throw new IllegalStateException("Expected artifact ${expectedArtifact} matches multiple artifacts ${matches}")
}
}

ResolveResult resolveExpectedArtifacts(List<ExpectedArtifact> expectedArtifacts, List<Artifact> receivedArtifacts) {
ResolveResult result = new ResolveResult()

for (ExpectedArtifact expectedArtifact : expectedArtifacts) {
Artifact resolved = resolveSingleArtifact(expectedArtifact, receivedArtifacts)
if (resolved) {
result.resolvedArtifacts.add(resolved)
} else {
result.unresolvedExpectedArtifacts.add(expectedArtifact)
}
}

return result
}

static class ArtifactResolutionException extends RuntimeException {
ArtifactResolutionException(String message) {
super(message)
}
}

static class ResolveResult {
Set<Artifact> resolvedArtifacts = new HashSet<>()
Set<ExpectedArtifact> unresolvedExpectedArtifacts = new HashSet<>();
}

private static Closure startTimeOrId = { a, b ->
def aStartTime = a.startTime ?: 0
def bStartTime = b.startTime ?: 0

return bStartTime <=> aStartTime ?: b.id <=> a.id
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ class OperationsController {
@Autowired(required = false)
WebhookService webhookService

@Autowired(required = false)
ArtifactResolver artifactResolver

@RequestMapping(value = "/orchestrate", method = RequestMethod.POST)
Map<String, Object> orchestrate(@RequestBody Map pipeline, HttpServletResponse response) {
parsePipelineTrigger(executionRepository, buildService, pipeline)
Expand Down Expand Up @@ -161,7 +164,7 @@ class OperationsController {
}
}

ArtifactResolver.resolveArtifacts(pipeline)
artifactResolver?.resolveArtifacts(executionRepository, pipeline)
}

private void getBuildInfo(Map trigger) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package com.netflix.spinnaker.orca.controllers

import javax.servlet.http.HttpServletResponse
import com.netflix.spinnaker.kork.web.exceptions.InvalidRequestException
import com.netflix.spinnaker.orca.igor.BuildArtifactFilter
import com.netflix.spinnaker.orca.igor.BuildService
Expand All @@ -25,6 +24,7 @@ import com.netflix.spinnaker.orca.pipeline.ExecutionLauncher
import com.netflix.spinnaker.orca.pipeline.model.Execution
import com.netflix.spinnaker.orca.pipeline.persistence.ExecutionNotFoundException
import com.netflix.spinnaker.orca.pipeline.persistence.ExecutionRepository
import com.netflix.spinnaker.orca.pipeline.util.ArtifactResolver
import com.netflix.spinnaker.orca.pipeline.util.ContextParameterProcessor
import com.netflix.spinnaker.orca.pipelinetemplate.PipelineTemplateService
import com.netflix.spinnaker.orca.webhook.config.PreconfiguredWebhookProperties
Expand All @@ -40,6 +40,9 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders
import spock.lang.Specification
import spock.lang.Subject
import spock.lang.Unroll

import javax.servlet.http.HttpServletResponse

import static com.netflix.spinnaker.orca.ExecutionStatus.CANCELED
import static com.netflix.spinnaker.orca.ExecutionStatus.SUCCEEDED
import static com.netflix.spinnaker.orca.pipeline.model.Execution.ExecutionType
Expand All @@ -51,6 +54,7 @@ class OperationsControllerSpec extends Specification {

void setup() {
MDC.clear()
artifactResolver.objectMapper = mapper
}

def executionLauncher = Mock(ExecutionLauncher)
Expand All @@ -59,6 +63,7 @@ class OperationsControllerSpec extends Specification {
def executionRepository = Mock(ExecutionRepository)
def pipelineTemplateService = Mock(PipelineTemplateService)
def webhookService = Mock(WebhookService)
def artifactResolver = new ArtifactResolver()

def env = new MockEnvironment()
def buildArtifactFilter = new BuildArtifactFilter(environment: env)
Expand All @@ -72,7 +77,8 @@ class OperationsControllerSpec extends Specification {
pipelineTemplateService: pipelineTemplateService,
executionLauncher: executionLauncher,
contextParameterProcessor: new ContextParameterProcessor(),
webhookService: webhookService
webhookService: webhookService,
artifactResolver: artifactResolver
)

@Unroll
Expand Down

0 comments on commit 999cb22

Please sign in to comment.