Example use of Jenkins REST API with Golang

Hi!

I’m going to describe how to call Jenkins REST APIs using Go. We will create three Go files:
1. packages/helpers/helpers.go
2. packages/jenkins/jenkins.go
3. main.go

The source codes are available at https://github.com/mohdnaim/jenkins_rest_api

helpers.go contains function helpers. We put functions inside this file to maintain clean and organized codes.

jenkins.go contains functions related to Jenkins. Such functions are IsJobExist() to check whether a Jenkins job or build pipeline exists, CopyJenkinsJob() to copy a Jenkins job and DownloadConfigXML() to download the configurations of a job in XML format.

main.go is our main Go file, the starting point of our program.

main.go

package main

import (
	"fmt"
	"log"
	"strings"

	helpers "./packages/helpers"
	jenkins "./packages/jenkins"
)

func main() {
	// compulsory to set
	jenkinsURL := "http://127.0.0.1/"
	jenkinsUsername := "put your username here"
	jenkinsAPIToken := "put your API token here"

	jenkins.JenkinsDetails = jenkins.Details{jenkinsURL, jenkinsUsername, jenkinsAPIToken}
	xmlFolder := "xml"

	// 1. get all existing projects / jenkins jobs
	allProjectNames := jenkins.GetAllProjectNames()

	// 2. filter out projects that we want
	filteredProjectNames := make([]string, 0)
	for _, projectName := range allProjectNames {
		// do something

		// append to another slice based on condition
		if strings.HasPrefix(projectName, "prefix") {
			filteredProjectNames = append(filteredProjectNames, projectName)
		}
	}

	// 3. for each project, get its config.xml
	for _, projectName := range filteredProjectNames {
		xmlPath := fmt.Sprintf("%s/%s.xml", xmlFolder, projectName)
		if err := jenkins.DownloadConfigXML(projectName, xmlPath); err != nil {
			log.Println("error download config.xml for project:", projectName)
			continue // skip
		}
	}

	// 4. modify its config.xml
	files := helpers.GetFilenamesRecursively(xmlFolder)
	for _, xmlFile := range files {
		log.Println(xmlFile)
	}

	// 4b. rewrite config.xml

	// 5. http request POST updated config.xml
	for _, xmlFile := range files {
		tmpSlice := strings.Split(xmlFile, "/")
		projectName := tmpSlice[len(tmpSlice)-1]
		log.Println(projectName)

		if err := jenkins.PostConfigXML(projectName, xmlFile); err != nil {
			log.Println("error postconfigxml:", projectName)
		}
	}
}

We need to configure the details of the Jenkins instance. So set correct values to the following variables:
jenkinsURL := “http://127.0.0.1/”
jenkinsUsername := “put your username here”
jenkinsAPIToken := “put your API token here”

Line #18 creates a struct containing the three variables above. The struct is used by functions in jenkins.go so before we invoke any function in the jenkins.go module, we need to set the values.

Line #19 sets the folder name we store the XML files – which are the configuration file of Jenkins jobs.

Line #22 – we retrieve the names of all projects exist in the Jenkins instance. If we look at the implementation of the function GetAllProjectNames() in jenkins.go, the function invokes DownloadFileToBytes(). It makes HTTP request to /api/json with the username and password in the request header. The endpoint returns names of project in JSON format. It then converts the JSON format into ‘result’ map. For every name in the map, append the name into ‘allProjectNames’ array.

Line #24 – We filter out projects that we want. We iterate over the allProjectNames array and if a project meets certain criteria, we append the project name into ‘filteredProjectNames’ array which we will use onwards instead of ‘allProjectNames’.

Line #35 – For each project name in ‘filteredProjectNames’, we get its configuration file i.e. the config.xml.

Line #45 – We call a function GetFilenamesRecursively() in helpers.go to read all the file names in ‘xmlFolder’

Line #50 – We do whatever we want with the config.xml file such as renaming the project name, adding build parameters, change permission and so on.
I write a script in Python instead of Go to manipulate the config.xml files because Python has more libraries than Go that can help us to manipulate strings and files.

Line #52 – Finally after we make the change at line #50, we submit the change by executing POST request to Jenkins.

helpers.go

package helpers

import (
	"os"
	"path/filepath"
)

// StringInSlice ...
func StringInSlice(a string, list []string) bool {
	for _, b := range list {
		if b == a {
			return true
		}
	}
	return false
}

// GetFilenamesRecursively ...
func GetFilenamesRecursively(path string) []string {
	var files []string
	err := filepath.Walk("xml", func(path string, info os.FileInfo, err error) error {
		files = append(files, path)
		return nil
	})
	if err != nil {
		panic(err)
	}
	return files
}

