From ed0a9fd7567a992ea3549027d731a34e0b28fe51 Mon Sep 17 00:00:00 2001 From: Anbraten Date: Sun, 19 Dec 2021 01:12:09 +0100 Subject: [PATCH] Use gitlab generic webhooks instead of drone-ci-service (#620) Benefits: - the webhook delivery history of the drone-ci-service is broken (no way to check if a webhook was successfully delivered by Gitlab) - drone-ci-service has limited events support (for example no comment or branch deleted event) - independent from drone integration in general --- server/remote/gitlab/gitlab.go | 78 +++++++++++++++++++---- server/remote/gitlab/testdata/projects.go | 50 +++++++++++++++ server/remote/gitlab/testdata/testdata.go | 26 +++----- server/shared/configFetcher.go | 4 +- shared/token/token.go | 8 ++- 5 files changed, 133 insertions(+), 33 deletions(-) diff --git a/server/remote/gitlab/gitlab.go b/server/remote/gitlab/gitlab.go index 25fac278a..2d41bc87f 100644 --- a/server/remote/gitlab/gitlab.go +++ b/server/remote/gitlab/gitlab.go @@ -388,6 +388,16 @@ func (g *Gitlab) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { }, nil } +func (g *Gitlab) getTokenAndWebURL(link string) (token string, webURL string, err error) { + uri, err := url.Parse(link) + if err != nil { + return "", "", err + } + token = uri.Query().Get("access_token") + webURL = fmt.Sprintf("%s://%s/api/hook", uri.Scheme, uri.Host) + return token, webURL, nil +} + // Activate activates a repository by adding a Post-commit hook and // a Public Deploy key, if applicable. func (g *Gitlab) Activate(ctx context.Context, user *model.User, repo *model.Repo, link string) error { @@ -395,23 +405,31 @@ func (g *Gitlab) Activate(ctx context.Context, user *model.User, repo *model.Rep if err != nil { return err } - uri, err := url.Parse(link) - if err != nil { - return err - } - token := uri.Query().Get("access_token") - webURL := fmt.Sprintf("%s://%s", uri.Scheme, uri.Host) _repo, err := g.getProject(ctx, client, repo.Owner, repo.Name) if err != nil { return err } - // TODO: "WoodpeckerCIService" - _, err = client.Services.SetDroneCIService(_repo.ID, &gitlab.SetDroneCIServiceOptions{ - Token: &token, - DroneURL: &webURL, + + token, webURL, err := g.getTokenAndWebURL(link) + if err != nil { + return err + } + + if len(token) == 0 { + return fmt.Errorf("no token found") + } + + _, _, err = client.Projects.AddProjectHook(_repo.ID, &gitlab.AddProjectHookOptions{ + URL: gitlab.String(webURL), + Token: gitlab.String(token), + PushEvents: gitlab.Bool(true), + TagPushEvents: gitlab.Bool(true), + MergeRequestsEvents: gitlab.Bool(true), + DeploymentEvents: gitlab.Bool(true), EnableSSLVerification: gitlab.Bool(!g.SkipVerify), }, gitlab.WithContext(ctx)) + return err } @@ -427,8 +445,44 @@ func (g *Gitlab) Deactivate(ctx context.Context, user *model.User, repo *model.R if err != nil { return err } - // TODO: "WoodpeckerCIService" - _, err = client.Services.DeleteDroneCIService(_repo.ID, gitlab.WithContext(ctx)) + + _, webURL, err := g.getTokenAndWebURL(link) + if err != nil { + return err + } + + hookID := -1 + listProjectHooksOptions := &gitlab.ListProjectHooksOptions{ + PerPage: 10, + Page: 1, + } + for { + hooks, resp, err := client.Projects.ListProjectHooks(_repo.ID, listProjectHooksOptions, gitlab.WithContext(ctx)) + if err != nil { + return err + } + + for _, hook := range hooks { + if hook.URL == webURL { + hookID = hook.ID + break + } + } + + // Exit the loop when we've seen all pages + if resp.CurrentPage >= resp.TotalPages { + break + } + + // Update the page number to get the next page + listProjectHooksOptions.Page = resp.NextPage + } + + if hookID == -1 { + return fmt.Errorf("could not find hook to delete") + } + + _, err = client.Projects.DeleteProjectHook(_repo.ID, hookID, gitlab.WithContext(ctx)) return err } diff --git a/server/remote/gitlab/testdata/projects.go b/server/remote/gitlab/testdata/projects.go index 0e689c8af..2ff7d00b7 100644 --- a/server/remote/gitlab/testdata/projects.go +++ b/server/remote/gitlab/testdata/projects.go @@ -224,3 +224,53 @@ var project6Paylod = []byte(` } } `) + +var project4PayloadHook = []byte(` +{ + "id": 10717088, + "url": "http://example.com/api/hook", + "created_at": "2021-12-18T23:29:33.852Z", + "push_events": true, + "tag_push_events": true, + "merge_requests_events": true, + "repository_update_events": false, + "enable_ssl_verification": true, + "project_id": 4, + "issues_events": false, + "confidential_issues_events": false, + "note_events": false, + "confidential_note_events": null, + "pipeline_events": false, + "wiki_page_events": false, + "deployment_events": true, + "job_events": false, + "releases_events": false, + "push_events_branch_filter": null +} +`) + +var project4PayloadHooks = []byte(` +[ + { + "id": 10717088, + "url": "http://example.com/api/hook", + "created_at": "2021-12-18T23:29:33.852Z", + "push_events": true, + "tag_push_events": true, + "merge_requests_events": true, + "repository_update_events": false, + "enable_ssl_verification": true, + "project_id": 4, + "issues_events": false, + "confidential_issues_events": false, + "note_events": false, + "confidential_note_events": null, + "pipeline_events": false, + "wiki_page_events": false, + "deployment_events": true, + "job_events": false, + "releases_events": false, + "push_events_branch_filter": null + } +] +`) diff --git a/server/remote/gitlab/testdata/testdata.go b/server/remote/gitlab/testdata/testdata.go index 577b1af67..71f30f6e6 100644 --- a/server/remote/gitlab/testdata/testdata.go +++ b/server/remote/gitlab/testdata/testdata.go @@ -15,13 +15,9 @@ package testdata import ( - "encoding/json" - "io" "net/http" "net/http/httptest" "testing" - - "github.com/stretchr/testify/assert" ) // NewServer setup a mock server for testing purposes. @@ -52,23 +48,17 @@ func NewServer(t *testing.T) *httptest.Server { case "/api/v4/projects/brightbox/puppet": w.Write(project6Paylod) return - case "/api/v4/projects/4/services/drone-ci": + case "/api/v4/projects/4/hooks": switch r.Method { - case "PUT": - body, _ := io.ReadAll(r.Body) - opts := make(map[string]interface{}) - assert.NoError(t, json.Unmarshal(body, &opts)) - token, ok := opts["token"].(string) - assert.True(t, ok) - if token == "" { - w.WriteHeader(404) - } else { - w.WriteHeader(201) - } - case "DELETE": + case "GET": + w.Write(project4PayloadHooks) + case "POST": + w.Write(project4PayloadHook) w.WriteHeader(201) } - + return + case "/api/v4/projects/4/hooks/10717088": + w.WriteHeader(201) return case "/oauth/token": w.Write(accessTokenPayload) diff --git a/server/shared/configFetcher.go b/server/shared/configFetcher.go index e6b9b0748..89ef1ce74 100644 --- a/server/shared/configFetcher.go +++ b/server/shared/configFetcher.go @@ -43,7 +43,9 @@ func (cf *configFetcher) Fetch(ctx context.Context) (files []*remote.FileMeta, e // try to fetch 3 times, timeout is one second longer each time for i := 0; i < 3; i++ { files, err = cf.fetch(ctx, time.Second*time.Duration(configFetchTimeout), strings.TrimSpace(cf.repo.Config)) - log.Trace().Msgf("%d try failed: %v", i, err) + if err != nil { + log.Trace().Err(err).Msgf("%d. try failed", i+1) + } if errors.Is(err, context.DeadlineExceeded) { continue } diff --git a/shared/token/token.go b/shared/token/token.go index 96bd848ad..3f1553718 100644 --- a/shared/token/token.go +++ b/shared/token/token.go @@ -52,10 +52,9 @@ func parse(raw string, fn SecretFunc) (*Token, error) { } func ParseRequest(r *http.Request, fn SecretFunc) (*Token, error) { - token := r.Header.Get("Authorization") - // first we attempt to get the token from the // authorization header. + token := r.Header.Get("Authorization") if len(token) != 0 { log.Trace().Msgf("token.ParseRequest: found token in header: %s", token) bearer := token @@ -65,6 +64,11 @@ func ParseRequest(r *http.Request, fn SecretFunc) (*Token, error) { return parse(bearer, fn) } + token = r.Header.Get("X-Gitlab-Token") + if len(token) != 0 { + return parse(token, fn) + } + // then we attempt to get the token from the // access_token url query parameter token = r.FormValue("access_token")