mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-12-22 16:36:30 +00:00
599 lines
15 KiB
Go
599 lines
15 KiB
Go
package gitlab
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/drone/drone/model"
|
|
"github.com/drone/drone/shared/httputil"
|
|
"github.com/drone/drone/shared/oauth2"
|
|
"github.com/drone/drone/shared/token"
|
|
|
|
"github.com/drone/drone/remote/gitlab/client"
|
|
)
|
|
|
|
const (
|
|
DefaultScope = "api"
|
|
)
|
|
|
|
type Gitlab struct {
|
|
URL string
|
|
Client string
|
|
Secret string
|
|
AllowedOrgs []string
|
|
CloneMode string
|
|
Open bool
|
|
PrivateMode bool
|
|
SkipVerify bool
|
|
HideArchives bool
|
|
Search bool
|
|
}
|
|
|
|
func Load(config string) *Gitlab {
|
|
url_, err := url.Parse(config)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
params := url_.Query()
|
|
url_.RawQuery = ""
|
|
|
|
gitlab := Gitlab{}
|
|
gitlab.URL = url_.String()
|
|
gitlab.Client = params.Get("client_id")
|
|
gitlab.Secret = params.Get("client_secret")
|
|
gitlab.AllowedOrgs = params["orgs"]
|
|
gitlab.SkipVerify, _ = strconv.ParseBool(params.Get("skip_verify"))
|
|
gitlab.HideArchives, _ = strconv.ParseBool(params.Get("hide_archives"))
|
|
gitlab.Open, _ = strconv.ParseBool(params.Get("open"))
|
|
|
|
switch params.Get("clone_mode") {
|
|
case "oauth":
|
|
gitlab.CloneMode = "oauth"
|
|
default:
|
|
gitlab.CloneMode = "token"
|
|
}
|
|
|
|
// this is a temp workaround
|
|
gitlab.Search, _ = strconv.ParseBool(params.Get("search"))
|
|
|
|
return &gitlab
|
|
}
|
|
|
|
// Login authenticates the session and returns the
|
|
// remote user details.
|
|
func (g *Gitlab) Login(res http.ResponseWriter, req *http.Request) (*model.User, bool, error) {
|
|
|
|
var config = &oauth2.Config{
|
|
ClientId: g.Client,
|
|
ClientSecret: g.Secret,
|
|
Scope: DefaultScope,
|
|
AuthURL: fmt.Sprintf("%s/oauth/authorize", g.URL),
|
|
TokenURL: fmt.Sprintf("%s/oauth/token", g.URL),
|
|
RedirectURL: fmt.Sprintf("%s/authorize", httputil.GetURL(req)),
|
|
}
|
|
|
|
trans_ := &http.Transport{
|
|
Proxy: http.ProxyFromEnvironment,
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: g.SkipVerify},
|
|
}
|
|
|
|
// get the OAuth code
|
|
var code = req.FormValue("code")
|
|
if len(code) == 0 {
|
|
http.Redirect(res, req, config.AuthCodeURL("drone"), http.StatusSeeOther)
|
|
return nil, false, nil
|
|
}
|
|
|
|
var trans = &oauth2.Transport{Config: config, Transport: trans_}
|
|
var token_, err = trans.Exchange(code)
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("Error exchanging token. %s", err)
|
|
}
|
|
|
|
client := NewClient(g.URL, token_.AccessToken, g.SkipVerify)
|
|
login, err := client.CurrentUser()
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
if len(g.AllowedOrgs) != 0 {
|
|
groups, err := client.AllGroups()
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("Could not check org membership. %s", err)
|
|
}
|
|
|
|
var member bool
|
|
for _, group := range groups {
|
|
for _, allowedOrg := range g.AllowedOrgs {
|
|
if group.Path == allowedOrg {
|
|
member = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if !member {
|
|
return nil, false, fmt.Errorf("User does not belong to correct group. Must belong to %v", g.AllowedOrgs)
|
|
}
|
|
}
|
|
|
|
user := &model.User{}
|
|
user.Login = login.Username
|
|
user.Email = login.Email
|
|
user.Token = token_.AccessToken
|
|
user.Secret = token_.RefreshToken
|
|
|
|
if strings.HasPrefix(login.AvatarUrl, "http") {
|
|
user.Avatar = login.AvatarUrl
|
|
} else {
|
|
user.Avatar = g.URL + "/" + login.AvatarUrl
|
|
}
|
|
|
|
return user, g.Open, nil
|
|
}
|
|
|
|
func (g *Gitlab) Auth(token, secret string) (string, error) {
|
|
client := NewClient(g.URL, token, g.SkipVerify)
|
|
login, err := client.CurrentUser()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return login.Username, nil
|
|
}
|
|
|
|
// Repo fetches the named repository from the remote system.
|
|
func (g *Gitlab) Repo(u *model.User, owner, name string) (*model.Repo, error) {
|
|
client := NewClient(g.URL, u.Token, g.SkipVerify)
|
|
id, err := GetProjectId(g, client, owner, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
repo_, err := client.Project(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
repo := &model.Repo{}
|
|
repo.Owner = owner
|
|
repo.Name = name
|
|
repo.FullName = repo_.PathWithNamespace
|
|
repo.Link = repo_.Url
|
|
repo.Clone = repo_.HttpRepoUrl
|
|
repo.Branch = "master"
|
|
|
|
repo.Avatar = repo_.AvatarUrl
|
|
|
|
if len(repo.Avatar) != 0 && !strings.HasPrefix(repo.Avatar, "http") {
|
|
repo.Avatar = fmt.Sprintf("%s/%s", g.URL, repo.Avatar)
|
|
}
|
|
|
|
if repo_.DefaultBranch != "" {
|
|
repo.Branch = repo_.DefaultBranch
|
|
}
|
|
|
|
if g.PrivateMode {
|
|
repo.IsPrivate = true
|
|
} else {
|
|
repo.IsPrivate = !repo_.Public
|
|
}
|
|
|
|
return repo, err
|
|
}
|
|
|
|
// Repos fetches a list of repos from the remote system.
|
|
func (g *Gitlab) Repos(u *model.User) ([]*model.RepoLite, error) {
|
|
client := NewClient(g.URL, u.Token, g.SkipVerify)
|
|
|
|
var repos = []*model.RepoLite{}
|
|
|
|
all, err := client.AllProjects(g.HideArchives)
|
|
if err != nil {
|
|
return repos, err
|
|
}
|
|
|
|
for _, repo := range all {
|
|
var parts = strings.Split(repo.PathWithNamespace, "/")
|
|
var owner = parts[0]
|
|
var name = parts[1]
|
|
var avatar = repo.AvatarUrl
|
|
|
|
if len(avatar) != 0 && !strings.HasPrefix(avatar, "http") {
|
|
avatar = fmt.Sprintf("%s/%s", g.URL, avatar)
|
|
}
|
|
|
|
repos = append(repos, &model.RepoLite{
|
|
Owner: owner,
|
|
Name: name,
|
|
FullName: repo.PathWithNamespace,
|
|
Avatar: avatar,
|
|
})
|
|
}
|
|
|
|
return repos, err
|
|
}
|
|
|
|
// Perm fetches the named repository from the remote system.
|
|
func (g *Gitlab) Perm(u *model.User, owner, name string) (*model.Perm, error) {
|
|
|
|
client := NewClient(g.URL, u.Token, g.SkipVerify)
|
|
id, err := GetProjectId(g, client, owner, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
repo, err := client.Project(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// repo owner is granted full access
|
|
if repo.Owner != nil && repo.Owner.Username == u.Login {
|
|
return &model.Perm{true, true, true}, nil
|
|
}
|
|
|
|
// check permission for current user
|
|
m := &model.Perm{}
|
|
m.Admin = IsAdmin(repo)
|
|
m.Pull = IsRead(repo)
|
|
m.Push = IsWrite(repo)
|
|
return m, nil
|
|
}
|
|
|
|
// File fetches a file from the remote repository and returns in string format.
|
|
func (g *Gitlab) File(user *model.User, repo *model.Repo, build *model.Build, f string) ([]byte, error) {
|
|
var client = NewClient(g.URL, user.Token, g.SkipVerify)
|
|
id, err := GetProjectId(g, client, repo.Owner, repo.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
out, err := client.RepoRawFile(id, build.Commit, f)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return out, err
|
|
}
|
|
|
|
// NOTE Currently gitlab doesn't support status for commits and events,
|
|
// also if we want get MR status in gitlab we need implement a special plugin for gitlab,
|
|
// gitlab uses API to fetch build status on client side. But for now we skip this.
|
|
func (g *Gitlab) Status(u *model.User, repo *model.Repo, b *model.Build, link string) error {
|
|
client := NewClient(g.URL, u.Token, g.SkipVerify)
|
|
|
|
status := getStatus(b.Status)
|
|
desc := getDesc(b.Status)
|
|
|
|
client.SetStatus(
|
|
ns(repo.Owner, repo.Name),
|
|
b.Commit,
|
|
status,
|
|
desc,
|
|
strings.Replace(b.Ref, "refs/heads/", "", -1),
|
|
link,
|
|
)
|
|
|
|
// Gitlab statuses it's a new feature, just ignore error
|
|
// if gitlab version not support this
|
|
return nil
|
|
}
|
|
|
|
// Netrc returns a .netrc file that can be used to clone
|
|
// private repositories from a remote system.
|
|
func (g *Gitlab) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) {
|
|
url_, err := url.Parse(g.URL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
netrc := &model.Netrc{}
|
|
netrc.Machine = url_.Host
|
|
|
|
switch g.CloneMode {
|
|
case "oauth":
|
|
netrc.Login = "oauth2"
|
|
netrc.Password = u.Token
|
|
case "token":
|
|
t := token.New(token.HookToken, r.FullName)
|
|
netrc.Login = "drone-ci-token"
|
|
netrc.Password, err = t.Sign(r.Hash)
|
|
}
|
|
return netrc, err
|
|
}
|
|
|
|
// Activate activates a repository by adding a Post-commit hook and
|
|
// a Public Deploy key, if applicable.
|
|
func (g *Gitlab) Activate(user *model.User, repo *model.Repo, k *model.Key, link string) error {
|
|
var client = NewClient(g.URL, user.Token, g.SkipVerify)
|
|
id, err := GetProjectId(g, client, repo.Owner, repo.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
uri, err := url.Parse(link)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
droneUrl := fmt.Sprintf("%s://%s", uri.Scheme, uri.Host)
|
|
droneToken := uri.Query().Get("access_token")
|
|
ssl_verify := strconv.FormatBool(!g.SkipVerify)
|
|
|
|
return client.AddDroneService(id, map[string]string{
|
|
"token": droneToken,
|
|
"drone_url": droneUrl,
|
|
"enable_ssl_verification": ssl_verify,
|
|
})
|
|
}
|
|
|
|
// Deactivate removes a repository by removing all the post-commit hooks
|
|
// which are equal to link and removing the SSH deploy key.
|
|
func (g *Gitlab) Deactivate(user *model.User, repo *model.Repo, link string) error {
|
|
var client = NewClient(g.URL, user.Token, g.SkipVerify)
|
|
id, err := GetProjectId(g, client, repo.Owner, repo.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return client.DeleteDroneService(id)
|
|
}
|
|
|
|
// ParseHook parses the post-commit hook from the Request body
|
|
// and returns the required data in a standard format.
|
|
func (g *Gitlab) Hook(req *http.Request) (*model.Repo, *model.Build, error) {
|
|
defer req.Body.Close()
|
|
var payload, _ = ioutil.ReadAll(req.Body)
|
|
var parsed, err = client.ParseHook(payload)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
switch parsed.ObjectKind {
|
|
case "merge_request":
|
|
return mergeRequest(parsed, req)
|
|
case "tag_push", "push":
|
|
return push(parsed, req)
|
|
default:
|
|
return nil, nil, nil
|
|
}
|
|
}
|
|
|
|
func mergeRequest(parsed *client.HookPayload, req *http.Request) (*model.Repo, *model.Build, error) {
|
|
|
|
repo := &model.Repo{}
|
|
|
|
obj := parsed.ObjectAttributes
|
|
if obj == nil {
|
|
return nil, nil, fmt.Errorf("object_attributes key expected in merge request hook")
|
|
}
|
|
|
|
target := obj.Target
|
|
source := obj.Source
|
|
|
|
if target == nil && source == nil {
|
|
return nil, nil, fmt.Errorf("target and source keys expected in merge request hook")
|
|
} else if target == nil {
|
|
return nil, nil, fmt.Errorf("target key expected in merge request hook")
|
|
} else if source == nil {
|
|
return nil, nil, fmt.Errorf("source key exptected in merge request hook")
|
|
}
|
|
|
|
if target.PathWithNamespace != "" {
|
|
var err error
|
|
if repo.Owner, repo.Name, err = ExtractFromPath(target.PathWithNamespace); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
repo.FullName = target.PathWithNamespace
|
|
} else {
|
|
repo.Owner = req.FormValue("owner")
|
|
repo.Name = req.FormValue("name")
|
|
repo.FullName = fmt.Sprintf("%s/%s", repo.Owner, repo.Name)
|
|
}
|
|
|
|
repo.Link = target.WebUrl
|
|
|
|
if target.GitHttpUrl != "" {
|
|
repo.Clone = target.GitHttpUrl
|
|
} else {
|
|
repo.Clone = target.HttpUrl
|
|
}
|
|
|
|
if target.DefaultBranch != "" {
|
|
repo.Branch = target.DefaultBranch
|
|
} else {
|
|
repo.Branch = "master"
|
|
}
|
|
|
|
if target.AvatarUrl != "" {
|
|
repo.Avatar = target.AvatarUrl
|
|
}
|
|
|
|
build := &model.Build{}
|
|
build.Event = "pull_request"
|
|
|
|
lastCommit := obj.LastCommit
|
|
if lastCommit == nil {
|
|
return nil, nil, fmt.Errorf("last_commit key expected in merge request hook")
|
|
}
|
|
|
|
build.Message = lastCommit.Message
|
|
build.Commit = lastCommit.Id
|
|
//build.Remote = parsed.ObjectAttributes.Source.HttpUrl
|
|
|
|
if obj.SourceProjectId == obj.TargetProjectId {
|
|
build.Ref = fmt.Sprintf("refs/heads/%s", obj.SourceBranch)
|
|
} else {
|
|
build.Ref = fmt.Sprintf("refs/merge-requests/%d/head", obj.IId)
|
|
}
|
|
|
|
build.Branch = obj.SourceBranch
|
|
|
|
author := lastCommit.Author
|
|
if author == nil {
|
|
return nil, nil, fmt.Errorf("author key expected in merge request hook")
|
|
}
|
|
|
|
build.Author = author.Name
|
|
build.Email = author.Email
|
|
|
|
if len(build.Email) != 0 {
|
|
build.Avatar = GetUserAvatar(build.Email)
|
|
}
|
|
|
|
build.Title = obj.Title
|
|
build.Link = obj.Url
|
|
|
|
return repo, build, nil
|
|
}
|
|
|
|
func push(parsed *client.HookPayload, req *http.Request) (*model.Repo, *model.Build, error) {
|
|
repo := &model.Repo{}
|
|
|
|
// Since gitlab 8.5, used project instead repository key
|
|
// see https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/web_hooks/web_hooks.md#web-hooks
|
|
if project := parsed.Project; project != nil {
|
|
var err error
|
|
if repo.Owner, repo.Name, err = ExtractFromPath(project.PathWithNamespace); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
repo.Avatar = project.AvatarUrl
|
|
repo.Link = project.WebUrl
|
|
repo.Clone = project.GitHttpUrl
|
|
repo.FullName = project.PathWithNamespace
|
|
repo.Branch = project.DefaultBranch
|
|
|
|
switch project.VisibilityLevel {
|
|
case 0:
|
|
repo.IsPrivate = true
|
|
case 10:
|
|
repo.IsPrivate = true
|
|
case 20:
|
|
repo.IsPrivate = false
|
|
}
|
|
} else if repository := parsed.Repository; repository != nil {
|
|
repo.Owner = req.FormValue("owner")
|
|
repo.Name = req.FormValue("name")
|
|
repo.Link = repository.URL
|
|
repo.Clone = repository.GitHttpUrl
|
|
repo.Branch = "master"
|
|
repo.FullName = fmt.Sprintf("%s/%s", req.FormValue("owner"), req.FormValue("name"))
|
|
|
|
switch repository.VisibilityLevel {
|
|
case 0:
|
|
repo.IsPrivate = true
|
|
case 10:
|
|
repo.IsPrivate = true
|
|
case 20:
|
|
repo.IsPrivate = false
|
|
}
|
|
} else {
|
|
return nil, nil, fmt.Errorf("No project/repository keys given")
|
|
}
|
|
|
|
build := &model.Build{}
|
|
build.Event = model.EventPush
|
|
build.Commit = parsed.After
|
|
build.Branch = parsed.Branch()
|
|
build.Ref = parsed.Ref
|
|
// hook.Commit.Remote = cloneUrl
|
|
|
|
var head = parsed.Head()
|
|
build.Message = head.Message
|
|
// build.Timestamp = head.Timestamp
|
|
|
|
// extracts the commit author (ideally email)
|
|
// from the post-commit hook
|
|
switch {
|
|
case head.Author != nil:
|
|
build.Email = head.Author.Email
|
|
build.Author = parsed.UserName
|
|
if len(build.Email) != 0 {
|
|
build.Avatar = GetUserAvatar(build.Email)
|
|
}
|
|
case head.Author == nil:
|
|
build.Author = parsed.UserName
|
|
}
|
|
|
|
if strings.HasPrefix(build.Ref, "refs/tags/") {
|
|
build.Event = model.EventTag
|
|
}
|
|
|
|
return repo, build, nil
|
|
}
|
|
|
|
// ¯\_(ツ)_/¯
|
|
func (g *Gitlab) Oauth2Transport(r *http.Request) *oauth2.Transport {
|
|
return &oauth2.Transport{
|
|
Config: &oauth2.Config{
|
|
ClientId: g.Client,
|
|
ClientSecret: g.Secret,
|
|
Scope: DefaultScope,
|
|
AuthURL: fmt.Sprintf("%s/oauth/authorize", g.URL),
|
|
TokenURL: fmt.Sprintf("%s/oauth/token", g.URL),
|
|
RedirectURL: fmt.Sprintf("%s/authorize", httputil.GetURL(r)),
|
|
//settings.Server.Scheme, settings.Server.Hostname),
|
|
},
|
|
Transport: &http.Transport{
|
|
Proxy: http.ProxyFromEnvironment,
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: g.SkipVerify},
|
|
},
|
|
}
|
|
}
|
|
|
|
const (
|
|
StatusPending = "pending"
|
|
StatusRunning = "running"
|
|
StatusSuccess = "success"
|
|
StatusFailure = "failed"
|
|
StatusCanceled = "canceled"
|
|
)
|
|
|
|
const (
|
|
DescPending = "this build is pending"
|
|
DescRunning = "this buils is running"
|
|
DescSuccess = "the build was successful"
|
|
DescFailure = "the build failed"
|
|
DescCanceled = "the build canceled"
|
|
)
|
|
|
|
// getStatus is a helper functin that converts a Drone
|
|
// status to a GitHub status.
|
|
func getStatus(status string) string {
|
|
switch status {
|
|
case model.StatusPending:
|
|
return StatusPending
|
|
case model.StatusRunning:
|
|
return StatusRunning
|
|
case model.StatusSuccess:
|
|
return StatusSuccess
|
|
case model.StatusFailure, model.StatusError:
|
|
return StatusFailure
|
|
case model.StatusKilled:
|
|
return StatusCanceled
|
|
default:
|
|
return StatusFailure
|
|
}
|
|
}
|
|
|
|
// getDesc is a helper function that generates a description
|
|
// message for the build based on the status.
|
|
func getDesc(status string) string {
|
|
switch status {
|
|
case model.StatusPending:
|
|
return DescPending
|
|
case model.StatusRunning:
|
|
return DescRunning
|
|
case model.StatusSuccess:
|
|
return DescSuccess
|
|
case model.StatusFailure, model.StatusError:
|
|
return DescFailure
|
|
case model.StatusKilled:
|
|
return DescCanceled
|
|
default:
|
|
return DescFailure
|
|
}
|
|
}
|