Using Native Groovy for Jenkins Restful API Calls

Jenkins and GroovyAn Elegant Way to use Native Groovy to Integrate Your Pipeline

Part of the Jenkins Continuous Delivery process is having a strong Pipeline. Typically, Blue Ocean is used to automate the execution of workflows across that Pipeline. This automation requires communicating with other tools in the tool chain. Most tools today support Restful API calls as an integration point. Making Restful API calls from the Jenkins Groovy Pipeline script can be difficult since the version embedded in Jenkins can be limited in it functionality. There is a quick and dirty way to make Restful API calls by using a script, curl or wget. Then there is the elegant way by using native Groovy.

The Messy Way for Rest Calls

The Jenkin’s Pipelines use Groovy as the scripting language to bridge between Jenkins and the outside world. Typically the shell (sh)  call is made. This works but is messy since you end up writing external scripts to wrapper command line calls or end up with many shell calls.

For example, a REST call is needed to login and then another REST call to query the user profile.

sh('curl --cookie-jar session.txt -s http://deployhub.Pro.com/API/login?user=admin&pass=123456") sh('curl -b session -X POST http://deployhub.Pro.com/API/UserProfile?user=admin')

Adding error handling around the curl commands will make your Jenkinsfile file grow and be more difficult to understand.

The way to fix this is to use Groovy class files and move the coding from the Pipeline to the class. This will cleanup the Jenkins file and enable reuse. The problem with Jenkins is that its difficult if not impossible to bring in extra Groovy libraries. The Jenkin’s Grapes/Grab implementation works with some versions of  Groovy and then breaks with others. See Jenkins-15512 to get around this stick with native Groovy classes and methods.

Normally, for Rest API calls one would use HttpBuilder class library. Jenkins won’t let you import that into a Jenkins Pipeline, but with straight Groovy it will work.

Easy clean way for Rest Calls

The Url and HttpUrlConnection classes enable the same functionality as HttpBuilder but you need to add more code to handle Headers and Cookie processing.

I created a new class that includes the Rest API code and my application level code.

// vars/deployhub.groovy class deployhub { String body=""; String message=""; String cookie=""; String url=""; String userid=""; String pw=""; Integer statusCode; boolean failure = false; def String msg() { return "Loading dhactions"; } def parseResponse(HttpURLConnection connection){ this.statusCode = connection.responseCode; this.message = connection.responseMessage; this.failure = false; if(statusCode == 200 || statusCode == 201){ this.body = connection.content.text;//this would fail the pipeline if there was a 400 }else{ this.failure = true; this.body = connection.getErrorStream().text; } if (cookie.length() == 0) { String headerName=null; for (int i=1; (headerName = connection.getHeaderFieldKey(i))!=null; i++) { if (headerName.equals("Set-Cookie")) { String c = connection.getHeaderField(i); cookie += c + "; "; } } } } def doGetHttpRequest(String requestUrl){ URL url = new URL(requestUrl); HttpURLConnection connection = url.openConnection(); connection.setRequestMethod("GET"); connection.setRequestProperty("Cookie", cookie); connection.doOutput = true; //get the request connection.connect(); //parse the response parseResponse(connection); if(failure){ error("\nGET from URL: $requestUrl\n HTTP Status: $resp.statusCode\n Message: $resp.message\n Response Body: $resp.body"); } this.printDebug("Request (GET):\n URL: $requestUrl"); this.printDebug("Response:\n HTTP Status: $resp.statusCode\n Message: $resp.message\n Response Body: $resp.body"); } /** * Gets the json content to the given url and ensures a 200 or 201 status on the response. * If a negative status is returned, an error will be raised and the pipeline will fail. */ def Object doGetHttpRequestWithJson(String requestUrl){ return doHttpRequestWithJson("", requestUrl, "GET"); } /** * Posts the json content to the given url and ensures a 200 or 201 status on the response. * If a negative status is returned, an error will be raised and the pipeline will fail. */ def Object doPostHttpRequestWithJson(String json, String requestUrl){ return doHttpRequestWithJson(json, requestUrl, "POST"); } /** * Posts the json content to the given url and ensures a 200 or 201 status on the response. * If a negative status is returned, an error will be raised and the pipeline will fail. */ def Object doPutHttpRequestWithJson(String json, String requestUrl){ return doHttpRequestWithJson(json, requestUrl, "PUT"); } /** * Post/Put the json content to the given url and ensures a 200 or 201 status on the response. * If a negative status is returned, an error will be raised and the pipeline will fail. * verb - PUT or POST */ def String enc(String p) { return java.net.URLEncoder.encode(p, "UTF-8"); } def Object doHttpRequestWithJson(String json, String requestUrl, String verb){ URL url = new URL(requestUrl); HttpURLConnection connection = url.openConnection(); connection.setRequestMethod(verb); connection.setRequestProperty("Content-Type", "application/json"); if (cookie.length() > 0) connection.setRequestProperty("Cookie", cookie); connection.doOutput = true; if (json.length() > 0) { //write the payload to the body of the request def writer = new OutputStreamWriter(connection.outputStream); writer.write(json); writer.flush(); writer.close(); } //post the request connection.connect(); //parse the response parseResponse(connection); if(failure){ error("\n$verb to URL: $requestUrl\n JSON: $json\n HTTP Status: $statusCode\n Message: $message\n Response Body: $body"); return null; } def jsonSlurper = new groovy.json.JsonSlurper(); return jsonSlurper.parseText(body); // println("Request ($verb):\n URL: $requestUrl\n JSON: $json"); // println("Response:\n HTTP Status: $statusCode\n Message: $message\n Response Body: $body"); } }

Application level code is defined in the same class to keep my Jenkinsfile clean.

 def boolean login(String url, String userid, String pw) { this.url = url; this.userid = userid; this.pw = pw; def res = doGetHttpRequestWithJson("${url}/dmadminweb/API/login?user=" + enc(userid) + "&pass=" + enc(pw)); return res.success; } def moveApplication(String url, String userid, String pw, String Application, String FromDomain, String Task) { if (this.url.length() == 0) { if (!login(url,userid,pw)) return [false,"Could not login to " + url]; } // Get appid def data = doGetHttpRequestWithJson("${url}/dmadminweb/API/application/" + enc(Application)); def appid = data.result.id; // Get from domainid data = doGetHttpRequestWithJson("${url}/dmadminweb/API/domain/" + enc(FromDomain)); def fromid = data.result.id; // Get from Tasks data = doGetHttpRequestWithJson("${url}/dmadminweb/GetTasks?domainid=" + fromid); if (data.size() == 0) return [false,"Could not move the Application '" + Application + "' from '" + FromDomain + "' using the '" + Task + "' Task"]; def i=0; def taskid = 0; for (i=0;i<data.size();i++) { if (data[i].name.equalsIgnoreCase(Task)) { taskid = data[i].id; break; } }

The end result is the Jenkinsfile that is nice and clean.

@Library('deployhub') _ def dh = new deployhub(); def data = dh.moveApplication("http://rocket:8080","admin","123456","Uptime War for Tomcat;10","GLOBAL.My Pipeline.Development","Move to Integration"); println(data[0]); println(data[1]);

The error checking and parsing are done in the class so the Jenkinsfile has little to do other than being the driver of the Pipeline.

Full Example

The working example can be found on GitHub – JenkinsCI/DeployHub-Plugin. Look at the var folder for the full deployhub.groovy class.