job.go 11.8 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
// FileParameterDefinition is the definition for file parameter
const FileParameterDefinition = "FileParameterDefinition"

26
// JobClient is client for operate jobs
LinuxSuRen's avatar
LinuxSuRen 已提交
27 28
type JobClient struct {
	JenkinsCore
LinuxSuRen's avatar
LinuxSuRen 已提交
29 30

	Parent string
LinuxSuRen's avatar
LinuxSuRen 已提交
31 32 33
}

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

// Build trigger a job
LinuxSuRen's avatar
LinuxSuRen 已提交
42
func (q *JobClient) Build(jobName string) (err error) {
43
	path := ParseJobPath(jobName)
44
	_, err = q.RequestWithoutData("POST", fmt.Sprintf("%s/build", path), nil, nil, 201)
LinuxSuRen's avatar
LinuxSuRen 已提交
45 46 47
	return
}

48
// GetBuild get build information of a job
LinuxSuRen's avatar
LinuxSuRen 已提交
49
func (q *JobClient) GetBuild(jobName string, id int) (job *JobBuild, err error) {
50
	path := ParseJobPath(jobName)
LinuxSuRen's avatar
LinuxSuRen 已提交
51 52
	var api string
	if id == -1 {
53
		api = fmt.Sprintf("%s/lastBuild/api/json", path)
LinuxSuRen's avatar
LinuxSuRen 已提交
54
	} else {
55
		api = fmt.Sprintf("%s/%d/api/json", path, id)
LinuxSuRen's avatar
LinuxSuRen 已提交
56
	}
57

LinuxSuRen's avatar
LinuxSuRen 已提交
58
	err = q.RequestWithData("GET", api, nil, nil, 200, &job)
LinuxSuRen's avatar
LinuxSuRen 已提交
59 60 61
	return
}

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

67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
	fileParameters := make([]ParameterDefinition, 0, len(parameters))
	for _, parameter := range parameters {
		if parameter.Type == FileParameterDefinition {
			fileParameters = append(fileParameters, parameter)
		}
	}

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

	// upload file
	for _, parameter := range fileParameters {
		var file *os.File
		file, err = os.Open(parameter.Filepath)
		if err != nil {
			return err
		}
		defer file.Close()

		var fWriter io.Writer
		fWriter, err = writer.CreateFormFile(parameter.Filepath, filepath.Base(parameter.Filepath))
		if err != nil {
			return err
		}
		_, err = io.Copy(fWriter, file)
	}

95 96 97 98 99 100
	var paramJSON []byte
	if len(parameters) == 1 {
		paramJSON, err = json.Marshal(parameters[0])
	} else {
		paramJSON, err = json.Marshal(parameters)
	}
101 102 103
	if err != nil {
		return err
	}
104

105 106 107
	if err = writer.WriteField("json", fmt.Sprintf("{\"parameter\": %s}", string(paramJSON))); err != nil {
		return err
	}
108

109 110
	if err = writer.Close(); err != nil {
		return err
111
	}
112 113 114 115

	_, err = q.RequestWithoutData("POST", api,
		map[string]string{util.ContentType: writer.FormDataContentType()}, body, 201)

116 117 118
	return
}

LinuxSuRen's avatar
LinuxSuRen 已提交
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
// DisableJob disable a job
func (q *JobClient) DisableJob(jobName string) (err error) {
	path := ParseJobPath(jobName)
	api := fmt.Sprintf("%s/disable", path)

	_, err = q.RequestWithoutData("POST", api, nil, nil, 200)
	return
}

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

	_, err = q.RequestWithoutData("POST", api, nil, nil, 200)
	return
}

137
// StopJob stops a job build
LinuxSuRen's avatar
LinuxSuRen 已提交
138
func (q *JobClient) StopJob(jobName string, num int) (err error) {
139
	path := ParseJobPath(jobName)
LinuxSuRen's avatar
LinuxSuRen 已提交
140 141 142 143 144 145 146

	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 已提交
147

148
	_, err = q.RequestWithoutData("POST", api, nil, nil, 200)
LinuxSuRen's avatar
LinuxSuRen 已提交
149 150 151
	return
}

152
// GetJob returns the job info
LinuxSuRen's avatar
LinuxSuRen 已提交
153
func (q *JobClient) GetJob(name string) (job *Job, err error) {
154
	path := ParseJobPath(name)
155
	api := fmt.Sprintf("%s/api/json", path)
LinuxSuRen's avatar
LinuxSuRen 已提交
156

LinuxSuRen's avatar
LinuxSuRen 已提交
157
	err = q.RequestWithData("GET", api, nil, nil, 200, &job)
LinuxSuRen's avatar
LinuxSuRen 已提交
158 159 160
	return
}

