diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..31407ab7 --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -0,0 +1,8 @@ +Thank you for taking the time to use Drone and file an issue or feature request. Before filing an issue please ensure the following boxes are checked, if applicable: + +- [ ] I have searched for existing issues +- [ ] I have discussed the issue with the community at https://gitter.im/drone/drone +- [ ] I have provided a sample `.drone.yml` file to help the team reproduce +- [ ] I have provided details from the build logs +- [ ] I have provided details from the server logs by running `docker logs drone` +- [ ] I am not using the issue tracker to ask why my build failed diff --git a/docs/setup/gitlab.md b/docs/setup/gitlab.md index 705b9078..a13dcbf2 100644 --- a/docs/setup/gitlab.md +++ b/docs/setup/gitlab.md @@ -33,6 +33,7 @@ This section lists all connection options used in the connection string format. * `open=false` allows users to self-register. Defaults to false for security reasons. * `orgs=drone&orgs=docker` restricts access to these GitLab organizations. **Optional** * `skip_verify=false` skip ca verification if self-signed certificate. Defaults to false for security reasons. +* `hide_archives=false` hide projects archived in GitLab from the listing. * `clone_mode=token` a strategy for clone authorization, by default use repo token, but can be changed to `oauth` ( This is not secure, because your user token, with full access to your gitlab account will be written to .netrc, and it can be read by all who have access to project builds ) ## Gitlab registration diff --git a/engine/engine.go b/engine/engine.go index 29552f34..f3d0c0c2 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -329,6 +329,11 @@ func (e *engine) runJob(c context.Context, r *Task, updater *updater, client doc info, builderr := docker.Wait(client, name) switch { + case info.State.Running: + // A build unblocked before actually being completed. + log.Errorf("incomplete build: %s", name) + r.Job.ExitCode = 1 + r.Job.Status = model.StatusError case info.State.ExitCode == 128: r.Job.ExitCode = info.State.ExitCode r.Job.Status = model.StatusKilled diff --git a/remote/gitlab/client/groups.go b/remote/gitlab/client/groups.go new file mode 100644 index 00000000..0e8f30d2 --- /dev/null +++ b/remote/gitlab/client/groups.go @@ -0,0 +1,53 @@ +package client + +import ( + "encoding/json" + "strconv" +) + +const ( + groupsUrl = "/groups" +) + +// Get a list of all projects owned by the authenticated user. +func (g *Client) AllGroups() ([]*Namespace, error) { + var perPage = 100 + var groups []*Namespace + + for i := 1; true; i++ { + contents, err := g.Groups(i, perPage) + if err != nil { + return groups, err + } + + for _, value := range contents { + groups = append(groups, value) + } + + if len(groups) == 0 { + break + } + + if len(groups)/i < perPage { + break + } + } + + return groups, nil +} + +func (g *Client) Groups(page, perPage int) ([]*Namespace, error) { + url, opaque := g.ResourceUrl(groupsUrl, nil, QMap{ + "page": strconv.Itoa(page), + "per_page": strconv.Itoa(perPage), + }) + + var groups []*Namespace + + contents, err := g.Do("GET", url, opaque, nil) + if err == nil { + err = json.Unmarshal(contents, &groups) + } + + return groups, err +} diff --git a/remote/gitlab/client/project.go b/remote/gitlab/client/project.go index f6f22765..6140ab32 100644 --- a/remote/gitlab/client/project.go +++ b/remote/gitlab/client/project.go @@ -15,12 +15,12 @@ const ( ) // Get a list of all projects owned by the authenticated user. -func (g *Client) AllProjects() ([]*Project, error) { +func (g *Client) AllProjects(hide_archives bool) ([]*Project, error) { var per_page = 100 var projects []*Project for i := 1; true; i++ { - contents, err := g.Projects(i, per_page) + contents, err := g.Projects(i, per_page, hide_archives) if err != nil { return projects, err } @@ -42,12 +42,17 @@ func (g *Client) AllProjects() ([]*Project, error) { } // Get a list of projects owned by the authenticated user. -func (c *Client) Projects(page int, per_page int) ([]*Project, error) { - - url, opaque := c.ResourceUrl(projectsUrl, nil, QMap{ +func (c *Client) Projects(page int, per_page int, hide_archives bool) ([]*Project, error) { + projectsOptions := QMap{ "page": strconv.Itoa(page), "per_page": strconv.Itoa(per_page), - }) + } + + if hide_archives { + projectsOptions["archived"] = "false" + } + + url, opaque := c.ResourceUrl(projectsUrl, nil, projectsOptions) var projects []*Project diff --git a/remote/gitlab/client/types.go b/remote/gitlab/client/types.go index d89311a2..0c6424e0 100644 --- a/remote/gitlab/client/types.go +++ b/remote/gitlab/client/types.go @@ -48,12 +48,14 @@ type Project struct { SshRepoUrl string `json:"ssh_url_to_repo"` HttpRepoUrl string `json:"http_url_to_repo"` Url string `json:"web_url"` + AvatarUrl string `json:"avatar_url"` Permissions *Permissions `json:"permissions,omitempty"` } type Namespace struct { Id int `json:"id,omitempty"` Name string `json:"name,omitempty"` + Path string `json:"path,omitempty"` } type Person struct { @@ -62,12 +64,17 @@ type Person struct { } type hProject struct { - Name string `json:"name"` - SshUrl string `json:"ssh_url"` - HttpUrl string `json:"http_url"` - VisibilityLevel int `json:"visibility_level"` - WebUrl string `json:"web_url"` - Namespace string `json:"namespace"` + Name string `json:"name"` + SshUrl string `json:"ssh_url"` + HttpUrl string `json:"http_url"` + GitSshUrl string `json:"git_ssh_url"` + GitHttpUrl string `json:"git_http_url"` + AvatarUrl string `json:"avatar_url"` + VisibilityLevel int `json:"visibility_level"` + WebUrl string `json:"web_url"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + Namespace string `json:"namespace"` } type hRepository struct { @@ -122,6 +129,7 @@ type HookPayload struct { UserId int `json:"user_id,omitempty"` UserName string `json:"user_name,omitempty"` ProjectId int `json:"project_id,omitempty"` + Project *hProject `json:"project,omitempty"` Repository *hRepository `json:"repository,omitempty"` Commits []hCommit `json:"commits,omitempty"` TotalCommitsCount int `json:"total_commits_count,omitempty"` diff --git a/remote/gitlab/client/util.go b/remote/gitlab/client/util.go index 7cc5c37f..c7418525 100644 --- a/remote/gitlab/client/util.go +++ b/remote/gitlab/client/util.go @@ -5,8 +5,18 @@ import ( "strings" ) +var encodeMap = map[string]string{ + ".": "%252E", +} + func encodeParameter(value string) string { - return strings.Replace(url.QueryEscape(value), "/", "%2F", 0) + value = url.QueryEscape(value) + + for before, after := range encodeMap { + value = strings.Replace(value, before, after, -1) + } + + return value } // Tag returns current tag for push event hook payload diff --git a/remote/gitlab/gitlab.go b/remote/gitlab/gitlab.go index 026a63de..e6aef04f 100644 --- a/remote/gitlab/gitlab.go +++ b/remote/gitlab/gitlab.go @@ -23,15 +23,16 @@ const ( ) type Gitlab struct { - URL string - Client string - Secret string - AllowedOrgs []string - CloneMode string - Open bool - PrivateMode bool - SkipVerify bool - Search bool + URL string + Client string + Secret string + AllowedOrgs []string + CloneMode string + Open bool + PrivateMode bool + SkipVerify bool + HideArchives bool + Search bool } func Load(env envconfig.Env) *Gitlab { @@ -50,6 +51,7 @@ func Load(env envconfig.Env) *Gitlab { 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") { @@ -101,6 +103,28 @@ func (g *Gitlab) Login(res http.ResponseWriter, req *http.Request) (*model.User, 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 @@ -113,7 +137,7 @@ func (g *Gitlab) Login(res http.ResponseWriter, req *http.Request) (*model.User, user.Avatar = g.URL + "/" + login.AvatarUrl } - return user, true, nil + return user, g.Open, nil } func (g *Gitlab) Auth(token, secret string) (string, error) { @@ -145,6 +169,12 @@ func (g *Gitlab) Repo(u *model.User, owner, name string) (*model.Repo, error) { 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 } @@ -164,7 +194,7 @@ func (g *Gitlab) Repos(u *model.User) ([]*model.RepoLite, error) { var repos = []*model.RepoLite{} - all, err := client.AllProjects() + all, err := client.AllProjects(g.HideArchives) if err != nil { return repos, err } @@ -173,15 +203,20 @@ func (g *Gitlab) Repos(u *model.User) ([]*model.RepoLite, error) { 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, }) - - // TODO: add repo.AvatarUrl } + return repos, err } @@ -201,7 +236,7 @@ func (g *Gitlab) Perm(u *model.User, owner, name string) (*model.Perm, error) { // repo owner is granted full access if repo.Owner != nil && repo.Owner.Username == u.Login { - return &model.Perm{true, true, true}, nil + return &model.Perm{true, true, true}, nil } // check permission for current user @@ -337,58 +372,136 @@ func (g *Gitlab) Hook(req *http.Request) (*model.Repo, *model.Build, error) { func mergeRequest(parsed *client.HookPayload, req *http.Request) (*model.Repo, *model.Build, error) { repo := &model.Repo{} - repo.Owner = req.FormValue("owner") - repo.Name = req.FormValue("name") - repo.FullName = fmt.Sprintf("%s/%s", repo.Owner, repo.Name) - repo.Link = parsed.ObjectAttributes.Target.WebUrl - repo.Clone = parsed.ObjectAttributes.Target.HttpUrl - repo.Branch = "master" + + 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" - build.Message = parsed.ObjectAttributes.LastCommit.Message - build.Commit = parsed.ObjectAttributes.LastCommit.Id - //build.Remote = parsed.ObjectAttributes.Source.HttpUrl - if parsed.ObjectAttributes.SourceProjectId == parsed.ObjectAttributes.TargetProjectId { - build.Ref = fmt.Sprintf("refs/heads/%s", parsed.ObjectAttributes.SourceBranch) - } else { - build.Ref = fmt.Sprintf("refs/merge-requests/%d/head", parsed.ObjectAttributes.IId) + lastCommit := obj.LastCommit + if lastCommit == nil { + return nil, nil, fmt.Errorf("last_commit key expected in merge request hook") } - build.Branch = parsed.ObjectAttributes.SourceBranch - // build.Timestamp = parsed.ObjectAttributes.LastCommit.Timestamp + build.Message = lastCommit.Message + build.Commit = lastCommit.Id + //build.Remote = parsed.ObjectAttributes.Source.HttpUrl - build.Author = parsed.ObjectAttributes.LastCommit.Author.Name - build.Email = parsed.ObjectAttributes.LastCommit.Author.Email - build.Title = parsed.ObjectAttributes.Title - build.Link = parsed.ObjectAttributes.Url + 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) { - var cloneUrl = parsed.Repository.GitHttpUrl - repo := &model.Repo{} - repo.Owner = req.FormValue("owner") - repo.Name = req.FormValue("name") - repo.FullName = fmt.Sprintf("%s/%s", repo.Owner, repo.Name) - repo.Link = parsed.Repository.URL - repo.Clone = cloneUrl - repo.Branch = "master" - switch parsed.Repository.VisibilityLevel { - case 0: - repo.IsPrivate = true - case 10: - repo.IsPrivate = true - case 20: - repo.IsPrivate = false + // 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") } - repo.FullName = fmt.Sprintf("%s/%s", req.FormValue("owner"), req.FormValue("name")) - build := &model.Build{} build.Event = model.EventPush build.Commit = parsed.After @@ -406,6 +519,9 @@ func push(parsed *client.HookPayload, req *http.Request) (*model.Repo, *model.Bu 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 } @@ -436,25 +552,6 @@ func (g *Gitlab) Oauth2Transport(r *http.Request) *oauth2.Transport { } } -// Accessor method, to allowed remote organizations field. -func (g *Gitlab) GetOrgs() []string { - return g.AllowedOrgs -} - -// Accessor method, to open field. -func (g *Gitlab) GetOpen() bool { - return g.Open -} - -// return default scope for GitHub -func (g *Gitlab) Scope() string { - return DefaultScope -} - -func (g *Gitlab) String() string { - return "gitlab" -} - const ( StatusPending = "pending" StatusRunning = "running" diff --git a/remote/gitlab/gitlab_test.go b/remote/gitlab/gitlab_test.go index 2e876d88..c6faa05d 100644 --- a/remote/gitlab/gitlab_test.go +++ b/remote/gitlab/gitlab_test.go @@ -32,6 +32,25 @@ func Test_Gitlab(t *testing.T) { g := goblin.Goblin(t) g.Describe("Gitlab Plugin", func() { + // Test projects method + g.Describe("AllProjects", func() { + g.It("Should return only non-archived projects is hidden", func() { + gitlab.HideArchives = true + _projects, err := gitlab.Repos(&user) + + g.Assert(err == nil).IsTrue() + g.Assert(len(_projects)).Equal(1) + }) + + g.It("Should return all the projects", func() { + gitlab.HideArchives = false + _projects, err := gitlab.Repos(&user) + + g.Assert(err == nil).IsTrue() + g.Assert(len(_projects)).Equal(2) + }) + }) + // Test repository method g.Describe("Repo", func() { g.It("Should return valid repo", func() { @@ -115,52 +134,114 @@ func Test_Gitlab(t *testing.T) { // Test hook method g.Describe("Hook", func() { - g.It("Should parse push hoook", func() { - req, _ := http.NewRequest( - "POST", - "http://example.com/api/hook?owner=diaspora&name=diaspora-client", - bytes.NewReader(testdata.PushHook), - ) + g.Describe("Push hook", func() { + g.It("Should parse actual push hoook", func() { + req, _ := http.NewRequest( + "POST", + "http://example.com/api/hook?owner=diaspora&name=diaspora-client", + bytes.NewReader(testdata.PushHook), + ) - repo, build, err := gitlab.Hook(req) + repo, build, err := gitlab.Hook(req) - g.Assert(err == nil).IsTrue() - g.Assert(repo.Owner).Equal("diaspora") - g.Assert(repo.Name).Equal("diaspora-client") - g.Assert(build.Ref).Equal("refs/heads/master") + g.Assert(err == nil).IsTrue() + g.Assert(repo.Owner).Equal("mike") + g.Assert(repo.Name).Equal("diaspora") + g.Assert(repo.Avatar).Equal("http://example.com/uploads/project/avatar/555/Outh-20-Logo.jpg") + g.Assert(repo.Branch).Equal("develop") + g.Assert(build.Ref).Equal("refs/heads/master") + }) + + g.It("Should parse legacy push hoook", func() { + req, _ := http.NewRequest( + "POST", + "http://example.com/api/hook?owner=diaspora&name=diaspora-client", + bytes.NewReader(testdata.LegacyPushHook), + ) + + repo, build, err := gitlab.Hook(req) + + g.Assert(err == nil).IsTrue() + g.Assert(repo.Owner).Equal("diaspora") + g.Assert(repo.Name).Equal("diaspora-client") + g.Assert(repo.Avatar).Equal("") + g.Assert(repo.Branch).Equal("master") + g.Assert(build.Ref).Equal("refs/heads/master") + + }) }) - g.It("Should parse tag push hook", func() { - req, _ := http.NewRequest( - "POST", - "http://example.com/api/hook?owner=diaspora&name=diaspora-client", - bytes.NewReader(testdata.TagHook), - ) + g.Describe("Tag push hook", func() { + g.It("Should parse tag push hook", func() { + req, _ := http.NewRequest( + "POST", + "http://example.com/api/hook?owner=diaspora&name=diaspora-client", + bytes.NewReader(testdata.TagHook), + ) - repo, build, err := gitlab.Hook(req) + repo, build, err := gitlab.Hook(req) - g.Assert(err == nil).IsTrue() - g.Assert(repo.Owner).Equal("diaspora") - g.Assert(repo.Name).Equal("diaspora-client") - g.Assert(build.Ref).Equal("refs/tags/v1.0.0") + g.Assert(err == nil).IsTrue() + g.Assert(repo.Owner).Equal("jsmith") + g.Assert(repo.Name).Equal("example") + g.Assert(repo.Avatar).Equal("http://example.com/uploads/project/avatar/555/Outh-20-Logo.jpg") + g.Assert(repo.Branch).Equal("develop") + g.Assert(build.Ref).Equal("refs/tags/v1.0.0") + }) + + g.It("Should parse legacy tag push hook", func() { + req, _ := http.NewRequest( + "POST", + "http://example.com/api/hook?owner=diaspora&name=diaspora-client", + bytes.NewReader(testdata.LegacyTagHook), + ) + + repo, build, err := gitlab.Hook(req) + + g.Assert(err == nil).IsTrue() + g.Assert(repo.Owner).Equal("diaspora") + g.Assert(repo.Name).Equal("diaspora-client") + g.Assert(build.Ref).Equal("refs/tags/v1.0.0") + + }) }) - g.It("Should parse merge request hook", func() { - req, _ := http.NewRequest( - "POST", - "http://example.com/api/hook?owner=diaspora&name=diaspora-client", - bytes.NewReader(testdata.MergeRequestHook), - ) + g.Describe("Merge request hook", func() { + g.It("Should parse merge request hook", func() { + req, _ := http.NewRequest( + "POST", + "http://example.com/api/hook?owner=diaspora&name=diaspora-client", + bytes.NewReader(testdata.MergeRequestHook), + ) - repo, build, err := gitlab.Hook(req) + repo, build, err := gitlab.Hook(req) - g.Assert(err == nil).IsTrue() - g.Assert(repo.Owner).Equal("diaspora") - g.Assert(repo.Name).Equal("diaspora-client") + g.Assert(err == nil).IsTrue() + g.Assert(repo.Avatar).Equal("http://example.com/uploads/project/avatar/555/Outh-20-Logo.jpg") + g.Assert(repo.Branch).Equal("develop") + g.Assert(repo.Owner).Equal("awesome_space") + g.Assert(repo.Name).Equal("awesome_project") - g.Assert(build.Title).Equal("MS-Viewport") + g.Assert(build.Title).Equal("MS-Viewport") + }) + + g.It("Should parse legacy merge request hook", func() { + req, _ := http.NewRequest( + "POST", + "http://example.com/api/hook?owner=diaspora&name=diaspora-client", + bytes.NewReader(testdata.LegacyMergeRequestHook), + ) + + repo, build, err := gitlab.Hook(req) + + g.Assert(err == nil).IsTrue() + g.Assert(repo.Owner).Equal("diaspora") + g.Assert(repo.Name).Equal("diaspora-client") + + g.Assert(build.Title).Equal("MS-Viewport") + }) }) }) }) diff --git a/remote/gitlab/helper.go b/remote/gitlab/helper.go index 62edc019..38b3bb88 100644 --- a/remote/gitlab/helper.go +++ b/remote/gitlab/helper.go @@ -1,13 +1,20 @@ package gitlab import ( + "crypto/md5" + "encoding/hex" "fmt" "net/url" "strconv" + "strings" "github.com/drone/drone/remote/gitlab/client" ) +const ( + gravatarBase = "https://www.gravatar.com/avatar" +) + // NewClient is a helper function that returns a new GitHub // client using the provided OAuth token. func NewClient(url, accessToken string, skipVerify bool) *client.Client { @@ -79,6 +86,26 @@ func ns(owner, name string) string { return fmt.Sprintf("%s%%2F%s", owner, name) } +func GetUserAvatar(email string) string { + hasher := md5.New() + hasher.Write([]byte(email)) + + return fmt.Sprintf( + "%s/%v.jpg?s=%s", + gravatarBase, + hex.EncodeToString(hasher.Sum(nil)), + "128", + ) +} + +func ExtractFromPath(str string) (string, string, error) { + s := strings.Split(str, "/") + if len(s) < 2 { + return "", "", fmt.Errorf("Minimum match not found") + } + return s[0], s[1], nil +} + func GetUserEmail(c *client.Client, defaultURL string) (*client.Client, error) { return c, nil } diff --git a/remote/gitlab/testdata/hooks.go b/remote/gitlab/testdata/hooks.go index 9f4a5cea..d7e01450 100644 --- a/remote/gitlab/testdata/hooks.go +++ b/remote/gitlab/testdata/hooks.go @@ -1,6 +1,46 @@ package testdata var TagHook = []byte(` +{ + "object_kind": "tag_push", + "ref": "refs/tags/v1.0.0", + "before": "0000000000000000000000000000000000000000", + "after": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7", + "user_id": 1, + "user_name": "John Smith", + "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s=80", + "project_id": 1, + "project":{ + "name":"Example", + "description":"", + "web_url":"http://example.com/jsmith/example", + "avatar_url":"http://example.com/uploads/project/avatar/555/Outh-20-Logo.jpg", + "git_ssh_url":"git@example.com:jsmith/example.git", + "git_http_url":"http://example.com/jsmith/example.git", + "namespace":"Jsmith", + "visibility_level":0, + "path_with_namespace":"jsmith/example", + "default_branch":"develop", + "homepage":"http://example.com/jsmith/example", + "url":"git@example.com:jsmith/example.git", + "ssh_url":"git@example.com:jsmith/example.git", + "http_url":"http://example.com/jsmith/example.git" + }, + "repository":{ + "name": "jsmith", + "url": "ssh://git@example.com/jsmith/example.git", + "description": "", + "homepage": "http://example.com/jsmith/example", + "git_http_url":"http://example.com/jsmith/example.git", + "git_ssh_url":"git@example.com:jsmith/example.git", + "visibility_level":0 + }, + "commits": [], + "total_commits_count": 0 +} +`) + +var LegacyTagHook = []byte(` { "object_kind": "tag_push", "ref": "refs/tags/v1.0.0", @@ -45,6 +85,86 @@ var TagHook = []byte(` `) var MergeRequestHook = []byte(` +{ + "object_kind": "merge_request", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "object_attributes": { + "id": 99, + "target_branch": "master", + "source_branch": "ms-viewport", + "source_project_id": 14, + "author_id": 51, + "assignee_id": 6, + "title": "MS-Viewport", + "created_at": "2013-12-03T17:23:34Z", + "updated_at": "2013-12-03T17:23:34Z", + "st_commits": null, + "st_diffs": null, + "milestone_id": null, + "state": "opened", + "merge_status": "unchecked", + "target_project_id": 14, + "iid": 1, + "description": "", + "source":{ + "name":"Awesome Project", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/awesome_space/awesome_project", + "avatar_url":"http://example.com/uploads/project/avatar/555/Outh-20-Logo.jpg", + "git_ssh_url":"git@example.com:awesome_space/awesome_project.git", + "git_http_url":"http://example.com/awesome_space/awesome_project.git", + "namespace":"Awesome Space", + "visibility_level":20, + "path_with_namespace":"awesome_space/awesome_project", + "default_branch":"master", + "homepage":"http://example.com/awesome_space/awesome_project", + "url":"http://example.com/awesome_space/awesome_project.git", + "ssh_url":"git@example.com:awesome_space/awesome_project.git", + "http_url":"http://example.com/awesome_space/awesome_project.git" + }, + "target": { + "name":"Awesome Project", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/awesome_space/awesome_project", + "avatar_url":"http://example.com/uploads/project/avatar/555/Outh-20-Logo.jpg", + "git_ssh_url":"git@example.com:awesome_space/awesome_project.git", + "git_http_url":"http://example.com/awesome_space/awesome_project.git", + "namespace":"Awesome Space", + "visibility_level":20, + "path_with_namespace":"awesome_space/awesome_project", + "default_branch":"develop", + "homepage":"http://example.com/awesome_space/awesome_project", + "url":"http://example.com/awesome_space/awesome_project.git", + "ssh_url":"git@example.com:awesome_space/awesome_project.git", + "http_url":"http://example.com/awesome_space/awesome_project.git" + }, + "last_commit": { + "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "message": "fixed readme", + "timestamp": "2012-01-03T23:36:29+02:00", + "url": "http://example.com/awesome_space/awesome_project/commits/da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "author": { + "name": "GitLab dev user", + "email": "gitlabdev@dv6700.(none)" + } + }, + "work_in_progress": false, + "url": "http://example.com/diaspora/merge_requests/1", + "action": "open", + "assignee": { + "name": "User1", + "username": "user1", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + } + } +} +`) + +var LegacyMergeRequestHook = []byte(` { "object_kind": "merge_request", "user": { @@ -101,6 +221,74 @@ var MergeRequestHook = []byte(` `) var PushHook = []byte(` +{ + "object_kind": "push", + "before": "95790bf891e76fee5e1747ab589903a6a1f80f22", + "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "ref": "refs/heads/master", + "user_id": 4, + "user_name": "John Smith", + "user_email": "john@example.com", + "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80", + "project_id": 15, + "project":{ + "name":"Diaspora", + "description":"", + "web_url":"http://example.com/mike/diaspora", + "avatar_url":"http://example.com/uploads/project/avatar/555/Outh-20-Logo.jpg", + "git_ssh_url":"git@example.com:mike/diaspora.git", + "git_http_url":"http://example.com/mike/diaspora.git", + "namespace":"Mike", + "visibility_level":0, + "path_with_namespace":"mike/diaspora", + "default_branch":"develop", + "homepage":"http://example.com/mike/diaspora", + "url":"git@example.com:mike/diasporadiaspora.git", + "ssh_url":"git@example.com:mike/diaspora.git", + "http_url":"http://example.com/mike/diaspora.git" + }, + "repository":{ + "name": "Diaspora", + "url": "git@example.com:mike/diasporadiaspora.git", + "description": "", + "homepage": "http://example.com/mike/diaspora", + "git_http_url":"http://example.com/mike/diaspora.git", + "git_ssh_url":"git@example.com:mike/diaspora.git", + "visibility_level":0 + }, + "commits": [ + { + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "message": "Update Catalan translation to e38cb41.", + "timestamp": "2011-12-12T14:27:31+02:00", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "author": { + "name": "Jordi Mallach", + "email": "jordi@softcatala.org" + }, + "added": ["CHANGELOG"], + "modified": ["app/controller/application.rb"], + "removed": [] + }, + { + "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "message": "fixed readme", + "timestamp": "2012-01-03T23:36:29+02:00", + "url": "http://example.com/mike/diaspora/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "author": { + "name": "GitLab dev user", + "email": "gitlabdev@dv6700.(none)" + }, + "added": ["CHANGELOG"], + "modified": ["app/controller/application.rb"], + "removed": [] + } + ], + "total_commits_count": 4 +} +`) + +var LegacyPushHook = []byte(` { "object_kind": "push", "before": "95790bf891e76fee5e1747ab589903a6a1f80f22", @@ -114,7 +302,7 @@ var PushHook = []byte(` "name": "Diaspora", "url": "git@example.com:mike/diasporadiaspora.git", "description": "", - "homepage": "http://example.com/mike/diaspora", + "homepage": "http://example.com/mike/diaspora", "git_http_url":"http://example.com/mike/diaspora.git", "git_ssh_url":"git@example.com:mike/diaspora.git", "visibility_level":0 diff --git a/remote/gitlab/testdata/projects.go b/remote/gitlab/testdata/projects.go index ebe9bd39..1dd069e3 100644 --- a/remote/gitlab/testdata/projects.go +++ b/remote/gitlab/testdata/projects.go @@ -1,7 +1,7 @@ package testdata // sample repository list -var projectsPayload = []byte(` +var allProjectsPayload = []byte(` [ { "id": 4, @@ -73,6 +73,47 @@ var projectsPayload = []byte(` "path": "brightbox", "updated_at": "2013-09-30T13:46:02Z" }, + "archived": true + } +] +`) + +var notArchivedProjectsPayload = []byte(` +[ + { + "id": 4, + "description": null, + "default_branch": "master", + "public": false, + "visibility_level": 0, + "ssh_url_to_repo": "git@example.com:diaspora/diaspora-client.git", + "http_url_to_repo": "http://example.com/diaspora/diaspora-client.git", + "web_url": "http://example.com/diaspora/diaspora-client", + "owner": { + "id": 3, + "name": "Diaspora", + "username": "some_user", + "created_at": "2013-09-30T13: 46: 02Z" + }, + "name": "Diaspora Client", + "name_with_namespace": "Diaspora / Diaspora Client", + "path": "diaspora-client", + "path_with_namespace": "diaspora/diaspora-client", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "snippets_enabled": false, + "created_at": "2013-09-30T13: 46: 02Z", + "last_activity_at": "2013-09-30T13: 46: 02Z", + "namespace": { + "created_at": "2013-09-30T13: 46: 02Z", + "description": "", + "id": 3, + "name": "Diaspora", + "owner_id": 1, + "path": "diaspora", + "updated_at": "2013-09-30T13: 46: 02Z" + }, "archived": false } ] diff --git a/remote/gitlab/testdata/testdata.go b/remote/gitlab/testdata/testdata.go index 3702c13d..2fa2322b 100644 --- a/remote/gitlab/testdata/testdata.go +++ b/remote/gitlab/testdata/testdata.go @@ -16,7 +16,12 @@ func NewServer() *httptest.Server { // evaluate the path to serve a dummy data file switch r.URL.Path { case "/api/v3/projects": - w.Write(projectsPayload) + if r.URL.Query().Get("archived") == "false" { + w.Write(notArchivedProjectsPayload) + } else { + w.Write(allProjectsPayload) + } + return case "/api/v3/projects/diaspora/diaspora-client": w.Write(project4Paylod) diff --git a/router/middleware/header/header.go b/router/middleware/header/header.go index 0e0eaee7..577d7a7d 100644 --- a/router/middleware/header/header.go +++ b/router/middleware/header/header.go @@ -25,8 +25,9 @@ func Options(c *gin.Context) { if c.Request.Method != "OPTIONS" { c.Next() } else { + c.Header("Access-Control-Allow-Origin", "*") c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS") - c.Header("Access-Control-Allow-Headers", "Authorization") + c.Header("Access-Control-Allow-Headers", "authorization, origin, content-type, accept") c.Header("Allow", "HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS") c.Header("Content-Type", "application/json") c.AbortWithStatus(200) diff --git a/router/middleware/session/repo.go b/router/middleware/session/repo.go index 34dd047c..6962bdb4 100644 --- a/router/middleware/session/repo.go +++ b/router/middleware/session/repo.go @@ -2,6 +2,7 @@ package session import ( "net/http" + "os" "github.com/drone/drone/model" "github.com/drone/drone/remote" @@ -104,6 +105,8 @@ func Perm(c *gin.Context) *model.Perm { } func SetPerm() gin.HandlerFunc { + PUBLIC_MODE := os.Getenv("PUBLIC_MODE") + return func(c *gin.Context) { user := User(c) repo := Repo(c) @@ -164,6 +167,11 @@ func SetPerm() gin.HandlerFunc { } } + // all build logs are visible in public mode + if PUBLIC_MODE != "" { + perm.Pull = true + } + if user != nil { log.Debugf("%s granted %+v permission to %s", user.Login, perm, repo.FullName) diff --git a/router/middleware/session/repo_test.go b/router/middleware/session/repo_test.go new file mode 100644 index 00000000..6d524a9b --- /dev/null +++ b/router/middleware/session/repo_test.go @@ -0,0 +1,44 @@ +package session + +import ( + "os" + "testing" + + "github.com/drone/drone/model" + "github.com/franela/goblin" + "github.com/gin-gonic/gin" +) + +func TestSetPerm(t *testing.T) { + g := goblin.Goblin(t) + g.Describe("SetPerm", func() { + g.BeforeEach(func() { + os.Unsetenv("PUBLIC_MODE") + }) + g.It("Should set pull to false (private repo, user not logged in)", func() { + c := gin.Context{} + c.Set("repo", &model.Repo{ + IsPrivate: true, + }) + SetPerm()(&c) + v, ok := c.Get("perm") + g.Assert(ok).IsTrue("perm was not set") + p, ok := v.(*model.Perm) + g.Assert(ok).IsTrue("perm was the wrong type") + g.Assert(p.Pull).IsFalse("pull should be false") + }) + g.It("Should set pull to true (private repo, user not logged in, public mode)", func() { + os.Setenv("PUBLIC_MODE", "true") + c := gin.Context{} + c.Set("repo", &model.Repo{ + IsPrivate: true, + }) + SetPerm()(&c) + v, ok := c.Get("perm") + g.Assert(ok).IsTrue("perm was not set") + p, ok := v.(*model.Perm) + g.Assert(ok).IsTrue("perm was the wrong type") + g.Assert(p.Pull).IsTrue("pull should be true") + }) + }) +} diff --git a/shared/docker/docker.go b/shared/docker/docker.go index 29ed1061..e08ec95e 100644 --- a/shared/docker/docker.go +++ b/shared/docker/docker.go @@ -1,9 +1,9 @@ package docker import ( - "io" - "io/ioutil" + "errors" + log "github.com/Sirupsen/logrus" "github.com/samalba/dockerclient" ) @@ -77,33 +77,21 @@ func Wait(client dockerclient.Client, name string) (*dockerclient.ContainerInfo, client.KillContainer(name, "9") }() - errc := make(chan error, 1) - infoc := make(chan *dockerclient.ContainerInfo, 1) - go func() { - - // blocks and waits for the container to finish - // by streaming the logs (to /dev/null). Ideally - // we could use the `wait` function instead - rc, err := client.ContainerLogs(name, LogOptsTail) - if err != nil { - errc <- err - return - } - io.Copy(ioutil.Discard, rc) - rc.Close() + for attempts := 0; attempts < 5; attempts++ { + done := client.Wait(name) + <-done info, err := client.InspectContainer(name) if err != nil { - errc <- err - return + return nil, err } - infoc <- info - }() - select { - case info := <-infoc: - return info, nil - case err := <-errc: - return nil, err + if !info.State.Running { + return info, nil + } + + log.Debugf("attempting to resume waiting after %d attempts.\n", attempts) } + + return nil, errors.New("reached maximum wait attempts") } diff --git a/static/images/dummy.png b/static/images/dummy.png new file mode 100644 index 00000000..f8b12dcd Binary files /dev/null and b/static/images/dummy.png differ diff --git a/template/amber/repo.amber b/template/amber/repo.amber index 79e7d9dc..d7358c69 100644 --- a/template/amber/repo.amber +++ b/template/amber/repo.amber @@ -32,7 +32,10 @@ block content each $build in $group.Builds a.card[href=$repo.Name+"/"+$build.Number][data-build=$build.Number] div.card-header - img[src=$build.Avatar] + if $build.Avatar != "" + img[src=$build.Avatar] + else + img[src="/static/images/dummy.png"] div.card-block div div.status[class=$build.Status] #{$build.Status} diff --git a/template/amber/repos.amber b/template/amber/repos.amber index 37081942..86af56eb 100644 --- a/template/amber/repos.amber +++ b/template/amber/repos.amber @@ -20,7 +20,10 @@ block content div.col-sm-4 a.card[href="/"+$repo.FullName] div.card-header - img.avatar[src=$repo.Avatar] + if $repo.Avatar != "" + img.avatar[src=$repo.Avatar] + else + img.avatar[src="/static/images/dummy.png"] div.card-block h3.login #{$repo.Name} div.full_name.hidden #{$repo.FullName}