diff --git a/internal/gitsources/agolagit/agolagit.go b/internal/gitsources/agolagit/agolagit.go new file mode 100644 index 0000000..ce50a19 --- /dev/null +++ b/internal/gitsources/agolagit/agolagit.go @@ -0,0 +1,167 @@ +// Copyright 2019 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package agolagit + +import ( + "crypto/tls" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "net/url" + "strings" + "time" + + "github.com/pkg/errors" + gitsource "github.com/sorintlab/agola/internal/gitsources" + "github.com/sorintlab/agola/internal/services/types" +) + +var jsonContent = http.Header{"content-type": []string{"application/json"}} + +// Client represents a Gogs API client. +type Client struct { + url string + client *http.Client +} + +// NewClient initializes and returns a API client. +func New(url string) *Client { + // copied from net/http until it has a clone function: https://github.com/golang/go/issues/26013 + transport := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + }).DialContext, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + + httpClient := &http.Client{Transport: transport} + return &Client{ + url: strings.TrimSuffix(url, "/"), + client: httpClient, + } +} + +// SetHTTPClient replaces default http.Client with user given one. +func (c *Client) SetHTTPClient(client *http.Client) { + c.client = client +} + +func (c *Client) doRequest(method, path string, query url.Values, header http.Header, ibody io.Reader) (*http.Response, error) { + u, err := url.Parse(c.url + "/" + path) + if err != nil { + return nil, err + } + u.RawQuery = query.Encode() + + req, err := http.NewRequest(method, u.String(), ibody) + if err != nil { + return nil, err + } + for k, v := range header { + req.Header[k] = v + } + + return c.client.Do(req) +} + +func (c *Client) getResponse(method, path string, query url.Values, header http.Header, ibody io.Reader) (*http.Response, error) { + resp, err := c.doRequest(method, path, query, header, ibody) + if err != nil { + return nil, err + } + + if resp.StatusCode/100 != 2 { + defer resp.Body.Close() + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if len(data) <= 1 { + return resp, errors.New(resp.Status) + } + + // TODO(sgotti) use a json error response + + return resp, errors.New(string(data)) + } + + return resp, nil +} + +func (c *Client) getParsedResponse(method, path string, query url.Values, header http.Header, ibody io.Reader, obj interface{}) (*http.Response, error) { + resp, err := c.getResponse(method, path, query, header, ibody) + if err != nil { + return resp, err + } + defer resp.Body.Close() + + d := json.NewDecoder(resp.Body) + + return resp, d.Decode(obj) +} + +func (c *Client) GetUserInfo() (*gitsource.UserInfo, error) { + return nil, nil +} + +func (c *Client) GetFile(owner, repo, commit, file string) ([]byte, error) { + resp, err := c.getResponse("GET", fmt.Sprintf("%s/%s/raw/%s/%s", owner, repo, commit, file), nil, nil, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + data, err := ioutil.ReadAll(resp.Body) + return data, err +} + +func (c *Client) CreateDeployKey(owner, repo, title, pubKey string, readonly bool) error { + return nil +} + +func (c *Client) DeleteDeployKey(owner, repo, title string) error { + return nil +} + +func (c *Client) UpdateDeployKey(owner, repo, title, pubKey string, readonly bool) error { + return nil +} + +func (c *Client) CreateRepoWebhook(owner, repo, url, secret string) error { + return nil +} + +func (c *Client) DeleteRepoWebhook(owner, repo, u string) error { + return nil +} + +func (c *Client) CreateStatus(owner, repo, commitSHA string, status gitsource.CommitStatus, targetURL, description, context string) error { + return nil +} + +func (c *Client) ParseWebhook(r *http.Request) (*types.WebhookData, error) { + return parseWebhook(r) +} diff --git a/internal/gitsources/agolagit/parse.go b/internal/gitsources/agolagit/parse.go new file mode 100644 index 0000000..b4012a1 --- /dev/null +++ b/internal/gitsources/agolagit/parse.go @@ -0,0 +1,153 @@ +// Copyright 2019 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package agolagit + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "strconv" + "strings" + + "github.com/sorintlab/agola/internal/services/types" + "github.com/sorintlab/agola/internal/util" + + "github.com/pkg/errors" +) + +const ( + hookEvent = "X-Gitea-Event" + + hookPush = "push" + hookPullRequest = "pull_request" + + prStateOpen = "open" + + prActionOpen = "opened" + prActionSync = "synchronized" +) + +func parseWebhook(r *http.Request) (*types.WebhookData, error) { + switch r.Header.Get(hookEvent) { + case hookPush: + return parsePushHook(r.Body) + case hookPullRequest: + return parsePullRequestHook(r.Body) + default: + return nil, errors.Errorf("unknown webhook event type: %q", r.Header.Get(hookEvent)) + } +} + +func parsePush(r io.Reader) (*pushHook, error) { + push := new(pushHook) + err := json.NewDecoder(r).Decode(push) + return push, err +} + +func parsePullRequest(r io.Reader) (*pullRequestHook, error) { + pr := new(pullRequestHook) + err := json.NewDecoder(r).Decode(pr) + return pr, err +} + +func parsePushHook(payload io.Reader) (*types.WebhookData, error) { + push, err := parsePush(payload) + if err != nil { + return nil, err + } + + return webhookDataFromPush(push) +} + +func parsePullRequestHook(payload io.Reader) (*types.WebhookData, error) { + prhook, err := parsePullRequest(payload) + if err != nil { + return nil, err + } + + // skip non open pull requests + if prhook.PullRequest.State != prStateOpen { + return nil, nil + } + // only accept actions that have new commits + if prhook.Action != prActionOpen && prhook.Action != prActionSync { + return nil, nil + } + + return webhookDataFromPullRequest(prhook), nil +} + +func webhookDataFromPush(hook *pushHook) (*types.WebhookData, error) { + log.Printf("hook: %s", util.Dump(hook)) + sender := hook.Sender.Username + if sender == "" { + sender = hook.Sender.Login + } + + // common data + whd := &types.WebhookData{ + CommitSHA: hook.After, + Ref: hook.Ref, + CompareLink: hook.Compare, + CommitLink: fmt.Sprintf("%s/commit/%s", hook.Repo.URL, hook.After), + Sender: sender, + + Repo: types.WebhookDataRepo{ + Name: hook.Repo.Name, + Owner: hook.Repo.Owner.Username, + FullName: hook.Repo.FullName, + RepoURL: hook.Repo.URL, + }, + } + + whd.Event = types.WebhookEventPush + whd.Branch = strings.TrimPrefix(hook.Ref, "refs/heads/") + whd.BranchLink = fmt.Sprintf("%s/src/branch/%s", hook.Repo.URL, whd.Branch) + if len(hook.Commits) > 0 { + whd.Message = hook.Commits[0].Message + } + + return whd, nil +} + +// helper function that extracts the Build data from a Gitea pull_request hook +func webhookDataFromPullRequest(hook *pullRequestHook) *types.WebhookData { + log.Printf("hook: %s", util.Dump(hook)) + sender := hook.Sender.Username + if sender == "" { + sender = hook.Sender.Login + } + build := &types.WebhookData{ + Event: types.WebhookEventPullRequest, + CommitSHA: hook.PullRequest.Head.Sha, + Ref: fmt.Sprintf("refs/pull/%d/head", hook.Number), + CommitLink: fmt.Sprintf("%s/commit/%s", hook.Repo.URL, hook.PullRequest.Head.Sha), + Branch: hook.PullRequest.Base.Ref, + Message: hook.PullRequest.Title, + Sender: sender, + PullRequestID: strconv.FormatInt(hook.PullRequest.ID, 10), + PullRequestLink: hook.PullRequest.URL, + + Repo: types.WebhookDataRepo{ + Name: hook.Repo.Name, + Owner: hook.Repo.Owner.Username, + FullName: hook.Repo.FullName, + RepoURL: hook.Repo.URL, + }, + } + return build +} diff --git a/internal/gitsources/agolagit/types.go b/internal/gitsources/agolagit/types.go new file mode 100644 index 0000000..6cb8e67 --- /dev/null +++ b/internal/gitsources/agolagit/types.go @@ -0,0 +1,140 @@ +// Copyright 2019 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package agolagit + +type pushHook struct { + Sha string `json:"sha"` + Ref string `json:"ref"` + Before string `json:"before"` + After string `json:"after"` + Compare string `json:"compare_url"` + RefType string `json:"ref_type"` + + Pusher struct { + Name string `json:"name"` + Email string `json:"email"` + Login string `json:"login"` + Username string `json:"username"` + } `json:"pusher"` + + Repo struct { + ID int64 `json:"id"` + Name string `json:"name"` + FullName string `json:"full_name"` + URL string `json:"html_url"` + Private bool `json:"private"` + Owner struct { + Name string `json:"name"` + Email string `json:"email"` + Username string `json:"username"` + } `json:"owner"` + } `json:"repository"` + + Commits []struct { + ID string `json:"id"` + Message string `json:"message"` + URL string `json:"url"` + } `json:"commits"` + + Sender struct { + ID int64 `json:"id"` + Login string `json:"login"` + Username string `json:"username"` + Email string `json:"email"` + Avatar string `json:"avatar_url"` + } `json:"sender"` +} + +type pullRequestHook struct { + Action string `json:"action"` + Number int64 `json:"number"` + PullRequest struct { + ID int64 `json:"id"` + User struct { + ID int64 `json:"id"` + Username string `json:"username"` + Name string `json:"full_name"` + Email string `json:"email"` + Avatar string `json:"avatar_url"` + } `json:"user"` + Title string `json:"title"` + Body string `json:"body"` + State string `json:"state"` + URL string `json:"html_url"` + Mergeable bool `json:"mergeable"` + Merged bool `json:"merged"` + MergeBase string `json:"merge_base"` + Base struct { + Label string `json:"label"` + Ref string `json:"ref"` + Sha string `json:"sha"` + Repo struct { + ID int64 `json:"id"` + Name string `json:"name"` + FullName string `json:"full_name"` + URL string `json:"html_url"` + Private bool `json:"private"` + Owner struct { + ID int64 `json:"id"` + Username string `json:"username"` + Name string `json:"full_name"` + Email string `json:"email"` + Avatar string `json:"avatar_url"` + } `json:"owner"` + } `json:"repo"` + } `json:"base"` + Head struct { + Label string `json:"label"` + Ref string `json:"ref"` + Sha string `json:"sha"` + Repo struct { + ID int64 `json:"id"` + Name string `json:"name"` + FullName string `json:"full_name"` + URL string `json:"html_url"` + Private bool `json:"private"` + Owner struct { + ID int64 `json:"id"` + Username string `json:"username"` + Name string `json:"full_name"` + Email string `json:"email"` + Avatar string `json:"avatar_url"` + } `json:"owner"` + } `json:"repo"` + } `json:"head"` + } `json:"pull_request"` + Repo struct { + ID int64 `json:"id"` + Name string `json:"name"` + FullName string `json:"full_name"` + URL string `json:"html_url"` + Private bool `json:"private"` + Owner struct { + ID int64 `json:"id"` + Username string `json:"username"` + Name string `json:"full_name"` + Email string `json:"email"` + Avatar string `json:"avatar_url"` + } `json:"owner"` + } `json:"repository"` + Sender struct { + ID int64 `json:"id"` + Login string `json:"login"` + Username string `json:"username"` + Name string `json:"full_name"` + Email string `json:"email"` + Avatar string `json:"avatar_url"` + } `json:"sender"` +}