job.go 13.2 KB
Newer Older
LinuxSuRen's avatar
LinuxSuRen 已提交
1 2 3
package client

import (
4
	"bytes"
LinuxSuRen's avatar
LinuxSuRen 已提交
5 6
	"encoding/json"
	"fmt"
7
	"io"
LinuxSuRen's avatar
LinuxSuRen 已提交
8
	"io/ioutil"
9
	"mime/multipart"
LinuxSuRen's avatar
LinuxSuRen 已提交
10
	"net/http"
LinuxSuRen's avatar
LinuxSuRen 已提交
11
	"net/url"
12 13
	"os"
	"path/filepath"
LinuxSuRen's avatar
LinuxSuRen 已提交
14 15
	"strconv"
	"strings"
16

17 18 19
	"go.uber.org/zap"
	"moul.io/http2curl"

20
	"github.com/jenkins-zh/jenkins-cli/util"
LinuxSuRen's avatar
LinuxSuRen 已提交
21 22
)

23 24 25 26 27 28
const (
	// StringParameterDefinition is the definition for string parameter
	StringParameterDefinition = "StringParameterDefinition"
	// FileParameterDefinition is the definition for file parameter
	FileParameterDefinition = "FileParameterDefinition"
)
29

30
// JobClient is client for operate jobs
LinuxSuRen's avatar
LinuxSuRen 已提交
31 32
type JobClient struct {
	JenkinsCore
LinuxSuRen's avatar
LinuxSuRen 已提交
33 34

	Parent string
LinuxSuRen's avatar
LinuxSuRen 已提交
35 36 37
}

// Search find a set of jobs by name
38
func (q *JobClient) Search(name, kind string, start, limit int) (items []JenkinsItem, err error) {
39
	err = q.RequestWithData(http.MethodGet, fmt.Sprintf("/items/list?name=%s&type=%s&start=%d&limit=%d&parent=%s",
LinuxSuRen's avatar
LinuxSuRen 已提交
40
		name, kind, start, limit, q.Parent),
41
		nil, nil, 200, &items)
LinuxSuRen's avatar
LinuxSuRen 已提交
42 43
	return
}
44 45

// Build trigger a job
LinuxSuRen's avatar
LinuxSuRen 已提交
46
func (q *JobClient) Build(jobName string) (err error) {
47
	path := ParseJobPath(jobName)
48
	_, err = q.RequestWithoutData(http.MethodPost, fmt.Sprintf("%s/build", path), nil, nil, 201)
LinuxSuRen's avatar
LinuxSuRen 已提交
49 50 51
	return
}

52 53 54 55 56 57 58 59
// IdentityBuild is the build which carry the identity cause
type IdentityBuild struct {
	Build JobBuild
	Cause IdentityCause
}

// IdentityCause carray a identity cause
type IdentityCause struct {
60
	UUID             string `json:"uuid"`
61
	ShortDescription string `json:"shortDescription"`
62
	Message          string
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
}

// BuildAndReturn trigger a job then returns the build info
func (q *JobClient) BuildAndReturn(jobName, cause string, timeout, delay int) (build IdentityBuild, err error) {
	path := ParseJobPath(jobName)

	api := fmt.Sprintf("%s/restFul/build?1=1", path)
	if timeout >= 0 {
		api += fmt.Sprintf("&timeout=%d", timeout)
	}
	if delay >= 0 {
		api += fmt.Sprintf("&delay=%d", delay)
	}
	if cause != "" {
		api += fmt.Sprintf("&identifyCause=%s", cause)
	}

	err = q.RequestWithData(http.MethodPost, api, nil, nil, 200, &build)
	return
}

84
// GetBuild get build information of a job
LinuxSuRen's avatar
LinuxSuRen 已提交
85
func (q *JobClient) GetBuild(jobName string, id int) (job *JobBuild, err error) {
86
	path := ParseJobPath(jobName)
LinuxSuRen's avatar
LinuxSuRen 已提交
87 88
	var api string
	if id == -1 {
89
		api = fmt.Sprintf("%s/lastBuild/api/json", path)
LinuxSuRen's avatar
LinuxSuRen 已提交
90
	} else {
91
		api = fmt.Sprintf("%s/%d/api/json", path, id)
LinuxSuRen's avatar
LinuxSuRen 已提交
92
	}
93

LinuxSuRen's avatar
LinuxSuRen 已提交
94
	err = q.RequestWithData("GET", api, nil, nil, 200, &job)
LinuxSuRen's avatar
LinuxSuRen 已提交
95 96 97
	return
}

