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
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 }