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
This commit is contained in:
Anbraten 2021-12-19 01:12:09 +01:00 committed by GitHub
parent ce462ce4ac
commit ed0a9fd756
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 133 additions and 33 deletions

View file

@ -388,6 +388,16 @@ func (g *Gitlab) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) {
}, nil }, 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 // Activate activates a repository by adding a Post-commit hook and
// a Public Deploy key, if applicable. // a Public Deploy key, if applicable.
func (g *Gitlab) Activate(ctx context.Context, user *model.User, repo *model.Repo, link string) error { 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 { if err != nil {
return err 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) _repo, err := g.getProject(ctx, client, repo.Owner, repo.Name)
if err != nil { if err != nil {
return err return err
} }
// TODO: "WoodpeckerCIService"
_, err = client.Services.SetDroneCIService(_repo.ID, &gitlab.SetDroneCIServiceOptions{ token, webURL, err := g.getTokenAndWebURL(link)
Token: &token, if err != nil {
DroneURL: &webURL, 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), EnableSSLVerification: gitlab.Bool(!g.SkipVerify),
}, gitlab.WithContext(ctx)) }, gitlab.WithContext(ctx))
return err return err
} }
@ -427,8 +445,44 @@ func (g *Gitlab) Deactivate(ctx context.Context, user *model.User, repo *model.R
if err != nil { if err != nil {
return err 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 return err
} }

View file

@ -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
}
]
`)

View file

@ -15,13 +15,9 @@
package testdata package testdata
import ( import (
"encoding/json"
"io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
// NewServer setup a mock server for testing purposes. // 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": case "/api/v4/projects/brightbox/puppet":
w.Write(project6Paylod) w.Write(project6Paylod)
return return
case "/api/v4/projects/4/services/drone-ci": case "/api/v4/projects/4/hooks":
switch r.Method { switch r.Method {
case "PUT": case "GET":
body, _ := io.ReadAll(r.Body) w.Write(project4PayloadHooks)
opts := make(map[string]interface{}) case "POST":
assert.NoError(t, json.Unmarshal(body, &opts)) w.Write(project4PayloadHook)
token, ok := opts["token"].(string)
assert.True(t, ok)
if token == "" {
w.WriteHeader(404)
} else {
w.WriteHeader(201)
}
case "DELETE":
w.WriteHeader(201) w.WriteHeader(201)
} }
return
case "/api/v4/projects/4/hooks/10717088":
w.WriteHeader(201)
return return
case "/oauth/token": case "/oauth/token":
w.Write(accessTokenPayload) w.Write(accessTokenPayload)

View file

@ -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 // try to fetch 3 times, timeout is one second longer each time
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
files, err = cf.fetch(ctx, time.Second*time.Duration(configFetchTimeout), strings.TrimSpace(cf.repo.Config)) 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) { if errors.Is(err, context.DeadlineExceeded) {
continue continue
} }

View file

@ -52,10 +52,9 @@ func parse(raw string, fn SecretFunc) (*Token, error) {
} }
func ParseRequest(r *http.Request, 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 // first we attempt to get the token from the
// authorization header. // authorization header.
token := r.Header.Get("Authorization")
if len(token) != 0 { if len(token) != 0 {
log.Trace().Msgf("token.ParseRequest: found token in header: %s", token) log.Trace().Msgf("token.ParseRequest: found token in header: %s", token)
bearer := token bearer := token
@ -65,6 +64,11 @@ func ParseRequest(r *http.Request, fn SecretFunc) (*Token, error) {
return parse(bearer, fn) 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 // then we attempt to get the token from the
// access_token url query parameter // access_token url query parameter
token = r.FormValue("access_token") token = r.FormValue("access_token")