mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-10-04 01:12:12 +00:00
6f10a57f4a
GitLab merge request hook payload doesn't provide any information regarding source commits. And since Drone currently doesn't support manual fetch from another remote upstream, merge request hook only supported for maerge request fro within the same repository.
345 lines
10 KiB
Go
345 lines
10 KiB
Go
package handler
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/drone/drone/pkg/build/script"
|
|
"github.com/drone/drone/pkg/database"
|
|
. "github.com/drone/drone/pkg/model"
|
|
"github.com/drone/drone/pkg/queue"
|
|
"github.com/plouc/go-gitlab-client"
|
|
)
|
|
|
|
type GitlabHandler struct {
|
|
queue *queue.Queue
|
|
apiPath string
|
|
}
|
|
|
|
func NewGitlabHandler(queue *queue.Queue) *GitlabHandler {
|
|
return &GitlabHandler{
|
|
queue: queue,
|
|
apiPath: "/api/v3",
|
|
}
|
|
}
|
|
|
|
func (g *GitlabHandler) Add(w http.ResponseWriter, r *http.Request, u *User) error {
|
|
settings := database.SettingsMust()
|
|
teams, err := database.ListTeams(u.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
data := struct {
|
|
User *User
|
|
Teams []*Team
|
|
Settings *Settings
|
|
}{u, teams, settings}
|
|
// if the user hasn't linked their GitLab account
|
|
// render a different template
|
|
if len(u.GitlabToken) == 0 {
|
|
return RenderTemplate(w, "gitlab_link.html", &data)
|
|
}
|
|
// otherwise display the template for adding
|
|
// a new GitLab repository.
|
|
return RenderTemplate(w, "gitlab_add.html", &data)
|
|
}
|
|
|
|
func (g *GitlabHandler) Link(w http.ResponseWriter, r *http.Request, u *User) error {
|
|
token := strings.TrimSpace(r.FormValue("token"))
|
|
|
|
if len(u.GitlabToken) == 0 || token != u.GitlabToken && len(token) > 0 {
|
|
u.GitlabToken = token
|
|
settings := database.SettingsMust()
|
|
gl := gogitlab.NewGitlab(settings.GitlabApiUrl, g.apiPath, u.GitlabToken)
|
|
_, err := gl.CurrentUser()
|
|
if err != nil {
|
|
return fmt.Errorf("Private Token is not valid: %q", err)
|
|
}
|
|
if err := database.SaveUser(u); err != nil {
|
|
return RenderError(w, err, http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
http.Redirect(w, r, "/new/gitlab", http.StatusSeeOther)
|
|
return nil
|
|
}
|
|
|
|
func (g *GitlabHandler) ReLink(w http.ResponseWriter, r *http.Request, u *User) error {
|
|
data := struct {
|
|
User *User
|
|
}{u}
|
|
return RenderTemplate(w, "gitlab_link.html", &data)
|
|
}
|
|
|
|
func (g *GitlabHandler) Create(w http.ResponseWriter, r *http.Request, u *User) error {
|
|
teamName := r.FormValue("team")
|
|
owner := r.FormValue("owner")
|
|
name := r.FormValue("name")
|
|
|
|
repo, err := g.newGitlabRepo(u, owner, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(teamName) > 0 {
|
|
team, err := database.GetTeamSlug(teamName)
|
|
if err != nil {
|
|
return fmt.Errorf("Unable to find Team %s.", teamName)
|
|
}
|
|
|
|
// user must be an admin member of the team
|
|
if ok, _ := database.IsMemberAdmin(u.ID, team.ID); !ok {
|
|
return fmt.Errorf("Invalid permission to access Team %s.", teamName)
|
|
}
|
|
repo.TeamID = team.ID
|
|
}
|
|
|
|
// Save to the database
|
|
if err := database.SaveRepo(repo); err != nil {
|
|
return fmt.Errorf("Error saving repository to the database. %s", err)
|
|
}
|
|
|
|
return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
|
|
}
|
|
|
|
func (g *GitlabHandler) newGitlabRepo(u *User, owner, name string) (*Repo, error) {
|
|
settings := database.SettingsMust()
|
|
gl := gogitlab.NewGitlab(settings.GitlabApiUrl, g.apiPath, u.GitlabToken)
|
|
|
|
project, err := gl.Project(ns(owner, name))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var cloneUrl string
|
|
if project.Public {
|
|
cloneUrl = project.HttpRepoUrl
|
|
} else {
|
|
cloneUrl = project.SshRepoUrl
|
|
}
|
|
|
|
repo, err := NewRepo(settings.GitlabDomain, owner, name, ScmGit, cloneUrl)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
repo.UserID = u.ID
|
|
repo.Private = !project.Public
|
|
if repo.Private {
|
|
// name the key
|
|
keyName := fmt.Sprintf("%s@%s", repo.Owner, settings.Domain)
|
|
|
|
// TODO: (fudanchii) check if we already opted to use UserKey
|
|
|
|
// create the github key, or update if one already exists
|
|
if err := gl.AddProjectDeployKey(ns(owner, name), keyName, repo.PublicKey); err != nil {
|
|
return nil, fmt.Errorf("Unable to add Public Key to your GitLab repository.")
|
|
}
|
|
}
|
|
|
|
link := fmt.Sprintf("%s://%s/hook/gitlab?id=%s", settings.Scheme, settings.Domain, repo.Slug)
|
|
if err := gl.AddProjectHook(ns(owner, name), link, true, false, true); err != nil {
|
|
return nil, fmt.Errorf("Unable to add Hook to your GitLab repository.")
|
|
}
|
|
|
|
return repo, err
|
|
}
|
|
|
|
func (g *GitlabHandler) Hook(w http.ResponseWriter, r *http.Request) error {
|
|
payload, _ := ioutil.ReadAll(r.Body)
|
|
parsed, err := gogitlab.ParseHook(payload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if parsed.ObjectKind == "merge_request" {
|
|
fmt.Println(string(payload))
|
|
return g.PullRequestHook(w, r)
|
|
}
|
|
|
|
if len(parsed.After) == 0 {
|
|
return RenderText(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
|
}
|
|
|
|
rID := r.FormValue("id")
|
|
repo, err := database.GetRepoSlug(rID)
|
|
if err != nil {
|
|
return RenderText(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
|
}
|
|
|
|
user, err := database.GetUser(repo.UserID)
|
|
if err != nil {
|
|
return RenderText(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
|
}
|
|
|
|
_, err = database.GetCommitHash(parsed.After, repo.ID)
|
|
if err != nil && err != sql.ErrNoRows {
|
|
println("commit already exists")
|
|
return RenderText(w, http.StatusText(http.StatusBadGateway), http.StatusBadGateway)
|
|
}
|
|
|
|
commit := &Commit{}
|
|
commit.RepoID = repo.ID
|
|
commit.Branch = parsed.Branch()
|
|
commit.Hash = parsed.After
|
|
commit.Status = "Pending"
|
|
commit.Created = time.Now().UTC()
|
|
|
|
head := parsed.Head()
|
|
commit.Message = head.Message
|
|
commit.Timestamp = head.Timestamp
|
|
if head.Author != nil {
|
|
commit.SetAuthor(head.Author.Email)
|
|
} else {
|
|
commit.SetAuthor(parsed.UserName)
|
|
}
|
|
|
|
// get the github settings from the database
|
|
settings := database.SettingsMust()
|
|
|
|
// get the drone.yml file from GitHub
|
|
client := gogitlab.NewGitlab(settings.GitlabApiUrl, g.apiPath, user.GitlabToken)
|
|
|
|
content, err := client.RepoRawFile(ns(repo.Owner, repo.Name), commit.Hash, ".drone.yml")
|
|
if err != nil {
|
|
msg := "No .drone.yml was found in this repository. You need to add one.\n"
|
|
if err := saveFailedBuild(commit, msg); err != nil {
|
|
return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
}
|
|
return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
|
}
|
|
|
|
// parse the build script
|
|
buildscript, err := script.ParseBuild(content, repo.Params)
|
|
if err != nil {
|
|
msg := "Could not parse your .drone.yml file. It needs to be a valid drone yaml file.\n\n" + err.Error() + "\n"
|
|
if err := saveFailedBuild(commit, msg); err != nil {
|
|
return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
}
|
|
return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
|
}
|
|
|
|
// save the commit to the database
|
|
if err := database.SaveCommit(commit); err != nil {
|
|
return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
}
|
|
|
|
// save the build to the database
|
|
build := &Build{}
|
|
build.Slug = "1" // TODO
|
|
build.CommitID = commit.ID
|
|
build.Created = time.Now().UTC()
|
|
build.Status = "Pending"
|
|
if err := database.SaveBuild(build); err != nil {
|
|
return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
}
|
|
|
|
g.queue.Add(&queue.BuildTask{Repo: repo, Commit: commit, Build: build, Script: buildscript})
|
|
|
|
// OK!
|
|
return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
|
|
|
|
}
|
|
|
|
func (g *GitlabHandler) PullRequestHook(w http.ResponseWriter, r *http.Request) error {
|
|
payload, _ := ioutil.ReadAll(r.Body)
|
|
p, err := gogitlab.ParseHook(payload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
obj := p.ObjectAttributes
|
|
|
|
// Gitlab may trigger multiple hooks upon updating merge requests status
|
|
// only build when it was just opened and the merge hasn't been checked yet.
|
|
if !(obj.State == "opened" && obj.MergeStatus == "unchecked") {
|
|
fmt.Printf("Ignore GitLab Merge Requests")
|
|
return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
|
|
}
|
|
|
|
repo, err := database.GetRepoSlug(r.FormValue("id"))
|
|
if err != nil {
|
|
return RenderText(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
|
}
|
|
|
|
user, err := database.GetUser(repo.UserID)
|
|
if err != nil {
|
|
return RenderText(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
|
}
|
|
|
|
settings := database.SettingsMust()
|
|
|
|
client := gogitlab.NewGitlab(settings.GitlabApiUrl, g.apiPath, user.GitlabToken)
|
|
|
|
// GitLab merge-requests hook doesn't include repository data.
|
|
// Have to fetch it manually
|
|
src, err := client.RepoBranch(strconv.Itoa(obj.SourceProjectId), obj.SourceBranch)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = database.GetCommitHash(src.Commit.Id, repo.ID)
|
|
if err != nil && err != sql.ErrNoRows {
|
|
println("commit already exists")
|
|
return RenderText(w, http.StatusText(http.StatusBadGateway), http.StatusBadGateway)
|
|
}
|
|
|
|
commit := &Commit{}
|
|
commit.RepoID = repo.ID
|
|
commit.Branch = src.Name
|
|
commit.Hash = src.Commit.Id
|
|
commit.Status = "Pending"
|
|
commit.Created = time.Now().UTC()
|
|
|
|
commit.Message = src.Commit.Message
|
|
commit.Timestamp = src.Commit.AuthoredDateRaw
|
|
commit.SetAuthor(src.Commit.Author.Email)
|
|
|
|
content, err := client.RepoRawFile(strconv.Itoa(obj.SourceProjectId), commit.Hash, ".drone.yml")
|
|
if err != nil {
|
|
msg := "No .drone.yml was found in this repository. You need to add one.\n"
|
|
if err := saveFailedBuild(commit, msg); err != nil {
|
|
return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
}
|
|
return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
|
}
|
|
|
|
// parse the build script
|
|
buildscript, err := script.ParseBuild(content, repo.Params)
|
|
if err != nil {
|
|
msg := "Could not parse your .drone.yml file. It needs to be a valid drone yaml file.\n\n" + err.Error() + "\n"
|
|
if err := saveFailedBuild(commit, msg); err != nil {
|
|
return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
}
|
|
return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
|
}
|
|
|
|
// save the commit to the database
|
|
if err := database.SaveCommit(commit); err != nil {
|
|
return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
}
|
|
|
|
// save the build to the database
|
|
build := &Build{}
|
|
build.Slug = "1" // TODO
|
|
build.CommitID = commit.ID
|
|
build.Created = time.Now().UTC()
|
|
build.Status = "Pending"
|
|
if err := database.SaveBuild(build); err != nil {
|
|
return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
}
|
|
|
|
g.queue.Add(&queue.BuildTask{Repo: repo, Commit: commit, Build: build, Script: buildscript})
|
|
|
|
return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
|
|
}
|
|
|
|
// ns namespaces user and repo.
|
|
// Returns user%2Frepo
|
|
func ns(user, repo string) string {
|
|
return fmt.Sprintf("%s%%2F%s", user, repo)
|
|
}
|