161
// GetJobTypeCategories returns all categories of jobs
162 163
func (q *JobClient) GetJobTypeCategories() (jobCategories []JobCategory, err error) {
	var (
164 165
		statusCode int
		data       []byte
166 167
	)

168 169
	if statusCode, data, err = q.Request("GET", "/view/all/itemCategories?depth=3", nil, nil); err == nil {
		if statusCode == 200 {
170 171 172 173 174 175 176
			type innerJobCategories struct {
				Categories []JobCategory
			}
			result := &innerJobCategories{}
			err = json.Unmarshal(data, result)
			jobCategories = result.Categories
		} else {
177
			err = fmt.Errorf("unexpected status code: %d", statusCode)
178 179 180 181 182
		}
	}
	return
}

LinuxSuRen's avatar
LinuxSuRen 已提交
183 184
// GetPipeline return the pipeline object
func (q *JobClient) GetPipeline(name string) (pipeline *Pipeline, err error) {
185
	path := ParseJobPath(name)
LinuxSuRen's avatar
LinuxSuRen 已提交
186 187
	api := fmt.Sprintf("%s/restFul", path)
	err = q.RequestWithData("GET", api, nil, nil, 200, &pipeline)
LinuxSuRen's avatar
LinuxSuRen 已提交
188 189 190
	return
}

LinuxSuRen's avatar
LinuxSuRen 已提交
191 192
// UpdatePipeline updates the pipeline script
func (q *JobClient) UpdatePipeline(name, script string) (err error) {
193 194 195
	formData := url.Values{}
	formData.Add("script", script)

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

199
	_, err = q.RequestWithoutData("POST", api, nil, nil, 200)
LinuxSuRen's avatar
LinuxSuRen 已提交
200 201 202
	return
}

203
// GetHistory returns the build history of a job
LinuxSuRen's avatar
LinuxSuRen 已提交
204
func (q *JobClient) GetHistory(name string) (builds []*JobBuild, err error) {
LinuxSuRen's avatar
LinuxSuRen 已提交
205 206
	var job *Job
	if job, err = q.GetJob(name); err == nil {
LinuxSuRen's avatar
LinuxSuRen 已提交
207 208 209 210 211 212 213
		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
214
			}
LinuxSuRen's avatar
LinuxSuRen 已提交
215
			builds = append(builds, build)
216
		}
LinuxSuRen's avatar
LinuxSuRen 已提交
217 218 219 220
	}
	return
}

LinuxSuRen's avatar
LinuxSuRen 已提交
221
// Log get the log of a job
LinuxSuRen's avatar
LinuxSuRen 已提交
222
func (q *JobClient) Log(jobName string, history int, start int64) (jobLog JobLog, err error) {
223
	path := ParseJobPath(jobName)
LinuxSuRen's avatar
LinuxSuRen 已提交
224 225
	var api string
	if history == -1 {
LinuxSuRen's avatar
LinuxSuRen 已提交
226
		api = fmt.Sprintf("%s%s/lastBuild/logText/progressiveText?start=%d", q.URL, path, start)
LinuxSuRen's avatar
LinuxSuRen 已提交
227
	} else {
LinuxSuRen's avatar
LinuxSuRen 已提交
228
		api = fmt.Sprintf("%s%s/%d/logText/progressiveText?start=%d", q.URL, path, history, start)
LinuxSuRen's avatar
LinuxSuRen 已提交
229
	}
LinuxSuRen's avatar
LinuxSuRen 已提交
230 231 232 233 234 235 236
	var (
		req      *http.Request
		response *http.Response
	)

	req, err = http.NewRequest("GET", api, nil)
	if err == nil {
LinuxSuRen's avatar
LinuxSuRen 已提交
237 238 239
		err = q.AuthHandle(req)
	}
	if err != nil {
LinuxSuRen's avatar
LinuxSuRen 已提交
240 241 242 243 244 245 246 247 248
		return
	}

	client := q.GetClient()
	jobLog = JobLog{
		HasMore:   false,
		Text:      "",
		NextStart: int64(0),
	}
249 250 251 252 253

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

LinuxSuRen's avatar
LinuxSuRen 已提交
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269
	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
}

270 271 272 273 274 275
// CreateJobPayload the payload for creating a job
type CreateJobPayload struct {
	Name string `json:"name"`
	Mode string `json:"mode"`
	From string `json:"from"`
}
276

277 278 279
// Create can create a job
func (q *JobClient) Create(jobPayload CreateJobPayload) (err error) {
	playLoadData, _ := json.Marshal(jobPayload)
280 281
	formData := url.Values{
		"json": {string(playLoadData)},
282 283 284
		"name": {jobPayload.Name},
		"mode": {jobPayload.Mode},
		"from": {jobPayload.From},
285 286 287
	}
	payload := strings.NewReader(formData.Encode())

LinuxSuRen's avatar
LinuxSuRen 已提交
288
	var code int
289 290
	code, err = q.RequestWithoutData("POST", "/view/all/createItem",
		map[string]string{util.ContentType: util.ApplicationForm}, payload, 200)
LinuxSuRen's avatar
LinuxSuRen 已提交
291 292
	if code == 302 {
		err = nil
293 294 295 296
	}
	return
}

