diff --git a/remote/gitlab/client/drone.go b/remote/gitlab/client/drone.go new file mode 100644 index 000000000..8915e8455 --- /dev/null +++ b/remote/gitlab/client/drone.go @@ -0,0 +1,27 @@ +package client + +const ( + droneServiceUrl = "/projects/:id/services/drone-ci" +) + +func (c *Client) AddDroneService(id string, params QMap) error { + url, opaque := c.ResourceUrl( + droneServiceUrl, + QMap{":id": id}, + params, + ) + + _, err := c.Do("PUT", url, opaque, nil) + return err +} + +func (c *Client) DeleteDroneService(id string) error { + url, opaque := c.ResourceUrl( + droneServiceUrl, + QMap{":id": id}, + nil, + ) + + _, err := c.Do("DELETE", url, opaque, nil) + return err +} diff --git a/remote/gitlab/client/gitlab.go b/remote/gitlab/client/gitlab.go new file mode 100644 index 000000000..1b4bad749 --- /dev/null +++ b/remote/gitlab/client/gitlab.go @@ -0,0 +1,96 @@ +package client + +import ( + "bytes" + "crypto/tls" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" +) + +type Client struct { + BaseUrl string + ApiPath string + Token string + Client *http.Client +} + +func New(baseUrl, apiPath, token string, skipVerify bool) *Client { + config := &tls.Config{InsecureSkipVerify: skipVerify} + tr := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + TLSClientConfig: config, + } + client := &http.Client{Transport: tr} + + return &Client{ + BaseUrl: baseUrl, + ApiPath: apiPath, + Token: token, + Client: client, + } +} + +func (c *Client) ResourceUrl(u string, params, query QMap) (string, string) { + if params != nil { + for key, val := range params { + u = strings.Replace(u, key, encodeParameter(val), -1) + } + } + + query_params := url.Values{} + + if query != nil { + for key, val := range query { + query_params.Set(key, val) + } + } + + u = c.BaseUrl + c.ApiPath + u + "?" + query_params.Encode() + p, err := url.Parse(u) + if err != nil { + return u, "" + } + + opaque := "//" + p.Host + p.Path + return u, opaque +} + +func (c *Client) Do(method, url, opaque string, body []byte) ([]byte, error) { + var req *http.Request + var err error + + if body != nil { + reader := bytes.NewReader(body) + req, err = http.NewRequest(method, url, reader) + } else { + req, err = http.NewRequest(method, url, nil) + } + if err != nil { + return nil, fmt.Errorf("Error while building gitlab request") + } + + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.Token)) + + if len(opaque) > 0 { + req.URL.Opaque = opaque + } + + resp, err := c.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("Client.Do error: %q", err) + } + defer resp.Body.Close() + contents, err := ioutil.ReadAll(resp.Body) + if err != nil { + fmt.Printf("%s", err) + } + + if resp.StatusCode >= 400 { + err = fmt.Errorf("*Gitlab.buildAndExecRequest failed: <%d> %s", resp.StatusCode, req.URL) + } + + return contents, err +} diff --git a/remote/gitlab/client/hook.go b/remote/gitlab/client/hook.go new file mode 100644 index 000000000..8ea89689e --- /dev/null +++ b/remote/gitlab/client/hook.go @@ -0,0 +1,41 @@ +package client + +import ( + "encoding/json" + "fmt" +) + +// ParseHook parses hook payload from GitLab +func ParseHook(payload []byte) (*HookPayload, error) { + hp := HookPayload{} + if err := json.Unmarshal(payload, &hp); err != nil { + return nil, err + } + + // Basic sanity check + switch { + case len(hp.ObjectKind) == 0: + // Assume this is a post-receive within repository + if len(hp.After) == 0 { + return nil, fmt.Errorf("Invalid hook received, commit hash not found.") + } + case hp.ObjectKind == "push": + if hp.Repository == nil { + return nil, fmt.Errorf("Invalid push hook received, attributes not found") + } + case hp.ObjectKind == "tag_push": + if hp.Repository == nil { + return nil, fmt.Errorf("Invalid tag push hook received, attributes not found") + } + case hp.ObjectKind == "issue": + fallthrough + case hp.ObjectKind == "merge_request": + if hp.ObjectAttributes == nil { + return nil, fmt.Errorf("Invalid hook received, attributes not found.") + } + default: + return nil, fmt.Errorf("Invalid hook received, payload format not recognized.") + } + + return &hp, nil +} diff --git a/remote/gitlab/client/project.go b/remote/gitlab/client/project.go new file mode 100644 index 000000000..f6f227653 --- /dev/null +++ b/remote/gitlab/client/project.go @@ -0,0 +1,138 @@ +package client + +import ( + "encoding/json" + "strconv" + "strings" +) + +const ( + searchUrl = "/projects/search/:query" + projectsUrl = "/projects" + projectUrl = "/projects/:id" + repoUrlRawFile = "/projects/:id/repository/blobs/:sha" + commitStatusUrl = "/projects/:id/statuses/:sha" +) + +// Get a list of all projects owned by the authenticated user. +func (g *Client) AllProjects() ([]*Project, error) { + var per_page = 100 + var projects []*Project + + for i := 1; true; i++ { + contents, err := g.Projects(i, per_page) + if err != nil { + return projects, err + } + + for _, value := range contents { + projects = append(projects, value) + } + + if len(projects) == 0 { + break + } + + if len(projects)/i < per_page { + break + } + } + + return projects, nil +} + +// 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{ + "page": strconv.Itoa(page), + "per_page": strconv.Itoa(per_page), + }) + + var projects []*Project + + contents, err := c.Do("GET", url, opaque, nil) + if err == nil { + err = json.Unmarshal(contents, &projects) + } + + return projects, err +} + +// Get a project by id +func (c *Client) Project(id string) (*Project, error) { + url, opaque := c.ResourceUrl(projectUrl, QMap{":id": id}, nil) + + var project *Project + + contents, err := c.Do("GET", url, opaque, nil) + if err == nil { + err = json.Unmarshal(contents, &project) + } + + return project, err +} + +// Get Raw file content +func (c *Client) RepoRawFile(id, sha, filepath string) ([]byte, error) { + url, opaque := c.ResourceUrl( + repoUrlRawFile, + QMap{ + ":id": id, + ":sha": sha, + }, + QMap{ + "filepath": filepath, + }, + ) + + contents, err := c.Do("GET", url, opaque, nil) + + return contents, err +} + +// +func (c *Client) SetStatus(id, sha, state, desc, ref, link string) error { + url, opaque := c.ResourceUrl( + commitStatusUrl, + QMap{ + ":id": id, + ":sha": sha, + }, + QMap{ + "state": state, + "ref": ref, + "target_url": link, + "description": desc, + "context": "ci/drone", + }, + ) + + _, err := c.Do("POST", url, opaque, nil) + return err +} + +// Get a list of projects by query owned by the authenticated user. +func (c *Client) SearchProjectId(namespace string, name string) (id int, err error) { + + url, opaque := c.ResourceUrl(searchUrl, nil, QMap{ + ":query": strings.ToLower(name), + }) + + var projects []*Project + + contents, err := c.Do("GET", url, opaque, nil) + if err == nil { + err = json.Unmarshal(contents, &projects) + } else { + return id, err + } + + for _, project := range projects { + if project.Namespace.Name == namespace && strings.ToLower(project.Name) == strings.ToLower(name) { + id = project.Id + } + } + + return id, err +} diff --git a/remote/gitlab/client/types.go b/remote/gitlab/client/types.go new file mode 100644 index 000000000..7a5668a62 --- /dev/null +++ b/remote/gitlab/client/types.go @@ -0,0 +1,119 @@ +package client + +type QMap map[string]string + +type User struct { + Id int `json:"id,omitempty"` + Username string `json:"username,omitempty"` + Email string `json:"email,omitempty"` + AvatarUrl string `json:"avatar_url,omitempty"` + Name string `json:"name,omitempty"` +} + +type ProjectAccess struct { + AccessLevel int `json:"access_level,omitempty"` + NotificationLevel int `json:"notification_level,omitempty"` +} + +type GroupAccess struct { + AccessLevel int `json:"access_level,omitempty"` + NotificationLevel int `json:"notification_level,omitempty"` +} + +type Permissions struct { + ProjectAccess *ProjectAccess `json:"project_access,omitempty"` + GroupAccess *GroupAccess `json:"group_access,omitempty"` +} + +type Project struct { + Id int `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + DefaultBranch string `json:"default_branch,omitempty"` + Public bool `json:"public,omitempty"` + Path string `json:"path,omitempty"` + PathWithNamespace string `json:"path_with_namespace,omitempty"` + Namespace *Namespace `json:"namespace,omitempty"` + SshRepoUrl string `json:"ssh_url_to_repo"` + HttpRepoUrl string `json:"http_url_to_repo"` + Url string `json:"web_url"` + Permissions *Permissions `json:"permissions,omitempty"` +} + +type Namespace struct { + Id int `json:"id,omitempty"` + Name string `json:"name,omitempty"` +} + +type Person struct { + Name string `json:"name"` + Email string `json:"email"` +} + +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"` +} + +type hRepository struct { + Name string `json:"name,omitempty"` + URL string `json:"url,omitempty"` + Description string `json:"description,omitempty"` + Homepage string `json:"homepage,omitempty"` + GitHttpUrl string `json:"git_http_url,omitempty"` + GitSshUrl string `json:"git_ssh_url,omitempty"` + VisibilityLevel int `json:"visibility_level,omitempty"` +} + +type hCommit struct { + Id string `json:"id,omitempty"` + Message string `json:"message,omitempty"` + Timestamp string `json:"timestamp,omitempty"` + URL string `json:"url,omitempty"` + Author *Person `json:"author,omitempty"` +} + +type HookObjAttr struct { + Id int `json:"id,omitempty"` + Title string `json:"title,omitempty"` + AssigneeId int `json:"assignee_id,omitempty"` + AuthorId int `json:"author_id,omitempty"` + ProjectId int `json:"project_id,omitempty"` + CreatedAt string `json:"created_at,omitempty"` + UpdatedAt string `json:"updated_at,omitempty"` + Position int `json:"position,omitempty"` + BranchName string `json:"branch_name,omitempty"` + Description string `json:"description,omitempty"` + MilestoneId int `json:"milestone_id,omitempty"` + State string `json:"state,omitempty"` + IId int `json:"iid,omitempty"` + TargetBranch string `json:"target_branch,omitempty"` + SourceBranch string `json:"source_branch,omitempty"` + SourceProjectId int `json:"source_project_id,omitempty"` + StCommits string `json:"st_commits,omitempty"` + StDiffs string `json:"st_diffs,omitempty"` + MergeStatus string `json:"merge_status,omitempty"` + TargetProjectId int `json:"target_project_id,omitempty"` + Url string `json:"url,omiyempty"` + Source *hProject `json:"source,omitempty"` + Target *hProject `json:"target,omitempty"` + LastCommit *hCommit `json:"last_commit,omitempty"` +} + +type HookPayload struct { + Before string `json:"before,omitempty"` + After string `json:"after,omitempty"` + Ref string `json:"ref,omitempty"` + UserId int `json:"user_id,omitempty"` + UserName string `json:"user_name,omitempty"` + ProjectId int `json:"project_id,omitempty"` + Repository *hRepository `json:"repository,omitempty"` + Commits []hCommit `json:"commits,omitempty"` + TotalCommitsCount int `json:"total_commits_count,omitempty"` + ObjectKind string `json:"object_kind,omitempty"` + ObjectAttributes *HookObjAttr `json:"object_attributes,omitempty"` +} diff --git a/remote/gitlab/client/user.go b/remote/gitlab/client/user.go new file mode 100644 index 000000000..bafc7c47d --- /dev/null +++ b/remote/gitlab/client/user.go @@ -0,0 +1,21 @@ +package client + +import ( + "encoding/json" +) + +const ( + currentUserUrl = "/user" +) + +func (c *Client) CurrentUser() (User, error) { + url, opaque := c.ResourceUrl(currentUserUrl, nil, nil) + var user User + + contents, err := c.Do("GET", url, opaque, nil) + if err == nil { + err = json.Unmarshal(contents, &user) + } + + return user, err +} diff --git a/remote/gitlab/client/util.go b/remote/gitlab/client/util.go new file mode 100644 index 000000000..7cc5c37f4 --- /dev/null +++ b/remote/gitlab/client/util.go @@ -0,0 +1,33 @@ +package client + +import ( + "net/url" + "strings" +) + +func encodeParameter(value string) string { + return strings.Replace(url.QueryEscape(value), "/", "%2F", 0) +} + +// Tag returns current tag for push event hook payload +// This function returns empty string for any other events +func (h *HookPayload) Tag() string { + return strings.TrimPrefix(h.Ref, "refs/tags/") +} + +// Branch returns current branch for push event hook payload +// This function returns empty string for any other events +func (h *HookPayload) Branch() string { + return strings.TrimPrefix(h.Ref, "refs/heads/") +} + +// Head returns the latest changeset for push event hook payload +func (h *HookPayload) Head() hCommit { + c := hCommit{} + for _, cm := range h.Commits { + if h.After == cm.Id { + return cm + } + } + return c +} diff --git a/remote/gitlab/gitlab.go b/remote/gitlab/gitlab.go index f20ebf2d8..026a63de1 100644 --- a/remote/gitlab/gitlab.go +++ b/remote/gitlab/gitlab.go @@ -15,7 +15,7 @@ import ( "github.com/drone/drone/shared/oauth2" "github.com/drone/drone/shared/token" - "github.com/Bugagazavr/go-gitlab-client" + "github.com/drone/drone/remote/gitlab/client" ) const ( @@ -236,6 +236,22 @@ func (g *Gitlab) Script(user *model.User, repo *model.Repo, build *model.Build) // 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 } @@ -303,7 +319,7 @@ func (g *Gitlab) Deactivate(user *model.User, repo *model.Repo, link string) err 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 = gogitlab.ParseHook(payload) + var parsed, err = client.ParseHook(payload) if err != nil { return nil, nil, err } @@ -318,7 +334,7 @@ func (g *Gitlab) Hook(req *http.Request) (*model.Repo, *model.Build, error) { } } -func mergeRequest(parsed *gogitlab.HookPayload, 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") @@ -351,7 +367,7 @@ func mergeRequest(parsed *gogitlab.HookPayload, req *http.Request) (*model.Repo, return repo, build, nil } -func push(parsed *gogitlab.HookPayload, req *http.Request) (*model.Repo, *model.Build, error) { +func push(parsed *client.HookPayload, req *http.Request) (*model.Repo, *model.Build, error) { var cloneUrl = parsed.Repository.GitHttpUrl repo := &model.Repo{} @@ -438,3 +454,57 @@ func (g *Gitlab) Scope() string { func (g *Gitlab) String() string { return "gitlab" } + +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 + } +} diff --git a/remote/gitlab/helper.go b/remote/gitlab/helper.go index 25b7f1d5c..62edc0191 100644 --- a/remote/gitlab/helper.go +++ b/remote/gitlab/helper.go @@ -5,20 +5,19 @@ import ( "net/url" "strconv" - "github.com/Bugagazavr/go-gitlab-client" + "github.com/drone/drone/remote/gitlab/client" ) // NewClient is a helper function that returns a new GitHub // client using the provided OAuth token. -func NewClient(url, accessToken string, skipVerify bool) *gogitlab.Gitlab { - client := gogitlab.NewGitlabCert(url, "/api/v3", accessToken, skipVerify) - client.Bearer = true +func NewClient(url, accessToken string, skipVerify bool) *client.Client { + client := client.New(url, "/api/v3", accessToken, skipVerify) return client } // IsRead is a helper function that returns true if the // user has Read-only access to the repository. -func IsRead(proj *gogitlab.Project) bool { +func IsRead(proj *client.Project) bool { var user = proj.Permissions.ProjectAccess var group = proj.Permissions.GroupAccess @@ -36,7 +35,7 @@ func IsRead(proj *gogitlab.Project) bool { // IsWrite is a helper function that returns true if the // user has Read-Write access to the repository. -func IsWrite(proj *gogitlab.Project) bool { +func IsWrite(proj *client.Project) bool { var user = proj.Permissions.ProjectAccess var group = proj.Permissions.GroupAccess @@ -52,7 +51,7 @@ func IsWrite(proj *gogitlab.Project) bool { // IsAdmin is a helper function that returns true if the // user has Admin access to the repository. -func IsAdmin(proj *gogitlab.Project) bool { +func IsAdmin(proj *client.Project) bool { var user = proj.Permissions.ProjectAccess var group = proj.Permissions.GroupAccess @@ -80,13 +79,13 @@ func ns(owner, name string) string { return fmt.Sprintf("%s%%2F%s", owner, name) } -func GetUserEmail(client *gogitlab.Gitlab, defaultURL string) (*gogitlab.Gitlab, error) { - return client, nil +func GetUserEmail(c *client.Client, defaultURL string) (*client.Client, error) { + return c, nil } -func GetProjectId(r *Gitlab, client *gogitlab.Gitlab, owner, name string) (projectId string, err error) { +func GetProjectId(r *Gitlab, c *client.Client, owner, name string) (projectId string, err error) { if r.Search { - _projectId, err := client.SearchProjectId(owner, name) + _projectId, err := c.SearchProjectId(owner, name) if err != nil || _projectId == 0 { return "", err }