github.com/alloyci/alloy-runner@v1.0.1-0.20180222164613-925503ccafd6/network/gitlab.go (about) 1 package network 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "mime/multipart" 9 "net/http" 10 "net/url" 11 "os" 12 "path/filepath" 13 "runtime" 14 "strconv" 15 "sync" 16 17 "github.com/Sirupsen/logrus" 18 "gitlab.com/gitlab-org/gitlab-runner/common" 19 "gitlab.com/gitlab-org/gitlab-runner/helpers" 20 ) 21 22 const clientError = -100 23 24 type GitLabClient struct { 25 clients map[string]*client 26 lock sync.Mutex 27 } 28 29 func (n *GitLabClient) getClient(credentials requestCredentials) (c *client, err error) { 30 n.lock.Lock() 31 defer n.lock.Unlock() 32 33 if n.clients == nil { 34 n.clients = make(map[string]*client) 35 } 36 key := fmt.Sprintf("%s_%s_%s_%s", credentials.GetURL(), credentials.GetToken(), credentials.GetTLSCAFile(), credentials.GetTLSCertFile()) 37 c = n.clients[key] 38 if c == nil { 39 c, err = newClient(credentials) 40 if err != nil { 41 return 42 } 43 n.clients[key] = c 44 } 45 46 return 47 } 48 49 func (n *GitLabClient) getLastUpdate(credentials requestCredentials) (lu string) { 50 cli, err := n.getClient(credentials) 51 if err != nil { 52 return "" 53 } 54 return cli.getLastUpdate() 55 } 56 57 func (n *GitLabClient) getRunnerVersion(config common.RunnerConfig) common.VersionInfo { 58 info := common.VersionInfo{ 59 Name: common.NAME, 60 Version: common.VERSION, 61 Revision: common.REVISION, 62 Platform: runtime.GOOS, 63 Architecture: runtime.GOARCH, 64 Executor: config.Executor, 65 } 66 67 if executor := common.GetExecutor(config.Executor); executor != nil { 68 executor.GetFeatures(&info.Features) 69 } 70 71 if shell := common.GetShell(config.Shell); shell != nil { 72 shell.GetFeatures(&info.Features) 73 } 74 75 return info 76 } 77 78 func (n *GitLabClient) doRaw(credentials requestCredentials, method, uri string, request io.Reader, requestType string, headers http.Header) (res *http.Response, err error) { 79 c, err := n.getClient(credentials) 80 if err != nil { 81 return nil, err 82 } 83 84 return c.do(uri, method, request, requestType, headers) 85 } 86 87 func (n *GitLabClient) doJSON(credentials requestCredentials, method, uri string, statusCode int, request interface{}, response interface{}) (int, string, ResponseTLSData) { 88 c, err := n.getClient(credentials) 89 if err != nil { 90 return clientError, err.Error(), ResponseTLSData{} 91 } 92 93 return c.doJSON(uri, method, statusCode, request, response) 94 } 95 96 func (n *GitLabClient) RegisterRunner(runner common.RunnerCredentials, description, tags string, runUntagged, locked bool) *common.RegisterRunnerResponse { 97 // TODO: pass executor 98 request := common.RegisterRunnerRequest{ 99 Token: runner.Token, 100 Description: description, 101 Info: n.getRunnerVersion(common.RunnerConfig{}), 102 Locked: locked, 103 RunUntagged: runUntagged, 104 Tags: tags, 105 } 106 107 var response common.RegisterRunnerResponse 108 result, statusText, _ := n.doJSON(&runner, "POST", "runners", http.StatusCreated, &request, &response) 109 110 switch result { 111 case http.StatusCreated: 112 runner.Log().Println("Registering runner...", "succeeded") 113 return &response 114 case http.StatusForbidden: 115 runner.Log().Errorln("Registering runner...", "forbidden (check registration token)") 116 return nil 117 case clientError: 118 runner.Log().WithField("status", statusText).Errorln("Registering runner...", "error") 119 return nil 120 default: 121 runner.Log().WithField("status", statusText).Errorln("Registering runner...", "failed") 122 return nil 123 } 124 } 125 126 func (n *GitLabClient) VerifyRunner(runner common.RunnerCredentials) bool { 127 request := common.VerifyRunnerRequest{ 128 Token: runner.Token, 129 } 130 131 result, statusText, _ := n.doJSON(&runner, "POST", "runners/verify", http.StatusOK, &request, nil) 132 133 switch result { 134 case http.StatusOK: 135 // this is expected due to fact that we ask for non-existing job 136 runner.Log().Println("Verifying runner...", "is alive") 137 return true 138 case http.StatusForbidden: 139 runner.Log().Errorln("Verifying runner...", "is removed") 140 return false 141 case clientError: 142 runner.Log().WithField("status", statusText).Errorln("Verifying runner...", "error") 143 return true 144 default: 145 runner.Log().WithField("status", statusText).Errorln("Verifying runner...", "failed") 146 return true 147 } 148 } 149 150 func (n *GitLabClient) UnregisterRunner(runner common.RunnerCredentials) bool { 151 request := common.UnregisterRunnerRequest{ 152 Token: runner.Token, 153 } 154 155 result, statusText, _ := n.doJSON(&runner, "DELETE", "runners", http.StatusNoContent, &request, nil) 156 157 const baseLogText = "Unregistering runner from GitLab" 158 switch result { 159 case http.StatusNoContent: 160 runner.Log().Println(baseLogText, "succeeded") 161 return true 162 case http.StatusForbidden: 163 runner.Log().Errorln(baseLogText, "forbidden") 164 return false 165 case clientError: 166 runner.Log().WithField("status", statusText).Errorln(baseLogText, "error") 167 return false 168 default: 169 runner.Log().WithField("status", statusText).Errorln(baseLogText, "failed") 170 return false 171 } 172 } 173 174 func addTLSData(response *common.JobResponse, tlsData ResponseTLSData) { 175 if tlsData.CAChain != "" { 176 response.TLSCAChain = tlsData.CAChain 177 } 178 179 if tlsData.CertFile != "" && tlsData.KeyFile != "" { 180 data, err := ioutil.ReadFile(tlsData.CertFile) 181 if err == nil { 182 response.TLSAuthCert = string(data) 183 } 184 data, err = ioutil.ReadFile(tlsData.KeyFile) 185 if err == nil { 186 response.TLSAuthKey = string(data) 187 } 188 189 } 190 } 191 192 func (n *GitLabClient) RequestJob(config common.RunnerConfig) (*common.JobResponse, bool) { 193 request := common.JobRequest{ 194 Info: n.getRunnerVersion(config), 195 Token: config.Token, 196 LastUpdate: n.getLastUpdate(&config.RunnerCredentials), 197 } 198 199 var response common.JobResponse 200 result, statusText, tlsData := n.doJSON(&config.RunnerCredentials, "POST", "jobs/request", http.StatusCreated, &request, &response) 201 202 switch result { 203 case http.StatusCreated: 204 config.Log().WithFields(logrus.Fields{ 205 "job": strconv.Itoa(response.ID), 206 "repo_url": response.RepoCleanURL(), 207 }).Println("Checking for jobs...", "received") 208 addTLSData(&response, tlsData) 209 return &response, true 210 case http.StatusForbidden: 211 config.Log().Errorln("Checking for jobs...", "forbidden") 212 return nil, false 213 case http.StatusNoContent: 214 config.Log().Debugln("Checking for jobs...", "nothing") 215 return nil, true 216 case clientError: 217 config.Log().WithField("status", statusText).Errorln("Checking for jobs...", "error") 218 return nil, false 219 default: 220 config.Log().WithField("status", statusText).Warningln("Checking for jobs...", "failed") 221 return nil, true 222 } 223 } 224 225 func (n *GitLabClient) UpdateJob(config common.RunnerConfig, jobCredentials *common.JobCredentials, jobInfo common.UpdateJobInfo) common.UpdateState { 226 request := common.UpdateJobRequest{ 227 Info: n.getRunnerVersion(config), 228 Token: jobCredentials.Token, 229 State: jobInfo.State, 230 FailureReason: jobInfo.FailureReason, 231 Trace: jobInfo.Trace, 232 } 233 234 log := config.Log().WithField("job", jobInfo.ID) 235 236 result, statusText, _ := n.doJSON(&config.RunnerCredentials, "PUT", fmt.Sprintf("jobs/%d", jobInfo.ID), http.StatusOK, &request, nil) 237 switch result { 238 case http.StatusOK: 239 log.Debugln("Submitting job to coordinator...", "ok") 240 return common.UpdateSucceeded 241 case http.StatusNotFound: 242 log.Warningln("Submitting job to coordinator...", "aborted") 243 return common.UpdateAbort 244 case http.StatusForbidden: 245 log.WithField("status", statusText).Errorln("Submitting job to coordinator...", "forbidden") 246 return common.UpdateAbort 247 case clientError: 248 log.WithField("status", statusText).Errorln("Submitting job to coordinator...", "error") 249 return common.UpdateAbort 250 default: 251 log.WithField("status", statusText).Warningln("Submitting job to coordinator...", "failed") 252 return common.UpdateFailed 253 } 254 } 255 256 func (n *GitLabClient) PatchTrace(config common.RunnerConfig, jobCredentials *common.JobCredentials, tracePatch common.JobTracePatch) common.UpdateState { 257 id := jobCredentials.ID 258 259 contentRange := fmt.Sprintf("%d-%d", tracePatch.Offset(), tracePatch.Limit()) 260 headers := make(http.Header) 261 headers.Set("Content-Range", contentRange) 262 headers.Set("JOB-TOKEN", jobCredentials.Token) 263 264 uri := fmt.Sprintf("jobs/%d/trace", id) 265 request := bytes.NewReader(tracePatch.Patch()) 266 267 response, err := n.doRaw(&config.RunnerCredentials, "PATCH", uri, request, "text/plain", headers) 268 if err != nil { 269 config.Log().Errorln("Appending trace to coordinator...", "error", err.Error()) 270 return common.UpdateFailed 271 } 272 273 defer response.Body.Close() 274 defer io.Copy(ioutil.Discard, response.Body) 275 276 tracePatchResponse := NewTracePatchResponse(response) 277 log := config.Log().WithFields(logrus.Fields{ 278 "job": id, 279 "sent-log": contentRange, 280 "job-log": tracePatchResponse.RemoteRange, 281 "job-status": tracePatchResponse.RemoteState, 282 "code": response.StatusCode, 283 "status": response.Status, 284 }) 285 286 switch { 287 case tracePatchResponse.IsAborted(): 288 log.Warningln("Appending trace to coordinator", "aborted") 289 return common.UpdateAbort 290 case response.StatusCode == http.StatusAccepted: 291 log.Debugln("Appending trace to coordinator...", "ok") 292 return common.UpdateSucceeded 293 case response.StatusCode == http.StatusNotFound: 294 log.Warningln("Appending trace to coordinator...", "not-found") 295 return common.UpdateNotFound 296 case response.StatusCode == http.StatusRequestedRangeNotSatisfiable: 297 log.Warningln("Appending trace to coordinator...", "range mismatch") 298 tracePatch.SetNewOffset(tracePatchResponse.NewOffset()) 299 return common.UpdateRangeMismatch 300 case response.StatusCode == clientError: 301 log.Errorln("Appending trace to coordinator...", "error") 302 return common.UpdateAbort 303 default: 304 log.Warningln("Appending trace to coordinator...", "failed") 305 return common.UpdateFailed 306 } 307 } 308 309 func (n *GitLabClient) createArtifactsForm(mpw *multipart.Writer, reader io.Reader, baseName string) error { 310 wr, err := mpw.CreateFormFile("file", baseName) 311 if err != nil { 312 return err 313 } 314 315 _, err = io.Copy(wr, reader) 316 if err != nil { 317 return err 318 } 319 return nil 320 } 321 322 func (n *GitLabClient) UploadRawArtifacts(config common.JobCredentials, reader io.Reader, baseName string, expireIn string) common.UploadState { 323 pr, pw := io.Pipe() 324 defer pr.Close() 325 326 mpw := multipart.NewWriter(pw) 327 328 go func() { 329 defer pw.Close() 330 defer mpw.Close() 331 err := n.createArtifactsForm(mpw, reader, baseName) 332 if err != nil { 333 pw.CloseWithError(err) 334 } 335 }() 336 337 query := url.Values{} 338 if expireIn != "" { 339 query.Set("expire_in", expireIn) 340 } 341 342 headers := make(http.Header) 343 headers.Set("JOB-TOKEN", config.Token) 344 res, err := n.doRaw(&config, "POST", fmt.Sprintf("jobs/%d/artifacts?%s", config.ID, query.Encode()), pr, mpw.FormDataContentType(), headers) 345 346 log := logrus.WithFields(logrus.Fields{ 347 "id": config.ID, 348 "token": helpers.ShortenToken(config.Token), 349 }) 350 351 if res != nil { 352 log = log.WithField("responseStatus", res.Status) 353 } 354 355 if err != nil { 356 log.WithError(err).Errorln("Uploading artifacts to coordinator...", "error") 357 return common.UploadFailed 358 } 359 defer res.Body.Close() 360 defer io.Copy(ioutil.Discard, res.Body) 361 362 switch res.StatusCode { 363 case http.StatusCreated: 364 log.Println("Uploading artifacts to coordinator...", "ok") 365 return common.UploadSucceeded 366 case http.StatusForbidden: 367 log.WithField("status", res.Status).Errorln("Uploading artifacts to coordinator...", "forbidden") 368 return common.UploadForbidden 369 case http.StatusRequestEntityTooLarge: 370 log.WithField("status", res.Status).Errorln("Uploading artifacts to coordinator...", "too large archive") 371 return common.UploadTooLarge 372 default: 373 log.WithField("status", res.Status).Warningln("Uploading artifacts to coordinator...", "failed") 374 return common.UploadFailed 375 } 376 } 377 378 func (n *GitLabClient) UploadArtifacts(config common.JobCredentials, artifactsFile string) common.UploadState { 379 log := logrus.WithFields(logrus.Fields{ 380 "id": config.ID, 381 "token": helpers.ShortenToken(config.Token), 382 }) 383 384 file, err := os.Open(artifactsFile) 385 if err != nil { 386 log.WithError(err).Errorln("Uploading artifacts to coordinator...", "error") 387 return common.UploadFailed 388 } 389 defer file.Close() 390 391 fi, err := file.Stat() 392 if err != nil { 393 log.WithError(err).Errorln("Uploading artifacts to coordinator...", "error") 394 return common.UploadFailed 395 } 396 if fi.IsDir() { 397 log.WithField("error", "cannot upload directories").Errorln("Uploading artifacts to coordinator...", "error") 398 return common.UploadFailed 399 } 400 401 baseName := filepath.Base(artifactsFile) 402 return n.UploadRawArtifacts(config, file, baseName, "") 403 } 404 405 func (n *GitLabClient) DownloadArtifacts(config common.JobCredentials, artifactsFile string) common.DownloadState { 406 headers := make(http.Header) 407 headers.Set("JOB-TOKEN", config.Token) 408 res, err := n.doRaw(&config, "GET", fmt.Sprintf("jobs/%d/artifacts", config.ID), nil, "", headers) 409 410 log := logrus.WithFields(logrus.Fields{ 411 "id": config.ID, 412 "token": helpers.ShortenToken(config.Token), 413 }) 414 415 if res != nil { 416 log = log.WithField("responseStatus", res.Status) 417 } 418 419 if err != nil { 420 log.Errorln("Downloading artifacts from coordinator...", "error", err.Error()) 421 return common.DownloadFailed 422 } 423 defer res.Body.Close() 424 defer io.Copy(ioutil.Discard, res.Body) 425 426 switch res.StatusCode { 427 case http.StatusOK: 428 file, err := os.Create(artifactsFile) 429 if err == nil { 430 defer file.Close() 431 _, err = io.Copy(file, res.Body) 432 } 433 if err != nil { 434 file.Close() 435 os.Remove(file.Name()) 436 log.WithError(err).Errorln("Downloading artifacts from coordinator...", "error") 437 return common.DownloadFailed 438 } 439 log.Println("Downloading artifacts from coordinator...", "ok") 440 return common.DownloadSucceeded 441 case http.StatusForbidden: 442 log.WithField("status", res.Status).Errorln("Downloading artifacts from coordinator...", "forbidden") 443 return common.DownloadForbidden 444 case http.StatusNotFound: 445 log.Errorln("Downloading artifacts from coordinator...", "not found") 446 return common.DownloadNotFound 447 default: 448 log.WithField("status", res.Status).Warningln("Downloading artifacts from coordinator...", "failed") 449 return common.DownloadFailed 450 } 451 } 452 453 func (n *GitLabClient) ProcessJob(config common.RunnerConfig, jobCredentials *common.JobCredentials) common.JobTrace { 454 trace := newJobTrace(n, config, jobCredentials) 455 trace.start() 456 return trace 457 } 458 459 func NewGitLabClient() *GitLabClient { 460 return &GitLabClient{} 461 }