github.com/marinho/drone@v0.2.1-0.20140504195434-d3ba962e89a7/pkg/handler/gitlab.go (about)

     1  package handler
     2  
     3  import (
     4  	"database/sql"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"net/http"
     8  	"strconv"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/drone/drone/pkg/build/script"
    13  	"github.com/drone/drone/pkg/database"
    14  	. "github.com/drone/drone/pkg/model"
    15  	"github.com/drone/drone/pkg/queue"
    16  	"github.com/plouc/go-gitlab-client"
    17  )
    18  
    19  type GitlabHandler struct {
    20  	queue   *queue.Queue
    21  	apiPath string
    22  }
    23  
    24  func NewGitlabHandler(queue *queue.Queue) *GitlabHandler {
    25  	return &GitlabHandler{
    26  		queue:   queue,
    27  		apiPath: "/api/v3",
    28  	}
    29  }
    30  
    31  func (g *GitlabHandler) Add(w http.ResponseWriter, r *http.Request, u *User) error {
    32  	settings := database.SettingsMust()
    33  	teams, err := database.ListTeams(u.ID)
    34  	if err != nil {
    35  		return err
    36  	}
    37  	data := struct {
    38  		User     *User
    39  		Teams    []*Team
    40  		Settings *Settings
    41  	}{u, teams, settings}
    42  	// if the user hasn't linked their GitLab account
    43  	// render a different template
    44  	if len(u.GitlabToken) == 0 {
    45  		return RenderTemplate(w, "gitlab_link.html", &data)
    46  	}
    47  	// otherwise display the template for adding
    48  	// a new GitLab repository.
    49  	return RenderTemplate(w, "gitlab_add.html", &data)
    50  }
    51  
    52  func (g *GitlabHandler) Link(w http.ResponseWriter, r *http.Request, u *User) error {
    53  	token := strings.TrimSpace(r.FormValue("token"))
    54  
    55  	if len(u.GitlabToken) == 0 || token != u.GitlabToken && len(token) > 0 {
    56  		u.GitlabToken = token
    57  		settings := database.SettingsMust()
    58  		gl := gogitlab.NewGitlab(settings.GitlabApiUrl, g.apiPath, u.GitlabToken)
    59  		_, err := gl.CurrentUser()
    60  		if err != nil {
    61  			return fmt.Errorf("Private Token is not valid: %q", err)
    62  		}
    63  		if err := database.SaveUser(u); err != nil {
    64  			return RenderError(w, err, http.StatusBadRequest)
    65  		}
    66  	}
    67  
    68  	http.Redirect(w, r, "/new/gitlab", http.StatusSeeOther)
    69  	return nil
    70  }
    71  
    72  func (g *GitlabHandler) ReLink(w http.ResponseWriter, r *http.Request, u *User) error {
    73  	data := struct {
    74  		User *User
    75  	}{u}
    76  	return RenderTemplate(w, "gitlab_link.html", &data)
    77  }
    78  
    79  func (g *GitlabHandler) Create(w http.ResponseWriter, r *http.Request, u *User) error {
    80  	teamName := r.FormValue("team")
    81  	owner := r.FormValue("owner")
    82  	name := r.FormValue("name")
    83  
    84  	repo, err := g.newGitlabRepo(u, owner, name)
    85  	if err != nil {
    86  		return err
    87  	}
    88  
    89  	if len(teamName) > 0 {
    90  		team, err := database.GetTeamSlug(teamName)
    91  		if err != nil {
    92  			return fmt.Errorf("Unable to find Team %s.", teamName)
    93  		}
    94  
    95  		// user must be an admin member of the team
    96  		if ok, _ := database.IsMemberAdmin(u.ID, team.ID); !ok {
    97  			return fmt.Errorf("Invalid permission to access Team %s.", teamName)
    98  		}
    99  		repo.TeamID = team.ID
   100  	}
   101  
   102  	// Save to the database
   103  	if err := database.SaveRepo(repo); err != nil {
   104  		return fmt.Errorf("Error saving repository to the database. %s", err)
   105  	}
   106  
   107  	return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
   108  }
   109  
   110  func (g *GitlabHandler) newGitlabRepo(u *User, owner, name string) (*Repo, error) {
   111  	settings := database.SettingsMust()
   112  	gl := gogitlab.NewGitlab(settings.GitlabApiUrl, g.apiPath, u.GitlabToken)
   113  
   114  	project, err := gl.Project(ns(owner, name))
   115  	if err != nil {
   116  		return nil, err
   117  	}
   118  
   119  	var cloneUrl string
   120  	if project.Public {
   121  		cloneUrl = project.HttpRepoUrl
   122  	} else {
   123  		cloneUrl = project.SshRepoUrl
   124  	}
   125  
   126  	repo, err := NewRepo(settings.GitlabDomain, owner, name, ScmGit, cloneUrl)
   127  	if err != nil {
   128  		return nil, err
   129  	}
   130  
   131  	repo.UserID = u.ID
   132  	repo.Private = !project.Public
   133  	if repo.Private {
   134  		// name the key
   135  		keyName := fmt.Sprintf("%s@%s", repo.Owner, settings.Domain)
   136  
   137  		// TODO: (fudanchii) check if we already opted to use UserKey
   138  
   139  		// create the github key, or update if one already exists
   140  		if err := gl.AddProjectDeployKey(ns(owner, name), keyName, repo.PublicKey); err != nil {
   141  			return nil, fmt.Errorf("Unable to add Public Key to your GitLab repository.")
   142  		}
   143  	}
   144  
   145  	link := fmt.Sprintf("%s://%s/hook/gitlab?id=%s", settings.Scheme, settings.Domain, repo.Slug)
   146  	if err := gl.AddProjectHook(ns(owner, name), link, true, false, true); err != nil {
   147  		return nil, fmt.Errorf("Unable to add Hook to your GitLab repository.")
   148  	}
   149  
   150  	return repo, err
   151  }
   152  
   153  func (g *GitlabHandler) Hook(w http.ResponseWriter, r *http.Request) error {
   154  	rID := r.FormValue("id")
   155  	repo, err := database.GetRepoSlug(rID)
   156  	if err != nil {
   157  		return RenderText(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
   158  	}
   159  
   160  	user, err := database.GetUser(repo.UserID)
   161  	if err != nil {
   162  		return RenderText(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
   163  	}
   164  
   165  	payload, _ := ioutil.ReadAll(r.Body)
   166  	parsed, err := gogitlab.ParseHook(payload)
   167  	if err != nil {
   168  		return err
   169  	}
   170  	if parsed.ObjectKind == "merge_request" {
   171  		fmt.Println(string(payload))
   172  		if err := g.PullRequestHook(parsed, repo, user); err != nil {
   173  			return err
   174  		}
   175  		return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
   176  	}
   177  
   178  	if len(parsed.After) == 0 {
   179  		return RenderText(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
   180  	}
   181  
   182  	_, err = database.GetCommitHash(parsed.After, repo.ID)
   183  	if err != nil && err != sql.ErrNoRows {
   184  		fmt.Println("commit already exists")
   185  		return RenderText(w, http.StatusText(http.StatusBadGateway), http.StatusBadGateway)
   186  	}
   187  
   188  	commit := &Commit{}
   189  	commit.RepoID = repo.ID
   190  	commit.Branch = parsed.Branch()
   191  	commit.Hash = parsed.After
   192  	commit.Status = "Pending"
   193  	commit.Created = time.Now().UTC()
   194  
   195  	head := parsed.Head()
   196  	commit.Message = head.Message
   197  	commit.Timestamp = head.Timestamp
   198  	if head.Author != nil {
   199  		commit.SetAuthor(head.Author.Email)
   200  	} else {
   201  		commit.SetAuthor(parsed.UserName)
   202  	}
   203  
   204  	// get the github settings from the database
   205  	settings := database.SettingsMust()
   206  
   207  	// get the drone.yml file from GitHub
   208  	client := gogitlab.NewGitlab(settings.GitlabApiUrl, g.apiPath, user.GitlabToken)
   209  
   210  	content, err := client.RepoRawFile(ns(repo.Owner, repo.Name), commit.Hash, ".drone.yml")
   211  	if err != nil {
   212  		msg := "No .drone.yml was found in this repository.  You need to add one.\n"
   213  		if err := saveFailedBuild(commit, msg); err != nil {
   214  			return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
   215  		}
   216  		return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
   217  	}
   218  
   219  	// parse the build script
   220  	buildscript, err := script.ParseBuild(content, repo.Params)
   221  	if err != nil {
   222  		msg := "Could not parse your .drone.yml file.  It needs to be a valid drone yaml file.\n\n" + err.Error() + "\n"
   223  		if err := saveFailedBuild(commit, msg); err != nil {
   224  			return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
   225  		}
   226  		return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
   227  	}
   228  
   229  	// save the commit to the database
   230  	if err := database.SaveCommit(commit); err != nil {
   231  		return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
   232  	}
   233  
   234  	// save the build to the database
   235  	build := &Build{}
   236  	build.Slug = "1" // TODO
   237  	build.CommitID = commit.ID
   238  	build.Created = time.Now().UTC()
   239  	build.Status = "Pending"
   240  	if err := database.SaveBuild(build); err != nil {
   241  		return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
   242  	}
   243  
   244  	g.queue.Add(&queue.BuildTask{Repo: repo, Commit: commit, Build: build, Script: buildscript})
   245  
   246  	// OK!
   247  	return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
   248  
   249  }
   250  
   251  func (g *GitlabHandler) PullRequestHook(p *gogitlab.HookPayload, repo *Repo, user *User) error {
   252  	obj := p.ObjectAttributes
   253  
   254  	// Gitlab may trigger multiple hooks upon updating merge requests status
   255  	// only build when it was just opened and the merge hasn't been checked yet.
   256  	if !(obj.State == "opened" && obj.MergeStatus == "unchecked") {
   257  		fmt.Println("Ignore GitLab Merge Requests")
   258  		return nil
   259  	}
   260  
   261  	settings := database.SettingsMust()
   262  
   263  	client := gogitlab.NewGitlab(settings.GitlabApiUrl, g.apiPath, user.GitlabToken)
   264  
   265  	// GitLab merge-requests hook doesn't include repository data.
   266  	// Have to fetch it manually
   267  	src, err := client.RepoBranch(strconv.Itoa(obj.SourceProjectId), obj.SourceBranch)
   268  	if err != nil {
   269  		return err
   270  	}
   271  
   272  	_, err = database.GetCommitHash(src.Commit.Id, repo.ID)
   273  	if err != nil && err != sql.ErrNoRows {
   274  		fmt.Println("commit already exists")
   275  		return err
   276  	}
   277  
   278  	commit := &Commit{}
   279  	commit.RepoID = repo.ID
   280  	commit.Branch = src.Name
   281  	commit.Hash = src.Commit.Id
   282  	commit.Status = "Pending"
   283  	commit.Created = time.Now().UTC()
   284  	commit.PullRequest = strconv.Itoa(obj.IId)
   285  
   286  	commit.Message = src.Commit.Message
   287  	commit.Timestamp = src.Commit.AuthoredDateRaw
   288  	commit.SetAuthor(src.Commit.Author.Email)
   289  
   290  	content, err := client.RepoRawFile(strconv.Itoa(obj.SourceProjectId), commit.Hash, ".drone.yml")
   291  	if err != nil {
   292  		msg := "No .drone.yml was found in this repository.  You need to add one.\n"
   293  		if err := saveFailedBuild(commit, msg); err != nil {
   294  			return fmt.Errorf("Failed to save build: %q", err)
   295  		}
   296  		return fmt.Errorf("Error to fetch build script: %q", err)
   297  	}
   298  
   299  	// parse the build script
   300  	buildscript, err := script.ParseBuild(content, repo.Params)
   301  	if err != nil {
   302  		msg := "Could not parse your .drone.yml file.  It needs to be a valid drone yaml file.\n\n" + err.Error() + "\n"
   303  		if err := saveFailedBuild(commit, msg); err != nil {
   304  			return fmt.Errorf("Failed to save build: %q", err)
   305  		}
   306  		return fmt.Errorf("Failed to parse build script: %q", err)
   307  	}
   308  
   309  	// save the commit to the database
   310  	if err := database.SaveCommit(commit); err != nil {
   311  		return fmt.Errorf("Failed to save commit: %q", err)
   312  	}
   313  
   314  	// save the build to the database
   315  	build := &Build{}
   316  	build.Slug = "1" // TODO
   317  	build.CommitID = commit.ID
   318  	build.Created = time.Now().UTC()
   319  	build.Status = "Pending"
   320  	if err := database.SaveBuild(build); err != nil {
   321  		return fmt.Errorf("Failed to save build: %q", err)
   322  	}
   323  
   324  	g.queue.Add(&queue.BuildTask{Repo: repo, Commit: commit, Build: build, Script: buildscript})
   325  
   326  	return nil
   327  }
   328  
   329  // ns namespaces user and repo.
   330  // Returns user%2Frepo
   331  func ns(user, repo string) string {
   332  	return fmt.Sprintf("%s%%2F%s", user, repo)
   333  }