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  }