sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/jenkins/jenkins.go (about) 1 /* 2 Copyright 2016 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package jenkins 18 19 import ( 20 "crypto/tls" 21 "encoding/json" 22 "errors" 23 "fmt" 24 stdio "io" 25 "net/http" 26 "net/url" 27 "strings" 28 "sync" 29 "time" 30 31 "github.com/sirupsen/logrus" 32 wait "k8s.io/apimachinery/pkg/util/wait" 33 34 prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1" 35 "sigs.k8s.io/prow/pkg/pjutil" 36 "sigs.k8s.io/prow/pkg/pod-utils/downwardapi" 37 ) 38 39 const ( 40 // Maximum retries for a request to Jenkins. 41 // Retries on transport failures and 500s. 42 maxRetries = 5 43 // Backoff delay used after a request retry. 44 // Doubles on every retry. 45 retryDelay = 100 * time.Millisecond 46 // Key for unique build number across Jenkins builds. 47 // Used for allowing tools to group artifacts in GCS. 48 statusBuildID = "BUILD_ID" 49 // Key for unique build number across Jenkins builds. 50 // Used for correlating Jenkins builds to ProwJobs. 51 prowJobID = "PROW_JOB_ID" 52 ) 53 54 const ( 55 success = "SUCCESS" 56 failure = "FAILURE" 57 unstable = "UNSTABLE" 58 aborted = "ABORTED" 59 ) 60 61 // NotFoundError is returned by the Jenkins client when 62 // a job does not exist in Jenkins. 63 type NotFoundError struct { 64 e error 65 } 66 67 func (e NotFoundError) Error() string { 68 return e.e.Error() 69 } 70 71 // NewNotFoundError creates a new NotFoundError. 72 func NewNotFoundError(e error) NotFoundError { 73 return NotFoundError{e: e} 74 } 75 76 // Action holds a list of parameters 77 type Action struct { 78 Parameters []Parameter `json:"parameters"` 79 } 80 81 // Parameter configures some aspect of the job. 82 type Parameter struct { 83 Name string `json:"name"` 84 // This needs to be an interface so we won't clobber 85 // json unmarshaling when the Jenkins job has more 86 // parameter types than strings. 87 Value interface{} `json:"value"` 88 } 89 90 // Build holds information about an instance of a jenkins job. 91 type Build struct { 92 Actions []Action `json:"actions"` 93 Task struct { 94 // Used for tracking unscheduled builds for jobs. 95 Name string `json:"name"` 96 } `json:"task"` 97 Number int `json:"number"` 98 Result *string `json:"result"` 99 enqueued bool 100 } 101 102 // ParameterDefinition holds information about a build parameter 103 type ParameterDefinition struct { 104 DefaultParameterValue Parameter `json:"defaultParameterValue,omitempty"` 105 Description string `json:"description"` 106 Name string `json:"name"` 107 Type string `json:"type"` 108 } 109 110 // JobProperty is a generic Jenkins job property, 111 // but ParameterDefinitions is specific to Build Parameters 112 type JobProperty struct { 113 Class string `json:"_class"` 114 ParameterDefinitions []ParameterDefinition `json:"parameterDefinitions,omitempty"` 115 } 116 117 // JobInfo holds infofmation about a job from $job/api/json endpoint 118 type JobInfo struct { 119 Builds []Build `json:"builds"` 120 LastBuild *Build `json:"lastBuild,omitempty"` 121 Property []JobProperty `json:"property"` 122 } 123 124 // IsRunning means the job started but has not finished. 125 func (jb *Build) IsRunning() bool { 126 return jb.Result == nil 127 } 128 129 // IsSuccess means the job passed 130 func (jb *Build) IsSuccess() bool { 131 return jb.Result != nil && *jb.Result == success 132 } 133 134 // IsFailure means the job completed with problems. 135 func (jb *Build) IsFailure() bool { 136 return jb.Result != nil && (*jb.Result == failure || *jb.Result == unstable) 137 } 138 139 // IsAborted means something stopped the job before it could finish. 140 func (jb *Build) IsAborted() bool { 141 return jb.Result != nil && *jb.Result == aborted 142 } 143 144 // IsEnqueued means the job has created but has not started. 145 func (jb *Build) IsEnqueued() bool { 146 return jb.enqueued 147 } 148 149 // ProwJobID extracts the ProwJob identifier for the 150 // Jenkins build in order to correlate the build with 151 // a ProwJob. If the build has an empty PROW_JOB_ID 152 // it didn't start by prow. 153 func (jb *Build) ProwJobID() string { 154 for _, action := range jb.Actions { 155 for _, p := range action.Parameters { 156 if p.Name == prowJobID { 157 value, ok := p.Value.(string) 158 if !ok { 159 logrus.Errorf("Cannot determine %s value for %#v", p.Name, jb) 160 continue 161 } 162 return value 163 } 164 } 165 } 166 return "" 167 } 168 169 // BuildID extracts the build identifier used for 170 // placing and discovering build artifacts. 171 // This identifier can either originate from tot 172 // or the snowflake library, depending on how the 173 // Jenkins operator is configured to run. 174 // We return an empty string if we are dealing with 175 // a build that does not have the ProwJobID set 176 // explicitly, as in that case the Jenkins build has 177 // not started by prow. 178 func (jb *Build) BuildID() string { 179 var buildID string 180 hasProwJobID := false 181 for _, action := range jb.Actions { 182 for _, p := range action.Parameters { 183 hasProwJobID = hasProwJobID || p.Name == prowJobID 184 if p.Name == statusBuildID { 185 value, ok := p.Value.(string) 186 if !ok { 187 logrus.Errorf("Cannot determine %s value for %#v", p.Name, jb) 188 continue 189 } 190 buildID = value 191 } 192 } 193 } 194 195 if !hasProwJobID { 196 return "" 197 } 198 return buildID 199 } 200 201 // Client can interact with jenkins to create/manage builds. 202 type Client struct { 203 // If logger is non-nil, log all method calls with it. 204 logger *logrus.Entry 205 dryRun bool 206 207 client *http.Client 208 baseURL string 209 authConfig *AuthConfig 210 211 metrics *ClientMetrics 212 } 213 214 // AuthConfig configures how we auth with Jenkins. 215 // Only one of the fields will be non-nil. 216 type AuthConfig struct { 217 // Basic is used for doing basic auth with Jenkins. 218 Basic *BasicAuthConfig 219 // BearerToken is used for doing oauth-based authentication 220 // with Jenkins. Works ootb with the Openshift Jenkins image. 221 BearerToken *BearerTokenAuthConfig 222 // CSRFProtect ensures the client will acquire a CSRF protection 223 // token from Jenkins to use it in mutating requests. Required 224 // for masters that prevent cross site request forgery exploits. 225 CSRFProtect bool 226 // csrfToken is the token acquired from Jenkins for CSRF protection. 227 // Needs to be used as the header value in subsequent mutating requests. 228 csrfToken string 229 // csrfRequestField is a key acquired from Jenkins for CSRF protection. 230 // Needs to be used as the header key in subsequent mutating requests. 231 csrfRequestField string 232 } 233 234 // BasicAuthConfig authenticates with jenkins using user/pass. 235 type BasicAuthConfig struct { 236 User string 237 GetToken func() []byte 238 } 239 240 // BearerTokenAuthConfig authenticates jenkins using an oauth bearer token. 241 type BearerTokenAuthConfig struct { 242 GetToken func() []byte 243 } 244 245 // BuildQueryParams is used to query Jenkins for running and enqueued builds 246 type BuildQueryParams struct { 247 JobName string 248 ProwJobID string 249 } 250 251 // NewClient instantiates a client with provided values. 252 // 253 // url: the jenkins master to connect to. 254 // dryRun: mutating calls such as starting/aborting a build will be skipped. 255 // tlsConfig: configures client transport if set, may be nil. 256 // authConfig: configures the client to connect to Jenkins via basic auth/bearer token 257 // 258 // and optionally enables csrf protection 259 // 260 // logger: creates a standard logger if nil. 261 // metrics: gathers prometheus metrics for the Jenkins client if set. 262 func NewClient( 263 url string, 264 dryRun bool, 265 tlsConfig *tls.Config, 266 authConfig *AuthConfig, 267 logger *logrus.Entry, 268 metrics *ClientMetrics, 269 ) (*Client, error) { 270 if logger == nil { 271 logger = logrus.NewEntry(logrus.StandardLogger()) 272 } 273 c := &Client{ 274 logger: logger.WithField("client", "jenkins"), 275 dryRun: dryRun, 276 baseURL: url, 277 authConfig: authConfig, 278 client: &http.Client{ 279 Timeout: 30 * time.Second, 280 }, 281 metrics: metrics, 282 } 283 if tlsConfig != nil { 284 c.client.Transport = &http.Transport{TLSClientConfig: tlsConfig} 285 } 286 if c.authConfig.CSRFProtect { 287 if err := c.CrumbRequest(); err != nil { 288 return nil, fmt.Errorf("cannot get Jenkins crumb: %w", err) 289 } 290 } 291 return c, nil 292 } 293 294 // CrumbRequest requests a CSRF protection token from Jenkins to 295 // use it in subsequent requests. Required for Jenkins masters that 296 // prevent cross site request forgery exploits. 297 func (c *Client) CrumbRequest() error { 298 if c.authConfig.csrfToken != "" && c.authConfig.csrfRequestField != "" { 299 return nil 300 } 301 c.logger.Debug("CrumbRequest") 302 data, err := c.GetSkipMetrics("/crumbIssuer/api/json") 303 if err != nil { 304 return err 305 } 306 crumbResp := struct { 307 Crumb string `json:"crumb"` 308 CrumbRequestField string `json:"crumbRequestField"` 309 }{} 310 if err := json.Unmarshal(data, &crumbResp); err != nil { 311 return fmt.Errorf("cannot unmarshal crumb response: %w", err) 312 } 313 c.authConfig.csrfToken = crumbResp.Crumb 314 c.authConfig.csrfRequestField = crumbResp.CrumbRequestField 315 return nil 316 } 317 318 // measure records metrics about the provided method, path, and code. 319 // start needs to be recorded before doing the request. 320 func (c *Client) measure(method, path string, code int, start time.Time) { 321 if c.metrics == nil { 322 return 323 } 324 c.metrics.RequestLatency.WithLabelValues(method, path).Observe(time.Since(start).Seconds()) 325 c.metrics.Requests.WithLabelValues(method, path, fmt.Sprintf("%d", code)).Inc() 326 } 327 328 // GetSkipMetrics fetches the data found in the provided path. It returns the 329 // content of the response or any errors that occurred during the request or 330 // http errors. Metrics will not be gathered for this request. 331 func (c *Client) GetSkipMetrics(path string) ([]byte, error) { 332 resp, err := c.request(http.MethodGet, path, nil, false) 333 if err != nil { 334 return nil, err 335 } 336 return readResp(resp) 337 } 338 339 // Get fetches the data found in the provided path. It returns the 340 // content of the response or any errors that occurred during the 341 // request or http errors. 342 func (c *Client) Get(path string) ([]byte, error) { 343 resp, err := c.request(http.MethodGet, path, nil, true) 344 if err != nil { 345 return nil, err 346 } 347 return readResp(resp) 348 } 349 350 func readResp(resp *http.Response) ([]byte, error) { 351 defer resp.Body.Close() 352 353 if resp.StatusCode == 404 { 354 return nil, NewNotFoundError(errors.New(resp.Status)) 355 } 356 if resp.StatusCode < 200 || resp.StatusCode >= 300 { 357 return nil, fmt.Errorf("response not 2XX: %s", resp.Status) 358 } 359 buf, err := stdio.ReadAll(resp.Body) 360 if err != nil { 361 return nil, err 362 } 363 return buf, nil 364 } 365 366 // request executes a request with the provided method and path. 367 // It retries on transport failures and 500s. measure is provided 368 // to enable or disable gathering metrics for specific requests 369 // to avoid high-cardinality metrics. 370 func (c *Client) request(method, path string, params url.Values, measure bool) (*http.Response, error) { 371 var resp *http.Response 372 var err error 373 backoff := retryDelay 374 375 urlPath := fmt.Sprintf("%s%s", c.baseURL, path) 376 if params != nil { 377 urlPath = fmt.Sprintf("%s?%s", urlPath, params.Encode()) 378 } 379 380 start := time.Now() 381 for retries := 0; retries < maxRetries; retries++ { 382 resp, err = c.doRequest(method, urlPath) 383 if err == nil && resp.StatusCode < 500 { 384 break 385 } else if err == nil && retries+1 < maxRetries { 386 resp.Body.Close() 387 } 388 // Capture the retry in a metric. 389 if measure && c.metrics != nil { 390 c.metrics.RequestRetries.Inc() 391 } 392 time.Sleep(backoff) 393 backoff *= 2 394 } 395 if measure && resp != nil { 396 c.measure(method, path, resp.StatusCode, start) 397 } 398 return resp, err 399 } 400 401 // doRequest executes a request with the provided method and path 402 // exactly once. It sets up authentication if the jenkins client 403 // is configured accordingly. It's up to callers of this function 404 // to build retries and error handling. 405 func (c *Client) doRequest(method, path string) (*http.Response, error) { 406 req, err := http.NewRequest(method, path, nil) 407 if err != nil { 408 return nil, err 409 } 410 if c.authConfig != nil { 411 if c.authConfig.Basic != nil { 412 req.SetBasicAuth(c.authConfig.Basic.User, string(c.authConfig.Basic.GetToken())) 413 } 414 if c.authConfig.BearerToken != nil { 415 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.authConfig.BearerToken.GetToken())) 416 } 417 if c.authConfig.CSRFProtect && c.authConfig.csrfRequestField != "" && c.authConfig.csrfToken != "" { 418 req.Header.Set(c.authConfig.csrfRequestField, c.authConfig.csrfToken) 419 } 420 } 421 return c.client.Do(req) 422 } 423 424 // getJobName generates the correct job name for this job type 425 func getJobName(spec *prowapi.ProwJobSpec) string { 426 jobName := spec.Job 427 if strings.Contains(jobName, "/") { 428 jobParts := strings.Split(strings.Trim(jobName, "/"), "/") 429 jobName = strings.Join(jobParts, "/job/") 430 } 431 432 if spec.JenkinsSpec != nil && spec.JenkinsSpec.GitHubBranchSourceJob && spec.Refs != nil { 433 if len(spec.Refs.Pulls) > 0 { 434 return fmt.Sprintf("%s/view/change-requests/job/PR-%d", jobName, spec.Refs.Pulls[0].Number) 435 } 436 437 return fmt.Sprintf("%s/job/%s", jobName, spec.Refs.BaseRef) 438 } 439 440 return jobName 441 } 442 443 // getJobInfoPath builds an approriate path to use for this Jenkins Job to get the job information 444 func getJobInfoPath(spec *prowapi.ProwJobSpec) string { 445 jenkinsJobName := getJobName(spec) 446 jenkinsPath := fmt.Sprintf("/job/%s/api/json", jenkinsJobName) 447 448 return jenkinsPath 449 } 450 451 // getBuildPath builds a path to trigger a regular build for this job 452 func getBuildPath(spec *prowapi.ProwJobSpec) string { 453 jenkinsJobName := getJobName(spec) 454 jenkinsPath := fmt.Sprintf("/job/%s/build", jenkinsJobName) 455 456 return jenkinsPath 457 } 458 459 // getBuildWithParametersPath builds a path to trigger a build with parameters for this job 460 func getBuildWithParametersPath(spec *prowapi.ProwJobSpec) string { 461 jenkinsJobName := getJobName(spec) 462 jenkinsPath := fmt.Sprintf("/job/%s/buildWithParameters", jenkinsJobName) 463 464 return jenkinsPath 465 } 466 467 // GetJobInfo retrieves Jenkins job information 468 func (c *Client) GetJobInfo(spec *prowapi.ProwJobSpec) (*JobInfo, error) { 469 path := getJobInfoPath(spec) 470 c.logger.Debugf("getJobInfoPath: %s", path) 471 472 data, err := c.Get(path) 473 474 if err != nil { 475 c.logger.Errorf("Failed to get job info: %v", err) 476 return nil, err 477 } 478 479 var jobInfo JobInfo 480 481 if err := json.Unmarshal(data, &jobInfo); err != nil { 482 return nil, fmt.Errorf("Cannot unmarshal job info from API: %w", err) 483 } 484 485 c.logger.Tracef("JobInfo: %+v", jobInfo) 486 487 return &jobInfo, nil 488 } 489 490 // JobParameterized tells us if the Jenkins job for this ProwJob is parameterized 491 func (c *Client) JobParameterized(jobInfo *JobInfo) bool { 492 for _, prop := range jobInfo.Property { 493 if prop.ParameterDefinitions != nil && len(prop.ParameterDefinitions) > 0 { 494 return true 495 } 496 } 497 498 return false 499 } 500 501 // EnsureBuildableJob attempts to detect a job that hasn't yet ran and populated 502 // its parameters. If detected, it tries to run a build until the job parameters 503 // are processed, then it aborts the build. 504 func (c *Client) EnsureBuildableJob(spec *prowapi.ProwJobSpec) error { 505 var jobInfo *JobInfo 506 507 // wait at most 20 seconds for the job to appear 508 getJobInfoBackoff := wait.Backoff{ 509 Duration: time.Duration(10) * time.Second, 510 Factor: 1, 511 Jitter: 0, 512 Steps: 2, 513 } 514 515 getJobErr := wait.ExponentialBackoff(getJobInfoBackoff, func() (bool, error) { 516 var jobErr error 517 jobInfo, jobErr = c.GetJobInfo(spec) 518 519 if jobErr != nil && !strings.Contains(strings.ToLower(jobErr.Error()), "404 not found") { 520 return false, jobErr 521 } 522 523 return jobInfo != nil, nil 524 }) 525 526 if getJobErr != nil { 527 return fmt.Errorf("Job %v does not exist", spec.Job) 528 } 529 530 isParameterized := c.JobParameterized(jobInfo) 531 532 c.logger.Tracef("JobHasParameters: %v", isParameterized) 533 534 if isParameterized || len(jobInfo.Builds) > 0 { 535 return nil 536 } 537 538 buildErr := c.LaunchBuild(spec, nil) 539 540 if buildErr != nil { 541 return buildErr 542 } 543 544 backoff := wait.Backoff{ 545 Duration: time.Duration(5) * time.Second, 546 Factor: 1, 547 Jitter: 1, 548 Steps: 10, 549 } 550 551 return wait.ExponentialBackoff(backoff, func() (bool, error) { 552 c.logger.Debugf("Waiting for job %v to become parameterized", spec.Job) 553 554 jobInfo, _ := c.GetJobInfo(spec) 555 isParameterized := false 556 557 if jobInfo != nil { 558 isParameterized = c.JobParameterized(jobInfo) 559 560 if isParameterized && jobInfo.LastBuild != nil { 561 c.logger.Debugf("Job %v is now parameterized, aborting the build", spec.Job) 562 err := c.Abort(getJobName(spec), jobInfo.LastBuild) 563 564 if err != nil { 565 c.logger.Infof("Couldn't abort build #%v for job %v: %v", jobInfo.LastBuild.Number, spec.Job, err) 566 } 567 } 568 } 569 570 // don't stop on (possibly) intermittent errors 571 return isParameterized, nil 572 }) 573 } 574 575 // LaunchBuild launches a regular or parameterized Jenkins build, depending on 576 // whether or not we have `params` to POST 577 func (c *Client) LaunchBuild(spec *prowapi.ProwJobSpec, params url.Values) error { 578 var path string 579 580 if params != nil { 581 path = getBuildWithParametersPath(spec) 582 } else { 583 path = getBuildPath(spec) 584 } 585 586 c.logger.Debugf("getBuildPath/getBuildWithParametersPath: %s", path) 587 588 resp, err := c.request(http.MethodPost, path, params, true) 589 590 if err != nil { 591 return err 592 } 593 594 defer resp.Body.Close() 595 596 if resp.StatusCode != 201 { 597 return fmt.Errorf("response not 201: %s", resp.Status) 598 } 599 600 return nil 601 } 602 603 // Build triggers a Jenkins build for the provided ProwJob. The name of 604 // the ProwJob is going to be used as the Prow Job ID parameter that will 605 // help us track the build before it's scheduled by Jenkins. 606 func (c *Client) Build(pj *prowapi.ProwJob, buildID string) error { 607 c.logger.WithFields(pjutil.ProwJobFields(pj)).Info("Build") 608 return c.BuildFromSpec(&pj.Spec, buildID, pj.ObjectMeta.Name) 609 } 610 611 // BuildFromSpec triggers a Jenkins build for the provided ProwJobSpec. 612 // prowJobID helps us track the build before it's scheduled by Jenkins. 613 func (c *Client) BuildFromSpec(spec *prowapi.ProwJobSpec, buildID, prowJobID string) error { 614 if c.dryRun { 615 return nil 616 } 617 env, err := downwardapi.EnvForSpec(downwardapi.NewJobSpec(*spec, buildID, prowJobID)) 618 if err != nil { 619 return err 620 } 621 params := url.Values{} 622 for key, value := range env { 623 params.Set(key, value) 624 } 625 626 if err := c.EnsureBuildableJob(spec); err != nil { 627 return fmt.Errorf("Job %v cannot be build: %w", spec.Job, err) 628 } 629 630 return c.LaunchBuild(spec, params) 631 } 632 633 // ListBuilds returns a list of all Jenkins builds for the 634 // provided jobs (both scheduled and enqueued). 635 func (c *Client) ListBuilds(jobs []BuildQueryParams) (map[string]Build, error) { 636 // Get queued builds. 637 jenkinsBuilds, err := c.GetEnqueuedBuilds(jobs) 638 if err != nil { 639 return nil, err 640 } 641 642 buildChan := make(chan map[string]Build, len(jobs)) 643 errChan := make(chan error, len(jobs)) 644 wg := &sync.WaitGroup{} 645 wg.Add(len(jobs)) 646 647 // Get all running builds for all provided jobs. 648 for _, job := range jobs { 649 // Start a goroutine per list 650 go func(job string) { 651 defer wg.Done() 652 653 builds, err := c.GetBuilds(job) 654 if err != nil { 655 errChan <- err 656 } else { 657 buildChan <- builds 658 } 659 }(job.JobName) 660 } 661 wg.Wait() 662 663 close(buildChan) 664 close(errChan) 665 666 for err := range errChan { 667 if err != nil { 668 return nil, err 669 } 670 } 671 672 for builds := range buildChan { 673 for id, build := range builds { 674 jenkinsBuilds[id] = build 675 } 676 } 677 678 return jenkinsBuilds, nil 679 } 680 681 // GetEnqueuedBuilds lists all enqueued builds for the provided jobs. 682 func (c *Client) GetEnqueuedBuilds(jobs []BuildQueryParams) (map[string]Build, error) { 683 c.logger.Debug("GetEnqueuedBuilds") 684 685 data, err := c.Get("/queue/api/json?tree=items[task[name],actions[parameters[name,value]]]") 686 if err != nil { 687 return nil, fmt.Errorf("cannot list builds from the queue: %w", err) 688 } 689 page := struct { 690 QueuedBuilds []Build `json:"items"` 691 }{} 692 if err := json.Unmarshal(data, &page); err != nil { 693 return nil, fmt.Errorf("cannot unmarshal builds from the queue: %w", err) 694 } 695 jenkinsBuilds := make(map[string]Build) 696 for _, jb := range page.QueuedBuilds { 697 prowJobID := jb.ProwJobID() 698 // Ignore builds with missing buildID parameters. 699 if prowJobID == "" { 700 continue 701 } 702 // Ignore builds for jobs we didn't ask for. 703 var exists bool 704 for _, job := range jobs { 705 if prowJobID == job.ProwJobID { 706 exists = true 707 break 708 } 709 } 710 if !exists { 711 continue 712 } 713 jb.enqueued = true 714 jenkinsBuilds[prowJobID] = jb 715 } 716 return jenkinsBuilds, nil 717 } 718 719 // GetBuilds lists all scheduled builds for the provided job. 720 // In newer Jenkins versions, this also includes enqueued 721 // builds (tested in 2.73.2). 722 func (c *Client) GetBuilds(job string) (map[string]Build, error) { 723 c.logger.Debugf("GetBuilds(%v)", job) 724 725 data, err := c.Get(fmt.Sprintf("/job/%s/api/json?tree=builds[number,result,actions[parameters[name,value]]]", job)) 726 if err != nil { 727 // Ignore 404s so we will not block processing the rest of the jobs. 728 if _, isNotFound := err.(NotFoundError); isNotFound { 729 c.logger.WithError(err).Warnf("Cannot list builds for job %q", job) 730 return nil, nil 731 } 732 return nil, fmt.Errorf("cannot list builds for job %q: %w", job, err) 733 } 734 page := struct { 735 Builds []Build `json:"builds"` 736 }{} 737 if err := json.Unmarshal(data, &page); err != nil { 738 return nil, fmt.Errorf("cannot unmarshal builds for job %q: %w", job, err) 739 } 740 jenkinsBuilds := make(map[string]Build) 741 for _, jb := range page.Builds { 742 prowJobID := jb.ProwJobID() 743 // Ignore builds with missing buildID parameters. 744 if prowJobID == "" { 745 continue 746 } 747 jenkinsBuilds[prowJobID] = jb 748 } 749 return jenkinsBuilds, nil 750 } 751 752 // Abort aborts the provided Jenkins build for job. 753 func (c *Client) Abort(job string, build *Build) error { 754 c.logger.Debugf("Abort(%v %v)", job, build.Number) 755 if c.dryRun { 756 return nil 757 } 758 resp, err := c.request(http.MethodPost, fmt.Sprintf("/job/%s/%d/stop", job, build.Number), nil, false) 759 if err != nil { 760 return err 761 } 762 defer resp.Body.Close() 763 if resp.StatusCode < 200 || resp.StatusCode >= 300 { 764 return fmt.Errorf("response not 2XX: %s", resp.Status) 765 } 766 return nil 767 }