github.com/shashidharatd/test-infra@v0.0.0-20171006011030-71304e1ca560/prow/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  	"encoding/json"
    21  	"fmt"
    22  	"io/ioutil"
    23  	"net/http"
    24  	"net/url"
    25  	"strings"
    26  	"time"
    27  
    28  	"k8s.io/test-infra/prow/kube"
    29  	"k8s.io/test-infra/prow/pjutil"
    30  )
    31  
    32  const (
    33  	maxRetries = 5
    34  	retryDelay = 100 * time.Millisecond
    35  	buildID    = "buildId"
    36  )
    37  
    38  const (
    39  	Succeess = "SUCCESS"
    40  	Failure  = "FAILURE"
    41  	Aborted  = "ABORTED"
    42  )
    43  
    44  type Logger interface {
    45  	Debugf(s string, v ...interface{})
    46  }
    47  
    48  type JenkinsBuild struct {
    49  	Actions []struct {
    50  		Parameters []struct {
    51  			Name string `json:"name"`
    52  			// This needs to be an interface so we won't clobber
    53  			// json unmarshaling when the Jenkins job has more
    54  			// parameter types than strings.
    55  			Value interface{} `json:"value"`
    56  		} `json:"parameters"`
    57  	} `json:"actions"`
    58  	Task struct {
    59  		// Used for tracking unscheduled builds for jobs.
    60  		Name string `json:"name"`
    61  	} `json:"task"`
    62  	Number   int     `json:"number"`
    63  	Result   *string `json:"result"`
    64  	enqueued bool
    65  }
    66  
    67  func (jb *JenkinsBuild) IsRunning() bool {
    68  	return jb.Result == nil
    69  }
    70  
    71  func (jb *JenkinsBuild) IsSuccess() bool {
    72  	return jb.Result != nil && *jb.Result == Succeess
    73  }
    74  
    75  func (jb *JenkinsBuild) IsFailure() bool {
    76  	return jb.Result != nil && (*jb.Result == Failure || *jb.Result == Aborted)
    77  }
    78  
    79  func (jb *JenkinsBuild) IsEnqueued() bool {
    80  	return jb.enqueued
    81  }
    82  
    83  func (jb *JenkinsBuild) BuildID() string {
    84  	for _, action := range jb.Actions {
    85  		for _, p := range action.Parameters {
    86  			if p.Name == buildID {
    87  				// This is not safe as far as Go is concerned. Consider
    88  				// stop using Jenkins if this ever breaks.
    89  				return p.Value.(string)
    90  			}
    91  		}
    92  	}
    93  	return ""
    94  }
    95  
    96  type Client struct {
    97  	// If Logger is non-nil, log all method calls with it.
    98  	Logger Logger
    99  
   100  	client     *http.Client
   101  	baseURL    string
   102  	authConfig *AuthConfig
   103  }
   104  
   105  // AuthConfig configures how we auth with Jenkins.
   106  // Only one of the fields will be non-nil.
   107  type AuthConfig struct {
   108  	Basic       *BasicAuthConfig
   109  	BearerToken *BearerTokenAuthConfig
   110  }
   111  
   112  type BasicAuthConfig struct {
   113  	User  string
   114  	Token string
   115  }
   116  
   117  type BearerTokenAuthConfig struct {
   118  	Token string
   119  }
   120  
   121  func NewClient(url string, authConfig *AuthConfig) *Client {
   122  	return &Client{
   123  		baseURL:    url,
   124  		authConfig: authConfig,
   125  		client:     &http.Client{},
   126  	}
   127  }
   128  
   129  func (c *Client) log(methodName string, args ...interface{}) {
   130  	if c.Logger == nil {
   131  		return
   132  	}
   133  	var as []string
   134  	for _, arg := range args {
   135  		as = append(as, fmt.Sprintf("%v", arg))
   136  	}
   137  	c.Logger.Debugf("%s(%s)", methodName, strings.Join(as, ", "))
   138  }
   139  
   140  func (c *Client) get(path string) ([]byte, error) {
   141  	resp, err := c.request(http.MethodGet, path)
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  	defer resp.Body.Close()
   146  	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
   147  		return nil, fmt.Errorf("response not 2XX: %s", resp.Status)
   148  	}
   149  	buf, err := ioutil.ReadAll(resp.Body)
   150  	if err != nil {
   151  		return nil, err
   152  	}
   153  	return buf, nil
   154  }
   155  
   156  // Retry on transport failures and 500s.
   157  func (c *Client) request(method, path string) (*http.Response, error) {
   158  	var resp *http.Response
   159  	var err error
   160  	backoff := retryDelay
   161  	for retries := 0; retries < maxRetries; retries++ {
   162  		resp, err = c.doRequest(method, path)
   163  		if err == nil && resp.StatusCode < 500 {
   164  			break
   165  		} else if err == nil && retries+1 < maxRetries {
   166  			resp.Body.Close()
   167  		}
   168  
   169  		time.Sleep(backoff)
   170  		backoff *= 2
   171  	}
   172  	return resp, err
   173  }
   174  
   175  func (c *Client) doRequest(method, path string) (*http.Response, error) {
   176  	req, err := http.NewRequest(method, path, nil)
   177  	if err != nil {
   178  		return nil, err
   179  	}
   180  	if c.authConfig.Basic != nil {
   181  		req.SetBasicAuth(c.authConfig.Basic.User, c.authConfig.Basic.Token)
   182  	}
   183  	if c.authConfig.BearerToken != nil {
   184  		req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.authConfig.BearerToken.Token))
   185  	}
   186  	return c.client.Do(req)
   187  }
   188  
   189  // Build triggers a Jenkins build for the provided ProwJob. The name of
   190  // the ProwJob is going to be used as the buildId parameter that will help
   191  // us track the build before it's scheduled by Jenkins.
   192  func (c *Client) Build(pj *kube.ProwJob) error {
   193  	c.log(fmt.Sprintf("Build (type=%s job=%s buildId=%s)", pj.Spec.Type, pj.Spec.Job, pj.Metadata.Name))
   194  	u, err := url.Parse(fmt.Sprintf("%s/job/%s/buildWithParameters", c.baseURL, pj.Spec.Job))
   195  	if err != nil {
   196  		return err
   197  	}
   198  	env := pjutil.EnvForSpec(pj.Spec)
   199  	env[buildID] = pj.Metadata.Name
   200  
   201  	q := u.Query()
   202  	for key, value := range env {
   203  		q.Set(key, value)
   204  	}
   205  	u.RawQuery = q.Encode()
   206  	resp, err := c.request(http.MethodPost, u.String())
   207  	if err != nil {
   208  		return err
   209  	}
   210  	defer resp.Body.Close()
   211  	if resp.StatusCode != 201 {
   212  		return fmt.Errorf("response not 201: %s", resp.Status)
   213  	}
   214  	return nil
   215  }
   216  
   217  // ListJenkinsBuilds returns a list of all in-flight builds for the
   218  // provided jobs. Both running and unscheduled builds will be returned.
   219  func (c *Client) ListJenkinsBuilds(jobs map[string]struct{}) (map[string]JenkinsBuild, error) {
   220  	jenkinsBuilds := make(map[string]JenkinsBuild)
   221  
   222  	// Get queued builds.
   223  	queuePath := "/queue/api/json?tree=items[task[name],actions[parameters[name,value]]]"
   224  	c.log("ListJenkinsBuilds", queuePath)
   225  	queueURL := fmt.Sprintf("%s%s", c.baseURL, queuePath)
   226  	data, err := c.get(queueURL)
   227  	if err != nil {
   228  		return nil, fmt.Errorf("cannot list jenkins builds from the queue: %v", err)
   229  	}
   230  	page := struct {
   231  		QueuedBuilds []JenkinsBuild `json:"items"`
   232  	}{}
   233  	if err := json.Unmarshal(data, &page); err != nil {
   234  		return nil, err
   235  	}
   236  	for _, jb := range page.QueuedBuilds {
   237  		buildID := jb.BuildID()
   238  		// Ignore builds with missing buildId parameters.
   239  		if buildID == "" {
   240  			continue
   241  		}
   242  		// Ignore builds we didn't ask for.
   243  		if _, exists := jobs[jb.Task.Name]; !exists {
   244  			continue
   245  		}
   246  		jb.enqueued = true
   247  		jenkinsBuilds[buildID] = jb
   248  	}
   249  
   250  	// Get all running builds for all provided jobs.
   251  	for job := range jobs {
   252  		path := fmt.Sprintf("/job/%s/api/json?tree=builds[number,result,actions[parameters[name,value]]]", job)
   253  		c.log("ListJenkinsBuilds", path)
   254  		u := fmt.Sprintf("%s%s", c.baseURL, path)
   255  		data, err := c.get(u)
   256  		if err != nil {
   257  			return nil, fmt.Errorf("cannot list jenkins builds for job %q: %v", job, err)
   258  		}
   259  		page := struct {
   260  			Builds []JenkinsBuild `json:"builds"`
   261  		}{}
   262  		if err := json.Unmarshal(data, &page); err != nil {
   263  			return nil, err
   264  		}
   265  		for _, jb := range page.Builds {
   266  			buildID := jb.BuildID()
   267  			// Ignore builds with missing buildId parameters.
   268  			if buildID == "" {
   269  				continue
   270  			}
   271  			jenkinsBuilds[buildID] = jb
   272  		}
   273  	}
   274  
   275  	return jenkinsBuilds, nil
   276  }
   277  
   278  func (c *Client) GetLog(job string, build int) ([]byte, error) {
   279  	c.log("GetLog", job, build)
   280  	u := fmt.Sprintf("%s/job/%s/%d/consoleText", c.baseURL, job, build)
   281  	resp, err := c.request(http.MethodGet, u)
   282  	if err != nil {
   283  		return nil, err
   284  	}
   285  	defer resp.Body.Close()
   286  	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
   287  		return nil, fmt.Errorf("response not 2XX: %s: (%s)", resp.Status, u)
   288  	}
   289  	buf, err := ioutil.ReadAll(resp.Body)
   290  	if err != nil {
   291  		return nil, err
   292  	}
   293  	return buf, nil
   294  }
   295  
   296  // Abort aborts the provided Jenkins build for job. Only running
   297  // builds are aborted.
   298  func (c *Client) Abort(job string, build *JenkinsBuild) error {
   299  	if build.IsEnqueued() {
   300  		return fmt.Errorf("aborting enqueued builds is not supported (tried to abort a build for %s)", job)
   301  	}
   302  
   303  	c.log("Abort", job, build.Number)
   304  	u := fmt.Sprintf("%s/job/%s/%d/stop", c.baseURL, job, build.Number)
   305  	resp, err := c.request(http.MethodPost, u)
   306  	if err != nil {
   307  		return err
   308  	}
   309  	defer resp.Body.Close()
   310  	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
   311  		return fmt.Errorf("response not 2XX: %s: (%s)", resp.Status, u)
   312  	}
   313  	return nil
   314  }