gitlab.com/jfprevost/gitlab-runner-notlscheck@v11.11.4+incompatible/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 "runtime" 13 "strconv" 14 "sync" 15 16 "github.com/prometheus/client_golang/prometheus" 17 "github.com/sirupsen/logrus" 18 19 "gitlab.com/gitlab-org/gitlab-runner/common" 20 "gitlab.com/gitlab-org/gitlab-runner/helpers" 21 ) 22 23 const clientError = -100 24 25 var apiRequestStatuses = prometheus.NewDesc( 26 "gitlab_runner_api_request_statuses_total", 27 "The total number of api requests, partitioned by runner, endpoint and status.", 28 []string{"runner", "endpoint", "status"}, 29 nil, 30 ) 31 32 type APIEndpoint string 33 34 const ( 35 APIEndpointRequestJob APIEndpoint = "request_job" 36 APIEndpointUpdateJob APIEndpoint = "update_job" 37 APIEndpointPatchTrace APIEndpoint = "patch_trace" 38 ) 39 40 type apiRequestStatusPermutation struct { 41 runnerID string 42 endpoint APIEndpoint 43 status int 44 } 45 46 type APIRequestStatusesMap struct { 47 internal map[apiRequestStatusPermutation]int 48 lock sync.RWMutex 49 } 50 51 func (arspm *APIRequestStatusesMap) Append(runnerID string, endpoint APIEndpoint, status int) { 52 arspm.lock.Lock() 53 defer arspm.lock.Unlock() 54 55 permutation := apiRequestStatusPermutation{runnerID: runnerID, endpoint: endpoint, status: status} 56 57 if _, ok := arspm.internal[permutation]; !ok { 58 arspm.internal[permutation] = 0 59 } 60 61 arspm.internal[permutation]++ 62 } 63 64 // Describe implements prometheus.Collector. 65 func (arspm *APIRequestStatusesMap) Describe(ch chan<- *prometheus.Desc) { 66 ch <- apiRequestStatuses 67 } 68 69 // Collect implements prometheus.Collector. 70 func (arspm *APIRequestStatusesMap) Collect(ch chan<- prometheus.Metric) { 71 arspm.lock.RLock() 72 defer arspm.lock.RUnlock() 73 74 for permutation, count := range arspm.internal { 75 ch <- prometheus.MustNewConstMetric( 76 apiRequestStatuses, 77 prometheus.CounterValue, 78 float64(count), 79 permutation.runnerID, 80 string(permutation.endpoint), 81 strconv.Itoa(permutation.status), 82 ) 83 } 84 } 85 86 func NewAPIRequestStatusesMap() *APIRequestStatusesMap { 87 return &APIRequestStatusesMap{ 88 internal: make(map[apiRequestStatusPermutation]int), 89 } 90 } 91 92 type GitLabClient struct { 93 clients map[string]*client 94 lock sync.Mutex 95 96 requestsStatusesMap *APIRequestStatusesMap 97 } 98 99 func (n *GitLabClient) getClient(credentials requestCredentials) (c *client, err error) { 100 n.lock.Lock() 101 defer n.lock.Unlock() 102 103 if n.clients == nil { 104 n.clients = make(map[string]*client) 105 } 106 key := fmt.Sprintf("%s_%s_%s_%s", credentials.GetURL(), credentials.GetToken(), credentials.GetTLSCAFile(), credentials.GetTLSCertFile()) 107 c = n.clients[key] 108 if c == nil { 109 c, err = newClient(credentials) 110 if err != nil { 111 return 112 } 113 n.clients[key] = c 114 } 115 116 return 117 } 118 119 func (n *GitLabClient) getLastUpdate(credentials requestCredentials) (lu string) { 120 cli, err := n.getClient(credentials) 121 if err != nil { 122 return "" 123 } 124 return cli.getLastUpdate() 125 } 126 127 func (n *GitLabClient) getRunnerVersion(config common.RunnerConfig) common.VersionInfo { 128 info := common.VersionInfo{ 129 Name: common.NAME, 130 Version: common.VERSION, 131 Revision: common.REVISION, 132 Platform: runtime.GOOS, 133 Architecture: runtime.GOARCH, 134 Executor: config.Executor, 135 Shell: config.Shell, 136 } 137 138 if executor := common.GetExecutor(config.Executor); executor != nil { 139 executor.GetFeatures(&info.Features) 140 141 if info.Shell == "" { 142 info.Shell = executor.GetDefaultShell() 143 } 144 } 145 146 if shell := common.GetShell(info.Shell); shell != nil { 147 shell.GetFeatures(&info.Features) 148 } 149 150 return info 151 } 152 153 func (n *GitLabClient) doRaw(credentials requestCredentials, method, uri string, request io.Reader, requestType string, headers http.Header) (res *http.Response, err error) { 154 c, err := n.getClient(credentials) 155 if err != nil { 156 return nil, err 157 } 158 159 return c.do(uri, method, request, requestType, headers) 160 } 161 162 func (n *GitLabClient) doJSON(credentials requestCredentials, method, uri string, statusCode int, request interface{}, response interface{}) (int, string, ResponseTLSData, *http.Response) { 163 c, err := n.getClient(credentials) 164 if err != nil { 165 return clientError, err.Error(), ResponseTLSData{}, nil 166 } 167 168 return c.doJSON(uri, method, statusCode, request, response) 169 } 170 171 func (n *GitLabClient) RegisterRunner(runner common.RunnerCredentials, parameters common.RegisterRunnerParameters) *common.RegisterRunnerResponse { 172 // TODO: pass executor 173 request := common.RegisterRunnerRequest{ 174 RegisterRunnerParameters: parameters, 175 Token: runner.Token, 176 Info: n.getRunnerVersion(common.RunnerConfig{}), 177 } 178 179 var response common.RegisterRunnerResponse 180 result, statusText, _, _ := n.doJSON(&runner, "POST", "runners", http.StatusCreated, &request, &response) 181 182 switch result { 183 case http.StatusCreated: 184 runner.Log().Println("Registering runner...", "succeeded") 185 return &response 186 case http.StatusForbidden: 187 runner.Log().Errorln("Registering runner...", "forbidden (check registration token)") 188 return nil 189 case clientError: 190 runner.Log().WithField("status", statusText).Errorln("Registering runner...", "error") 191 return nil 192 default: 193 runner.Log().WithField("status", statusText).Errorln("Registering runner...", "failed") 194 return nil 195 } 196 } 197 198 func (n *GitLabClient) VerifyRunner(runner common.RunnerCredentials) bool { 199 request := common.VerifyRunnerRequest{ 200 Token: runner.Token, 201 } 202 203 result, statusText, _, _ := n.doJSON(&runner, "POST", "runners/verify", http.StatusOK, &request, nil) 204 205 switch result { 206 case http.StatusOK: 207 // this is expected due to fact that we ask for non-existing job 208 runner.Log().Println("Verifying runner...", "is alive") 209 return true 210 case http.StatusForbidden: 211 runner.Log().Errorln("Verifying runner...", "is removed") 212 return false 213 case clientError: 214 runner.Log().WithField("status", statusText).Errorln("Verifying runner...", "error") 215 return true 216 default: 217 runner.Log().WithField("status", statusText).Errorln("Verifying runner...", "failed") 218 return true 219 } 220 } 221 222 func (n *GitLabClient) UnregisterRunner(runner common.RunnerCredentials) bool { 223 request := common.UnregisterRunnerRequest{ 224 Token: runner.Token, 225 } 226 227 result, statusText, _, _ := n.doJSON(&runner, "DELETE", "runners", http.StatusNoContent, &request, nil) 228 229 const baseLogText = "Unregistering runner from GitLab" 230 switch result { 231 case http.StatusNoContent: 232 runner.Log().Println(baseLogText, "succeeded") 233 return true 234 case http.StatusForbidden: 235 runner.Log().Errorln(baseLogText, "forbidden") 236 return false 237 case clientError: 238 runner.Log().WithField("status", statusText).Errorln(baseLogText, "error") 239 return false 240 default: 241 runner.Log().WithField("status", statusText).Errorln(baseLogText, "failed") 242 return false 243 } 244 } 245 246 func addTLSData(response *common.JobResponse, tlsData ResponseTLSData) { 247 if tlsData.CAChain != "" { 248 response.TLSCAChain = tlsData.CAChain 249 } 250 251 if tlsData.CertFile != "" && tlsData.KeyFile != "" { 252 data, err := ioutil.ReadFile(tlsData.CertFile) 253 if err == nil { 254 response.TLSAuthCert = string(data) 255 } 256 data, err = ioutil.ReadFile(tlsData.KeyFile) 257 if err == nil { 258 response.TLSAuthKey = string(data) 259 } 260 261 } 262 } 263 264 func (n *GitLabClient) RequestJob(config common.RunnerConfig, sessionInfo *common.SessionInfo) (*common.JobResponse, bool) { 265 request := common.JobRequest{ 266 Info: n.getRunnerVersion(config), 267 Token: config.Token, 268 LastUpdate: n.getLastUpdate(&config.RunnerCredentials), 269 Session: sessionInfo, 270 } 271 272 var response common.JobResponse 273 result, statusText, tlsData, _ := n.doJSON(&config.RunnerCredentials, "POST", "jobs/request", http.StatusCreated, &request, &response) 274 275 n.requestsStatusesMap.Append(config.RunnerCredentials.ShortDescription(), APIEndpointRequestJob, result) 276 277 switch result { 278 case http.StatusCreated: 279 config.Log().WithFields(logrus.Fields{ 280 "job": strconv.Itoa(response.ID), 281 "repo_url": response.RepoCleanURL(), 282 }).Println("Checking for jobs...", "received") 283 addTLSData(&response, tlsData) 284 return &response, true 285 case http.StatusForbidden: 286 config.Log().Errorln("Checking for jobs...", "forbidden") 287 return nil, false 288 case http.StatusNoContent: 289 config.Log().Debugln("Checking for jobs...", "nothing") 290 return nil, true 291 case clientError: 292 config.Log().WithField("status", statusText).Errorln("Checking for jobs...", "error") 293 return nil, false 294 default: 295 config.Log().WithField("status", statusText).Warningln("Checking for jobs...", "failed") 296 return nil, true 297 } 298 } 299 300 func (n *GitLabClient) UpdateJob(config common.RunnerConfig, jobCredentials *common.JobCredentials, jobInfo common.UpdateJobInfo) common.UpdateState { 301 request := common.UpdateJobRequest{ 302 Info: n.getRunnerVersion(config), 303 Token: jobCredentials.Token, 304 State: jobInfo.State, 305 FailureReason: jobInfo.FailureReason, 306 } 307 308 result, statusText, _, response := n.doJSON(&config.RunnerCredentials, "PUT", fmt.Sprintf("jobs/%d", jobInfo.ID), http.StatusOK, &request, nil) 309 n.requestsStatusesMap.Append(config.RunnerCredentials.ShortDescription(), APIEndpointUpdateJob, result) 310 311 remoteJobStateResponse := NewRemoteJobStateResponse(response) 312 log := config.Log().WithFields(logrus.Fields{ 313 "code": result, 314 "job": jobInfo.ID, 315 "job-status": remoteJobStateResponse.RemoteState, 316 }) 317 318 switch { 319 case remoteJobStateResponse.IsAborted(): 320 log.Warningln("Submitting job to coordinator...", "aborted") 321 return common.UpdateAbort 322 case result == http.StatusOK: 323 log.Debugln("Submitting job to coordinator...", "ok") 324 return common.UpdateSucceeded 325 case result == http.StatusNotFound: 326 log.Warningln("Submitting job to coordinator...", "aborted") 327 return common.UpdateAbort 328 case result == http.StatusForbidden: 329 log.WithField("status", statusText).Errorln("Submitting job to coordinator...", "forbidden") 330 return common.UpdateAbort 331 case result == clientError: 332 log.WithField("status", statusText).Errorln("Submitting job to coordinator...", "error") 333 return common.UpdateAbort 334 default: 335 log.WithField("status", statusText).Warningln("Submitting job to coordinator...", "failed") 336 return common.UpdateFailed 337 } 338 } 339 340 func (n *GitLabClient) PatchTrace(config common.RunnerConfig, jobCredentials *common.JobCredentials, content []byte, startOffset int) (int, common.UpdateState) { 341 id := jobCredentials.ID 342 343 baseLog := config.Log().WithField("job", id) 344 if len(content) == 0 { 345 baseLog.Debugln("Appending trace to coordinator...", "skipped due to empty patch") 346 return startOffset, common.UpdateSucceeded 347 } 348 349 endOffset := startOffset + len(content) 350 contentRange := fmt.Sprintf("%d-%d", startOffset, endOffset-1) 351 352 headers := make(http.Header) 353 headers.Set("Content-Range", contentRange) 354 headers.Set("JOB-TOKEN", jobCredentials.Token) 355 356 uri := fmt.Sprintf("jobs/%d/trace", id) 357 request := bytes.NewReader(content) 358 359 response, err := n.doRaw(&config.RunnerCredentials, "PATCH", uri, request, "text/plain", headers) 360 if err != nil { 361 config.Log().Errorln("Appending trace to coordinator...", "error", err.Error()) 362 return startOffset, common.UpdateFailed 363 } 364 365 n.requestsStatusesMap.Append(config.RunnerCredentials.ShortDescription(), APIEndpointPatchTrace, response.StatusCode) 366 367 defer response.Body.Close() 368 defer io.Copy(ioutil.Discard, response.Body) 369 370 tracePatchResponse := NewTracePatchResponse(response) 371 log := baseLog.WithFields(logrus.Fields{ 372 "sent-log": contentRange, 373 "job-log": tracePatchResponse.RemoteRange, 374 "job-status": tracePatchResponse.RemoteState, 375 "code": response.StatusCode, 376 "status": response.Status, 377 }) 378 379 switch { 380 case tracePatchResponse.IsAborted(): 381 log.Warningln("Appending trace to coordinator...", "aborted") 382 return startOffset, common.UpdateAbort 383 case response.StatusCode == http.StatusAccepted: 384 log.Debugln("Appending trace to coordinator...", "ok") 385 return endOffset, common.UpdateSucceeded 386 case response.StatusCode == http.StatusNotFound: 387 log.Warningln("Appending trace to coordinator...", "not-found") 388 return startOffset, common.UpdateNotFound 389 case response.StatusCode == http.StatusRequestedRangeNotSatisfiable: 390 log.Warningln("Appending trace to coordinator...", "range mismatch") 391 return tracePatchResponse.NewOffset(), common.UpdateRangeMismatch 392 case response.StatusCode == clientError: 393 log.Errorln("Appending trace to coordinator...", "error") 394 return startOffset, common.UpdateAbort 395 default: 396 log.Warningln("Appending trace to coordinator...", "failed") 397 return startOffset, common.UpdateFailed 398 } 399 } 400 401 func (n *GitLabClient) createArtifactsForm(mpw *multipart.Writer, reader io.Reader, baseName string) error { 402 wr, err := mpw.CreateFormFile("file", baseName) 403 if err != nil { 404 return err 405 } 406 407 _, err = io.Copy(wr, reader) 408 if err != nil { 409 return err 410 } 411 return nil 412 } 413 414 func uploadRawArtifactsQuery(options common.ArtifactsOptions) url.Values { 415 q := url.Values{} 416 417 if options.ExpireIn != "" { 418 q.Set("expire_in", options.ExpireIn) 419 } 420 421 if options.Format != "" { 422 q.Set("artifact_format", string(options.Format)) 423 } 424 425 if options.Type != "" { 426 q.Set("artifact_type", options.Type) 427 } 428 429 return q 430 } 431 432 func (n *GitLabClient) UploadRawArtifacts(config common.JobCredentials, reader io.Reader, options common.ArtifactsOptions) common.UploadState { 433 pr, pw := io.Pipe() 434 defer pr.Close() 435 436 mpw := multipart.NewWriter(pw) 437 438 go func() { 439 defer pw.Close() 440 defer mpw.Close() 441 err := n.createArtifactsForm(mpw, reader, options.BaseName) 442 if err != nil { 443 pw.CloseWithError(err) 444 } 445 }() 446 447 query := uploadRawArtifactsQuery(options) 448 449 headers := make(http.Header) 450 headers.Set("JOB-TOKEN", config.Token) 451 res, err := n.doRaw(&config, "POST", fmt.Sprintf("jobs/%d/artifacts?%s", config.ID, query.Encode()), pr, mpw.FormDataContentType(), headers) 452 453 log := logrus.WithFields(logrus.Fields{ 454 "id": config.ID, 455 "token": helpers.ShortenToken(config.Token), 456 }) 457 458 if res != nil { 459 log = log.WithField("responseStatus", res.Status) 460 } 461 462 if err != nil { 463 log.WithError(err).Errorln("Uploading artifacts to coordinator...", "error") 464 return common.UploadFailed 465 } 466 defer res.Body.Close() 467 defer io.Copy(ioutil.Discard, res.Body) 468 469 switch res.StatusCode { 470 case http.StatusCreated: 471 log.Println("Uploading artifacts to coordinator...", "ok") 472 return common.UploadSucceeded 473 case http.StatusForbidden: 474 log.WithField("status", res.Status).Errorln("Uploading artifacts to coordinator...", "forbidden") 475 return common.UploadForbidden 476 case http.StatusRequestEntityTooLarge: 477 log.WithField("status", res.Status).Errorln("Uploading artifacts to coordinator...", "too large archive") 478 return common.UploadTooLarge 479 default: 480 log.WithField("status", res.Status).Warningln("Uploading artifacts to coordinator...", "failed") 481 return common.UploadFailed 482 } 483 } 484 485 func (n *GitLabClient) DownloadArtifacts(config common.JobCredentials, artifactsFile string) common.DownloadState { 486 headers := make(http.Header) 487 headers.Set("JOB-TOKEN", config.Token) 488 res, err := n.doRaw(&config, "GET", fmt.Sprintf("jobs/%d/artifacts", config.ID), nil, "", headers) 489 490 log := logrus.WithFields(logrus.Fields{ 491 "id": config.ID, 492 "token": helpers.ShortenToken(config.Token), 493 }) 494 495 if res != nil { 496 log = log.WithField("responseStatus", res.Status) 497 } 498 499 if err != nil { 500 log.Errorln("Downloading artifacts from coordinator...", "error", err.Error()) 501 return common.DownloadFailed 502 } 503 defer res.Body.Close() 504 defer io.Copy(ioutil.Discard, res.Body) 505 506 switch res.StatusCode { 507 case http.StatusOK: 508 file, err := os.Create(artifactsFile) 509 if err == nil { 510 defer file.Close() 511 _, err = io.Copy(file, res.Body) 512 } 513 if err != nil { 514 file.Close() 515 os.Remove(file.Name()) 516 log.WithError(err).Errorln("Downloading artifacts from coordinator...", "error") 517 return common.DownloadFailed 518 } 519 log.Println("Downloading artifacts from coordinator...", "ok") 520 return common.DownloadSucceeded 521 case http.StatusForbidden: 522 log.WithField("status", res.Status).Errorln("Downloading artifacts from coordinator...", "forbidden") 523 return common.DownloadForbidden 524 case http.StatusNotFound: 525 log.Errorln("Downloading artifacts from coordinator...", "not found") 526 return common.DownloadNotFound 527 default: 528 log.WithField("status", res.Status).Warningln("Downloading artifacts from coordinator...", "failed") 529 return common.DownloadFailed 530 } 531 } 532 533 func (n *GitLabClient) ProcessJob(config common.RunnerConfig, jobCredentials *common.JobCredentials) common.JobTrace { 534 trace := newJobTrace(n, config, jobCredentials) 535 trace.start() 536 return trace 537 } 538 539 func NewGitLabClientWithRequestStatusesMap(rsMap *APIRequestStatusesMap) *GitLabClient { 540 return &GitLabClient{ 541 requestsStatusesMap: rsMap, 542 } 543 } 544 545 func NewGitLabClient() *GitLabClient { 546 return NewGitLabClientWithRequestStatusesMap(NewAPIRequestStatusesMap()) 547 }