Skip to content

Commit a6069ca

Browse files
author
Jesse Zoldak
committed
Add ability to pass a custom message format with job name and parameters for triggering.
1 parent 793c367 commit a6069ca

File tree

6 files changed

+399
-16
lines changed

6 files changed

+399
-16
lines changed

src/main/java/com/base2services/jenkins/SqsQueueHandler.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import hudson.util.SequentialExecutionQueue;
1111
import hudson.util.TimeUnit2;
1212

13+
import java.util.ArrayList;
1314
import java.util.List;
1415
import java.util.concurrent.Executors;
1516
import java.util.logging.Level;
@@ -43,7 +44,7 @@ protected void doRun() throws Exception {
4344
}
4445
}
4546
} else {
46-
logger.fine("Currently Waiting for Messages from Queues");
47+
LOGGER.fine("Currently Waiting for Messages from Queues");
4748
}
4849
}
4950

@@ -66,15 +67,23 @@ public void run() {
6667
TriggerProcessor processor = profile.getTriggerProcessor();
6768
ReceiveMessageRequest receiveMessageRequest = new ReceiveMessageRequest(queueUrl);
6869
receiveMessageRequest.setWaitTimeSeconds(20);
69-
List<Message> messages = sqs.receiveMessage(receiveMessageRequest).getMessages();
70+
List<Message> messages = new ArrayList<Message>();
71+
// Try to pick up the messages from SQS, and log if an error was encountered,
72+
// for example a 403 Access to the resource is denied.
73+
try {
74+
messages = sqs.receiveMessage(receiveMessageRequest).getMessages();
75+
76+
} catch (Exception ex) {
77+
LOGGER.warning("Unable to retrieve messages from the queue. " + ex.getMessage());
78+
}
7079
for(Message message : messages) {
71-
//Process the message payload, it needs to conform to the GitHub Web-Hook JSON format
80+
//Process the message payload
7281
try {
73-
logger.fine("got payload\n" + message.getBody());
82+
LOGGER.fine("got payload\n" + message.getBody());
7483
processor.trigger(message.getBody());
7584

7685
} catch (Exception ex) {
77-
logger.log(Level.SEVERE,"unable to trigger builds " + ex.getMessage(),ex);
86+
LOGGER.log(Level.SEVERE,"unable to trigger builds " + ex.getMessage(),ex);
7887
} finally {
7988
//delete the message even if it failed
8089
sqs.deleteMessage(new DeleteMessageRequest()

src/main/java/com/base2services/jenkins/github/GitHubTriggerProcessor.java

Lines changed: 133 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,29 @@
55
import com.cloudbees.jenkins.GitHubRepositoryName;
66
import com.cloudbees.jenkins.GitHubTrigger;
77
import hudson.model.AbstractProject;
8+
import hudson.model.BooleanParameterValue;
9+
import hudson.model.Cause;
10+
import hudson.model.CauseAction;
811
import hudson.model.Hudson;
12+
import hudson.model.Job;
13+
import hudson.model.ParametersAction;
14+
import hudson.model.ParameterValue;
15+
import hudson.model.StringParameterValue;
16+
import hudson.model.queue.QueueTaskFuture;
917
import hudson.security.ACL;
1018
import hudson.triggers.Trigger;
19+
import hudson.triggers.TriggerDescriptor;
20+
import jenkins.model.Jenkins;
21+
import jenkins.model.ParameterizedJobMixIn;
22+
import net.sf.json.JSONArray;
23+
import net.sf.json.JSONException;
1124
import net.sf.json.JSONObject;
1225
import org.acegisecurity.Authentication;
1326
import org.acegisecurity.context.SecurityContextHolder;
1427

28+
import java.util.ArrayList;
29+
import java.util.List;
30+
import java.util.Map;
1531
import java.util.logging.Logger;
1632
import java.util.regex.Matcher;
1733
import java.util.regex.Pattern;
@@ -27,7 +43,22 @@ public class GitHubTriggerProcessor implements TriggerProcessor {
2743
private static final Logger LOGGER = Logger.getLogger(GitHubTriggerProcessor.class.getName());
2844

2945
public void trigger(String payload) {
30-
processGitHubPayload(payload, SqsBuildTrigger.class);
46+
JSONObject json = extractJsonFromPayload(payload);
47+
// You can signal through the payload that you will be using a custom format
48+
// by including a root object named "custom_format" of any type and value.
49+
if (json.has("custom_format")) {
50+
processCustomPayload(json, SqsBuildTrigger.class);
51+
}
52+
// The default format, e.g. payload passed directly from a GitHub webhook,
53+
// always includes a "repository" object.
54+
else if (json.has("repository")) {
55+
// Note that the payload will again be extracted from JSON at the start of the processGitHubPayload function.
56+
// Leaving it this way so as not to change the contract, to be backwards compatible with any integrations.
57+
processGitHubPayload(payload, SqsBuildTrigger.class);
58+
}
59+
else {
60+
LOGGER.warning("Unable to determine the format of the SQS message.");
61+
}
3162
}
3263

3364
public void processGitHubPayload(String payload, Class<? extends Trigger> triggerClass) {
@@ -71,22 +102,117 @@ public void processGitHubPayload(String payload, Class<? extends Trigger> trigge
71102
}
72103
}
73104

74-
private JSONObject extractJsonFromPayload(String payload) {
75-
JSONObject repository = null;
105+
public JSONObject extractJsonFromPayload(String payload) {
76106
JSONObject json = JSONObject.fromObject(payload);
77107
if(json.has("Type")) {
78108
String msg = json.getString("Message");
79109
if(msg != null) {
80110
char ch[] = msg.toCharArray();
81111
if((ch[0] == '"') && (ch[msg.length()-1]) == '"') {
82-
msg = msg.substring(1,msg.length()-1); //remove the leading and trailing double quotes
112+
msg = msg.substring(1,msg.length()-1); //remove the leading and trailing double quotes
83113
}
84114
return JSONObject.fromObject(msg);
85115
}
86-
} else if (json.has("repository")){
87-
return json;
116+
}
117+
return json;
118+
}
119+
120+
public void processCustomPayload(JSONObject json, Class<? extends Trigger> triggerClass) {
121+
// Note that custom payloads will only trigger jobs that are configured with this SQS trigger.
122+
// The custom payload must contain a root object named "job" of type string.
123+
String jobToTrigger = json.getString("job");
124+
if (jobToTrigger == null) {
125+
LOGGER.warning("Custom sqs message payload does not contain information about which job to trigger.");
126+
return;
127+
}
128+
// The custom payload can contain a root object named "parameters"
129+
// that contains a list of parameters to pass to the job when scheduled.
130+
// Each parameter object should contain information about its type, name, and value.
131+
// TODO this will only work with string or boolean parameters,
132+
// and you need to pass in ALL parameters for the job,
133+
// as the scheduled job will not fill in any defaults for you.
134+
List<ParameterValue> parameters = getParamsFromJson(json);
135+
136+
Authentication old = SecurityContextHolder.getContext().getAuthentication();
137+
SecurityContextHolder.getContext().setAuthentication(ACL.SYSTEM);
138+
try {
139+
Jenkins jenkins = Jenkins.getInstance();
140+
for (Job job: jenkins.getAllItems(Job.class)) {
141+
String jobName = job.getDisplayName();
142+
if (jobName.equals(jobToTrigger)) {
143+
// Custom triggers operate on Parameterized jobs only
144+
if (job instanceof ParameterizedJobMixIn.ParameterizedJob) {
145+
// Make sure the job is configured to use the SQS trigger
146+
ParameterizedJobMixIn.ParameterizedJob pJob = (ParameterizedJobMixIn.ParameterizedJob) job;
147+
final Map<TriggerDescriptor, Trigger<?>> pJobTriggers = pJob.getTriggers();
148+
SqsBuildTrigger.DescriptorImpl descriptor = jenkins.getDescriptorByType(SqsBuildTrigger.DescriptorImpl.class);
149+
if (!pJobTriggers.containsKey(descriptor)) {
150+
LOGGER.warning("The job " + jobName + " is not configured to use the SQS trigger.");
151+
} else {
152+
final Job theJob = job;
153+
ParameterizedJobMixIn mixin = new ParameterizedJobMixIn() {
154+
@Override
155+
protected Job asJob() {
156+
return theJob;
157+
}
158+
};
159+
Cause cause = new Cause.RemoteCause("SQS", "Triggered by SQS.");
160+
CauseAction cAction = new CauseAction(cause);
161+
ParametersAction pAction = new ParametersAction(parameters);
162+
final QueueTaskFuture queueTaskFuture = mixin.scheduleBuild2(0, cAction, pAction);
163+
if (queueTaskFuture == null) {
164+
LOGGER.warning("Unable to schedule the job " + jobName);
165+
}
166+
}
167+
} else {
168+
LOGGER.warning("The job " + jobName + " is not configured as a Parameterized Job.");
169+
}
170+
}
171+
}
172+
} finally {
173+
SecurityContextHolder.getContext().setAuthentication(old);
174+
}
175+
}
176+
177+
private Boolean isValidParameterJson(JSONObject param) {
178+
if (param.has("name") && param.has("value") && param.has("type")) {
179+
if (param.getString("type").matches("string|boolean")) {
180+
return true;
181+
} else {
182+
LOGGER.warning("'string' and 'boolean' are the only supported parameter types.");
183+
return false;
184+
}
185+
} else {
186+
LOGGER.warning("Parameters must contain key/value pairs for 'name', 'value', and 'type'.");
187+
return false;
188+
}
189+
}
88190

191+
public List<ParameterValue> getParamsFromJson(JSONObject json) {
192+
List<ParameterValue> params = new ArrayList<ParameterValue>();
193+
if (json.has("parameters")) {
194+
try {
195+
JSONArray parameters = json.getJSONArray("parameters");
196+
for (int i = 0; i < parameters.size(); i++) {
197+
JSONObject param = (JSONObject) parameters.get(i);
198+
if (isValidParameterJson(param)) {
199+
String name = param.getString("name");
200+
String type = param.getString("type");
201+
if (type.equals("boolean")) {
202+
Boolean value = param.getBoolean("value");
203+
BooleanParameterValue parameterValue = new BooleanParameterValue(name, value);
204+
params.add(parameterValue);
205+
} else {
206+
String value = param.getString("value");
207+
StringParameterValue parameterValue = new StringParameterValue(name, value);
208+
params.add(parameterValue);
209+
}
210+
}
211+
}
212+
} catch (JSONException e) {
213+
LOGGER.warning("Parameters must be passed as a JSONArray in the SQS message.");
214+
}
89215
}
90-
return null;
216+
return params;
91217
}
92218
}
Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,36 @@
11
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
22
<l:ajax>
33
<div>
4-
Don't let Jenkins talk to GitHub and manage the SQS Queue hook, and opt to do it manually.
5-
In this mode, in addition to configure projects with "Build when a message is published to an SQS Queue",
6-
you need to manually add the GitHub SQS Hook
4+
Don't let Jenkins talk to GitHub and manage the SQS Queue hook; instead opt to do it manually.
5+
<p>There are two cases in which you would want to do this:</p>
6+
<ol>
7+
<li>You want to control the GitHub hook yourself.</li>
8+
<li>You are sending a custom JSON payload.</li>
9+
</ol>
10+
<p>In both these cases, in addition to either manually adding the GitHub SQS Hook in the GitHub settings of
11+
your repo (default message payload) or setting up your own application that will publish messages (custom payload),
12+
you also need to configure your project(s) to "Build when a message is published to an SQS Queue."</p>
13+
<p>Custom payload technical details:</p>
14+
<ul>
15+
<li>Signal to the plugin that you are using a custom payload by including a root object named
16+
"custom_format" of any type and value.</li>
17+
<li>As noted above, custom payloads will only trigger jobs that are configured to use the SQS trigger.</li>
18+
<li>The custom payload must contain a root object named "job" of type string which contains the name of the job to trigger.</li>
19+
<li>The custom payload may contain a root object named "parameters" that contains a list of parameters to
20+
pass to the job when scheduled. Each parameter object should contain information about its type, name, and value.</li>
21+
<li>Valid parameter types are "string" and "boolean". These will get cast as
22+
<a href="http://javadoc.jenkins-ci.org/hudson/model/StringParameterValue.html">StringParameterValue</a> and
23+
<a href="http://javadoc.jenkins-ci.org/hudson/model/BooleanParameterValue.html">BooleanParameterValue</a>, respectively.
24+
</li>
25+
<li>You must pass all parameter values that you want to use for the job. Default values will <b>not</b> be determined and
26+
filled in automatically.</li>
27+
</ul>
28+
<p>Custom payload examples:</p>
29+
<ul>
30+
<li><pre>{"custom_format": true, "job": "myProject"}</pre></li>
31+
<li><pre>{"custom_format": true, "job": "myProject", "parameters": [{"name": "foo", "value": true, "type": "boolean"}]}</pre></li>
32+
<li><pre>{"custom_format": true, "job": "myProject", "parameters": [{"name": "foo", "value": true, "type": "boolean"}, {"name": "bar", "value": "baz", "type": "string"}]}</pre></li>
33+
</ul>
734
</div>
835
</l:ajax>
936
</j:jelly>
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package com.base2services.jenkins;
2+
3+
import com.base2services.jenkins.github.GitHubTriggerProcessor;
4+
import hudson.model.BooleanParameterValue;
5+
import hudson.model.ParameterValue;
6+
import hudson.model.StringParameterValue;
7+
import net.sf.json.JSONObject;
8+
import org.junit.Before;
9+
import org.junit.Test;
10+
11+
import java.util.List;
12+
13+
public class GetParamsFromJsonTest {
14+
15+
private String payload;
16+
private GitHubTriggerProcessor gtp;
17+
18+
@Before
19+
public void setUp() throws Exception {
20+
gtp = new GitHubTriggerProcessor();
21+
}
22+
23+
@Test
24+
public void testEmptyParams() throws Exception {
25+
payload = "{'job': 'testProject'}".replace("'", "\"");
26+
JSONObject json = gtp.extractJsonFromPayload(payload);
27+
List<ParameterValue> parameterValues = gtp.getParamsFromJson(json);
28+
assert parameterValues.size() == 0;
29+
}
30+
31+
@Test
32+
public void testNonArrayParams() throws Exception {
33+
payload = "{'parameters': 'foo'}".replace("'", "\"");
34+
JSONObject json = gtp.extractJsonFromPayload(payload);
35+
List<ParameterValue> parameterValues = gtp.getParamsFromJson(json);
36+
assert parameterValues.size() == 0;
37+
}
38+
39+
@Test
40+
public void testEmptyParamsArray() throws Exception {
41+
payload = "{'parameters': []}".replace("'", "\"");
42+
JSONObject json = gtp.extractJsonFromPayload(payload);
43+
List<ParameterValue> parameterValues = gtp.getParamsFromJson(json);
44+
assert parameterValues.size() == 0;
45+
}
46+
47+
@Test
48+
public void testInvalidParameterType() throws Exception {
49+
payload = "{'parameters': [{'name': 'foo', 'value': 'bar', 'type': 'blah'}]}".replace("'", "\"");
50+
GitHubTriggerProcessor gtp = new GitHubTriggerProcessor();
51+
JSONObject json = gtp.extractJsonFromPayload(payload);
52+
List<ParameterValue> parameterValues = gtp.getParamsFromJson(json);
53+
assert parameterValues.size() == 0;
54+
}
55+
56+
@Test
57+
public void testStringParameter() throws Exception {
58+
payload = "{'parameters': [{'name': 'foo', 'value': 'bar', 'type': 'string'}]}".replace("'", "\"");
59+
GitHubTriggerProcessor gtp = new GitHubTriggerProcessor();
60+
JSONObject json = gtp.extractJsonFromPayload(payload);
61+
List<ParameterValue> parameterValues = gtp.getParamsFromJson(json);
62+
assert parameterValues.size() == 1;
63+
assert parameterValues.get(0).getClass().equals(StringParameterValue.class);
64+
assert parameterValues.get(0).getName().equals("foo");
65+
assert parameterValues.get(0).getValue().equals("bar");
66+
}
67+
68+
@Test
69+
public void testBooleanParameter() throws Exception {
70+
payload = "{'parameters': [{'name': 'foo', 'value': true, 'type': 'boolean'}]}".replace("'", "\"");
71+
GitHubTriggerProcessor gtp = new GitHubTriggerProcessor();
72+
JSONObject json = gtp.extractJsonFromPayload(payload);
73+
List<ParameterValue> parameterValues = gtp.getParamsFromJson(json);
74+
assert parameterValues.size() == 1;
75+
assert parameterValues.get(0).getClass().equals(BooleanParameterValue.class);
76+
assert parameterValues.get(0).getName().equals("foo");
77+
assert parameterValues.get(0).getValue().equals(true);
78+
}
79+
80+
@Test
81+
public void testMultipleParameters() throws Exception {
82+
String payload_string = String.format("{'parameters': [%s]}",
83+
"{'name': 'foo', 'value': 'bar', 'type': 'string'}, {'name': 'hello', 'value': 'world', 'type': 'string'}");
84+
payload = payload_string.replace("'", "\"");
85+
GitHubTriggerProcessor gtp = new GitHubTriggerProcessor();
86+
JSONObject json = gtp.extractJsonFromPayload(payload);
87+
List<ParameterValue> parameterValues = gtp.getParamsFromJson(json);
88+
assert parameterValues.size() == 2;
89+
assert parameterValues.get(0).getName().equals("foo");
90+
assert parameterValues.get(0).getValue().equals("bar");
91+
assert parameterValues.get(1).getName().equals("hello");
92+
assert parameterValues.get(1).getValue().equals("world");
93+
}
94+
95+
@Test
96+
public void testInvalidParametersAreIgnored() throws Exception {
97+
String payload_string = String.format("{'parameters': [%s]}",
98+
"{'ihavenoname': 'blah'},{'name': 'foo', 'value': 'bar', 'type': 'string'}");
99+
payload = payload_string.replace("'", "\"");
100+
GitHubTriggerProcessor gtp = new GitHubTriggerProcessor();
101+
JSONObject json = gtp.extractJsonFromPayload(payload);
102+
List<ParameterValue> parameterValues = gtp.getParamsFromJson(json);
103+
assert parameterValues.size() == 1;
104+
assert parameterValues.get(0).getName().equals("foo");
105+
assert parameterValues.get(0).getValue().equals("bar");
106+
}
107+
}

src/test/java/com/base2services/jenkins/GitHubTriggerProcessorTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public void shouldNotTriggerWithAFork() throws Exception {
7272
public void shouldNotTriggerWithBadPayload() throws Exception {
7373
payload = "{'noRepoInfo': {'empty': 'https://github.com/foo/bar'}}".replace("'", "\"");
7474
GitHubTriggerProcessor gtp = new GitHubTriggerProcessor();
75-
gtp.processGitHubPayload(payload, sbt.getClass());
75+
gtp.trigger(payload);
7676
verify(sbt, never()).onPost();
7777
}
7878

0 commit comments

Comments
 (0)