github.com/lelandbatey/lab@v0.12.1-0.20180712064405-55bfd303a5f0/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  	"github.com/xanzy/go-gitlab"
    19  	"github.com/zaquestion/lab/internal/git"
    20  )
    21  
    22  var (
    23  	ErrProjectNotFound = errors.New("gitlab project not found")
    24  )
    25  
    26  var (
    27  	lab  *gitlab.Client
    28  	host string
    29  	user string
    30  )
    31  
    32  // Host exposes the GitLab scheme://hostname used to interact with the API
    33  func Host() string {
    34  	return host
    35  }
    36  
    37  // User exposes the configured GitLab user
    38  func User() string {
    39  	return user
    40  }
    41  
    42  // Init initializes a gitlab client for use throughout lab.
    43  func Init(_host, _user, _token string) {
    44  	if len(_host) > 0 && _host[len(_host)-1 : len(_host)][0] == '/' {
    45  		_host = _host[0 : len(_host)-1]
    46  	}
    47  	host = _host
    48  	user = _user
    49  	lab = gitlab.NewClient(nil, _token)
    50  	lab.SetBaseURL(host + "/api/v4")
    51  }
    52  
    53  // Defines filepath for default GitLab templates
    54  const (
    55  	TmplMR    = "merge_request_templates/default.md"
    56  	TmplIssue = "issue_templates/default.md"
    57  )
    58  
    59  // LoadGitLabTmpl loads gitlab templates for use in creating Issues and MRs
    60  //
    61  // https://gitlab.com/help/user/project/description_templates.md#setting-a-default-template-for-issues-and-merge-requests
    62  func LoadGitLabTmpl(tmplName string) string {
    63  	wd, err := git.WorkingDir()
    64  	if err != nil {
    65  		log.Fatal(err)
    66  	}
    67  
    68  	tmplFile := filepath.Join(wd, ".gitlab", tmplName)
    69  	f, err := os.Open(tmplFile)
    70  	if os.IsNotExist(err) {
    71  		return ""
    72  	} else if err != nil {
    73  		log.Fatal(err)
    74  	}
    75  
    76  	tmpl, err := ioutil.ReadAll(f)
    77  	if err != nil {
    78  		log.Fatal(err)
    79  	}
    80  
    81  	return strings.TrimSpace(string(tmpl))
    82  }
    83  
    84  var (
    85  	localProjects map[string]*gitlab.Project = make(map[string]*gitlab.Project)
    86  )
    87  
    88  // GetProject looks up a Gitlab project by ID.
    89  func GetProject(projectID int) (*gitlab.Project, error) {
    90  	target, resp, err := lab.Projects.GetProject(projectID)
    91  	if resp != nil && resp.StatusCode == http.StatusNotFound {
    92  		return nil, ErrProjectNotFound
    93  	}
    94  	if err != nil {
    95  		return nil, err
    96  	}
    97  	return target, nil
    98  }
    99  
   100  // FindProject looks up the Gitlab project. If the namespace is not provided in
   101  // the project string it will search for projects in the users namespace
   102  func FindProject(project string) (*gitlab.Project, error) {
   103  	if target, ok := localProjects[project]; ok {
   104  		return target, nil
   105  	}
   106  
   107  	search := project
   108  	// Assuming that a "/" in the project means its owned by an org
   109  	if !strings.Contains(project, "/") {
   110  		search = user + "/" + project
   111  	}
   112  
   113  	target, resp, err := lab.Projects.GetProject(search)
   114  	if resp != nil && resp.StatusCode == http.StatusNotFound {
   115  		return nil, ErrProjectNotFound
   116  	}
   117  	if err != nil {
   118  		return nil, err
   119  	}
   120  	// fwiw, I feel bad about this
   121  	localProjects[project] = target
   122  
   123  	return target, nil
   124  }
   125  
   126  // Fork creates a user fork of a GitLab project
   127  func Fork(project string) (string, error) {
   128  	if !strings.Contains(project, "/") {
   129  		return "", errors.New("remote must include namespace")
   130  	}
   131  	parts := strings.Split(project, "/")
   132  
   133  	// See if a fork already exists
   134  	target, err := FindProject(parts[1])
   135  	if err == nil {
   136  		return target.SSHURLToRepo, nil
   137  	} else if err != nil && err != ErrProjectNotFound {
   138  		return "", err
   139  	}
   140  
   141  	target, err = FindProject(project)
   142  	if err != nil {
   143  		return "", err
   144  	}
   145  
   146  	fork, _, err := lab.Projects.ForkProject(target.ID)
   147  	if err != nil {
   148  		return "", err
   149  	}
   150  
   151  	return fork.SSHURLToRepo, nil
   152  }
   153  
   154  // MRCreate opens a merge request on GitLab
   155  func MRCreate(project string, opts *gitlab.CreateMergeRequestOptions) (string, error) {
   156  	p, err := FindProject(project)
   157  	if err != nil {
   158  		return "", err
   159  	}
   160  
   161  	mr, _, err := lab.MergeRequests.CreateMergeRequest(p.ID, opts)
   162  	if err != nil {
   163  		return "", err
   164  	}
   165  	return mr.WebURL, nil
   166  }
   167  
   168  // MRGet retrieves the merge request from GitLab project
   169  func MRGet(project string, mrNum int) (*gitlab.MergeRequest, error) {
   170  	p, err := FindProject(project)
   171  	if err != nil {
   172  		return nil, err
   173  	}
   174  
   175  	mr, _, err := lab.MergeRequests.GetMergeRequest(p.ID, mrNum)
   176  	if err != nil {
   177  		return nil, err
   178  	}
   179  
   180  	return mr, nil
   181  }
   182  
   183  // MRList lists the MRs on a GitLab project
   184  func MRList(project string, opts *gitlab.ListProjectMergeRequestsOptions) ([]*gitlab.MergeRequest, error) {
   185  	p, err := FindProject(project)
   186  	if err != nil {
   187  		return nil, err
   188  	}
   189  
   190  	list, _, err := lab.MergeRequests.ListProjectMergeRequests(p.ID, opts)
   191  	if err != nil {
   192  		return nil, err
   193  	}
   194  	return list, nil
   195  }
   196  
   197  // MRClose closes an mr on a GitLab project
   198  func MRClose(pid interface{}, id int) error {
   199  	mr, _, err := lab.MergeRequests.GetMergeRequest(pid, id)
   200  	if err != nil {
   201  		return err
   202  	}
   203  	if mr.State == "closed" {
   204  		return fmt.Errorf("mr already closed")
   205  	}
   206  	_, _, err = lab.MergeRequests.UpdateMergeRequest(pid, int(id), &gitlab.UpdateMergeRequestOptions{
   207  		StateEvent: gitlab.String("close"),
   208  	})
   209  	if err != nil {
   210  		return err
   211  	}
   212  	return nil
   213  }
   214  
   215  // MRMerge merges an mr on a GitLab project
   216  func MRMerge(pid interface{}, id int) error {
   217  	_, _, err := lab.MergeRequests.AcceptMergeRequest(pid, int(id), &gitlab.AcceptMergeRequestOptions{
   218  		MergeWhenPipelineSucceeds: gitlab.Bool(true),
   219  	})
   220  	if err != nil {
   221  		return err
   222  	}
   223  	return nil
   224  }
   225  
   226  // IssueCreate opens a new issue on a GitLab Project
   227  func IssueCreate(project string, opts *gitlab.CreateIssueOptions) (string, error) {
   228  	p, err := FindProject(project)
   229  	if err != nil {
   230  		return "", err
   231  	}
   232  
   233  	mr, _, err := lab.Issues.CreateIssue(p.ID, opts)
   234  	if err != nil {
   235  		return "", err
   236  	}
   237  	return mr.WebURL, nil
   238  }
   239  
   240  // IssueGet retrieves the issue information from a GitLab project
   241  func IssueGet(project string, issueNum int) (*gitlab.Issue, error) {
   242  	p, err := FindProject(project)
   243  	if err != nil {
   244  		return nil, err
   245  	}
   246  
   247  	issue, _, err := lab.Issues.GetIssue(p.ID, issueNum)
   248  	if err != nil {
   249  		return nil, err
   250  	}
   251  
   252  	return issue, nil
   253  }
   254  
   255  // IssueList gets a list of issues on a GitLab Project
   256  func IssueList(project string, opts *gitlab.ListProjectIssuesOptions) ([]*gitlab.Issue, error) {
   257  	p, err := FindProject(project)
   258  	if err != nil {
   259  		return nil, err
   260  	}
   261  
   262  	list, _, err := lab.Issues.ListProjectIssues(p.ID, opts)
   263  	if err != nil {
   264  		return nil, err
   265  	}
   266  	return list, nil
   267  }
   268  
   269  // IssueClose closes an issue on a GitLab project
   270  func IssueClose(pid interface{}, id int) error {
   271  	_, err := lab.Issues.DeleteIssue(pid, int(id))
   272  	if err != nil {
   273  		return err
   274  	}
   275  	return nil
   276  }
   277  
   278  // BranchPushed checks if a branch exists on a GitLab project
   279  func BranchPushed(pid interface{}, branch string) bool {
   280  	b, _, err := lab.Branches.GetBranch(pid, branch)
   281  	if err != nil {
   282  		return false
   283  	}
   284  	return b != nil
   285  }
   286  
   287  // ProjectSnippetCreate creates a snippet in a project
   288  func ProjectSnippetCreate(pid interface{}, opts *gitlab.CreateProjectSnippetOptions) (*gitlab.Snippet, error) {
   289  	snip, _, err := lab.ProjectSnippets.CreateSnippet(pid, opts)
   290  	if err != nil {
   291  		return nil, err
   292  	}
   293  
   294  	return snip, nil
   295  }
   296  
   297  // ProjectSnippetDelete deletes a project snippet
   298  func ProjectSnippetDelete(pid interface{}, id int) error {
   299  	_, err := lab.ProjectSnippets.DeleteSnippet(pid, id)
   300  	return err
   301  }
   302  
   303  // ProjectSnippetList lists snippets on a project
   304  func ProjectSnippetList(pid interface{}, opts *gitlab.ListProjectSnippetsOptions) ([]*gitlab.Snippet, error) {
   305  	snips, _, err := lab.ProjectSnippets.ListSnippets(pid, opts)
   306  	if err != nil {
   307  		return nil, err
   308  	}
   309  	return snips, nil
   310  }
   311  
   312  // SnippetCreate creates a personal snippet
   313  func SnippetCreate(opts *gitlab.CreateSnippetOptions) (*gitlab.Snippet, error) {
   314  	snip, _, err := lab.Snippets.CreateSnippet(opts)
   315  	if err != nil {
   316  		return nil, err
   317  	}
   318  
   319  	return snip, nil
   320  }
   321  
   322  // SnippetDelete deletes a personal snippet
   323  func SnippetDelete(id int) error {
   324  	_, err := lab.Snippets.DeleteSnippet(id)
   325  	return err
   326  }
   327  
   328  // SnippetList lists snippets on a project
   329  func SnippetList(opts *gitlab.ListSnippetsOptions) ([]*gitlab.Snippet, error) {
   330  	snips, _, err := lab.Snippets.ListSnippets(opts)
   331  	if err != nil {
   332  		return nil, err
   333  	}
   334  	return snips, nil
   335  }
   336  
   337  // Lint validates .gitlab-ci.yml contents
   338  func Lint(content string) (bool, error) {
   339  	lint, _, err := lab.Validate.Lint(content)
   340  	if err != nil {
   341  		return false, err
   342  	}
   343  	if len(lint.Errors) > 0 {
   344  		return false, errors.New(strings.Join(lint.Errors, " - "))
   345  	}
   346  	return lint.Status == "valid", nil
   347  }
   348  
   349  // ProjectCreate creates a new project on GitLab
   350  func ProjectCreate(opts *gitlab.CreateProjectOptions) (*gitlab.Project, error) {
   351  	p, _, err := lab.Projects.CreateProject(opts)
   352  	if err != nil {
   353  		return nil, err
   354  	}
   355  	return p, nil
   356  }
   357  
   358  // ProjectDelete creates a new project on GitLab
   359  func ProjectDelete(pid interface{}) error {
   360  	_, err := lab.Projects.DeleteProject(pid)
   361  	if err != nil {
   362  		return err
   363  	}
   364  	return nil
   365  }
   366  
   367  // ProjectList gets a list of projects on GitLab
   368  func ProjectList(opts *gitlab.ListProjectsOptions) ([]*gitlab.Project, error) {
   369  	list, _, err := lab.Projects.ListProjects(opts)
   370  	if err != nil {
   371  		return nil, err
   372  	}
   373  	return list, nil
   374  }
   375  
   376  // CIJobs returns a list of jobs in a pipeline for a given sha. The jobs are
   377  // returned sorted by their CreatedAt time
   378  func CIJobs(pid interface{}, branch string) ([]*gitlab.Job, error) {
   379  	pipelines, _, err := lab.Pipelines.ListProjectPipelines(pid, &gitlab.ListProjectPipelinesOptions{
   380  		Ref: gitlab.String(branch),
   381  	})
   382  	if len(pipelines) == 0 || err != nil {
   383  		return nil, err
   384  	}
   385  	target := pipelines[0].ID
   386  	jobs, _, err := lab.Jobs.ListPipelineJobs(pid, target, &gitlab.ListJobsOptions{
   387  		ListOptions: gitlab.ListOptions{
   388  			PerPage: 500,
   389  		},
   390  	})
   391  	if err != nil {
   392  		return nil, err
   393  	}
   394  	return jobs, nil
   395  }
   396  
   397  // CITrace searches by name for a job and returns its trace file. The trace is
   398  // static so may only be a portion of the logs if the job is till running. If
   399  // no name is provided job is picked using the first available:
   400  // 1. Last Running Job
   401  // 2. First Pending Job
   402  // 3. Last Job in Pipeline
   403  func CITrace(pid interface{}, branch, name string) (io.Reader, *gitlab.Job, error) {
   404  	jobs, err := CIJobs(pid, branch)
   405  	if len(jobs) == 0 || err != nil {
   406  		return nil, nil, err
   407  	}
   408  	var (
   409  		job          *gitlab.Job
   410  		lastRunning  *gitlab.Job
   411  		firstPending *gitlab.Job
   412  	)
   413  
   414  	for _, j := range jobs {
   415  		if j.Status == "running" {
   416  			lastRunning = j
   417  		}
   418  		if j.Status == "pending" && firstPending == nil {
   419  			firstPending = j
   420  		}
   421  		if j.Name == name {
   422  			job = j
   423  			// don't break because there may be a newer version of the job
   424  		}
   425  	}
   426  	if job == nil {
   427  		job = lastRunning
   428  	}
   429  	if job == nil {
   430  		job = firstPending
   431  	}
   432  	if job == nil {
   433  		job = jobs[len(jobs)-1]
   434  	}
   435  	r, _, err := lab.Jobs.GetTraceFile(pid, job.ID)
   436  	if err != nil {
   437  		return nil, job, err
   438  	}
   439  
   440  	return r, job, err
   441  }
   442  
   443  // CIPlayOrRetry runs a job either by playing it for the first time or by
   444  // retrying it based on the currently known job state
   445  func CIPlayOrRetry(pid interface{}, jobID int, status string) (*gitlab.Job, error) {
   446  	switch status {
   447  	case "pending", "running":
   448  		return nil, nil
   449  	case "manual":
   450  		j, _, err := lab.Jobs.PlayJob(pid, jobID)
   451  		if err != nil {
   452  			return nil, err
   453  		}
   454  		return j, nil
   455  	default:
   456  
   457  		j, _, err := lab.Jobs.RetryJob(pid, jobID)
   458  		if err != nil {
   459  			return nil, err
   460  		}
   461  
   462  		return j, nil
   463  	}
   464  }
   465  
   466  // CICancel cancels a job for a given project by its ID.
   467  func CICancel(pid interface{}, jobID int) (*gitlab.Job, error) {
   468  	j, _, err := lab.Jobs.CancelJob(pid, jobID)
   469  	if err != nil {
   470  		return nil, err
   471  	}
   472  	return j, nil
   473  }
   474  
   475  // UserIDFromUsername returns the associated Users ID in GitLab. This is useful
   476  // for API calls that allow you to reference a user, but only by ID.
   477  func UserIDFromUsername(username string) (int, error) {
   478  	us, _, err := lab.Users.ListUsers(&gitlab.ListUsersOptions{
   479  		Username: gitlab.String(username),
   480  	})
   481  	if err != nil || len(us) == 0 {
   482  		return -1, err
   483  	}
   484  	return us[0].ID, nil
   485  }