98
// BuildWithParams build a job which has params
99
func (q *JobClient) BuildWithParams(jobName string, parameters []ParameterDefinition) (err error) {
100
	path := ParseJobPath(jobName)
LinuxSuRen's avatar
LinuxSuRen 已提交
101
	api := fmt.Sprintf("%s/build", path)
102

103 104 105 106
	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)
	defer writer.Close()

107 108 109 110 111 112 113 114 115 116 117
	hasFileParam := false
	stringParameters := make([]ParameterDefinition, 0, len(parameters))
	for _, parameter := range parameters {
		if parameter.Type == FileParameterDefinition {
			hasFileParam = true
			var file *os.File
			file, err = os.Open(parameter.Filepath)
			if err != nil {
				return err
			}
			defer file.Close()
118

119 120 121 122 123 124 125 126
			var fWriter io.Writer
			fWriter, err = writer.CreateFormFile(parameter.Filepath, filepath.Base(parameter.Filepath))
			if err != nil {
				return err
			}
			_, err = io.Copy(fWriter, file)
		} else {
			stringParameters = append(stringParameters, parameter)
127 128 129
		}
	}

130
	var paramJSON []byte
131 132
	if len(stringParameters) == 1 {
		paramJSON, err = json.Marshal(stringParameters[0])
133
	} else {
134
		paramJSON, err = json.Marshal(stringParameters)
135
	}
136
	if err != nil {
137
		return
138
	}
139

140 141 142 143
	if hasFileParam {
		if err = writer.WriteField("json", fmt.Sprintf("{\"parameter\": %s}", string(paramJSON))); err != nil {
			return
		}
144

145 146 147
		if err = writer.Close(); err != nil {
			return
		}
148

149 150 151 152 153
		_, err = q.RequestWithoutData(http.MethodPost, api,
			map[string]string{util.ContentType: writer.FormDataContentType()}, body, 201)
	} else {
		formData := url.Values{"json": {fmt.Sprintf("{\"parameter\": %s}", string(paramJSON))}}
		payload := strings.NewReader(formData.Encode())
154

155 156 157
		_, err = q.RequestWithoutData(http.MethodPost, api,
			map[string]string{util.ContentType: util.ApplicationForm}, payload, 201)
	}
158 159 160
	return
}

LinuxSuRen's avatar
LinuxSuRen 已提交
161 162 163 164 165
// DisableJob disable a job
func (q *JobClient) DisableJob(jobName string) (err error) {
	path := ParseJobPath(jobName)
	api := fmt.Sprintf("%s/disable", path)

166
	_, err = q.RequestWithoutData(http.MethodPost, api, nil, nil, 200)
LinuxSuRen's avatar
LinuxSuRen 已提交
167 168 169 170 171 172 173 174
	return
}

// EnableJob disable a job
func (q *JobClient) EnableJob(jobName string) (err error) {
	path := ParseJobPath(jobName)
	api := fmt.Sprintf("%s/enable", path)

175
	_, err = q.RequestWithoutData(http.MethodPost, api, nil, nil, 200)
LinuxSuRen's avatar
LinuxSuRen 已提交
176 177 178
	return
}

179
// StopJob stops a job build
LinuxSuRen's avatar
LinuxSuRen 已提交
180
func (q *JobClient) StopJob(jobName string, num int) (err error) {
181
	path := ParseJobPath(jobName)
LinuxSuRen's avatar
LinuxSuRen 已提交
182 183 184 185 186 187 188

	var api string
	if num <= 0 {
		api = fmt.Sprintf("%s/lastBuild/stop", path)
	} else {
		api = fmt.Sprintf("%s/%d/stop", path, num)
	}
LinuxSuRen's avatar
LinuxSuRen 已提交
189

190
	_, err = q.RequestWithoutData(http.MethodPost, api, nil, nil, 200)
LinuxSuRen's avatar
LinuxSuRen 已提交
191 192 193
	return
}

194
// GetJob returns the job info
LinuxSuRen's avatar
LinuxSuRen 已提交
195
func (q *JobClient) GetJob(name string) (job *Job, err error) {
196
	path := ParseJobPath(name)
197
	api := fmt.Sprintf("%s/api/json", path)
LinuxSuRen's avatar
LinuxSuRen 已提交
198

LinuxSuRen's avatar
LinuxSuRen 已提交
199
	err = q.RequestWithData("GET", api, nil, nil, 200, &job)
LinuxSuRen's avatar
LinuxSuRen 已提交
200 201 202
	return
}