jenkins.go

package jenkins

import (
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"reflect"
)

// Details ...
type Details struct {
	URL      string
	Username string
	APIToken string
}

// JenkinsDetails ...
var JenkinsDetails = Details{}

// IsJobExist ...
func IsJobExist(projectName string) bool {
	fullURL := fmt.Sprintf("%sjob/%s", JenkinsDetails.URL, projectName)
	request, err := http.NewRequest("GET", fullURL, nil)
	request.SetBasicAuth(JenkinsDetails.Username, JenkinsDetails.APIToken)
	client := &http.Client{}
	resp, err := client.Do(request)
	if err != nil {
		log.Println("project doesn't exist:", projectName)
		return false
	}
	if resp.StatusCode == 200 {
		// log.Println("project exists:", projectName)
		return true
	}
	log.Println("project doesn't exist:", projectName)
	return false
}

// CopyJenkinsJob ...
func CopyJenkinsJob(srcJob string, dstJob string) error {
	fullURL := fmt.Sprintf("%s%s?name=%s&mode=copy&from=%s", JenkinsDetails.URL, "createItem", dstJob, srcJob)
	request, err := http.NewRequest("POST", fullURL, nil)
	request.SetBasicAuth(JenkinsDetails.Username, JenkinsDetails.APIToken)
	client := &http.Client{}
	resp, err := client.Do(request)
	if err != nil {
		return err
	}
	if resp.StatusCode == 200 {
		return nil
	}
	return fmt.Errorf("error copying job: %s", srcJob)
}

// DownloadConfigXML ...
func DownloadConfigXML(projectName string, dstFilename string) error {
	fullURL := fmt.Sprintf("%sjob/%s/config.xml", JenkinsDetails.URL, projectName)
	if DownloadFile(fullURL, dstFilename) == nil {
		return nil
	}
	return fmt.Errorf("error downloading file %s", fullURL)
}

// PostConfigXML ...
func PostConfigXML(projectName string, filename string) error {
	// read content of file
	data, err := os.Open(filename)
	if err != nil {
		log.Fatal(err)
		return err
	}

	fullURL := fmt.Sprintf("%sjob/%s/config.xml", JenkinsDetails.URL, projectName)
	request, err := http.NewRequest("POST", fullURL, data)
	request.SetBasicAuth(JenkinsDetails.Username, JenkinsDetails.APIToken)
	client := &http.Client{}

	// perform the request
	resp, err := client.Do(request)
	if err != nil {
		return err
	}
	if resp.StatusCode == 200 {
		// log.Printf("success postConfigXML: %s %s %s", projectName, filename, resp.Status)
		return nil
	}
	return fmt.Errorf("error postConfigXML: %s %s %s", projectName, filename, resp.Status)
}

// DownloadFile ...
func DownloadFile(url string, filepath string) error {
	// Create the file
	out, err := os.Create(filepath)
	if err != nil {
		return err
	}
	defer out.Close()

	// Get the data
	request, err := http.NewRequest("GET", url, nil)
	request.SetBasicAuth(JenkinsDetails.Username, JenkinsDetails.APIToken)
	client := &http.Client{}
	resp, err := client.Do(request)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	// Write the body to file
	_, err = io.Copy(out, resp.Body)
	if err != nil {
		return err
	}

	return nil
}

// DownloadFileToBytes ...
func DownloadFileToBytes(url string) ([]byte, error) {
	request, err := http.NewRequest("GET", url, nil)
	request.SetBasicAuth(JenkinsDetails.Username, JenkinsDetails.APIToken)
	client := &http.Client{}
	resp, err := client.Do(request)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	bodyBytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err) // writes to standard error
	}
	return bodyBytes, nil
}

// GetAllProjectNames ...
func GetAllProjectNames() []string {
	allProjectNames := make([]string, 0)
	allProjectsURL := fmt.Sprintf("%sapi/json?pretty=true", JenkinsDetails.URL)
	if respBytes, err := DownloadFileToBytes(allProjectsURL); err == nil {
		// json --> map
		var result map[string]interface{}
		json.Unmarshal([]byte(respBytes), &result)

		// if map has slice 'jobs', iterate over it
		if jobs, keyIsPresent := result["jobs"]; keyIsPresent && reflect.TypeOf(jobs).Kind() == reflect.Slice {
			jobs2 := result["jobs"].([]interface{})
			for _, job := range jobs2 {
				// log.Println(job)
				_map, _ := job.(map[string]interface{}) // assert to map
				allProjectNames = append(allProjectNames, _map["name"].(string))
			}
		}
	}
	return allProjectNames
}

Leave a Reply