Skip to content

Commit

Permalink
Merge pull request #66 from tomaslin/stash-triggers
Browse files Browse the repository at this point in the history
stash trigger
  • Loading branch information
tomaslin committed Feb 4, 2016
2 parents 3bcfa3b + 082fb39 commit 8bb344c
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package com.netflix.spinnaker.echo.model

import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import groovy.transform.Canonical

Expand All @@ -26,17 +27,24 @@ class BuildEvent {
Content content;
Details details;

public static final String BUILD_EVENT_TYPE = "build";
public static final String GIT_EVENT_TYPE = "git";

@Canonical
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Content {
Project project;
String master;
String repoProject;
String slug;
String hash;
}

@Canonical
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Details {
String type;
String source;
}

@Canonical
Expand All @@ -57,4 +65,24 @@ class BuildEvent {
public enum Result {
SUCCESS, UNSTABLE, BUILDING, ABORTED, FAILURE, NOT_BUILT
}

@JsonIgnore
public boolean isBuild() {
return details.getType() == BUILD_EVENT_TYPE;
}

@JsonIgnore
public boolean isGit() {
return details.getType() == GIT_EVENT_TYPE;
}

@JsonIgnore
public int getBuildNumber() {
return isBuild() ? content.getProject().getLastBuild().getNumber() : 0;
}

@JsonIgnore
public String getHash() {
return isGit() ? content.hash : null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package com.netflix.spinnaker.echo.model

import com.fasterxml.jackson.annotation.JsonIgnoreProperties

/**
* Represents an event
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@
@JsonDeserialize(builder = Trigger.TriggerBuilder.class)
@Builder
@Wither
@ToString(of = {"type", "master", "job", "cronExpression"}, includeFieldNames = false)
@ToString(of = {"type", "master", "job", "cronExpression", "source", "project", "slug"}, includeFieldNames = false)
@Value
public class Trigger {
public enum Type {
CRON("cron"),
GIT("git"),
JENKINS("jenkins");

private final String type;
Expand All @@ -53,9 +54,17 @@ public String toString() {
Integer buildNumber;
String propertyFile;
String cronExpression;
String source;
String project;
String slug;
String hash;

public Trigger atBuildNumber(final int buildNumber) {
return new Trigger(enabled, id, type, master, job, buildNumber, propertyFile, cronExpression);
return new Trigger(enabled, id, type, master, job, buildNumber, propertyFile, cronExpression, source, project, slug, null);
}

public Trigger atHash(final String hash) {
return new Trigger(enabled, id, type, master, job, null, propertyFile, cronExpression, source, project, slug, hash);
}

@JsonPOJOBuilder(withPrefix = "")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
@Slf4j
public class BuildEventMonitor implements EchoEventListener {

public static final String ECHO_EVENT_TYPE = "build";
public static final String TRIGGER_TYPE = "jenkins";
public static final String JENKINS_TRIGGER_TYPE = "jenkins";
public static final String GIT_TRIGGER_TYPE = "git";

private final ObjectMapper objectMapper = new ObjectMapper();

Expand All @@ -48,12 +48,13 @@ public BuildEventMonitor(@NonNull PipelineCache pipelineCache,

@Override
public void processEvent(Event event) {
if (!event.getDetails().getType().equalsIgnoreCase(ECHO_EVENT_TYPE)) {
if (!event.getDetails().getType().equalsIgnoreCase(BuildEvent.BUILD_EVENT_TYPE) &&
!event.getDetails().getType().equalsIgnoreCase(BuildEvent.GIT_EVENT_TYPE)) {
return;
}

val buildEvent = objectMapper.convertValue(event, BuildEvent.class);
Observable.from(Collections.singletonList(buildEvent))
BuildEvent buildEvent = objectMapper.convertValue(event, BuildEvent.class);
Observable.just(buildEvent)
.doOnNext(this::onEchoResponse)
.subscribe(triggerEachMatchFrom(pipelineCache.getPipelines()));
}
Expand All @@ -79,38 +80,52 @@ private Action1<BuildEvent> triggerEachMatchFrom(final List<Pipeline> pipelines)
}

private boolean isSuccessfulBuild(final BuildEvent event) {
if (event.isGit()) {
return true;
}
BuildEvent.Build lastBuild = event.getContent().getProject().getLastBuild();
return lastBuild != null && !lastBuild.isBuilding() && lastBuild.getResult() == BuildEvent.Result.SUCCESS;
}

private Func1<Pipeline, Optional<Pipeline>> withMatchingTrigger(final BuildEvent event) {
val triggerPredicate = matchTriggerFor(event);
int buildNumber = event.getContent().getProject().getLastBuild().getNumber();
return pipeline -> {
if (pipeline.getTriggers() == null) {
return Optional.empty();
} else {
return pipeline.getTriggers()
.stream()
.filter(this::isEnabledJenkinsTrigger)
.filter(this::isValidTrigger)
.filter(triggerPredicate)
.findFirst()
.map(trigger -> pipeline.withTrigger(trigger.atBuildNumber(buildNumber)));
.map(trigger -> pipeline.withTrigger(event.isBuild() ? trigger.atBuildNumber(event.getBuildNumber()) : trigger.atHash(event.getHash())));
}
};
}

private boolean isEnabledJenkinsTrigger(final Trigger trigger) {
private boolean isValidTrigger(final Trigger trigger) {
return trigger.isEnabled() &&
TRIGGER_TYPE.equals(trigger.getType()) &&
trigger.getJob() != null &&
trigger.getMaster() != null;
(
(JENKINS_TRIGGER_TYPE.equals(trigger.getType()) &&
trigger.getJob() != null &&
trigger.getMaster() != null)
|| (GIT_TRIGGER_TYPE.equals(trigger.getType()) &&
trigger.getSource() != null &&
trigger.getProject() != null &&
trigger.getSlug() != null)
);
}

private Predicate<Trigger> matchTriggerFor(final BuildEvent event) {
if (event.getDetails().getType() == GIT_TRIGGER_TYPE) {
String source = event.getDetails().getSource();
String project = event.getContent().getRepoProject();
String slug = event.getContent().getSlug();
return trigger -> trigger.getType().equals(GIT_TRIGGER_TYPE) && trigger.getSource().equals(source) && trigger.getProject().equals(project) && trigger.getSlug().equals(slug);
}
String jobName = event.getContent().getProject().getName();
String master = event.getContent().getMaster();
return trigger -> trigger.getJob().equals(jobName) && trigger.getMaster().equals(master);
return trigger -> trigger.getType().equals(JENKINS_TRIGGER_TYPE) && trigger.getJob().equals(jobName) && trigger.getMaster().equals(master);
}

private void onEventProcessed(final BuildEvent event) {
Expand All @@ -121,8 +136,12 @@ private void onMatchingPipeline(Pipeline pipeline) {
log.info("Found matching pipeline {}:{}", pipeline.getApplication(), pipeline.getName());
val id = registry.createId("pipelines.triggered")
.withTag("application", pipeline.getApplication())
.withTag("name", pipeline.getName())
.withTag("job", pipeline.getTrigger().getJob());
.withTag("name", pipeline.getName());
if (pipeline.getTrigger().getType() == JENKINS_TRIGGER_TYPE) {
id.withTag("job", pipeline.getTrigger().getJob());
} else if (pipeline.getTrigger().getType() == GIT_TRIGGER_TYPE) {
id.withTag("repository", pipeline.getTrigger().getProject() + ' ' + pipeline.getTrigger().getSlug());
}
registry.counter(id).increment();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ class BuildEventMonitorSpec extends Specification implements RetrofitStubs {
@Subject
def monitor = new BuildEventMonitor(pipelineCache, subscriber, registry)

def "triggers pipelines for successful builds"() {
@Unroll
def "triggers pipelines for successful builds for #triggerType"() {
given:
def pipeline = createPipelineWith(trigger)
pipelineCache.getPipelines() >> [pipeline]

when:
Expand All @@ -39,11 +41,12 @@ class BuildEventMonitorSpec extends Specification implements RetrofitStubs {
})

where:
event = createBuildEventWith(SUCCESS)
pipeline = createPipelineWith(enabledJenkinsTrigger)
event | trigger | triggerType
createBuildEventWith(SUCCESS) | enabledJenkinsTrigger | 'jenkins'
createStashEvent() | enabledStashTrigger | 'stash'
}

def "attaches the trigger to the pipeline"() {
def "attaches jenkins trigger to the pipeline"() {
given:
pipelineCache.getPipelines() >> [pipeline]

Expand All @@ -63,6 +66,26 @@ class BuildEventMonitorSpec extends Specification implements RetrofitStubs {
pipeline = createPipelineWith(enabledJenkinsTrigger, nonJenkinsTrigger)
}

def "attaches stash trigger to the pipeline"() {
given:
pipelineCache.getPipelines() >> [pipeline]

when:
monitor.processEvent(objectMapper.convertValue(event, Event))

then:
1 * subscriber.call({
it.trigger.type == enabledStashTrigger.type
it.trigger.project == enabledStashTrigger.project
it.trigger.slug == enabledStashTrigger.slug
it.trigger.hash == event.content.hash
})

where:
event = createStashEvent()
pipeline = createPipelineWith(enabledJenkinsTrigger, nonJenkinsTrigger, enabledStashTrigger, disabledStashTrigger)
}

def "an event can trigger multiple pipelines"() {
given:
pipelineCache.getPipelines() >> pipelines
Expand All @@ -77,11 +100,11 @@ class BuildEventMonitorSpec extends Specification implements RetrofitStubs {
event = createBuildEventWith(SUCCESS)
pipelines = (1..2).collect {
Pipeline.builder()
.application("application")
.name("pipeline$it")
.id("id")
.triggers([enabledJenkinsTrigger])
.build()
.application("application")
.name("pipeline$it")
.id("id")
.triggers([enabledJenkinsTrigger])
.build()
}
}

Expand Down Expand Up @@ -122,6 +145,8 @@ class BuildEventMonitorSpec extends Specification implements RetrofitStubs {
where:
trigger | description
disabledJenkinsTrigger | "disabled"
enabledStashTrigger | "stash trigger"
disabledStashTrigger | "disabled stash trigger"
nonJenkinsTrigger | "non-Jenkins"
enabledJenkinsTrigger.withMaster("FOO") | "different master"
enabledJenkinsTrigger.withJob("FOO") | "different job"
Expand All @@ -130,6 +155,30 @@ class BuildEventMonitorSpec extends Specification implements RetrofitStubs {
event = createBuildEventWith(SUCCESS)
}

@Unroll
def "does not trigger #description pipelinesfor stash"() {
given:
pipelineCache.getPipelines() >> [pipeline]

when:
monitor.processEvent(objectMapper.convertValue(event, Event))

then:
0 * subscriber._

where:
trigger | description
disabledJenkinsTrigger | "jenkins disabled"
enabledJenkinsTrigger | "jenkins"
disabledStashTrigger | "disabled stash trigger"
enabledStashTrigger.withSlug("notSlug") | "different slug"
enabledStashTrigger.withSource("github") | "different source"
enabledStashTrigger.withProject("notProject") | "different project"

pipeline = createPipelineWith(trigger)
event = createStashEvent()
}

@Unroll
def "does not trigger a pipeline that has an enabled trigger with missing #field"() {
given:
Expand All @@ -151,4 +200,27 @@ class BuildEventMonitorSpec extends Specification implements RetrofitStubs {
goodPipeline = createPipelineWith(enabledJenkinsTrigger)
badPipeline = createPipelineWith(trigger)
}

@Unroll
def "does not trigger a pipeline that has an enabled stash trigger with missing #field"() {
given:
pipelineCache.getPipelines() >> [badPipeline, goodPipeline]
println objectMapper.writeValueAsString(createBuildEventWith(SUCCESS))

when:
monitor.processEvent(objectMapper.convertValue(event, Event))

then:
1 * subscriber.call({ it.id == goodPipeline.id })

where:
trigger | field
enabledStashTrigger.withSlug(null) | "slug"
enabledStashTrigger.withProject(null) | "project"
enabledStashTrigger.withSource(null) | "source"

event = createStashEvent()
goodPipeline = createPipelineWith(enabledStashTrigger)
badPipeline = createPipelineWith(trigger)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ import static rx.Observable.just
trait RetrofitStubs {

final String url = "http://echo"
final Trigger enabledJenkinsTrigger = new Trigger(true, null, 'jenkins', 'master', 'job', null, null, null)
final Trigger disabledJenkinsTrigger = new Trigger(false, null, 'jenkins', 'master', 'job', null, null, null)
final Trigger nonJenkinsTrigger = new Trigger(true, null, 'not jenkins', 'master', 'job', null, null, null)
final Trigger enabledJenkinsTrigger = new Trigger(true, null, 'jenkins', 'master', 'job', null, null, null, null, null, null, null)
final Trigger disabledJenkinsTrigger = new Trigger(false, null, 'jenkins', 'master', 'job', null, null, null, null, null, null, null)
final Trigger nonJenkinsTrigger = new Trigger(true, null, 'not jenkins', 'master', 'job', null, null, null, null, null, null, null)
final Trigger enabledStashTrigger = new Trigger(true, null, 'git', null, null, null, null, null, 'stash', 'project', 'slug', null)
final Trigger disabledStashTrigger = new Trigger(false, null, 'git', 'master', 'job', null, null, null, 'stash', 'project', 'slug', null)
final Trigger enabledGithubTrigger = new Trigger(false, null, 'git', 'master', 'job', null, null, null, 'github', 'project', 'slug', null)

private nextId = new AtomicInteger(1)

Expand All @@ -32,10 +35,17 @@ trait RetrofitStubs {
def build = result ? new BuildEvent.Build(result == BUILDING, 1, result) : null
new BuildEvent(new BuildEvent.Content(
new BuildEvent.Project("job", build), "master"),
new BuildEvent.Details(BuildEventMonitor.ECHO_EVENT_TYPE)
new BuildEvent.Details(BuildEvent.BUILD_EVENT_TYPE)
)
}

BuildEvent createStashEvent() {
new BuildEvent(new BuildEvent.Content(
null, null, "project", "slug", "hash"
),
new BuildEvent.Details(BuildEvent.GIT_EVENT_TYPE, "stash"))
}

Pipeline createPipelineWith(Trigger... triggers) {
Pipeline.builder()
.application("application")
Expand Down
Loading

0 comments on commit 8bb344c

Please sign in to comment.