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  }