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  }