297
// Delete will delete a job by name
298 299
func (q *JobClient) Delete(jobName string) (err error) {
	var (
300
		statusCode int
301
	)
302

303 304
	jobName = ParseJobPath(jobName)
	api := fmt.Sprintf("%s/doDelete", jobName)
305
	header := map[string]string{
LinuxSuRen's avatar
LinuxSuRen 已提交
306
		util.ContentType: util.ApplicationForm,
307 308
	}

LinuxSuRen's avatar
LinuxSuRen 已提交
309
	if statusCode, _, err = q.Request("POST", api, header, nil); err == nil {
LinuxSuRen's avatar
LinuxSuRen 已提交
310
		if statusCode != 200 && statusCode != 302 {
311
			err = fmt.Errorf("unexpected status code: %d", statusCode)
312 313 314 315 316
		}
	}
	return
}

317 318
// GetJobInputActions returns the all pending actions
func (q *JobClient) GetJobInputActions(jobName string, buildID int) (actions []JobInputItem, err error) {
319
	path := ParseJobPath(jobName)
320 321 322 323 324 325 326 327 328 329 330
	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) {
331
	jobPath := ParseJobPath(jobName)
332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
	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))
	_, err = q.RequestWithoutData("POST", api, nil, nil, 200)

	return
}

358 359 360
// ParseJobPath leads with slash
func ParseJobPath(jobName string) (path string) {
	path = jobName
361 362
	if jobName == "" || strings.HasPrefix(jobName, "/job/") ||
		strings.HasPrefix(jobName, "job/") {
363 364 365
		return
	}

366 367 368 369 370 371 372 373
	jobItems := strings.Split(jobName, " ")
	path = ""
	for _, item := range jobItems {
		path = fmt.Sprintf("%s/job/%s", path, item)
	}
	return
}

374
// JobLog holds the log text
LinuxSuRen's avatar
LinuxSuRen 已提交
375 376 377 378 379 380
type JobLog struct {
	HasMore   bool
	NextStart int64
	Text      string
}

381 382 383 384 385 386 387
// JenkinsItem represents the item of Jenkins
type JenkinsItem struct {
	Name        string
	DisplayName string
	URL         string
	Description string
	Type        string
388 389 390 391 392 393 394 395 396

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

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

LinuxSuRen's avatar
LinuxSuRen 已提交
399
// Job represents a job
LinuxSuRen's avatar
LinuxSuRen 已提交
400
type Job struct {
401
	Type            string `json:"_class"`
LinuxSuRen's avatar
LinuxSuRen 已提交
402 403 404 405 406 407 408
	Builds          []JobBuild
	Color           string
	ConcurrentBuild bool
	Name            string
	NextBuildNumber int
	URL             string
	Buildable       bool
409 410 411 412

	Property []ParametersDefinitionProperty
}

LinuxSuRen's avatar
LinuxSuRen 已提交
413
// ParametersDefinitionProperty holds the param definition property
414 415 416 417
type ParametersDefinitionProperty struct {
	ParameterDefinitions []ParameterDefinition
}

LinuxSuRen's avatar
LinuxSuRen 已提交
418
// ParameterDefinition holds the parameter definition
419 420 421 422 423
type ParameterDefinition struct {
	Description           string
	Name                  string `json:"name"`
	Type                  string
	Value                 string `json:"value"`
424
	Filepath              string `json:"file"`
425 426 427
	DefaultParameterValue DefaultParameterValue
}

LinuxSuRen's avatar
LinuxSuRen 已提交
428
// DefaultParameterValue represents the default value for param
429 430
type DefaultParameterValue struct {
	Description string
431
	Value       interface{}
LinuxSuRen's avatar
LinuxSuRen 已提交
432 433
}

LinuxSuRen's avatar
LinuxSuRen 已提交
434
// SimpleJobBuild represents a simple job build
LinuxSuRen's avatar
LinuxSuRen 已提交
435
type SimpleJobBuild struct {
LinuxSuRen's avatar
LinuxSuRen 已提交
436 437 438
	Number int
	URL    string
}
LinuxSuRen's avatar
LinuxSuRen 已提交
439

LinuxSuRen's avatar
LinuxSuRen 已提交
440
// JobBuild represents a job build
LinuxSuRen's avatar
LinuxSuRen 已提交
441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457
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 已提交
458
// Pipeline represents a pipeline
LinuxSuRen's avatar
LinuxSuRen 已提交
459 460 461 462
type Pipeline struct {
	Script  string
	Sandbox bool
}
463

LinuxSuRen's avatar
LinuxSuRen 已提交
464
// JobCategory represents a job category
465 466 467 468 469 470 471 472 473
type JobCategory struct {
	Description string
	ID          string
	Items       []JobCategoryItem
	MinToShow   int
	Name        string
	Order       int
}

LinuxSuRen's avatar
LinuxSuRen 已提交
474
// JobCategoryItem represents a job category item
475 476 477 478
type JobCategoryItem struct {
	Description string
	DisplayName string
	Order       int
479
	Class       string
480
}
481 482 483 484 485 486 487 488 489

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