Skip to main content

Part of the Continuous Deployment process is having a strong Pipeline. Typically, Jenkins is used to create that Pipeline. The Pipeline should automate the individual steps for each Stage of the 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 Groovy 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) Groovy 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 https://deployhub.openmakesoftware.com/API/login?user=admin&pass=123456")
sh('curl -b session -X POST https://deployhub.openmakesoftware.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 and Groovy is that its difficult if not impossible to bring in extra Groovy libraries. The Jenkin’s Grapes/Grab implementation works with some versions of Jenkins and 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. 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("https://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.