203
// GetJobTypeCategories returns all categories of jobs
204 205
func (q *JobClient) GetJobTypeCategories() (jobCategories []JobCategory, err error) {
	var (
206 207
		statusCode int
		data       []byte
208 209
	)

210 211
	if statusCode, data, err = q.Request("GET", "/view/all/itemCategories?depth=3", nil, nil); err == nil {
		if statusCode == 200 {
212 213 214 215 216 217 218
			type innerJobCategories struct {
				Categories []JobCategory
			}
			result := &innerJobCategories{}
			err = json.Unmarshal(data, result)
			jobCategories = result.Categories
		} else {
219
			err = fmt.Errorf("unexpected status code: %d", statusCode)
220 221 222 223 224
		}
	}
	return
}

LinuxSuRen's avatar
LinuxSuRen 已提交
225 226
// GetPipeline return the pipeline object
func (q *JobClient) GetPipeline(name string) (pipeline *Pipeline, err error) {
227
	path := ParseJobPath(name)
LinuxSuRen's avatar
LinuxSuRen 已提交
228 229
	api := fmt.Sprintf("%s/restFul", path)
	err = q.RequestWithData("GET", api, nil, nil, 200, &pipeline)
LinuxSuRen's avatar
LinuxSuRen 已提交
230 231 232
	return
}

LinuxSuRen's avatar
LinuxSuRen 已提交
233 234
// UpdatePipeline updates the pipeline script
func (q *JobClient) UpdatePipeline(name, script string) (err error) {
235 236 237
	formData := url.Values{}
	formData.Add("script", script)

238
	path := ParseJobPath(name)
239
	api := fmt.Sprintf("%s/restFul/update?%s", path, formData.Encode())
LinuxSuRen's avatar
LinuxSuRen 已提交
240

241
	_, err = q.RequestWithoutData(http.MethodPost, api, nil, nil, 200)
LinuxSuRen's avatar
LinuxSuRen 已提交
242 243 244
	return
}

245
// GetHistory returns the build history of a job
LinuxSuRen's avatar
LinuxSuRen 已提交
246
func (q *JobClient) GetHistory(name string) (builds []*JobBuild, err error) {
LinuxSuRen's avatar
LinuxSuRen 已提交
247 248
	var job *Job
	if job, err = q.GetJob(name); err == nil {
LinuxSuRen's avatar
LinuxSuRen 已提交
249 250 251 252 253 254 255
		buildList := job.Builds // only contains basic info

		var build *JobBuild
		for _, buildItem := range buildList {
			build, err = q.GetBuild(name, buildItem.Number)
			if err != nil {
				break
256
			}
LinuxSuRen's avatar
LinuxSuRen 已提交
257
			builds = append(builds, build)
258
		}
LinuxSuRen's avatar
LinuxSuRen 已提交
259 260 261 262
	}
	return
}

LinuxSuRen's avatar
LinuxSuRen 已提交
263
// Log get the log of a job
LinuxSuRen's avatar
LinuxSuRen 已提交
264
func (q *JobClient) Log(jobName string, history int, start int64) (jobLog JobLog, err error) {
265
	path := ParseJobPath(jobName)
LinuxSuRen's avatar
LinuxSuRen 已提交
266 267
	var api string
	if history == -1 {
LinuxSuRen's avatar
LinuxSuRen 已提交
268
		api = fmt.Sprintf("%s%s/lastBuild/logText/progressiveText?start=%d", q.URL, path, start)
LinuxSuRen's avatar
LinuxSuRen 已提交
269
	} else {
LinuxSuRen's avatar
LinuxSuRen 已提交
270
		api = fmt.Sprintf("%s%s/%d/logText/progressiveText?start=%d", q.URL, path, history, start)
LinuxSuRen's avatar
LinuxSuRen 已提交
271
	}
LinuxSuRen's avatar
LinuxSuRen 已提交
272 273 274 275 276 277 278
	var (
		req      *http.Request
		response *http.Response
	)

	req, err = http.NewRequest("GET", api, nil)
	if err == nil {
LinuxSuRen's avatar
LinuxSuRen 已提交
279 280 281
		err = q.AuthHandle(req)
	}
	if err != nil {
LinuxSuRen's avatar
LinuxSuRen 已提交
282 283 284 285 286 287 288 289 290
		return
	}

	client := q.GetClient()
	jobLog = JobLog{
		HasMore:   false,
		Text:      "",
		NextStart: int64(0),
	}
291 292 293 294 295

	if curlCmd, curlErr := http2curl.GetCurlCommand(req); curlErr == nil {
		logger.Debug("HTTP request as curl", zap.String("cmd", curlCmd.String()))
	}

LinuxSuRen's avatar
LinuxSuRen 已提交
296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311
	if response, err = client.Do(req); err == nil {
		code := response.StatusCode
		var data []byte
		data, err = ioutil.ReadAll(response.Body)
		if code == 200 {
			jobLog.Text = string(data)

			if response.Header != nil {
				jobLog.HasMore = strings.ToLower(response.Header.Get("X-More-Data")) == "true"
				jobLog.NextStart, _ = strconv.ParseInt(response.Header.Get("X-Text-Size"), 10, 64)
			}
		}
	}
	return
}

