Home > other >  Groovy/Jenkins: how to refactor sh(script:"curl ...") to URL?
Groovy/Jenkins: how to refactor sh(script:"curl ...") to URL?

Time:10-08

My Jenkins pipeline currently successfully invokes Bitbucket REST API by invoking curl in a shell, as in the code below:

// Jenkinsfile
@Library('my-sandbox-libs@dev') my_lib

pipeline {
  agent any

  stages {
    stage( "1" ) { steps { script { echo "hello" } } }

    stage( "2" ) {
      steps {
        script {
          log = new org.log.Log()
          def cred_id = "bitbucket_cred_id"
          def url_base = "https://bitbucket.company.com"
          def commit = "76136485c45df256a62cbc2c3c5f1f3efcc86258"
          def status =
                       //"INPROGRESS",
                       //"SUCCESSFUL",
                       "FAILED"
          def viz_url = "https://path/to/nowhere"
          try {
            my_lib.notifyBitbucketBuildStatus(cred_id,
                                              url_base,
                                              commit,
                                              status,
                                              "foo",
                                              42,
                                              viz_url,
                                              log)
          }
        }
      }
    }

    stage( "3" ) { steps { script { echo "world" } } }
  }

  post { always { script { echo log.asJsonString() } } }
}
import groovy.json.JsonOutput

def notifyBitbucketBuildStatus(cred_id,
                               url_base,
                               commit,
                               build_state,
                               build_info_name,
                               build_info_number,
                               viz_url,
                               log) {
  def rest_path = "rest/build-status/1.0/commits"
  def dict = [:]
  dict.state = build_state
  dict.key = "${build_info_name}_${build_info_number}"
  dict.url = viz_url
  withCredentials([string(credentialsId: cred_id,
                          variable: 'auth_token')]) {
    def cmd = "curl -f -L "  
              "-H \"Authorization: Bearer ${auth_token}\" "  
              "-H \"Content-Type:application/json\" "  
              "-X POST ${url_base}/${rest_path}/${commit} "  
              "-d \'${JsonOutput.toJson(dict)}\'"
    if ( 0 != sh(script: cmd, returnStatus: true) ) {
      log.warn("Failed updating build status with Bitbucket")
    }
  }
}

I would like to refactor function notifyBitbucketBuildStatus() to use a "native" Groovy-language solution, rather than invoking curl in a shell. I read the following on this topic:

  1. https://www.baeldung.com/groovy-web-services
  2. Groovy built-in REST/HTTP client?

...based on which I thought the refactored function would look like this:

def notifyBitbucketBuildStatus(cred_id,
                               url_base,
                               commit,
                               build_state,
                               build_info_name,
                               build_info_number,
                               viz_url,
                               log) {
  def rest_path = "rest/build-status/1.0/commits"
  def dict = [:]
  dict.state = build_state
  dict.key = "${build_info_name}_${build_info_number}"
  dict.url = viz_url
  def req = new URL("${url_base}/${rest_path}/${commit}").openConnection()
  req.setRequestMethod("POST")
  req.setDoOutput(true)
  req.setRequestProperty("Content-Type", "application/json")
  withCredentials([string(credentialsId: cred_id,
                          variable: 'auth_token')]) {
    req.setRequestProperty("Authorization", "Bearer ${auth_token}")
  }
  def msg = JsonOutput.toJson(dict)
  req.getOutputStream().write(msg.getBytes("UTF-8"));
  if ( 200 != req.getResponseCode() ) {
    log.warn("Failed updating build status with Bitbucket")
  }
}

...but this generates the exception java.io.NotSerializableException: sun.net.www.protocol.https.HttpsURLConnectionImpl

That "not serializable" made me think the error had something to do with a failure to transform something to a string, so I also tried this, but it did not change the error:

def msg = JsonOutput.toJson(dict).toString()

What is wrong with the refactored code that uses class URL, and what is the right way to use it to invoke the REST API?

For the life of me, I can't see what's different between the above and the linked Stack Overflow Q&A, and my inexperience with the language is such that I rely largely on adapting existing example.

CodePudding user response:

Solution

I would highly suggest you use the HTTP Request and the Pipeline Steps Utility plugin for this. You can then use those steps in a Groovy script as follows

node('master') {
    
    withCredentials([string(credentialsId: cred_id, variable: 'auth_token')]) {
        def response = httpRequest url: "https://jsonplaceholder.typicode.com/todos", customHeaders: [[name: 'Authorization', value: "Bearer ${auth_token}"]]
    }

    if( response.status != 200 ) {
        error("Service returned a ${response.status}")
    }

    def json = readJSON text: response.content
    
    println "The User ID is ${json[0]['userId']}"
    println "The follow json obj is ${json}"
    
}

Obviously you can modify the code if you want to build a method, and you will need to update with the appropriate URL.

CodePudding user response:

I found a sucky and unsatisfying answer - but an answer nonetheless - that I posted here: https://stackoverflow.com/a/69486890/5437543

I hate that solution because it would appear to demonstrate that the Jenkins/Groovy language itself imposes an artificial contrivance on how I can organize my code. Effectively, I am prevented from doing

// Jenkinsfile
@Library('my-sandbox-libs@dev') my_lib

pipeline {
  agent any

  stages {
    stage( "1" ) { steps { script { my_lib.func() } } }
  }
}
// vars/my_lib.groovy

def func() {
  def post = new URL("https://whatever").openConnection();
  ...
  withCredentials([string(credentialsId: cred_id,
                          variable: 'auth_token')]) {
    req.setRequestProperty("Authorization", "Bearer ${auth_token}")
  }
  ...
}

...and I am forced to do

// Jenkinsfile
@Library('my-sandbox-libs@dev') my_lib

pipeline {
  agent any

  stages {
    stage( "1" ) { steps { script { my_lib.func(my_lib.getCred()) } } }
  }
}
// vars/my_lib.groovy

def getCred() {
  withCredentials([string(credentialsId: cred_id,
                          variable: 'auth_token')]) {
    return auth_token
  }
}

def func(auth_token) {
  def post = new URL("https://whatever").openConnection();
  ...
  req.setRequestProperty("Authorization", "Bearer ${auth_token}")
  ...
}

Extremely dissatisfying conclusion. I hope another answerer can point out a solution that doesn't rely on this contrived code organization.

  • Related