github.com/matthewdale/lab@v0.14.0/internal/gitlab/gitlab.go (about)

     1  // Package gitlab is an internal wrapper for the go-gitlab package
     2  //
     3  // Most functions serve to expose debug logging if set and accept a project
     4  // name string over an ID.
     5  package gitlab
     6  
     7  import (
     8  	"fmt"
     9  	"io"
    10  	"io/ioutil"
    11  	"log"
    12  	"net/http"
    13  	"os"
    14  	"path/filepath"
    15  	"strings"
    16  
    17  	"github.com/pkg/errors"
    18  	gitlab "github.com/xanzy/go-gitlab"
    19  	"github.com/zaquestion/lab/internal/git"
    20  )
    21  
    22  var (
    23  	// ErrProjectNotFound is returned when a GitLab project cannot be found.
    24  	ErrProjectNotFound = errors.New("gitlab project not found")
    25  )
    26  
    27  var (
    28  	lab   *gitlab.Client
    29  	host  string
    30  	user  string
    31  	token string
    32  )
    33  
    34  // Host exposes the GitLab scheme://hostname used to interact with the API
    35  func Host() string {
    36  	return host
    37  }
    38  
    39  // User exposes the configured GitLab user
    40  func User() string {
    41  	return user
    42  }
    43  
    44  // Init initializes a gitlab client for use throughout lab.
    45  func Init(_host, _user, _token string) {
    46  	if len(_host) > 0 && _host[len(_host)-1 : len(_host)][0] == '/' {
    47  		_host = _host[0 : len(_host)-1]
    48  	}
    49  	host = _host
    50  	user = _user
    51  	token = _token
    52  	lab = gitlab.NewClient(nil, _token)
    53  	lab.SetBaseURL(host + "/api/v4")
    54  }
    55  
    56  // Defines filepath for default GitLab templates
    57  const (
    58  	TmplMR    = "merge_request_templates/default.md"
    59  	TmplIssue = "issue_templates/default.md"
    60  )
    61  
    62  // LoadGitLabTmpl loads gitlab templates for use in creating Issues and MRs
    63  //
    64  // https://gitlab.com/help/user/project/description_templates.md#setting-a-default-template-for-issues-and-merge-requests
    65  func LoadGitLabTmpl(tmplName string) string {
    66  	wd, err := git.WorkingDir()
    67  	if err != nil {
    68  		log.Fatal(err)
    69  	}
    70  
    71  	tmplFile := filepath.Join(wd, ".gitlab", tmplName)
    72  	f, err := os.Open(tmplFile)
    73  	if os.IsNotExist(err) {
    74  		return ""
    75  	} else if err != nil {
    76  		log.Fatal(err)
    77  	}
    78  
    79  	tmpl, err := ioutil.ReadAll(f)
    80  	if err != nil {
    81  		log.Fatal(err)
    82  	}
    83  
    84  	return strings.TrimSpace(string(tmpl))
    85  }
    86  
    87  var (
    88  	localProjects map[string]*gitlab.Project = make(map[string]*gitlab.Project)
    89  )
    90  
    91  // GetProject looks up a Gitlab project by ID.
    92  func GetProject(projectID interface{}) (*gitlab.Project, error) {
    93  	target, resp, err := lab.Projects.GetProject(projectID)
    94  	if resp != nil && resp.StatusCode == http.StatusNotFound {
    95  		return nil, ErrProjectNotFound
    96  	}
    97  	if err != nil {
    98  		return nil, err
    99  	}
   100  	return target, nil
   101  }
   102  
   103  // FindProject looks up the Gitlab project. If the namespace is not provided in
   104  // the project string it will search for projects in the users namespace
   105  func FindProject(project string) (*gitlab.Project, error) {
   106  	if target, ok := localProjects[project]; ok {
   107  		return target, nil
   108  	}
   109  
   110  	search := project
   111  	// Assuming that a "/" in the project means its owned by an org
   112  	if !strings.Contains(project, "/") {
   113  		search = user + "/" + project
   114  	}
   115  
   116  	target, resp, err := lab.Projects.GetProject(search)
   117  	if resp != nil && resp.StatusCode == http.StatusNotFound {
   118  		return nil, ErrProjectNotFound
   119  	}
   120  	if err != nil {
   121  		return nil, err
   122  	}
   123  	// fwiw, I feel bad about this
   124  	localProjects[project] = target
   125  
   126  	return target, nil
   127  }
   128  
   129  // Fork creates a user fork of a GitLab project
   130  func Fork(project string) (string, error) {
   131  	if !strings.Contains(project, "/") {
   132  		return "", errors.New("remote must include namespace")
   133  	}
   134  	parts := strings.Split(project, "/")
   135  
   136  	// See if a fork already exists
   137  	target, err := FindProject(parts[1])
   138  	if err == nil {
   139  		return target.SSHURLToRepo, nil
   140  	} else if err != nil && err != ErrProjectNotFound {
   141  		return "", err
   142  	}
   143  
   144  	target, err = FindProject(project)
   145  	if err != nil {
   146  		return "", err
   147  	}
   148  
   149  	fork, _, err := lab.Projects.ForkProject(target.ID)
   150  	if err != nil {
   151  		return "", err
   152  	}
   153  
   154  	return fork.SSHURLToRepo, nil
   155  }
   156  
   157  // MRCreate opens a merge request on GitLab
   158  func MRCreate(project string, opts *gitlab.CreateMergeRequestOptions) (string, error) {
   159  	p, err := FindProject(project)
   160  	if err != nil {
   161  		return "", err
   162  	}
   163  
   164  	mr, _, err := lab.MergeRequests.CreateMergeRequest(p.ID, opts)
   165  	if err != nil {
   166  		return "", err
   167  	}
   168  	return mr.WebURL, nil
   169  }
   170  
   171  // MRGet retrieves the merge request from GitLab project
   172  func MRGet(project string, mrNum int) (*gitlab.MergeRequest, error) {
   173  	p, err := FindProject(project)
   174  	if err != nil {
   175  		return nil, err
   176  	}
   177  
   178  	mr, _, err := lab.MergeRequests.GetMergeRequest(p.ID, mrNum)
   179  	if err != nil {
   180  		return nil, err
   181  	}
   182  
   183  	return mr, nil
   184  }
   185  
   186  // MRList lists the MRs on a GitLab project
   187  func MRList(project string, opts gitlab.ListProjectMergeRequestsOptions, n int) ([]*gitlab.MergeRequest, error) {
   188  	if n == -1 {
   189  		opts.PerPage = 100
   190  	}
   191  	p, err := FindProject(project)
   192  	if err != nil {
   193  		return nil, err
   194  	}
   195  
   196  	list, resp, err := lab.MergeRequests.ListProjectMergeRequests(p.ID, &opts)
   197  	if err != nil {
   198  		return nil, err
   199  	}
   200  	if resp.CurrentPage == resp.TotalPages {
   201  		return list, nil
   202  	}
   203  	opts.Page = resp.NextPage
   204  	for len(list) < n || n == -1 {
   205  		if n != -1 {
   206  			opts.PerPage = n - len(list)
   207  		}
   208  		mrs, resp, err := lab.MergeRequests.ListProjectMergeRequests(p.ID, &opts)
   209  		if err != nil {
   210  			return nil, err
   211  		}
   212  		opts.Page = resp.NextPage
   213  		list = append(list, mrs...)
   214  		if resp.CurrentPage == resp.TotalPages {
   215  			break
   216  		}
   217  	}
   218  	return list, nil
   219  }
   220  
   221  // MRClose closes an mr on a GitLab project
   222  func MRClose(pid interface{}, id int) error {
   223  	mr, _, err := lab.MergeRequests.GetMergeRequest(pid, id)
   224  	if err != nil {
   225  		return err
   226  	}
   227  	if mr.State == "closed" {
   228  		return fmt.Errorf("mr already closed")
   229  	}
   230  	_, _, err = lab.MergeRequests.UpdateMergeRequest(pid, int(id), &gitlab.UpdateMergeRequestOptions{
   231  		StateEvent: gitlab.String("close"),
   232  	})
   233  	if err != nil {
   234  		return err
   235  	}
   236  	return nil
   237  }
   238  
   239  // MRMerge merges an mr on a GitLab project
   240  func MRMerge(pid interface{}, id int) error {
   241  	_, _, err := lab.MergeRequests.AcceptMergeRequest(pid, int(id), &gitlab.AcceptMergeRequestOptions{
   242  		MergeWhenPipelineSucceeds: gitlab.Bool(true),
   243  	})
   244  	if err != nil {
   245  		return err
   246  	}
   247  	return nil
   248  }
   249  
   250  // MRApprove approves an mr on a GitLab project
   251  func MRApprove(pid interface{}, id int) error {
   252  	_, _, err := lab.MergeRequestApprovals.ApproveMergeRequest(pid, id, &gitlab.ApproveMergeRequestOptions{})
   253  	if err != nil {
   254  		return err
   255  	}
   256  	return nil
   257  }
   258  
   259  // MRThumbUp places a thumb up/down on a merge request
   260  func MRThumbUp(pid interface{}, id int) error {
   261  	_, _, err := lab.AwardEmoji.CreateMergeRequestAwardEmoji(pid, id, &gitlab.CreateAwardEmojiOptions{
   262  		Name: "thumbsup",
   263  	})
   264  	if err != nil {
   265  		return err
   266  	}
   267  	return nil
   268  }
   269  
   270  // MRThumbDown places a thumb up/down on a merge request
   271  func MRThumbDown(pid interface{}, id int) error {
   272  	_, _, err := lab.AwardEmoji.CreateMergeRequestAwardEmoji(pid, id, &gitlab.CreateAwardEmojiOptions{
   273  		Name: "thumbsdown",
   274  	})
   275  	if err != nil {
   276  		return err
   277  	}
   278  	return nil
   279  }
   280  
   281  // IssueCreate opens a new issue on a GitLab project
   282  func IssueCreate(project string, opts *gitlab.CreateIssueOptions) (string, error) {
   283  	p, err := FindProject(project)
   284  	if err != nil {
   285  		return "", err
   286  	}
   287  
   288  	mr, _, err := lab.Issues.CreateIssue(p.ID, opts)
   289  	if err != nil {
   290  		return "", err
   291  	}
   292  	return mr.WebURL, nil
   293  }
   294  
   295  // IssueGet retrieves the issue information from a GitLab project
   296  func IssueGet(project string, issueNum int) (*gitlab.Issue, error) {
   297  	p, err := FindProject(project)
   298  	if err != nil {
   299  		return nil, err
   300  	}
   301  
   302  	issue, _, err := lab.Issues.GetIssue(p.ID, issueNum)
   303  	if err != nil {
   304  		return nil, err
   305  	}
   306  
   307  	return issue, nil
   308  }
   309  
   310  // IssueList gets a list of issues on a GitLab Project
   311  func IssueList(project string, opts gitlab.ListProjectIssuesOptions, n int) ([]*gitlab.Issue, error) {
   312  	if n == -1 {
   313  		opts.PerPage = 100
   314  	}
   315  	p, err := FindProject(project)
   316  	if err != nil {
   317  		return nil, err
   318  	}
   319  
   320  	list, resp, err := lab.Issues.ListProjectIssues(p.ID, &opts)
   321  	if err != nil {
   322  		return nil, err
   323  	}
   324  	if resp.CurrentPage == resp.TotalPages {
   325  		return list, nil
   326  	}
   327  
   328  	opts.Page = resp.NextPage
   329  	for len(list) < n || n == -1 {
   330  		if n != -1 {
   331  			opts.PerPage = n - len(list)
   332  		}
   333  		issues, resp, err := lab.Issues.ListProjectIssues(p.ID, &opts)
   334  		if err != nil {
   335  			return nil, err
   336  		}
   337  		opts.Page = resp.NextPage
   338  		list = append(list, issues...)
   339  		if resp.CurrentPage == resp.TotalPages {
   340  			break
   341  		}
   342  	}
   343  	return list, nil
   344  }
   345  
   346  // IssueClose closes an issue on a GitLab project
   347  func IssueClose(pid interface{}, id int) error {
   348  	_, err := lab.Issues.DeleteIssue(pid, int(id))
   349  	if err != nil {
   350  		return err
   351  	}
   352  	return nil
   353  }
   354  
   355  // BranchPushed checks if a branch exists on a GitLab project
   356  func BranchPushed(pid interface{}, branch string) bool {
   357  	b, _, err := lab.Branches.GetBranch(pid, branch)
   358  	if err != nil {
   359  		return false
   360  	}
   361  	return b != nil
   362  }
   363  
   364  // ProjectSnippetCreate creates a snippet in a project
   365  func ProjectSnippetCreate(pid interface{}, opts *gitlab.CreateProjectSnippetOptions) (*gitlab.Snippet, error) {
   366  	snip, _, err := lab.ProjectSnippets.CreateSnippet(pid, opts)
   367  	if err != nil {
   368  		return nil, err
   369  	}
   370  
   371  	return snip, nil
   372  }
   373  
   374  // ProjectSnippetDelete deletes a project snippet
   375  func ProjectSnippetDelete(pid interface{}, id int) error {
   376  	_, err := lab.ProjectSnippets.DeleteSnippet(pid, id)
   377  	return err
   378  }
   379  
   380  // ProjectSnippetList lists snippets on a project
   381  func ProjectSnippetList(pid interface{}, opts gitlab.ListProjectSnippetsOptions, n int) ([]*gitlab.Snippet, error) {
   382  	if n == -1 {
   383  		opts.PerPage = 100
   384  	}
   385  	list, resp, err := lab.ProjectSnippets.ListSnippets(pid, &opts)
   386  	if err != nil {
   387  		return nil, err
   388  	}
   389  	if resp.CurrentPage == resp.TotalPages {
   390  		return list, nil
   391  	}
   392  	opts.Page = resp.NextPage
   393  	for len(list) < n || n == -1 {
   394  		if n != -1 {
   395  			opts.PerPage = n - len(list)
   396  		}
   397  		snips, resp, err := lab.ProjectSnippets.ListSnippets(pid, &opts)
   398  		if err != nil {
   399  			return nil, err
   400  		}
   401  		opts.Page = resp.NextPage
   402  		list = append(list, snips...)
   403  		if resp.CurrentPage == resp.TotalPages {
   404  			break
   405  		}
   406  	}
   407  	return list, nil
   408  }
   409  
   410  // SnippetCreate creates a personal snippet
   411  func SnippetCreate(opts *gitlab.CreateSnippetOptions) (*gitlab.Snippet, error) {
   412  	snip, _, err := lab.Snippets.CreateSnippet(opts)
   413  	if err != nil {
   414  		return nil, err
   415  	}
   416  
   417  	return snip, nil
   418  }
   419  
   420  // SnippetDelete deletes a personal snippet
   421  func SnippetDelete(id int) error {
   422  	_, err := lab.Snippets.DeleteSnippet(id)
   423  	return err
   424  }
   425  
   426  // SnippetList lists snippets on a project
   427  func SnippetList(opts gitlab.ListSnippetsOptions, n int) ([]*gitlab.Snippet, error) {
   428  	if n == -1 {
   429  		opts.PerPage = 100
   430  	}
   431  	list, resp, err := lab.Snippets.ListSnippets(&opts)
   432  	if err != nil {
   433  		return nil, err
   434  	}
   435  	if resp.CurrentPage == resp.TotalPages {
   436  		return list, nil
   437  	}
   438  	opts.Page = resp.NextPage
   439  	for len(list) < n || n == -1 {
   440  		if n != -1 {
   441  			opts.PerPage = n - len(list)
   442  		}
   443  		snips, resp, err := lab.Snippets.ListSnippets(&opts)
   444  		if err != nil {
   445  			return nil, err
   446  		}
   447  		opts.Page = resp.NextPage
   448  		list = append(list, snips...)
   449  		if resp.CurrentPage == resp.TotalPages {
   450  			break
   451  		}
   452  	}
   453  	return list, nil
   454  }
   455  
   456  // Lint validates .gitlab-ci.yml contents
   457  func Lint(content string) (bool, error) {
   458  	lint, _, err := lab.Validate.Lint(content)
   459  	if err != nil {
   460  		return false, err
   461  	}
   462  	if len(lint.Errors) > 0 {
   463  		return false, errors.New(strings.Join(lint.Errors, " - "))
   464  	}
   465  	return lint.Status == "valid", nil
   466  }
   467  
   468  // ProjectCreate creates a new project on GitLab
   469  func ProjectCreate(opts *gitlab.CreateProjectOptions) (*gitlab.Project, error) {
   470  	p, _, err := lab.Projects.CreateProject(opts)
   471  	if err != nil {
   472  		return nil, err
   473  	}
   474  	return p, nil
   475  }
   476  
   477  // ProjectDelete creates a new project on GitLab
   478  func ProjectDelete(pid interface{}) error {
   479  	_, err := lab.Projects.DeleteProject(pid)
   480  	if err != nil {
   481  		return err
   482  	}
   483  	return nil
   484  }
   485  
   486  // ProjectList gets a list of projects on GitLab
   487  func ProjectList(opts gitlab.ListProjectsOptions, n int) ([]*gitlab.Project, error) {
   488  	list, resp, err := lab.Projects.ListProjects(&opts)
   489  	if err != nil {
   490  		return nil, err
   491  	}
   492  	if resp.CurrentPage == resp.TotalPages {
   493  		return list, nil
   494  	}
   495  	opts.Page = resp.NextPage
   496  	for len(list) < n || n == -1 {
   497  		if n != -1 {
   498  			opts.PerPage = n - len(list)
   499  		}
   500  		projects, resp, err := lab.Projects.ListProjects(&opts)
   501  		if err != nil {
   502  			return nil, err
   503  		}
   504  		opts.Page = resp.NextPage
   505  		list = append(list, projects...)
   506  		if resp.CurrentPage == resp.TotalPages {
   507  			break
   508  		}
   509  	}
   510  	return list, nil
   511  }
   512  
   513  // CIJobs returns a list of jobs in a pipeline for a given sha. The jobs are
   514  // returned sorted by their CreatedAt time
   515  func CIJobs(pid interface{}, branch string) ([]*gitlab.Job, error) {
   516  	pipelines, _, err := lab.Pipelines.ListProjectPipelines(pid, &gitlab.ListProjectPipelinesOptions{
   517  		Ref: gitlab.String(branch),
   518  	})
   519  	if len(pipelines) == 0 || err != nil {
   520  		return nil, err
   521  	}
   522  	target := pipelines[0].ID
   523  	opts := &gitlab.ListJobsOptions{
   524  		ListOptions: gitlab.ListOptions{
   525  			PerPage: 500,
   526  		},
   527  	}
   528  	list, resp, err := lab.Jobs.ListPipelineJobs(pid, target, opts)
   529  	if err != nil {
   530  		return nil, err
   531  	}
   532  	if resp.CurrentPage == resp.TotalPages {
   533  		return list, nil
   534  	}
   535  	opts.Page = resp.NextPage
   536  	for {
   537  		jobs, resp, err := lab.Jobs.ListPipelineJobs(pid, target, opts)
   538  		if err != nil {
   539  			return nil, err
   540  		}
   541  		opts.Page = resp.NextPage
   542  		list = append(list, jobs...)
   543  		if resp.CurrentPage == resp.TotalPages {
   544  			break
   545  		}
   546  	}
   547  	return list, nil
   548  }
   549  
   550  // CITrace searches by name for a job and returns its trace file. The trace is
   551  // static so may only be a portion of the logs if the job is till running. If
   552  // no name is provided job is picked using the first available:
   553  // 1. Last Running Job
   554  // 2. First Pending Job
   555  // 3. Last Job in Pipeline
   556  func CITrace(pid interface{}, branch, name string) (io.Reader, *gitlab.Job, error) {
   557  	jobs, err := CIJobs(pid, branch)
   558  	if len(jobs) == 0 || err != nil {
   559  		return nil, nil, err
   560  	}
   561  	var (
   562  		job          *gitlab.Job
   563  		lastRunning  *gitlab.Job
   564  		firstPending *gitlab.Job
   565  	)
   566  
   567  	for _, j := range jobs {
   568  		if j.Status == "running" {
   569  			lastRunning = j
   570  		}
   571  		if j.Status == "pending" && firstPending == nil {
   572  			firstPending = j
   573  		}
   574  		if j.Name == name {
   575  			job = j
   576  			// don't break because there may be a newer version of the job
   577  		}
   578  	}
   579  	if job == nil {
   580  		job = lastRunning
   581  	}
   582  	if job == nil {
   583  		job = firstPending
   584  	}
   585  	if job == nil {
   586  		job = jobs[len(jobs)-1]
   587  	}
   588  	r, _, err := lab.Jobs.GetTraceFile(pid, job.ID)
   589  	if err != nil {
   590  		return nil, job, err
   591  	}
   592  
   593  	return r, job, err
   594  }
   595  
   596  // CIPlayOrRetry runs a job either by playing it for the first time or by
   597  // retrying it based on the currently known job state
   598  func CIPlayOrRetry(pid interface{}, jobID int, status string) (*gitlab.Job, error) {
   599  	switch status {
   600  	case "pending", "running":
   601  		return nil, nil
   602  	case "manual":
   603  		j, _, err := lab.Jobs.PlayJob(pid, jobID)
   604  		if err != nil {
   605  			return nil, err
   606  		}
   607  		return j, nil
   608  	default:
   609  
   610  		j, _, err := lab.Jobs.RetryJob(pid, jobID)
   611  		if err != nil {
   612  			return nil, err
   613  		}
   614  
   615  		return j, nil
   616  	}
   617  }
   618  
   619  // CICancel cancels a job for a given project by its ID.
   620  func CICancel(pid interface{}, jobID int) (*gitlab.Job, error) {
   621  	j, _, err := lab.Jobs.CancelJob(pid, jobID)
   622  	if err != nil {
   623  		return nil, err
   624  	}
   625  	return j, nil
   626  }
   627  
   628  // CICreate creates a pipeline for given ref
   629  func CICreate(pid interface{}, opts *gitlab.CreatePipelineOptions) (*gitlab.Pipeline, error) {
   630  	p, _, err := lab.Pipelines.CreatePipeline(pid, opts)
   631  	if err != nil {
   632  		return nil, err
   633  	}
   634  	return p, nil
   635  }
   636  
   637  // CITrigger triggers a pipeline for given ref
   638  func CITrigger(pid interface{}, opts gitlab.RunPipelineTriggerOptions) (*gitlab.Pipeline, error) {
   639  	p, _, err := lab.PipelineTriggers.RunPipelineTrigger(pid, &opts)
   640  	if err != nil {
   641  		return nil, err
   642  	}
   643  	return p, nil
   644  }
   645  
   646  // UserIDFromUsername returns the associated Users ID in GitLab. This is useful
   647  // for API calls that allow you to reference a user, but only by ID.
   648  func UserIDFromUsername(username string) (int, error) {
   649  	us, _, err := lab.Users.ListUsers(&gitlab.ListUsersOptions{
   650  		Username: gitlab.String(username),
   651  	})
   652  	if err != nil || len(us) == 0 {
   653  		return -1, err
   654  	}
   655  	return us[0].ID, nil
   656  }