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 }