diff --git a/api/repo/create.go b/api/repo/create.go index d163d6320..b1abe89de 100644 --- a/api/repo/create.go +++ b/api/repo/create.go @@ -238,10 +238,11 @@ func CreateRepo(c *gin.Context) { r.SetHash(dbRepo.GetHash()) } + hook := new(library.Hook) // check if we should create the webhook if c.Value("webhookvalidation").(bool) { // send API call to create the webhook - _, err = scm.FromContext(c).Enable(u, r.GetOrg(), r.GetName(), r.GetHash()) + hook, _, err = scm.FromContext(c).Enable(u, r) if err != nil { retErr := fmt.Errorf("unable to create webhook for %s: %w", r.GetFullName(), err) @@ -296,6 +297,21 @@ func CreateRepo(c *gin.Context) { r, _ = database.FromContext(c).GetRepoForOrg(r.GetOrg(), r.GetName()) } + // create init hook in the DB after repo has been added in order to capture its ID + if c.Value("webhookvalidation").(bool) { + // update initialization hook + hook.SetRepoID(r.GetID()) + // create first hook for repo in the database + err = database.FromContext(c).CreateHook(hook) + if err != nil { + retErr := fmt.Errorf("unable to create initialization webhook for %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + } + c.JSON(http.StatusCreated, r) } diff --git a/api/repo/repair.go b/api/repo/repair.go index cbe5295e1..0a5b7022a 100644 --- a/api/repo/repair.go +++ b/api/repo/repair.go @@ -78,7 +78,7 @@ func RepairRepo(c *gin.Context) { } // send API call to create the webhook - _, err = scm.FromContext(c).Enable(u, r.GetOrg(), r.GetName(), r.GetHash()) + hook, _, err := scm.FromContext(c).Enable(u, r) if err != nil { retErr := fmt.Errorf("unable to create webhook for %s: %w", r.GetFullName(), err) @@ -95,6 +95,17 @@ func RepairRepo(c *gin.Context) { return } + + hook.SetRepoID(r.GetID()) + + err = database.FromContext(c).CreateHook(hook) + if err != nil { + retErr := fmt.Errorf("unable to create initialization webhook for %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } } // if the repo was previously inactive, mark it as active diff --git a/api/repo/update.go b/api/repo/update.go index 9ca114270..dbe1333ba 100644 --- a/api/repo/update.go +++ b/api/repo/update.go @@ -15,6 +15,7 @@ import ( "github.com/go-vela/server/router/middleware/org" "github.com/go-vela/server/router/middleware/repo" "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/scm" "github.com/go-vela/server/util" "github.com/go-vela/types/constants" "github.com/go-vela/types/library" @@ -238,6 +239,50 @@ func UpdateRepo(c *gin.Context) { } } + // grab last hook from repo to fetch the webhook ID + lastHook, err := database.FromContext(c).LastHookForRepo(r) + if err != nil { + retErr := fmt.Errorf("unable to retrieve last hook for repo %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // if repo has no hook deliveries, skip webhook update + if lastHook.GetWebhookID() != 0 { + // if user is platform admin, fetch the repo owner token to make changes to webhook + if u.GetAdmin() { + // capture admin name for logging + admn := u.GetName() + + u, err = database.FromContext(c).GetUser(r.GetUserID()) + if err != nil { + retErr := fmt.Errorf("unable to get repo owner of %s for platform admin webhook update: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // log admin override update repo hook + logrus.WithFields(logrus.Fields{ + "org": o, + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("platform admin %s updating repo webhook events for repo %s", admn, r.GetFullName()) + } + // update webhook with new events + err = scm.FromContext(c).Update(u, r, lastHook.GetWebhookID()) + if err != nil { + retErr := fmt.Errorf("unable to update repo webhook for %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + } + // send API call to update the repo err = database.FromContext(c).UpdateRepo(r) if err != nil { diff --git a/scm/github/github.go b/scm/github/github.go index ef2854889..2bad8eca3 100644 --- a/scm/github/github.go +++ b/scm/github/github.go @@ -25,6 +25,7 @@ const ( eventDeployment = "deployment" eventIssueComment = "issue_comment" eventRepository = "repository" + eventInitialize = "initialize" ) var ctx = context.Background() diff --git a/scm/github/repo.go b/scm/github/repo.go index 924d36000..de39ca07b 100644 --- a/scm/github/repo.go +++ b/scm/github/repo.go @@ -151,47 +151,119 @@ func (c *client) Disable(u *library.User, org, name string) error { } // Enable activates a repo by creating the webhook. -func (c *client) Enable(u *library.User, org, name, secret string) (string, error) { +func (c *client) Enable(u *library.User, r *library.Repo) (*library.Hook, string, error) { c.Logger.WithFields(logrus.Fields{ - "org": org, - "repo": name, + "org": r.GetOrg(), + "repo": r.GetName(), "user": u.GetName(), - }).Tracef("creating repository webhook for %s/%s", org, name) + }).Tracef("creating repository webhook for %s/%s", r.GetOrg(), r.GetName()) // create GitHub OAuth client with user's token client := c.newClientToken(*u.Token) + // always listen to repository events in case of repo name change + events := []string{eventRepository} + + if r.GetAllowComment() { + events = append(events, eventIssueComment) + } + + if r.GetAllowDeploy() { + events = append(events, eventDeployment) + } + + if r.GetAllowPull() { + events = append(events, eventPullRequest) + } + + if r.GetAllowPush() || r.GetAllowTag() { + events = append(events, eventPush) + } + // create the hook object to make the API call hook := &github.Hook{ - Events: []string{ - eventPush, - eventPullRequest, - eventDeployment, - eventIssueComment, - eventRepository, - }, + Events: events, Config: map[string]interface{}{ "url": fmt.Sprintf("%s/webhook", c.config.ServerWebhookAddress), "content_type": "form", - "secret": secret, + "secret": r.GetHash(), }, Active: github.Bool(true), } // send API call to create the webhook - _, resp, err := client.Repositories.CreateHook(ctx, org, name, hook) + hookInfo, resp, err := client.Repositories.CreateHook(ctx, r.GetOrg(), r.GetName(), hook) + + // create the first hook for the repo and record its ID from GitHub + webhook := new(library.Hook) + webhook.SetWebhookID(hookInfo.GetID()) + webhook.SetSourceID(r.GetName() + "-" + eventInitialize) + webhook.SetCreated(hookInfo.GetCreatedAt().Unix()) + webhook.SetEvent(eventInitialize) + webhook.SetNumber(1) switch resp.StatusCode { case http.StatusUnprocessableEntity: - return "", fmt.Errorf("repo already enabled") + return nil, "", fmt.Errorf("repo already enabled") case http.StatusNotFound: - return "", fmt.Errorf("repo not found") + return nil, "", fmt.Errorf("repo not found") } // create the URL for the repo - url := fmt.Sprintf("%s/%s/%s", c.config.Address, org, name) + url := fmt.Sprintf("%s/%s/%s", c.config.Address, r.GetOrg(), r.GetName()) + + return webhook, url, err +} + +// Update edits a repo webhook. +func (c *client) Update(u *library.User, r *library.Repo, hookID int64) error { + c.Logger.WithFields(logrus.Fields{ + "org": r.GetOrg(), + "repo": r.GetName(), + "user": u.GetName(), + }).Tracef("updating repository webhook for %s/%s", r.GetOrg(), r.GetName()) + + // create GitHub OAuth client with user's token + client := c.newClientToken(*u.Token) + + // always listen to repository events in case of repo name change + events := []string{eventRepository} + + if r.GetAllowComment() { + events = append(events, eventIssueComment) + } + + if r.GetAllowDeploy() { + events = append(events, eventDeployment) + } + + if r.GetAllowPull() { + events = append(events, eventPullRequest) + } + + if r.GetAllowPush() || r.GetAllowTag() { + events = append(events, eventPush) + } + + // create the hook object to make the API call + hook := &github.Hook{ + Events: events, + Config: map[string]interface{}{ + "url": fmt.Sprintf("%s/webhook", c.config.ServerWebhookAddress), + "content_type": "form", + "secret": r.GetHash(), + }, + Active: github.Bool(true), + } + + // send API call to update the webhook + _, _, err := client.Repositories.EditHook(ctx, r.GetOrg(), r.GetName(), hookID, hook) + + if err != nil { + return err + } - return url, err + return nil } // Status sends the commit status for the given SHA from the GitHub repo. diff --git a/scm/github/repo_test.go b/scm/github/repo_test.go index 2d1d63ad9..6b56b6903 100644 --- a/scm/github/repo_test.go +++ b/scm/github/repo_test.go @@ -602,10 +602,26 @@ func TestGithub_Enable(t *testing.T) { u.SetName("foo") u.SetToken("bar") + wantHook := new(library.Hook) + wantHook.SetWebhookID(1) + wantHook.SetSourceID("bar-initialize") + wantHook.SetCreated(1315329987) + wantHook.SetNumber(1) + wantHook.SetEvent("initialize") + + r := new(library.Repo) + r.SetID(1) + r.SetName("bar") + r.SetOrg("foo") + r.SetHash("secret") + r.SetAllowPush(true) + r.SetAllowPull(true) + r.SetAllowDeploy(true) + client, _ := NewTest(s.URL) // run test - _, err := client.Enable(u, "foo", "bar", "secret") + got, _, err := client.Enable(u, r) if resp.Code != http.StatusOK { t.Errorf("Enable returned %v, want %v", resp.Code, http.StatusOK) @@ -614,6 +630,57 @@ func TestGithub_Enable(t *testing.T) { if err != nil { t.Errorf("Enable returned err: %v", err) } + + if !reflect.DeepEqual(wantHook, got) { + t.Errorf("Enable returned hook %v, want %v", got, wantHook) + } +} + +func TestGithub_Update(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + _, engine := gin.CreateTestContext(resp) + + // setup mock server + engine.PATCH("/api/v3/repos/:org/:repo/hooks/:hook_id", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + c.File("testdata/hook.json") + }) + + s := httptest.NewServer(engine) + defer s.Close() + + // setup types + u := new(library.User) + u.SetName("foo") + u.SetToken("bar") + + r := new(library.Repo) + r.SetID(1) + r.SetName("bar") + r.SetOrg("foo") + r.SetHash("secret") + r.SetAllowPush(true) + r.SetAllowPull(true) + r.SetAllowDeploy(true) + + hookID := int64(1) + + client, _ := NewTest(s.URL) + + // run test + err := client.Update(u, r, hookID) + + if resp.Code != http.StatusOK { + t.Errorf("Update returned %v, want %v", resp.Code, http.StatusOK) + } + + if err != nil { + t.Errorf("Update returned err: %v", err) + } } func TestGithub_Status_Deployment(t *testing.T) { diff --git a/scm/service.go b/scm/service.go index b6e2fa781..5c42105ba 100644 --- a/scm/service.go +++ b/scm/service.go @@ -98,7 +98,10 @@ type Service interface { Disable(*library.User, string, string) error // Enable defines a function that activates // a repo by creating the webhook. - Enable(*library.User, string, string, string) (string, error) + Enable(*library.User, *library.Repo) (*library.Hook, string, error) + // Update defines a function that updates + // a webhook for a specified repo. + Update(*library.User, *library.Repo, int64) error // Status defines a function that sends the // commit status for the given SHA from a repo. Status(*library.User, *library.Build, string, string) error