312 313 314 315 316 317
// CreateJobPayload the payload for creating a job
type CreateJobPayload struct {
	Name string `json:"name"`
	Mode string `json:"mode"`
	From string `json:"from"`
}
318

319 320 321
// Create can create a job
func (q *JobClient) Create(jobPayload CreateJobPayload) (err error) {
	playLoadData, _ := json.Marshal(jobPayload)
322 323
	formData := url.Values{
		"json": {string(playLoadData)},
324 325 326
		"name": {jobPayload.Name},
		"mode": {jobPayload.Mode},
		"from": {jobPayload.From},
327 328 329
	}
	payload := strings.NewReader(formData.Encode())

LinuxSuRen's avatar
LinuxSuRen 已提交
330
	var code int
331
	code, err = q.RequestWithoutData(http.MethodPost, "/view/all/createItem",
332
		map[string]string{util.ContentType: util.ApplicationForm}, payload, 200)
LinuxSuRen's avatar
LinuxSuRen 已提交
333 334
	if code == 302 {
		err = nil
335 336 337 338
	}
	return
}

339
// Delete will delete a job by name
340 341
func (q *JobClient) Delete(jobName string) (err error) {
	var (
342
		statusCode int
343
	)
344

345 346
	jobName = ParseJobPath(jobName)
	api := fmt.Sprintf("%s/doDelete", jobName)
347
	header := map[string]string{
LinuxSuRen's avatar
LinuxSuRen 已提交
348
		util.ContentType: util.ApplicationForm,
349 350
	}

351
	if statusCode, _, err = q.Request(http.MethodPost, api, header, nil); err == nil {
LinuxSuRen's avatar
LinuxSuRen 已提交
352
		if statusCode != 200 && statusCode != 302 {
353
			err = fmt.Errorf("unexpected status code: %d", statusCode)
354 355 356 357 358
		}
	}
	return
}

359 360
// GetJobInputActions returns the all pending actions
func (q *JobClient) GetJobInputActions(jobName string, buildID int) (actions []JobInputItem, err error) {
361
	path := ParseJobPath(jobName)
362 363 364 365 366 367 368 369 370 371 372
	err = q.RequestWithData("GET", fmt.Sprintf("%s/%d/wfapi/pendingInputActions", path, buildID), nil, nil, 200, &actions)
	return
}

// JenkinsInputParametersRequest represents the parameters for the Jenkins input request
type JenkinsInputParametersRequest struct {
	Parameter []ParameterDefinition `json:"parameter"`
}

// JobInputSubmit submit the pending input request
func (q *JobClient) JobInputSubmit(jobName, inputID string, buildID int, abort bool, params map[string]string) (err error) {
373
	jobPath := ParseJobPath(jobName)
374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394
	var api string
	if abort {
		api = fmt.Sprintf("%s/%d/input/%s/abort", jobPath, buildID, inputID)
	} else {
		api = fmt.Sprintf("%s/%d/input/%s/proceed", jobPath, buildID, inputID)
	}

	request := JenkinsInputParametersRequest{
		Parameter: make([]ParameterDefinition, 0),
	}

	for k, v := range params {
		request.Parameter = append(request.Parameter, ParameterDefinition{
			Name:  k,
			Value: v,
		})
	}

	paramData, _ := json.Marshal(request)

	api = fmt.Sprintf("%s?json=%s", api, string(paramData))
395
	_, err = q.RequestWithoutData(http.MethodPost, api, nil, nil, 200)
396 397 398 399

	return
}

400 401 402
// ParseJobPath leads with slash
func ParseJobPath(jobName string) (path string) {
	path = jobName
403 404
	if jobName == "" || strings.HasPrefix(jobName, "/job/") ||
		strings.HasPrefix(jobName, "job/") {
405 406 407
		return
	}

408 409 410 411 412 413 414 415
	jobItems := strings.Split(jobName, " ")
	path = ""
	for _, item := range jobItems {
		path = fmt.Sprintf("%s/job/%s", path, item)
	}
	return
}

