diff --git a/README.md b/README.md index e64c0c9..d8f329d 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,28 @@ ![GitHub](https://img.shields.io/github/license/joelee2012/go-jenkins) # go-jenkins -[Golang](https://golang.org/) client library for [Jenkins API](https://wiki.jenkins.io/display/JENKINS/Remote+access+API). -> Ported from [api4jenkins](https://github.com/joelee2012/api4jenkins>), a [Python3](https://www.python.org/) client library for [Jenkins API](https://wiki.jenkins.io/display/JENKINS/Remote+access+API). +[Golang](https://golang.org/) client library for accessing [Jenkins API](https://www.jenkins.io/doc/book/using/remote-access-api/). +> Ported from [api4jenkins](https://github.com/joelee2012/api4jenkins>), a [Python3](https://www.python.org/) client library for [Jenkins API](https://www.jenkins.io/doc/book/using/remote-access-api/). + +# Features +This API client package covers most of the existing Jenkins API calls and is updated regularly to add new and/or missing endpoints. + +Currently, the following are supported: + +- Job +- Build +- Credential +- View +- Queue +- Node # Usage +```go +import "github.com/joelee2012/go-jenkins" +``` + +## Example ```go package main @@ -16,37 +33,39 @@ import ( "log" "time" - "github.com/joelee2012/go-jenkins/jenkins" + "github.com/joelee2012/go-jenkins" ) func main() { + // Construct new client client, err := jenkins.NewClient("http://localhost:8080/", "admin", "1234") if err != nil { log.Fatalln(err) } xml := ` - + + pipeline { + agent any + stages { + stage('build'){ + steps{ + sh 'echo $JENKINS_VERSION' + } + } + } + } true - - false + + false ` - // create jenkins job + // create jenkins job if err := client.CreateJob("pipeline", xml); err != nil { log.Fatalln(err) } - qitem, err := client.BuildJob("pipeline", jenkins.ReqParams{}) + // Build Job and get BuildItem + qitem, err := client.BuildJob("pipeline", nil) if err != nil { log.Fatalln(err) } @@ -68,4 +87,7 @@ func main() { return nil }) } -``` \ No newline at end of file +``` + +# Documentation +See https://pkg.go.dev/github.com/joelee2012/go-jenkins \ No newline at end of file diff --git a/jenkins/build.go b/build.go similarity index 100% rename from jenkins/build.go rename to build.go diff --git a/jenkins/build_test.go b/build_test.go similarity index 99% rename from jenkins/build_test.go rename to build_test.go index af92eb2..cc52216 100644 --- a/jenkins/build_test.go +++ b/build_test.go @@ -36,6 +36,7 @@ func setupBuild(t *testing.T) *BuildItem { assert.Contains(t, strings.Join(output, ""), os.Getenv("JENKINS_VERSION")) return build } + func TestBuildItemIsBuilding(t *testing.T) { build := setupBuild(t) building, err := build.IsBuilding() diff --git a/jenkins/credential.go b/credential.go similarity index 100% rename from jenkins/credential.go rename to credential.go diff --git a/jenkins/item.go b/item.go similarity index 100% rename from jenkins/item.go rename to item.go diff --git a/jenkins/jenkins.go b/jenkins.go similarity index 63% rename from jenkins/jenkins.go rename to jenkins.go index c96b075..3df67e9 100644 --- a/jenkins/jenkins.go +++ b/jenkins.go @@ -11,9 +11,6 @@ import ( "github.com/imroc/req" ) -type JenkinsError struct { -} - type Client struct { URL string Header http.Header @@ -31,12 +28,65 @@ type Crumb struct { Value string `json:"crumb"` } -// Create new Client -// client, err = NewClient(os.Getenv("JENKINS_URL"), os.Getenv("JENKINS_USER"), os.Getenv("JENKINS_PASSWORD")) -// if err != nil { -// return err -// } -// fmt.Println(client) +// Init Jenkins client and create job to build +// package main +// +// import ( +// "log" +// "time" +// +// "github.com/joelee2012/go-jenkins/jenkins" +// ) +// +// func main() { +// client, err := jenkins.NewClient("http://localhost:8080/", "admin", "1234") +// if err != nil { +// log.Fatalln(err) +// } +// xml := ` +// +// +// +// true +// +// false +// ` +// // create jenkins job +// if err := client.CreateJob("pipeline", xml); err != nil { +// log.Fatalln(err) +// } +// qitem, err := client.BuildJob("pipeline", nil) +// if err != nil { +// log.Fatalln(err) +// } +// var build *Build +// for { +// time.Sleep(1 * time.Second) +// build, err = qitem.GetBuild() +// if err != nil { +// log.Fatalln(err) +// } +// if build != nil { +// break +// } +// } +// // tail the build log to end +// build.LoopProgressiveLog("text", func(line string) error { +// log.Println(line) +// time.Sleep(1 * time.Second) +// return nil +// }) +// } func NewClient(url, user, password string) (*Client, error) { url = appendSlash(url) c := &Client{URL: url, Header: make(http.Header), Req: req.New()} @@ -53,6 +103,7 @@ func NewClient(url, user, password string) (*Client, error) { return c, nil } +// Set content type for request, default is 'application/json' func (c *Client) SetContentType(ctype string) { if ctype == "" { c.Header.Set("Accept", "application/json") @@ -84,11 +135,41 @@ func (c *Client) GetCrumb() (*Crumb, error) { return c.Crumb, nil } +// Get job with fullname: +// job, err := client.GetJob("path/to/job") +// if err != nil { +// return err +// } +// fmt.Println(job) func (c *Client) GetJob(fullName string) (*JobItem, error) { folder, shortName := c.resolveJob(fullName) return folder.Get(shortName) } +// Create job with given xml config: +// xml := ` +// +// +// +// true +// +// false +// ` +// // create jenkins job +// if err := client.CreateJob("path/to/name", xml); err != nil { +// log.Fatalln(err) +// } func (c *Client) CreateJob(fullName, xml string) error { folder, shortName := c.resolveJob(fullName) return folder.Create(shortName, xml) @@ -108,6 +189,8 @@ func (c *Client) resolveJob(fullName string) (*JobItem, string) { return NewJobItem(url, "Folder", c), name } +// Covert fullname to url, eg: +// path/to/name -> http://jenkins/job/path/job/to/job/name func (c *Client) Name2URL(fullName string) string { if fullName == "" { return c.URL @@ -116,6 +199,8 @@ func (c *Client) Name2URL(fullName string) string { return appendSlash(c.URL + "job/" + path) } +// Covert url to full name, eg: +// http://jenkins/job/path/job/to/job/name -> path/to/name func (c *Client) URL2Name(url string) (string, error) { if !strings.HasPrefix(url, c.URL) { return "", fmt.Errorf("%s is not in %s", url, c.URL) @@ -124,6 +209,7 @@ func (c *Client) URL2Name(url string) (string, error) { return strings.Trim(strings.ReplaceAll(path, "/job/", "/"), "/"), nil } +// Get jenkins version number func (c *Client) GetVersion() (string, error) { resp, err := c.Req.Get(c.URL) if err != nil { @@ -132,15 +218,25 @@ func (c *Client) GetVersion() (string, error) { return resp.Response().Header.Get("X-Jenkins"), nil } +// Trigger job to build: +// // without parameters +// client.BuildJob("your job", nil) +// client.BuildJob("your job", jenkins.ReqParams{}) +// // with parameters +// client.BuildJob("your job", jenkins.ReqParams{"ARG1": "ARG1_VALUE"}) func (c *Client) BuildJob(fullName string, params ReqParams) (*OneQueueItem, error) { return NewJobItem(c.Name2URL(fullName), "Job", c).Build(params) } +// List job with depth func (c *Client) ListJobs(depth int) ([]*JobItem, error) { job := NewJobItem(c.URL, "Folder", c) return job.List(depth) } +// Send request to jenkins, +// // send request to get JSON data of jenkins +// client.Request("GET", "api/json") func (c *Client) Request(method, entry string, v ...interface{}) (*req.Resp, error) { return c.Do(method, c.URL+entry, v...) } @@ -203,6 +299,11 @@ func (c *Client) ExportJCasC(name string) error { return resp.ToFile(name) } +// Bind jenkins JSON data to interface, +// // bind json data to map +// data := make(map[string]string) +// client.BindAPIJson(jenkins.ReqParams{"tree":"description"}, &data) +// fmt.Println(data["description"]) func (c *Client) BindAPIJson(params ReqParams, v interface{}) error { resp, err := c.Request("GET", "api/json", params) if err != nil { diff --git a/jenkins/jenkins_test.go b/jenkins_test.go similarity index 100% rename from jenkins/jenkins_test.go rename to jenkins_test.go diff --git a/jenkins/job.go b/job.go similarity index 84% rename from jenkins/job.go rename to job.go index 03529c8..b023560 100644 --- a/jenkins/job.go +++ b/job.go @@ -3,6 +3,7 @@ package jenkins import ( "encoding/json" "fmt" + "net/url" "path" "strings" @@ -93,9 +94,10 @@ func (j *JobItem) IsBuildable() (bool, error) { } func (j *JobItem) setName() { - j.FullName, _ = j.client.URL2Name(j.URL) + urlPath, _ := j.client.URL2Name(j.URL) + j.FullName, _ = url.PathUnescape(urlPath) _, j.Name = path.Split(j.FullName) - j.FullDisplayName = strings.ReplaceAll(j.FullName, "/", " » ") + j.FullDisplayName, _ = url.PathUnescape(strings.ReplaceAll(j.FullName, "/", " » ")) } func (j *JobItem) GetDescription() (string, error) { @@ -146,7 +148,7 @@ func (j *JobItem) GetBuild(number int) (*BuildItem, error) { for _, build := range jobJson.Builds { if number == build.Number { - return NewBuildItem(build.URL, parseClass(build.Class), j.client), nil + return NewBuildItem(build.URL, build.Class, j.client), nil } } return nil, nil @@ -259,7 +261,41 @@ func (j *JobItem) ListBuilds() ([]*BuildItem, error) { } for _, build := range jobJson.Builds { - builds = append(builds, NewBuildItem(build.URL, parseClass(build.Class), j.client)) + builds = append(builds, NewBuildItem(build.URL, build.Class, j.client)) } return builds, nil } + +func (j *JobItem) SetNextBuildNumber(number int) error { + _, err := j.Request("POST", "nextbuildnumber/submit", ReqParams{"nextBuildNumber": number}) + return err +} + +func (j *JobItem) GetParameters() ([]*ParameterDefinition, error) { + jobJson := &Job{} + if err := j.BindAPIJson(nil, jobJson); err != nil { + return nil, err + } + for _, p := range jobJson.Property { + if p.Class == "hudson.model.ParametersDefinitionProperty" { + return p.ParameterDefinitions, nil + } + } + return nil, nil +} + +func (j *JobItem) SCMPolling() error { + _, err := j.Request("POST", "polling") + return err +} + +func (j *JobItem) GetMultibranchPipelineScanLog() (string, error) { + if j.Class != "WorkflowMultiBranchProject" { + return "", fmt.Errorf("%s is not a WorkflowMultiBranchProject", j) + } + resp, err := j.Request("POST", "indexing/consoleText") + if err != nil { + return "", err + } + return resp.String(), nil +} diff --git a/jenkins/job_test.go b/job_test.go similarity index 100% rename from jenkins/job_test.go rename to job_test.go diff --git a/jenkins/node.go b/node.go similarity index 100% rename from jenkins/node.go rename to node.go diff --git a/jenkins/node_test.go b/node_test.go similarity index 100% rename from jenkins/node_test.go rename to node_test.go diff --git a/jenkins/plugin.go b/plugin.go similarity index 100% rename from jenkins/plugin.go rename to plugin.go diff --git a/jenkins/queue.go b/queue.go similarity index 100% rename from jenkins/queue.go rename to queue.go diff --git a/jenkins/types.go b/types.go similarity index 99% rename from jenkins/types.go rename to types.go index 96b6ec0..d2f0334 100644 --- a/jenkins/types.go +++ b/types.go @@ -27,7 +27,7 @@ type Job struct { LastUnstableBuild *Build `json:"lastUnstableBuild"` LastUnsuccessfulBuild *Build `json:"lastUnsuccessfulBuild"` NextBuildNumber int `json:"nextBuildNumber"` - Property []Property `json:"property"` + Property []*Property `json:"property"` QueueItem interface{} `json:"queueItem"` ConcurrentBuild bool `json:"concurrentBuild"` ResumeBlocked bool `json:"resumeBlocked"` @@ -206,7 +206,7 @@ type DefaultParameterValue struct { Name string `json:"name"` Value string `json:"value"` } -type ParameterDefinitions struct { +type ParameterDefinition struct { Class string `json:"_class"` DefaultParameterValue DefaultParameterValue `json:"defaultParameterValue"` Description string `json:"description"` @@ -216,7 +216,7 @@ type ParameterDefinitions struct { } type Property struct { Class string `json:"_class"` - ParameterDefinitions []ParameterDefinitions `json:"parameterDefinitions,omitempty"` + ParameterDefinitions []*ParameterDefinition `json:"parameterDefinitions,omitempty"` } type ComputerSet struct { diff --git a/jenkins/view.go b/view.go similarity index 100% rename from jenkins/view.go rename to view.go diff --git a/jenkins/view_test.go b/view_test.go similarity index 100% rename from jenkins/view_test.go rename to view_test.go