Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# EditorConfig is awesome: http://EditorConfig.org

# top-most EditorConfig file
root = true

# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 2
9 changes: 7 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
target/

pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
Expand All @@ -7,7 +8,11 @@ release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.idea
jenkins-client.iml

# Exclude maven wrapper
!/.mvn/wrapper/maven-wrapper.jar

# Intellij Idea files
.idea
*.iml

14 changes: 12 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,23 @@
<dependency>
<groupId>com.offbytwo.jenkins</groupId>
<artifactId>jenkins-client</artifactId>
<version>0.3.6</version>
<version>0.3.7-SNAPSHOT</version>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do they have a CI running somewhere, where we can suck down the dependency ?
Not a blocker - just asking

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matzew good point!
I looked now, but couldn't find anything. I checked the pom.xml files in the source code to see some repository reference. There is one, but that's not the snapshot repository.
No mention of CI.

@khmarbaise Where can we suck Jenkins-Java-client SNAPSHOT? Unfortunately, I haven't still got the approval to post to mailing list (https://groups.google.com/d/forum/java-client-api).
As a quick introduction: we are building a mobile application build farm on top of Jenkins 😎

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

</dependency>
<dependency>
<groupId>org.jtwig</groupId>
<artifactId>jtwig-core</artifactId>
<version>5.65</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.21</version>
</dependency>
</dependencies>
<repositories>
<repository>
Expand All @@ -48,4 +58,4 @@
</plugin>
</plugins>
</build>
</project>
</project>
91 changes: 73 additions & 18 deletions src/main/java/com/redhat/digkins/DiggerClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,56 @@

import com.offbytwo.jenkins.JenkinsServer;
import com.redhat.digkins.services.CreateJobService;
import com.redhat.digkins.util.JenkinsAuth;
import com.redhat.digkins.services.TriggerBuildService;
import com.redhat.digkins.util.DiggerClientException;
import com.redhat.digkins.util.JenkinsAuth;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;