416
// JobLog holds the log text
LinuxSuRen's avatar
LinuxSuRen 已提交
417 418 419 420 421 422
type JobLog struct {
	HasMore   bool
	NextStart int64
	Text      string
}

423 424 425 426 427 428 429
// JenkinsItem represents the item of Jenkins
type JenkinsItem struct {
	Name        string
	DisplayName string
	URL         string
	Description string
	Type        string
430 431 432 433 434 435 436 437 438

	/** comes from Job */
	Buildable bool
	Building  bool
	InQueue   bool

	/** comes from ParameterizedJob */
	Parameterized bool
	Disabled      bool
LinuxSuRen's avatar
LinuxSuRen 已提交
439
}
LinuxSuRen's avatar
LinuxSuRen 已提交
440

LinuxSuRen's avatar
LinuxSuRen 已提交
441
// Job represents a job
LinuxSuRen's avatar
LinuxSuRen 已提交
442
type Job struct {
443
	Type            string `json:"_class"`
LinuxSuRen's avatar
LinuxSuRen 已提交
444 445 446 447 448 449 450
	Builds          []JobBuild
	Color           string
	ConcurrentBuild bool
	Name            string
	NextBuildNumber int
	URL             string
	Buildable       bool
451 452 453 454

	Property []ParametersDefinitionProperty
}

LinuxSuRen's avatar
LinuxSuRen 已提交
455
// ParametersDefinitionProperty holds the param definition property
456 457 458 459
type ParametersDefinitionProperty struct {
	ParameterDefinitions []ParameterDefinition
}

LinuxSuRen's avatar
LinuxSuRen 已提交
460
// ParameterDefinition holds the parameter definition
461 462 463 464 465
type ParameterDefinition struct {
	Description           string
	Name                  string `json:"name"`
	Type                  string
	Value                 string `json:"value"`
466
	Filepath              string `json:"file"`
467 468 469
	DefaultParameterValue DefaultParameterValue
}

LinuxSuRen's avatar
LinuxSuRen 已提交
470
// DefaultParameterValue represents the default value for param
471 472
type DefaultParameterValue struct {
	Description string
473
	Value       interface{}
LinuxSuRen's avatar
LinuxSuRen 已提交
474 475
}

LinuxSuRen's avatar
LinuxSuRen 已提交
476
// SimpleJobBuild represents a simple job build
LinuxSuRen's avatar
LinuxSuRen 已提交
477
type SimpleJobBuild struct {
LinuxSuRen's avatar
LinuxSuRen 已提交
478 479 480
	Number int
	URL    string
}
LinuxSuRen's avatar
LinuxSuRen 已提交
481

LinuxSuRen's avatar
LinuxSuRen 已提交
482
// JobBuild represents a job build
LinuxSuRen's avatar
LinuxSuRen 已提交
483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499
type JobBuild struct {
	SimpleJobBuild
	Building          bool
	Description       string
	DisplayName       string
	Duration          int64
	EstimatedDuration int64
	FullDisplayName   string
	ID                string
	KeepLog           bool
	QueueID           int
	Result            string
	Timestamp         int64
	PreviousBuild     SimpleJobBuild
	NextBuild         SimpleJobBuild
}

LinuxSuRen's avatar
LinuxSuRen 已提交
500
// Pipeline represents a pipeline
LinuxSuRen's avatar
LinuxSuRen 已提交
501 502 503 504
type Pipeline struct {
	Script  string
	Sandbox bool
}
505

LinuxSuRen's avatar
LinuxSuRen 已提交
506
// JobCategory represents a job category
507 508 509 510 511 512 513 514 515
type JobCategory struct {
	Description string
	ID          string
	Items       []JobCategoryItem
	MinToShow   int
	Name        string
	Order       int
}

LinuxSuRen's avatar
LinuxSuRen 已提交
516
// JobCategoryItem represents a job category item
517 518 519 520
type JobCategoryItem struct {
	Description string
	DisplayName string
	Order       int
521
	Class       string
522
}
523 524 525 526 527 528 529 530 531

// JobInputItem represents a job input action
type JobInputItem struct {
	ID                  string
	AbortURL            string
	Message             string
	ProceedText         string
	ProceedURL          string
	RedirectApprovalURL string
532
	Inputs              []ParameterDefinition
533
}