GitLab REST API

It's been w while! A quick note today to keep this blog still breathing!

What do we have today? Oh, yeah, GitLab REST API, so, today, almost the popular tools will support REST API as an extension point to let the other service trigger their functions.


Use Case


I have a need to create a merge request on feature branch after a pipeline is green (multibranch project) to master branch. Sound reasonable! 


Solution


The easiest way to implement the integration with other systems from Jenkins is to trigger theirs REST API. Let's start with GitLab API, they are for EE version, however, CE version is EE version with limitation so you still can use the API (not sure where they are limited).

So the steps for implementation will be:

  • Use API with authorization.
  • List all open requests to check if we should create a new merge request for that branch or not.
  • Create a merge request.
  • Accept the changes - this option is for the approver
Sound simple enough!

So if you're using other programming language, the standard solution will be personal access token, where you set up an access token from GitLab for a system account, then you will use this access key with "Private-token" header. Unfortunately, when you use Jenkins pipeline script, using Http Request plugin will reveal all the custom header which will contains you secret key within this header. So, a workaround is you can send the secret token into the url by adding "&private_token=<secret>". I don't like this solution because sending secret key on url is something not encourage me much. I'd like to use the traditional authorization header. So how? Then I found this discussion which drives me of using the resource owner password credential, this method might be deprecated soon because you have to use it within a secure environment means highly trusted between user and the client consumer, it's not support for using across internet.

You will send a request for access token to end point /oauth/token with a json of user name and password information with GET then receive a json response with an access token. To refresh the mind, I share a python code for this function (instead of Groovy :P)

import requests

def getAuthToken(server,  user,  pwd):
    apiEndpoint = "/oauth/token"
    jsonParams = {"grant_type": "password"}
    jsonParams["username"] = user
    jsonParams["password"] = pwd
    server += apiEndpoint
    res = requests.post(url=server,  params=jsonParams)
    if res.status_code != 200:
        print("Cannot retrieve your user token from GitLab!")
        return ''
    return res.json()["access_token"]


Alright, then you will have the access token and use it with Authorization header with "Bearer" scheme instead of "Basic" scheme. One more note with Http Request plugin (it's a pain of using other tools which locked you into their flows), "Bearer" scheme is not (or no longer) supported so you can't use auth parameter but use the customerheaders. 

Then now, come to the part of the Merge Request project (you can refer to GitLab API project in GitHub to find other project topics), you will list out all the open merge request on a specific project which assigned to specific owner.

Here is a sample code for getting a project open merge request

def getOpenMRList(server,  token, assignee,  project):
    #apiEndpoint = "/api/v4/projects/{0}/merge_requests?state=opened&assignee_id={1}".format(project,  assignee)
    # Not query on assignee for now because some MRs will be assigned to noone
    apiEndpoint = "/api/v4/projects/{0}/merge_requests?state=opened".format(project)
    server += apiEndpoint
    header = {"Authorization":  "Bearer {0}".format(token)}
    res = requests.get(url=server,  headers=header)
    if res.status_code != 200:
        print("No open merge request is assigned to you!")
        return None
    print("There are open merge request(s) that assigned to you: ")
    print("=================\n")
    jsonData = res.json()
    for mr in jsonData:
        print("RequestID: '{0}' from: '{1}'".format(mr["id"],  mr["author"]["name"]))
        print("Title: ",  mr["title"])
        print("=================")
    return jsonData


So now, you have a json object list for all open merge requests on a specific project. Please note, in my code above, I skip the part of filtering the assigned user because some users they will create a merge request without assignee so you will miss out those requests.

Go through a json list to check if there is no open merge request on your source branch because it will cause a conflict. Actually you can filter the open merge request by 'source_branch' option, however, seems it didn't work until GitLab 11 or at least my version. If there is no open merge request, you will create a new merge request on your feature branch to the target one. What is a merge title, my suggestion is a latest commit message, you can get it by:

git log -n 1 --pretty=format:'%B'

There here is the function to create a new merge request


def createMR(server,  token,  project, assignee,  src,  target,  title,  removeSrc):
    apiEndpoint = "/projects/{0}/merge_requests"
    server += apiEndpoint
    header = {"Authorization":  "Bearer {0}".format(token)}
    jsonParams = {"id": project}
    jsonParams["source_branch"] = src
    jsonParams["target_branch"] = target
    jsonParams["title"] = title
    jsonParams["assignee_id"] = assignee
    jsonParams["remove_source_branch"] = removeSrc
    res = requests.post(url=server,  headers=header,  params=jsonParams)
    if res.status_code != 200:
        print("Fail to create a merge request from '{0}' branch to '{1}' branch".format(src,  target))
    return

Then same as creation, you can accept the merge request by it's IID, remember IID not ID.

def acceptMR(server,  token,  project, iid,  msg,  removeSrc):
    apiEndpoint = "/projects/{0}/merge_requests/{1}/merge".format(project,  iid)
    server += apiEndpoint
    header = {"Authorization":  "Bearer {0}".format(token)}
    jsonParams = {"id": project}
    jsonParams["merge_request_iid"] = iid
    jsonParams["merge_commit_message"] = msg
    jsonParams["should_remove_source_branch"] = removeSrc
    print("Merge request ID: ",  iid)
    res = requests.put(url=server,  headers=header,  params=jsonParams)
    if res.status_code == 200:
        print("Merge RequestIID: {0} has accepted".format(iid))
    elif res.status_code == 405:
        print("Merge RequestIID: {0} has conflict(s) and cannot be merged".format(iid))
    elif res.status_code == 406:
        print("Merge RequestIID: {0} alread merged or closed".format(iid))
    elif res.status_code == 409:
        print("Merge RequestIID: {0} has SHA code doesn't match the HEAD".format(iid))
    return


So, that's all for the GitLab REST API. You can refer here for a source of using the api

Additional

However, however ... life is not so beautiful. If you are using python or other languages, you're good! When come to Jenkins environment, you will have a problem, same as above slogan, when you're using a tool, you will be obeyed it's rules. What's happened here? 

Jenkins supports the mechanism when its service has stopped, a running job can suspend and resume where is was when service restart. They will serialize all the memory objects within Jenkins JVM into files then deserialize them for resume. The problem here is JSON object implementation is a non-serializable object, so you will have problem with Jenkins sandbox. Almost the REST API will return a json string, which you will have problem when construct them into json object.

Here are the solutions out there from internet
  • Use JsonSlurperClassic which uses HashMap that is serializable
  • Move the functions out of Jenkins sandbox with annotation @NonCPS
Which I'd prefer the second option. So from your design standpoint, create a function to call Http Request then return a json String. Then you will create the @NonCPS functions to handle the logic with your json data out of Jenkins sandbox.

It's it done for today! Nice weekend all!


Comments