/**
* Digger Java Client
* <p>
* Interact with digger jenkins api!
* Digger Java Client interact with Digger Jenkins api.
*/
public class DiggerClient {

private static final Logger LOG = LoggerFactory.getLogger(DiggerClient.class);

public static final long DEFAULT_BUILD_TIMEOUT = 60 * 1000;

private final JenkinsServer jenkins;

public DiggerClient(JenkinsAuth auth) throws URISyntaxException {
this.jenkins = new JenkinsServer(new URI(auth.getUrl()), auth.getUser(), auth.getPassword());
}

/**
* Create new digger job on jenkins platform
* Create client using provided url and credentials
*
* @param name - job name that can be used later to reference job
* @param gitRepo - git repository url (full git repository url. e.g [email protected]:wtrocki/helloworld-android-gradle.git
* @param gitBranch - git repository branch (default branch used to checkout source code)
* @param url Jenkins url
* @param user Jenkins user
* @param password Jenkins password
* @return client instance
* @throws DiggerClientException if something goes wrong
*/
public static DiggerClient from(String url, String user, String password) throws DiggerClientException {
try {
JenkinsAuth jenkinsAuth = new JenkinsAuth(url, user, password);
return new DiggerClient(jenkinsAuth);
} catch (URISyntaxException e) {
throw new DiggerClientException("Invalid jenkins url format.");
}
}

/**
* Create new Digger job on Jenkins platform
*
* @param name job name that can be used later to reference job
* @param gitRepo git repository url (full git repository url. e.g [email protected]:wtrocki/helloworld-android-gradle.git
* @param gitBranch git repository branch (default branch used to checkout source code)
* @throws DiggerClientException if something goes wrong
*/
public void createJob(String name, String gitRepo, String gitBranch) throws DiggerClientException {
CreateJobService service = new CreateJobService(this.jenkins);
Expand All @@ -38,19 +63,49 @@ public void createJob(String name, String gitRepo, String gitBranch) throws Digg
}

/**
* Create client using provided url and credentials
* Triggers a build for the given job and waits until it leaves the queue and actually starts.
* <p>
* Jenkins puts the build requests in a queue and once there is a slave available, it starts building
* it and a build number is assigned to the build.
* <p>
* This method will block until there is a build number, or the given timeout period is passed. If the build is still in the queue
* after the given timeout period, -1 is returned as the build number.
* <p>
* Please note that timeout period is never meant to be very precise. It has the resolution of {@link TriggerBuildService#POLL_PERIOD} because
* timeout is checked before every pull.
* <p>
* Similarly, -1 is returned if the build is stuck or cancelled on Jenkins side.
*
* @param url - jenkins url
* @param user - jenkins user
* @param password - jenkins password
* @return client instance
* @param jobName name of the job to trigger the build
* @param timeout how many milliseconds should this call block before returning -1. Should be larger than {@link TriggerBuildService#FIRST_CHECK_DELAY}
* @return the build number. -1 if build is cancelled, stuck or in queue more than the given timeout period
* @throws DiggerClientException if connection problems occur during connecting to Jenkins
*/
public static DiggerClient from(String url, String user, String password) throws DiggerClientException {
public long build(String jobName, long timeout) throws DiggerClientException {
final TriggerBuildService triggerBuildService = new TriggerBuildService(jenkins);
try {
JenkinsAuth jenkinsAuth = new JenkinsAuth(url, user, password);
return new DiggerClient(jenkinsAuth);
} catch (URISyntaxException e) {
throw new DiggerClientException("Invalid jenkins url format.");
return triggerBuildService.build(jobName, timeout);
} catch (IOException e) {
LOG.debug("Exception while connecting to Jenkins", e);
throw new DiggerClientException(e);
} catch (InterruptedException e) {
LOG.debug("Exception while sleeping between checks", e);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rephrase: "Exception while sleeping between checks" - It's not clear what that means for me.

throw new DiggerClientException(e);
}
}

/**
* Triggers a build for the given job and waits until it leaves the queue and actually starts.
* <p>
* Calls {@link #build(String, long)} with a default timeout of {@link #DEFAULT_BUILD_TIMEOUT}.
*
* @param jobName name of the job
* @return the build number
* @throws DiggerClientException if connection problems occur during connecting to Jenkins
* @see #build(String, long)
*/
public long build(String jobName) throws DiggerClientException {
return this.build(jobName, DEFAULT_BUILD_TIMEOUT);
}

}
15 changes: 7 additions & 8 deletions src/main/java/com/redhat/digkins/services/CreateJobService.java
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
package com.redhat.digkins.services;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use org.aerogear.*** as package for all of this

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's do the package change in a SEPARATE PR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1


import com.offbytwo.jenkins.JenkinsServer;
import org.apache.commons.io.FileUtils;
import org.jtwig.JtwigModel;
import org.jtwig.JtwigTemplate;

import java.io.File;
import java.io.IOException;

/**
* Create digger job on jenkins platform
*/
public class CreateJobService {

private JenkinsServer jenkins;
private final static String GIT_REPO_URL = "GIT_REPO_URL";
private final static String GIT_REPO_BRANCH = "GIT_REPO_BRANCH";

private final static String GIT_REPO_URL = "GIT_REPO_URL", GIT_REPO_BRANCH = "GIT_REPO_BRANCH";
private JenkinsServer jenkins;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be final as in DiggerClient?


/**
* @param jenkins - jenkins api instance
* @param jenkins jenkins api instance
*/
public CreateJobService(JenkinsServer jenkins) {
this.jenkins = jenkins;
Expand All @@ -27,9 +26,9 @@ public CreateJobService(JenkinsServer jenkins) {
/**
* Create new digger job on jenkins platform
*
* @param name - job name that can be used later to reference job
* @param gitRepo - git repository url (full git repository url. e.g [email protected]:digger/helloworld.git
* @param gitBranch - git repository branch (default branch used to checkout source code)
* @param name job name that can be used later to reference job
* @param gitRepo git repository url (full git repository url. e.g [email protected]:digger/helloworld.git
* @param gitBranch git repository branch (default branch used to checkout source code)
*/
public void create(String name, String gitRepo, String gitBranch) throws IOException {
JtwigTemplate template = JtwigTemplate.classpathTemplate("templates/job.xml");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I strongly recommend to extract hardcoded strings into a constant field, this way it can be found, checked and modified easier when needed.

Expand Down
116 changes: 116 additions & 0 deletions src/main/java/com/redhat/digkins/services/TriggerBuildService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package com.redhat.digkins.services;

import com.offbytwo.jenkins.JenkinsServer;
import com.offbytwo.jenkins.model.Executable;
import com.offbytwo.jenkins.model.JobWithDetails;
import com.offbytwo.jenkins.model.QueueItem;
import com.offbytwo.jenkins.model.QueueReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;

/**
* Provides functionality to trigger a build.
**/
public class TriggerBuildService {

private static final Logger LOG = LoggerFactory.getLogger(TriggerBuildService.class);

/**
* How long should we wait before we start checking the queue item status.
*/
public static final long FIRST_CHECK_DELAY = 5 * 1000L;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a suggestion: the time unit should be always explicit. In this case I believe they are millis so I would either name it FIRST_CHECK_DELAY_MILLIS or include it in comment /** [...] to queue item status, in milli seconds. */


/**
* How long should we wait before checking the queue item status for next time.
*/
public static final long POLL_PERIOD = 2 * 1000L;


private JenkinsServer jenkins;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it be final?


/**
* @param jenkins jenkins api instance
*/
public TriggerBuildService(JenkinsServer jenkins) {
this.jenkins = jenkins;
}

/**
* See the documentation in {@link com.redhat.digkins.DiggerClient#build(String, long)}
*
* @param jobName name of the job
* @param timeout timeout
* @return the build number
* @throws IOException if connection problems occur during connecting to Jenkins
* @throws InterruptedException if a problem occurs during sleeping between checks
* @see com.redhat.digkins.DiggerClient#build(String, long)
*/
public long build(String jobName, long timeout) throws IOException, InterruptedException {
final long timeoutTime = System.currentTimeMillis() + timeout;

LOG.debug("Going to build job with name: {}", jobName);
LOG.debug("Going to timeout in {} msecs if build didn't start executing", timeout);

JobWithDetails job = jenkins.getJob(jobName);
if (job == null) {
throw new IllegalArgumentException("Unable to find job for name '" + jobName + "'");
}

final QueueReference queueReference = job.build();
if (queueReference == null) {
// this is probably an implementation problem we have here
throw new IllegalStateException("Queue reference cannot be null!");
}
LOG.debug("Build triggered; queue item reference: {}", queueReference.getQueueItemUrlPart());

// wait for N seconds, then fetch the queue item.
// do it until we have an executable.
// we would have an executable when the build leaves queue and starts building.

LOG.debug("Going to sleep {} msecs", FIRST_CHECK_DELAY);
Thread.sleep(FIRST_CHECK_DELAY);

QueueItem queueItem;
while (true) {
queueItem = jenkins.getQueueItem(queueReference);
LOG.debug("Queue item : {}", queueItem);

if (queueItem == null) {
// this is probably an implementation problem we have here
throw new IllegalStateException("Queue item cannot be null!");
}

LOG.debug("Build item cancelled:{}, blocked:{}, buildable:{}, stuck:{}", queueItem.isCancelled(), queueItem.isBlocked(), queueItem.isBuildable(), queueItem.isStuck());

if (queueItem.isCancelled()) {
LOG.debug("Queue item is cancelled. Returning -1");
return -1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not like this because we introducing magic number and missing actual problem here.
Best to introduce some object (model) that would contain build number + some info about this build. If something is wrong build number would be empty.
Alternative is to use Long and return null or provide specific client exception (Subclass our exception)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my other comment: #2 (comment)

} else if (queueItem.isStuck()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably would require us to return some exception to the client - job needs to be retrgered. I do not remember where item can be stuck, but it this is permanent we should let clients now.

Copy link
Contributor Author

@aliok aliok Dec 9, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wtrocki good point. This is a combined answer for the comment above #2 (comment) and this comment.

I don't like subclassing the client exception. That would mean some casting and instanceof calls.

Lets use this model then:

class BuildStatus{
   STATE state;
   int buildNumber;
}
enum STATE{
  BUILDING,
  QUEUE_ITEM_CANCELLED,
  QUEUE_ITEM_STUCK,
  TIMED_OUT
  ...
}
class DiggerClient{
...
  public BuildStatus build(String jobName, long timeout) throws DiggerClientException{...}
}

Method will only throw exception in case of

  • thread interruption during sleep calls
  • connection problem with Jenkins

LOG.debug("Queue item is stuck. Returning -1");
return -1;
}

// do not return -1 if blocked.
// we will wait until it is unblocked.

final Executable executable = queueItem.getExecutable();

if (executable != null) {
LOG.debug("Build has an executable. Returning build number: {}", executable.getNumber());
return executable.getNumber();
} else {
LOG.debug("Build did not start executing yet.");
if (timeoutTime < System.currentTimeMillis()) {
LOG.debug("Timeout period has not exceeded yet. Sleeping for {} msecs", POLL_PERIOD);
Thread.sleep(POLL_PERIOD);
} else {
LOG.debug("Timeout period has exceeded. Returning -1.");
return -1;
}
}

}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package com.redhat.digkins.util;

import java.util.TreeMap;

/**
* Represents internal client exception
*/
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/redhat/digkins/util/JenkinsAuth.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

/**
* Jenkins authentication object.
* <p>
* Holds credentials in it.
*/
public class JenkinsAuth {

Expand Down
22 changes: 22 additions & 0 deletions src/main/resources/log4j.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration debug="false"
xmlns:log4j='http://jakarta.apache.org/log4j/'>

<appender name="console" class="org.apache.log4j.ConsoleAppender">
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern"
value="%d{yyyy-MM-dd HH:mm:ss} %-5p [%t] %c{1}.%M(%L) - %m%n"/>
</layout>
</appender>

<logger name="com.redhat.digkins">
<level value="DEBUG"/>
</logger>

<root>
<level value="INFO" />
<appender-ref ref="console" />
</root>

</log4j:configuration>